2017-08-02 15:57:00 +03:00
|
|
|
const { detailedDiff } = require('deep-object-diff');
|
|
|
|
const Delay = require('delay');
|
|
|
|
const forEach = require('apr-for-each/series');
|
|
|
|
const map = require('apr-map/series');
|
2017-06-09 07:26:25 +03:00
|
|
|
const paramCase = require('param-case');
|
|
|
|
const camelCase = require('camel-case');
|
2017-07-20 13:57:33 +03:00
|
|
|
const buildArray = require('build-array');
|
2017-08-08 13:07:18 +03:00
|
|
|
const forceArray = require('force-array');
|
2017-07-14 14:15:13 +03:00
|
|
|
const lfind = require('lodash.find');
|
2017-07-20 13:57:33 +03:00
|
|
|
const findIndex = require('lodash.findindex');
|
2017-07-14 14:15:13 +03:00
|
|
|
const uniq = require('lodash.uniq');
|
2017-06-09 07:26:25 +03:00
|
|
|
const yaml = require('js-yaml');
|
2017-07-21 17:08:15 +03:00
|
|
|
const hasha = require('hasha');
|
2017-08-22 18:24:49 +03:00
|
|
|
const moment = require('moment');
|
2017-08-02 15:57:00 +03:00
|
|
|
const IPC = require('crocket');
|
|
|
|
const Boom = require('boom');
|
|
|
|
|
|
|
|
const { NODE_ENV, SOCK_PORT } = process.env;
|
|
|
|
const IS_TEST = (NODE_ENV || '').toLowerCase() === 'test';
|
|
|
|
|
|
|
|
const emit = (() => {
|
|
|
|
if (!IS_TEST) {
|
|
|
|
return () => null;
|
|
|
|
}
|
|
|
|
|
|
|
|
let index = 0;
|
|
|
|
const sock = new IPC();
|
|
|
|
|
|
|
|
sock.listen({ port: Number(SOCK_PORT) }, err => {
|
|
|
|
if (err) throw err;
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
console.log('sock bound to %d', SOCK_PORT);
|
|
|
|
});
|
|
|
|
|
|
|
|
return (name, payload) => {
|
|
|
|
sock.emit(name, { index: (index += 1), payload });
|
|
|
|
};
|
|
|
|
})();
|
2017-06-09 07:26:25 +03:00
|
|
|
|
2017-07-21 16:24:07 +03:00
|
|
|
const wpData = require('./wp-data.json');
|
|
|
|
const cpData = require('./cp-data.json');
|
|
|
|
const complexData = require('./complex-data.json');
|
2017-08-22 18:24:49 +03:00
|
|
|
const metricData = [
|
|
|
|
require('./metric-datasets-0.json'),
|
|
|
|
require('./metric-datasets-1.json'),
|
|
|
|
require('./metric-datasets-2.json')
|
|
|
|
];
|
2017-07-21 16:24:07 +03:00
|
|
|
|
2017-07-21 17:08:15 +03:00
|
|
|
const { datacenter, portal } = require('./data.json');
|
2017-05-18 21:21:33 +03:00
|
|
|
|
2017-07-21 17:08:15 +03:00
|
|
|
const deploymentGroups = [
|
|
|
|
wpData.deploymentGroup,
|
|
|
|
cpData.deploymentGroup,
|
|
|
|
complexData.deploymentGroup
|
|
|
|
];
|
|
|
|
|
|
|
|
const services = wpData.services
|
|
|
|
.concat(cpData.services)
|
|
|
|
.concat(complexData.services);
|
|
|
|
|
|
|
|
const instances = wpData.instances
|
|
|
|
.concat(cpData.instances)
|
|
|
|
.concat(complexData.instances);
|
2017-07-21 16:24:07 +03:00
|
|
|
|
2017-08-08 13:07:18 +03:00
|
|
|
const INTERPOLATE_REGEX = /\$([_a-z][_a-z0-9]*)/gi;
|
2017-08-02 15:57:00 +03:00
|
|
|
const wait = (s, fn) => setTimeout(fn, s * 1000);
|
|
|
|
// eslint-disable-next-line new-cap
|
|
|
|
const delay = s => Delay(s * 1000);
|
2017-08-08 13:07:18 +03:00
|
|
|
|
2017-05-18 21:21:33 +03:00
|
|
|
const find = (query = {}) => item =>
|
|
|
|
Object.keys(query).every(key => item[key] === query[key]);
|
|
|
|
|
|
|
|
const cleanQuery = (q = {}) => JSON.parse(JSON.stringify(q));
|
|
|
|
|
2017-08-22 18:24:49 +03:00
|
|
|
let metricDataIndex = 0;
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const cleanDiff = (diff, meta) => {
|
|
|
|
const obj = Object.assign(diff, meta);
|
|
|
|
|
|
|
|
return Object.keys(obj).reduce((acc, key) => {
|
|
|
|
if (
|
|
|
|
['added', 'deleted', 'updated'].indexOf(key) >= 0 &&
|
|
|
|
!Object.keys(obj[key]).length
|
|
|
|
) {
|
|
|
|
return acc;
|
|
|
|
}
|
|
|
|
|
|
|
|
acc[key] = obj[key];
|
|
|
|
return acc;
|
|
|
|
}, {});
|
|
|
|
};
|
|
|
|
|
|
|
|
const diff = (before, after, metaProp) => {
|
|
|
|
if (!IS_TEST) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const detail = detailedDiff(before, after);
|
|
|
|
const cleaned = cleanDiff(detail, {
|
|
|
|
[metaProp]: before[metaProp]
|
|
|
|
});
|
|
|
|
|
|
|
|
return ['added', 'deleted', 'updated'].reduce((diff, type) => {
|
|
|
|
if (!diff[type]) {
|
|
|
|
return diff;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Object.assign(diff, {
|
|
|
|
[type]: Object.keys(diff[type]).reduce(
|
|
|
|
(change, name) =>
|
|
|
|
Object.assign(change, {
|
|
|
|
[name]: {
|
|
|
|
before: before[name],
|
|
|
|
after: diff[type][name]
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
{}
|
|
|
|
)
|
|
|
|
});
|
|
|
|
}, cleaned);
|
|
|
|
};
|
|
|
|
|
|
|
|
/** *************************************************************************** */
|
|
|
|
|
2017-08-22 18:17:33 +03:00
|
|
|
const getMetrics = query => {
|
2017-08-28 22:21:08 +03:00
|
|
|
const { names, start, end, instanceId } = query;
|
2017-08-22 18:17:33 +03:00
|
|
|
|
2017-08-22 18:24:49 +03:00
|
|
|
const metrics = names.reduce((metrics, name) => {
|
|
|
|
// pick one of the three metric data jsons, so there's variety
|
2017-08-28 22:21:08 +03:00
|
|
|
const index = metricDataIndex % metricData.length;
|
2017-08-22 18:24:49 +03:00
|
|
|
metricDataIndex++;
|
|
|
|
|
|
|
|
const md = metricData[index].find(md => md.name === name);
|
|
|
|
const m = md.metrics;
|
|
|
|
|
|
|
|
const s = moment.utc(start);
|
|
|
|
const e = moment.utc(end);
|
|
|
|
|
|
|
|
// how many records do we need?
|
|
|
|
const duration = e.diff(s); // duration for which we need data
|
2017-08-28 22:21:08 +03:00
|
|
|
const records = Math.floor(duration / 15000); // new metric record every 15 secs
|
2017-08-22 18:24:49 +03:00
|
|
|
|
|
|
|
const requiredMetrics = [];
|
|
|
|
let i = 0;
|
|
|
|
const time = moment(s);
|
|
|
|
// start at a random point within the dataset for variety
|
|
|
|
const randomIndex = Math.round(Math.random() * m.length);
|
2017-08-28 22:21:08 +03:00
|
|
|
while (i < records) {
|
|
|
|
const index = (randomIndex + i) % m.length; // loop if not enough data
|
2017-08-22 18:24:49 +03:00
|
|
|
const requiredMetric = m[index];
|
2017-08-28 22:21:08 +03:00
|
|
|
requiredMetric.time = time
|
|
|
|
.add(15, 'seconds')
|
|
|
|
.utc()
|
|
|
|
.format(); // we should have a new record every 15 secs
|
2017-08-22 18:24:49 +03:00
|
|
|
requiredMetrics.push(requiredMetric);
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
|
|
|
|
const requiredMetricData = {
|
|
|
|
instance: instanceId,
|
|
|
|
name,
|
|
|
|
start: s.utc().format(),
|
|
|
|
end: time.utc().format(), // this will be used by the frontend for the next fetch
|
|
|
|
metrics: requiredMetrics
|
2017-08-28 22:21:08 +03:00
|
|
|
};
|
2017-08-22 18:24:49 +03:00
|
|
|
metrics.push(requiredMetricData);
|
|
|
|
return metrics;
|
2017-08-28 22:21:08 +03:00
|
|
|
}, []);
|
2017-08-22 18:17:33 +03:00
|
|
|
|
|
|
|
return Promise.resolve(metrics);
|
2017-08-28 22:21:08 +03:00
|
|
|
};
|
2017-08-22 18:17:33 +03:00
|
|
|
|
2017-09-06 12:46:34 +03:00
|
|
|
const getInstanceMetrics = ({ id }) => query =>
|
|
|
|
getMetrics(
|
|
|
|
Object.assign({}, query, {
|
|
|
|
instanceId: id
|
|
|
|
})
|
|
|
|
);
|
2017-08-22 18:17:33 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const updateInstance = async query => {
|
|
|
|
await delay(0.5);
|
|
|
|
|
|
|
|
const instanceIndex = findIndex(instances, ['id', query.id]);
|
|
|
|
const original = cleanQuery(instances[instanceIndex]);
|
|
|
|
|
2017-09-06 12:46:34 +03:00
|
|
|
instances[instanceIndex] = Object.assign(
|
|
|
|
instances[instanceIndex],
|
|
|
|
cleanQuery(query)
|
|
|
|
);
|
2017-08-02 15:57:00 +03:00
|
|
|
|
|
|
|
emit('instance-updated', diff(original, instances[instanceIndex], 'name'));
|
|
|
|
|
|
|
|
return instances[instanceIndex];
|
|
|
|
};
|
2017-08-22 18:17:33 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const getInstances = async query => {
|
|
|
|
await delay(0.1);
|
|
|
|
|
|
|
|
return instances.filter(find(cleanQuery(query))).map(instance =>
|
|
|
|
Object.assign({}, instance, {
|
|
|
|
metrics: getInstanceMetrics(instance)
|
|
|
|
})
|
2017-08-22 18:17:33 +03:00
|
|
|
);
|
2017-06-26 15:25:46 +03:00
|
|
|
};
|
2017-06-08 00:15:48 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const getInstance = async query => {
|
|
|
|
const instance = (await getInstances(query)).shift();
|
2017-05-18 21:21:33 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
if (!instance) {
|
|
|
|
throw Boom.notFound();
|
|
|
|
}
|
2017-05-18 21:21:33 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
return instance;
|
2017-06-08 00:15:48 +03:00
|
|
|
};
|
2017-05-18 21:21:33 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
// Just creates an instance, but doesn't move it out of PROVISIONING
|
|
|
|
// it doesn't append the isntance to the global list of instances
|
|
|
|
const createInstance = async (service, i) => {
|
|
|
|
await delay(0.5);
|
2017-08-14 13:21:45 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const _instance = {
|
|
|
|
name: camelCase(`${service.slug}_${i || 1}`),
|
|
|
|
deploymentGroupId: service.deploymentGroupId,
|
|
|
|
serviceId: service.id
|
|
|
|
};
|
2017-06-26 15:25:46 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const instance = Object.assign({}, _instance, {
|
|
|
|
id: hasha(JSON.stringify(_instance)),
|
|
|
|
status: 'PROVISIONING'
|
2017-08-28 22:21:08 +03:00
|
|
|
});
|
2017-08-02 15:57:00 +03:00
|
|
|
|
|
|
|
emit('instance-created', instance);
|
|
|
|
|
|
|
|
return instance;
|
2017-06-26 15:25:46 +03:00
|
|
|
};
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
// Takes a PROVISIONING instance and moves it to RUNNING
|
|
|
|
// it doesn't append the isntance to the global list of instances
|
|
|
|
const provisionInstance = async instance => {
|
|
|
|
await delay(3);
|
2017-05-18 21:21:33 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
await updateInstance({
|
|
|
|
id: instance.id,
|
|
|
|
status: 'READY'
|
|
|
|
});
|
2017-06-08 00:15:48 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
await delay(1);
|
|
|
|
|
|
|
|
await updateInstance({
|
|
|
|
id: instance.id,
|
|
|
|
status: 'RUNNING'
|
2017-08-07 20:11:13 +03:00
|
|
|
});
|
2017-08-02 15:57:00 +03:00
|
|
|
|
|
|
|
emit('instance-provisioned', instance.id);
|
|
|
|
|
|
|
|
return instance;
|
2017-06-08 00:15:48 +03:00
|
|
|
};
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const stopInstance = async id => {
|
|
|
|
const instance = await getInstance({ id });
|
2017-05-18 21:21:33 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
if (!instance) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (instance.status === 'STOPPED') {
|
|
|
|
return instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
await delay(1);
|
|
|
|
|
|
|
|
await updateInstance({
|
|
|
|
id,
|
|
|
|
status: 'STOPPING'
|
|
|
|
});
|
|
|
|
|
|
|
|
await delay(0.5);
|
|
|
|
|
|
|
|
await updateInstance({
|
|
|
|
id,
|
|
|
|
status: 'STOPPED'
|
|
|
|
});
|
|
|
|
|
|
|
|
emit('instance-stopped', id);
|
|
|
|
|
|
|
|
return getInstance({ id });
|
|
|
|
};
|
|
|
|
|
|
|
|
const deleteInstance = async id => {
|
|
|
|
const instance = await getInstance({ id });
|
|
|
|
|
|
|
|
if (!instance) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (instance.status === 'DELETED') {
|
|
|
|
return instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
await stopInstance(id);
|
|
|
|
|
|
|
|
await delay(1);
|
|
|
|
|
|
|
|
await updateInstance({
|
|
|
|
id,
|
|
|
|
status: 'DELETED'
|
|
|
|
});
|
|
|
|
|
|
|
|
emit('instance-deleted', id);
|
|
|
|
|
|
|
|
return instance;
|
|
|
|
};
|
|
|
|
|
|
|
|
const startInstance = async id => {
|
|
|
|
const instance = await getInstance({ id });
|
|
|
|
|
|
|
|
if (!instance) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (instance.status === 'RUNNING') {
|
|
|
|
return instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
await updateInstance({
|
|
|
|
id,
|
|
|
|
status: 'READY'
|
|
|
|
});
|
|
|
|
|
|
|
|
await delay(1);
|
|
|
|
|
|
|
|
await updateInstance({
|
|
|
|
id,
|
|
|
|
status: 'RUNNING'
|
|
|
|
});
|
|
|
|
|
|
|
|
emit('instance-started', id);
|
|
|
|
|
|
|
|
return instance;
|
|
|
|
};
|
|
|
|
|
|
|
|
const restartInstance = async id => {
|
|
|
|
const instance = await getInstance({ id });
|
|
|
|
|
|
|
|
if (!instance) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
await stopInstance(id);
|
|
|
|
await delay(1);
|
|
|
|
await startInstance(id);
|
|
|
|
|
|
|
|
emit('instance-restarted', id);
|
|
|
|
|
|
|
|
return instance;
|
|
|
|
};
|
|
|
|
|
|
|
|
const updateService = async query => {
|
|
|
|
await delay(0.5);
|
|
|
|
|
|
|
|
const serviceIndex = findIndex(services, ['id', query.id]);
|
|
|
|
const original = cleanQuery(services[serviceIndex]);
|
|
|
|
|
|
|
|
services[serviceIndex] = Object.assign(services[serviceIndex], query);
|
|
|
|
|
|
|
|
emit('service-updated', diff(original, services[serviceIndex], 'slug'));
|
|
|
|
|
|
|
|
return services[serviceIndex];
|
|
|
|
};
|
|
|
|
|
|
|
|
// Just creates a service, but doesn't move it out of PROVISIONING
|
|
|
|
// it doesn't append the service to the global list of services
|
|
|
|
const createService = async ({ deploymentGroupId, name }) => {
|
|
|
|
await delay(0.5);
|
|
|
|
|
|
|
|
const _service = {
|
|
|
|
deploymentGroupId,
|
2017-06-09 07:26:25 +03:00
|
|
|
slug: paramCase(name),
|
|
|
|
name
|
|
|
|
};
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const service = Object.assign({}, _service, {
|
|
|
|
id: hasha(JSON.stringify(_service)),
|
|
|
|
status: 'PROVISIONING'
|
2017-07-21 17:08:15 +03:00
|
|
|
});
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
emit('service-created', service);
|
2017-06-09 07:26:25 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
return service;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Takes a PROVISIONING service and moves it to ACTIVE
|
|
|
|
// it doesn't append the service to the global list of services
|
|
|
|
// it does append the created instances to the global list of instances
|
|
|
|
const provisionService = async service => {
|
|
|
|
const instance = await createInstance(service);
|
|
|
|
|
|
|
|
instances.push(instance);
|
|
|
|
|
|
|
|
await provisionInstance(instance);
|
|
|
|
|
|
|
|
await delay(0.5);
|
|
|
|
|
|
|
|
return updateService({
|
|
|
|
id: service.id,
|
|
|
|
status: 'ACTIVE'
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const deleteService = async id => {
|
|
|
|
const service = await getService({ id });
|
|
|
|
|
|
|
|
if (service.status !== 'DELETED') {
|
|
|
|
await updateService({
|
|
|
|
id,
|
|
|
|
status: 'DELETING'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const instances = await getInstances({ serviceId: id });
|
|
|
|
|
|
|
|
await forEach(instances.map(({ id }) => id), deleteInstance);
|
|
|
|
await delay(0.5);
|
|
|
|
|
|
|
|
if (service.status !== 'DELETED') {
|
|
|
|
await updateService({
|
|
|
|
id,
|
|
|
|
status: 'DELETED'
|
|
|
|
});
|
|
|
|
|
|
|
|
emit('service-deleted', id);
|
|
|
|
}
|
|
|
|
|
|
|
|
return getService({ id });
|
|
|
|
};
|
|
|
|
|
|
|
|
const deleteServices = ({ ids }) => {
|
|
|
|
wait(0.5, async () => {
|
|
|
|
await forEach(ids, deleteService);
|
|
|
|
emit('services-deleted', ids);
|
|
|
|
});
|
|
|
|
|
|
|
|
return forEach(ids, id => getService({ id }));
|
|
|
|
};
|
|
|
|
|
|
|
|
const getServiceInstances = async (query, { id }) => {
|
|
|
|
await delay(0.1);
|
|
|
|
|
|
|
|
const filter = Object.assign({}, query, {
|
|
|
|
serviceId: id
|
|
|
|
});
|
|
|
|
|
|
|
|
return (await getInstances(filter)).filter(
|
|
|
|
({ status }) => ['DELETED', 'EXITED'].indexOf(status) < 0
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const getBranchInstances = async (query, instanceIds) =>
|
2017-09-06 12:46:34 +03:00
|
|
|
map(instanceIds, id => getInstance({ id }));
|
2017-08-02 15:57:00 +03:00
|
|
|
|
|
|
|
const getServiceBranches = async (query, { branches }) => {
|
|
|
|
await delay(0.1);
|
|
|
|
|
|
|
|
return forceArray(branches)
|
|
|
|
.filter(find(cleanQuery(query)))
|
|
|
|
.map(branch =>
|
|
|
|
Object.assign({}, branch, {
|
|
|
|
id: hasha(JSON.stringify(branch)),
|
|
|
|
instances: query => getBranchInstances(query, branch.instances)
|
2017-07-26 17:28:14 +03:00
|
|
|
})
|
|
|
|
);
|
2017-08-02 15:57:00 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
const getServices = async query => {
|
|
|
|
await delay(0.1);
|
|
|
|
|
|
|
|
const _services = services.filter(find(cleanQuery(query))).map(service =>
|
|
|
|
Object.assign({}, service, {
|
|
|
|
instances: query => getServiceInstances(query, service),
|
|
|
|
branches: query => getServiceBranches(query, service)
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
if (
|
|
|
|
(query.id || query.name || query.slug) &&
|
|
|
|
(!_services || !_services.length)
|
|
|
|
) {
|
|
|
|
throw Boom.notFound();
|
|
|
|
}
|
|
|
|
|
|
|
|
return _services;
|
|
|
|
};
|
|
|
|
|
|
|
|
const getService = async query => {
|
|
|
|
const service = (await getServices(query)).shift();
|
|
|
|
|
|
|
|
if (!service) {
|
|
|
|
throw Boom.notFound();
|
|
|
|
}
|
|
|
|
|
|
|
|
return service;
|
|
|
|
};
|
2017-07-19 19:20:22 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const restartService = async id => {
|
|
|
|
await updateService({
|
|
|
|
id,
|
|
|
|
status: 'RESTARTING'
|
2017-08-08 13:07:18 +03:00
|
|
|
});
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const instances = await getInstances({ serviceId: id });
|
2017-06-09 07:26:25 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
await forEach(instances.map(({ id }) => id), restartInstance);
|
2017-08-08 13:07:18 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
await updateService({
|
|
|
|
id,
|
|
|
|
status: 'ACTIVE'
|
|
|
|
});
|
|
|
|
|
|
|
|
emit('service-restarted', id);
|
|
|
|
|
|
|
|
return getService({ id });
|
|
|
|
};
|
|
|
|
|
|
|
|
const restartServices = async ({ ids }) => {
|
|
|
|
wait(1, async () => {
|
|
|
|
await forEach(ids, restartService);
|
|
|
|
emit('services-restarted', ids);
|
|
|
|
});
|
|
|
|
|
|
|
|
return forEach(ids, id => getService({ id }));
|
|
|
|
};
|
|
|
|
|
|
|
|
const stopService = async id => {
|
|
|
|
const service = await getService({ id });
|
2017-06-09 07:26:25 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
if (service.status !== 'STOPPED') {
|
|
|
|
await updateService({
|
|
|
|
id,
|
|
|
|
status: 'STOPPING'
|
2017-07-21 17:08:15 +03:00
|
|
|
});
|
2017-08-02 15:57:00 +03:00
|
|
|
}
|
2017-07-21 17:08:15 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const instances = await getInstances({ serviceId: id });
|
|
|
|
|
|
|
|
await forEach(instances.map(({ id }) => id), stopInstance);
|
2017-06-09 07:26:25 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
if (service.status !== 'STOPPED') {
|
|
|
|
await updateService({
|
|
|
|
id,
|
|
|
|
status: 'STOPPED'
|
2017-07-21 17:08:15 +03:00
|
|
|
});
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
emit('service-stopped', id);
|
|
|
|
}
|
|
|
|
|
|
|
|
return getService({ id });
|
|
|
|
};
|
|
|
|
|
|
|
|
const stopServices = async ({ ids }) => {
|
|
|
|
wait(1, async () => {
|
|
|
|
await forEach(ids, stopService);
|
|
|
|
emit('services-stopped', ids);
|
2017-06-09 07:26:25 +03:00
|
|
|
});
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
return forEach(ids, id => getService({ id }));
|
|
|
|
};
|
|
|
|
|
|
|
|
const startService = async id => {
|
|
|
|
const instances = await getInstances({ serviceId: id });
|
|
|
|
|
|
|
|
await forEach(instances.map(({ id }) => id), startInstance);
|
|
|
|
const service = await getService({ id });
|
|
|
|
|
|
|
|
if (service.status !== 'ACTIVE') {
|
|
|
|
await updateService({
|
|
|
|
id,
|
|
|
|
status: 'ACTIVE'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return getService({ id });
|
|
|
|
};
|
|
|
|
|
|
|
|
const startServices = async ({ ids }) => {
|
|
|
|
wait(1, async () => {
|
|
|
|
await forEach(ids, startService);
|
|
|
|
emit('services-started', ids);
|
2017-08-08 13:07:18 +03:00
|
|
|
});
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
return forEach(ids, id => getService({ id }));
|
2017-06-09 07:26:25 +03:00
|
|
|
};
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const scale = async ({ serviceId, replicas }) => {
|
|
|
|
const service = lfind(services, ['id', serviceId]);
|
|
|
|
|
2017-07-20 13:57:33 +03:00
|
|
|
const currentScale = instances.filter(
|
|
|
|
find({
|
|
|
|
serviceId
|
|
|
|
})
|
|
|
|
).length;
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const dg = await getDeploymentGroup({ id: service.deploymentGroupId });
|
2017-07-20 13:57:33 +03:00
|
|
|
|
|
|
|
if (currentScale === replicas) {
|
2017-08-02 15:57:00 +03:00
|
|
|
return dg.version;
|
2017-07-20 13:57:33 +03:00
|
|
|
}
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const version = {
|
|
|
|
id: hasha(JSON.stringify({ currentScale, serviceId, replicas }))
|
2017-07-20 13:57:33 +03:00
|
|
|
};
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
await updateDeploymentGroup({
|
|
|
|
id: service.deploymentGroupId,
|
|
|
|
history: forceArray(dg.history).concat(version),
|
|
|
|
version
|
|
|
|
});
|
|
|
|
|
|
|
|
await updateService({
|
|
|
|
id: serviceId,
|
|
|
|
status: 'SCALING'
|
|
|
|
});
|
|
|
|
|
|
|
|
const up = async n =>
|
|
|
|
forEach(buildArray(n), async (_, i) => {
|
|
|
|
const instance = await createInstance(service, currentScale + i);
|
|
|
|
|
|
|
|
instances.push(instance);
|
|
|
|
|
|
|
|
await delay(3);
|
|
|
|
|
|
|
|
await updateInstance({
|
|
|
|
id: instance.id,
|
|
|
|
status: 'READY'
|
|
|
|
});
|
|
|
|
|
|
|
|
await delay(1);
|
|
|
|
|
|
|
|
await updateInstance({
|
|
|
|
id: instance.id,
|
|
|
|
status: 'RUNNING'
|
|
|
|
});
|
2017-07-20 13:57:33 +03:00
|
|
|
});
|
2017-08-02 15:57:00 +03:00
|
|
|
|
|
|
|
const down = async n => {
|
|
|
|
const instances = (await getInstances({ serviceId }))
|
|
|
|
.map(({ id }) => id)
|
|
|
|
.slice(0, n);
|
|
|
|
|
|
|
|
await forEach(instances, deleteInstance);
|
2017-07-20 13:57:33 +03:00
|
|
|
};
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
wait(1, async () => {
|
2017-07-20 13:57:33 +03:00
|
|
|
const diff = replicas - currentScale;
|
2017-08-02 15:57:00 +03:00
|
|
|
const fn = diff >= 0 ? up : down;
|
2017-07-20 13:57:33 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
await fn(Math.abs(diff));
|
|
|
|
await delay(1);
|
|
|
|
|
|
|
|
await updateService({
|
|
|
|
id: serviceId,
|
|
|
|
status: 'ACTIVE'
|
|
|
|
});
|
2017-07-20 13:57:33 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
emit('service-scaled', serviceId);
|
|
|
|
});
|
2017-07-20 13:57:33 +03:00
|
|
|
|
|
|
|
return version;
|
2017-06-16 17:12:28 +03:00
|
|
|
};
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const provisionManifest = async query => {
|
|
|
|
const { deploymentGroupId, raw } = query;
|
|
|
|
|
|
|
|
const version = {
|
|
|
|
id: hasha(JSON.stringify(query)),
|
|
|
|
type: query.type,
|
|
|
|
format: query.format
|
|
|
|
};
|
|
|
|
|
|
|
|
const dg = await getDeploymentGroup({ id: deploymentGroupId });
|
|
|
|
|
|
|
|
await updateDeploymentGroup({
|
|
|
|
id: deploymentGroupId,
|
|
|
|
status: 'PROVISIONING'
|
|
|
|
});
|
|
|
|
|
|
|
|
wait(3, async () => {
|
|
|
|
const _services = await map(Object.keys(yaml.safeLoad(raw)), async name => {
|
|
|
|
const service = await createService({ deploymentGroupId, name });
|
|
|
|
services.push(service);
|
|
|
|
return service;
|
|
|
|
});
|
|
|
|
|
|
|
|
await updateDeploymentGroup({
|
|
|
|
id: deploymentGroupId,
|
|
|
|
status: 'ACTIVE'
|
|
|
|
});
|
|
|
|
|
|
|
|
await forEach(_services, provisionService);
|
|
|
|
|
|
|
|
emit('manifest-provisioned', version.id);
|
2017-06-26 15:25:46 +03:00
|
|
|
});
|
2017-08-02 15:57:00 +03:00
|
|
|
|
|
|
|
await updateDeploymentGroup({
|
|
|
|
id: deploymentGroupId,
|
|
|
|
history: forceArray(dg.history).concat(version),
|
|
|
|
version
|
|
|
|
});
|
|
|
|
|
|
|
|
return version;
|
2017-06-26 15:25:46 +03:00
|
|
|
};
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const createDeploymentGroup = async ({ name }) => {
|
|
|
|
await delay(1);
|
|
|
|
|
|
|
|
const _dg = {
|
|
|
|
slug: paramCase(name),
|
|
|
|
name
|
|
|
|
};
|
|
|
|
|
|
|
|
const dg = Object.assign({}, _dg, {
|
|
|
|
id: hasha(JSON.stringify(_dg)),
|
|
|
|
status: 'ACTIVE'
|
2017-07-06 18:28:05 +03:00
|
|
|
});
|
2017-08-02 15:57:00 +03:00
|
|
|
|
|
|
|
deploymentGroups.push(dg);
|
|
|
|
|
|
|
|
emit('dg-created', dg);
|
|
|
|
|
|
|
|
return dg;
|
2017-06-26 15:25:46 +03:00
|
|
|
};
|
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const getDeploymentGroups = async query => {
|
|
|
|
await delay(0.1);
|
|
|
|
|
|
|
|
const addNestedResolvers = dg =>
|
|
|
|
Object.assign({}, dg, {
|
|
|
|
services: () => getServices({ deploymentGroupId: dg.id })
|
|
|
|
});
|
|
|
|
|
|
|
|
const dgs = deploymentGroups
|
|
|
|
.filter(find(cleanQuery(query)))
|
|
|
|
.map(addNestedResolvers);
|
|
|
|
|
|
|
|
if ((query.ids || query.name || query.slug) && (!dgs || !dgs.length)) {
|
|
|
|
throw Boom.notFound();
|
|
|
|
}
|
|
|
|
|
|
|
|
return dgs;
|
2017-07-14 14:15:13 +03:00
|
|
|
};
|
2017-06-26 15:25:46 +03:00
|
|
|
|
2017-08-02 15:57:00 +03:00
|
|
|
const getDeploymentGroup = async query =>
|
|
|
|
(await getDeploymentGroups(query)).shift();
|
|
|
|
|
|
|
|
const updateDeploymentGroup = async query => {
|
|
|
|
await delay(0.5);
|
|
|
|
|
|
|
|
const dgIndex = findIndex(deploymentGroups, ['id', query.id]);
|
|
|
|
const original = cleanQuery(deploymentGroups[dgIndex]);
|
|
|
|
|
|
|
|
deploymentGroups[dgIndex] = Object.assign(deploymentGroups[dgIndex], query);
|
|
|
|
|
|
|
|
emit('dg-updated', diff(original, deploymentGroups[dgIndex], 'slug'));
|
|
|
|
|
|
|
|
return deploymentGroups[dgIndex];
|
|
|
|
};
|
|
|
|
|
|
|
|
const deleteDeploymentGroup = async ({ id }) => {
|
|
|
|
const dg = await getDeploymentGroup({ id });
|
|
|
|
|
|
|
|
await updateDeploymentGroup({
|
|
|
|
id,
|
|
|
|
status: 'DELETING'
|
2017-08-21 15:39:02 +03:00
|
|
|
});
|
2017-08-02 15:57:00 +03:00
|
|
|
|
|
|
|
wait(1, async () => {
|
|
|
|
const services = await dg.services();
|
|
|
|
|
|
|
|
await forEach(services.map(({ id }) => id), deleteService);
|
|
|
|
|
|
|
|
await updateDeploymentGroup({
|
|
|
|
id,
|
|
|
|
status: 'DELETED'
|
|
|
|
});
|
|
|
|
|
|
|
|
emit('dg-deleted', id);
|
2017-08-21 15:39:02 +03:00
|
|
|
});
|
2017-08-02 15:57:00 +03:00
|
|
|
|
|
|
|
return getDeploymentGroup({ id });
|
|
|
|
};
|
|
|
|
|
|
|
|
const getPortal = async () => {
|
|
|
|
await delay(0.1);
|
|
|
|
|
|
|
|
return Object.assign({}, portal, {
|
|
|
|
datacenter,
|
|
|
|
deploymentGroups: getDeploymentGroups
|
2017-08-21 15:39:02 +03:00
|
|
|
});
|
2017-07-06 18:28:05 +03:00
|
|
|
};
|
|
|
|
|
2017-08-08 13:07:18 +03:00
|
|
|
const parseEnvVars = (str = '') =>
|
|
|
|
str
|
|
|
|
.split(/\n/g)
|
|
|
|
.filter(line => line.match(/\=/g))
|
|
|
|
.map(line => line.split(/\=/))
|
|
|
|
.reduce(
|
|
|
|
(acc, [name, value]) =>
|
|
|
|
Object.assign(acc, {
|
|
|
|
[name.trim()]: value.trim()
|
|
|
|
}),
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
|
|
|
|
const findEnvInterpolation = (str = '') =>
|
|
|
|
uniq(str.match(INTERPOLATE_REGEX).map(name => name.replace(/^\$/, '')));
|
|
|
|
|
2017-08-28 22:21:08 +03:00
|
|
|
const config = ({ environment = '', files = [], raw = '', _plain = false }) => {
|
2017-08-08 13:07:18 +03:00
|
|
|
const interpolatableNames = findEnvInterpolation(raw);
|
|
|
|
const interpolatableEnv = parseEnvVars(environment);
|
|
|
|
|
|
|
|
const interpolatedRaw = interpolatableNames.reduce(
|
|
|
|
(str = '', name) =>
|
|
|
|
str.replace(new RegExp(`\\$${name}`), interpolatableEnv[name]),
|
|
|
|
raw
|
|
|
|
);
|
|
|
|
|
|
|
|
const manifest = yaml.safeLoad(interpolatedRaw);
|
|
|
|
const services = manifest.services || manifest;
|
|
|
|
|
|
|
|
const config = Object.keys(services)
|
|
|
|
.map(name =>
|
|
|
|
Object.assign(services[name], {
|
|
|
|
name
|
|
|
|
})
|
|
|
|
)
|
|
|
|
// eslint-disable-next-line camelcase
|
|
|
|
.map(({ name, image, env_file, environment }) => ({
|
|
|
|
name,
|
|
|
|
slug: paramCase(name),
|
|
|
|
instances: [],
|
|
|
|
config: {
|
|
|
|
image,
|
|
|
|
environment: forceArray(env_file).reduce(
|
|
|
|
(env, file) =>
|
|
|
|
Object.assign(
|
|
|
|
env,
|
|
|
|
parseEnvVars(lfind(files, ['name', file]).value)
|
|
|
|
),
|
|
|
|
forceArray(environment)
|
|
|
|
.map(parseEnvVars)
|
|
|
|
.reduce(
|
|
|
|
(genv, variable) => Object.assign(genv, variable),
|
|
|
|
interpolatableEnv
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
.map(service =>
|
|
|
|
Object.assign(service, {
|
|
|
|
id: hasha(JSON.stringify(service)),
|
|
|
|
config: Object.assign(service.config, {
|
|
|
|
id: hasha(JSON.stringify(service.config)),
|
|
|
|
environment: Object.keys(service.config.environment).map(name => ({
|
2017-08-28 22:21:08 +03:00
|
|
|
name,
|
|
|
|
id: hasha(JSON.stringify(service.config.environment[name])),
|
|
|
|
value: service.config.environment[name]
|
|
|
|
}))
|
2017-08-08 13:07:18 +03:00
|
|
|
})
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
return _plain ? config : Promise.resolve(config);
|
|
|
|
};
|
|
|
|
|
2017-05-18 21:21:33 +03:00
|
|
|
module.exports = {
|
2017-06-08 00:15:48 +03:00
|
|
|
portal: getPortal,
|
|
|
|
deploymentGroups: getDeploymentGroups,
|
2017-08-02 15:57:00 +03:00
|
|
|
deploymentGroup: getDeploymentGroup,
|
2017-06-08 00:15:48 +03:00
|
|
|
services: getServices,
|
2017-08-02 15:57:00 +03:00
|
|
|
service: getService,
|
2017-06-08 00:15:48 +03:00
|
|
|
instances: getInstances,
|
2017-08-02 15:57:00 +03:00
|
|
|
instance: getInstance,
|
2017-06-09 07:26:25 +03:00
|
|
|
createDeploymentGroup,
|
2017-08-02 15:57:00 +03:00
|
|
|
config,
|
|
|
|
provisionManifest,
|
|
|
|
deleteDeploymentGroup,
|
|
|
|
deleteServices,
|
|
|
|
scale,
|
|
|
|
restartServices,
|
|
|
|
stopServices,
|
|
|
|
startServices,
|
|
|
|
getMetrics
|
2017-05-18 21:21:33 +03:00
|
|
|
};
|