import { Factory, trait } from 'ember-cli-mirage'; import faker from 'nomad-ui/mirage/faker'; import { provide } from '../utils'; import { generateResources } from '../common'; import { dasherize } from '@ember/string'; const DISK_RESERVATIONS = [200, 500, 1000, 2000, 5000, 10000, 100000]; export default Factory.extend({ name: (id) => `${dasherize(faker.hacker.noun())}-g-${id}`, count: () => faker.random.number({ min: 1, max: 2 }), ephemeralDisk: () => ({ Sticky: faker.random.boolean(), SizeMB: faker.helpers.randomize(DISK_RESERVATIONS), Migrate: faker.random.boolean(), }), noHostVolumes: trait({ volumes: () => ({}), }), withScaling: faker.random.boolean, volumes: makeHostVolumes(), // Directive used to control whether or not allocations are automatically // created. createAllocations: true, // Directived used to control whether or not the allocation should fail // and reschedule, creating reschedule events. withRescheduling: false, // Directive used to control whether the task group should have services. withServices: false, // Whether the tasks themselves should have services. withTaskServices: false, // Directive used to control whether dynamic application sizing recommendations // should be created. createRecommendations: false, // When true, only creates allocations shallow: false, // When set, passed into tasks to set resource values resourceSpec: null, afterCreate(group, server) { let taskIds = []; let volumes = Object.keys(group.volumes); if (group.withScaling) { group.update({ scaling: { Min: 1, Max: 5, Policy: faker.random.boolean() && { EvaluationInterval: '10s', Cooldown: '2m', Check: { avg_conn: { Source: 'prometheus', Query: 'scalar(avg((haproxy_server_current_sessions{backend="http_back"}) and (haproxy_server_up{backend="http_back"} == 1)))', Strategy: { 'target-value': { target: 20, }, }, }, }, }, }, }); } if (!group.shallow) { const resources = group.resourceSpec && divide(group.count, parseResourceSpec(group.resourceSpec)); const tasks = provide(group.count, (_, idx) => { const mounts = faker.helpers .shuffle(volumes) .slice(0, faker.random.number({ min: 1, max: 3 })); const maybeResources = {}; if (resources) { maybeResources.originalResources = generateResources(resources[idx]); } return server.create('task', { taskGroupID: group.id, ...maybeResources, withServices: group.withTaskServices, volumeMounts: mounts.map((mount) => ({ Volume: mount, Destination: `/${faker.internet.userName()}/${faker.internet.domainWord()}/${faker.internet.color()}`, PropagationMode: '', ReadOnly: faker.random.boolean(), })), createRecommendations: group.createRecommendations, }); }); taskIds = tasks.mapBy('id'); } group.update({ taskIds: taskIds, }); if (group.createAllocations) { Array(group.count) .fill(null) .forEach((_, i) => { const props = { jobId: group.job.id, namespace: group.job.namespace, taskGroup: group.name, name: `${group.name}.[${i}]`, rescheduleSuccess: group.withRescheduling ? faker.random.boolean() : null, rescheduleAttempts: group.withRescheduling ? faker.random.number({ min: 1, max: 5 }) : 0, }; if (group.withRescheduling) { server.create('allocation', 'rescheduled', props); } else { server.create('allocation', props); } }); } if (group.withServices) { const services = server.createList('service-fragment', 5, { taskGroupId: group.id, taskGroup: group, provider: 'nomad', }); services.push( server.create('service-fragment', { taskGroupId: group.id, taskGroup: group, provider: 'consul', }) ); services.forEach((fragment) => { server.create('service', { serviceName: fragment.name, id: `${faker.internet.domainWord()}-group-${fragment.name}`, }); server.create('service', { serviceName: fragment.name, id: `${faker.internet.domainWord()}-group-${fragment.name}`, }); server.create('service', { serviceName: fragment.name, id: `${faker.internet.domainWord()}-group-${fragment.name}`, }); }); group.update({ services, }); } }, }); function makeHostVolumes() { const generate = () => ({ Name: faker.internet.domainWord(), Type: 'host', Source: faker.internet.domainWord(), ReadOnly: faker.random.boolean(), }); const volumes = provide(faker.random.number({ min: 1, max: 5 }), generate); return volumes.reduce((hash, volume) => { hash[volume.Name] = volume; return hash; }, {}); } function parseResourceSpec(spec) { const mapping = { M: 'MemoryMB', C: 'CPU', D: 'DiskMB', I: 'IOPS', }; const terms = spec.split(',').map((t) => { const [k, v] = t .trim() .split(':') .map((kv) => kv.trim()); return [k, +v]; }); return terms.reduce((hash, term) => { hash[mapping[term[0]]] = term[1]; return hash; }, {}); } // Split a single resources object into N resource objects where // the sum of each property of the new resources objects equals // the original resources properties // ex: divide(2, { Mem: 400, Cpu: 250 }) -> [{ Mem: 80, Cpu: 50 }, { Mem: 320, Cpu: 200 }] function divide(count, resources) { const wheel = roulette(1, count); const ret = provide(count, (_, idx) => { return Object.keys(resources).reduce((hash, key) => { hash[key] = Math.round(resources[key] * wheel[idx]); return hash; }, {}); }); return ret; } // Roulette splits a number into N divisions // Variance is a value between 0 and 1 that determines how much each division in // size. At 0 each division is even, at 1, it's entirely random but the sum of all // divisions is guaranteed to equal the total value. function roulette(total, divisions, variance = 0.8) { let roulette = new Array(divisions).fill(total / divisions); roulette.forEach((v, i) => { if (i === roulette.length - 1) return; roulette.splice( i, 2, ...rngDistribute(roulette[i], roulette[i + 1], variance) ); }); return roulette; } function rngDistribute(a, b, variance = 0.8) { const move = a * faker.random.number({ min: 0, max: variance, precision: 0.01 }); return [a - move, b + move]; }