mirror of
https://github.com/yldio/copilot.git
synced 2025-01-07 09:30:13 +02:00
560 lines
14 KiB
JavaScript
560 lines
14 KiB
JavaScript
const { v4: uuid } = require('uuid');
|
|
const paramCase = require('param-case');
|
|
const camelCase = require('camel-case');
|
|
const buildArray = require('build-array');
|
|
const forceArray = require('force-array');
|
|
const lfind = require('lodash.find');
|
|
const findIndex = require('lodash.findindex');
|
|
const flatten = require('lodash.flatten');
|
|
const random = require('lodash.random');
|
|
const uniq = require('lodash.uniq');
|
|
const yaml = require('js-yaml');
|
|
const hasha = require('hasha');
|
|
const Boom = require('boom');
|
|
|
|
const wpData = require('./wp-data.json');
|
|
const cpData = require('./cp-data.json');
|
|
const complexData = require('./complex-data.json');
|
|
const metricData = require('./metric-data.json');
|
|
|
|
const { datacenter, portal } = require('./data.json');
|
|
|
|
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);
|
|
|
|
const INTERPOLATE_REGEX = /\$([_a-z][_a-z0-9]*)/gi;
|
|
|
|
const find = (query = {}) => item =>
|
|
Object.keys(query).every(key => item[key] === query[key]);
|
|
|
|
const cleanQuery = (q = {}) => JSON.parse(JSON.stringify(q));
|
|
|
|
const getInstances = query => {
|
|
return Promise.resolve(instances.filter(find(cleanQuery(query))));
|
|
};
|
|
|
|
const getUnfilteredServices = query => {
|
|
const instancesResolver = ({ id }) => query =>
|
|
getInstances(
|
|
Object.assign({}, query, {
|
|
serviceId: id
|
|
})
|
|
);
|
|
|
|
const addNestedResolvers = service =>
|
|
Object.assign({}, service, {
|
|
instances: instancesResolver(service),
|
|
branches: (service.branches || []).map(service =>
|
|
Object.assign({}, service, {
|
|
id: hasha(JSON.stringify(service)),
|
|
instances: query =>
|
|
Promise.resolve(
|
|
flatten(
|
|
service.instances.map(id =>
|
|
instances.filter(find(Object.assign({}, query, { id })))
|
|
)
|
|
)
|
|
)
|
|
})
|
|
)
|
|
});
|
|
|
|
return Promise.resolve(
|
|
services.filter(find(cleanQuery(query))).map(addNestedResolvers)
|
|
);
|
|
};
|
|
|
|
const getServices = query => {
|
|
// get all services
|
|
|
|
const services = getUnfilteredServices(query)
|
|
// get all instances
|
|
.then(services =>
|
|
Promise.all(
|
|
services.map(service => service.instances())
|
|
).then(instances => ({
|
|
services,
|
|
instances
|
|
}))
|
|
)
|
|
.then(({ services, instances }) => {
|
|
// filter all available instances
|
|
const availableInstances = flatten(
|
|
instances.filter(
|
|
({ status }) => ['DELETED', 'EXITED'].indexOf(status) < 0
|
|
)
|
|
);
|
|
// get all the serviceIds of the available instances
|
|
// and then get the servcies with those ids
|
|
const ret = uniq(
|
|
availableInstances.map(({ serviceId }) => serviceId)
|
|
).map(serviceId => lfind(services, ['id', serviceId]));
|
|
return ret;
|
|
});
|
|
|
|
return Promise.resolve(services)
|
|
.then((services) => {
|
|
if(!services || !services.length) {
|
|
throw Boom.notFound();
|
|
}
|
|
return services;
|
|
});
|
|
};
|
|
|
|
const getDeploymentGroups = query => {
|
|
const servicesResolver = ({ id }) => query =>
|
|
getServices(
|
|
Object.assign({}, query, {
|
|
deploymentGroupId: id
|
|
})
|
|
);
|
|
|
|
const addNestedResolvers = dg =>
|
|
Object.assign({}, dg, {
|
|
services: servicesResolver(dg)
|
|
});
|
|
|
|
return Promise.resolve(
|
|
deploymentGroups.filter(find(cleanQuery(query))).map(addNestedResolvers)
|
|
).then((deploymentGroups) => {
|
|
if(!deploymentGroups || !deploymentGroups.length) {
|
|
throw Boom.notFound();
|
|
}
|
|
return deploymentGroups;
|
|
});
|
|
};
|
|
|
|
const getPortal = () =>
|
|
Promise.resolve(
|
|
Object.assign({}, portal, {
|
|
datacenter,
|
|
deploymentGroups: getDeploymentGroups
|
|
})
|
|
);
|
|
|
|
const createDeploymentGroup = ({ name }) => {
|
|
const _dg = {
|
|
slug: paramCase(name),
|
|
name
|
|
};
|
|
|
|
const dg = Object.assign({}, _dg, {
|
|
id: hasha(JSON.stringify(_dg))
|
|
});
|
|
|
|
deploymentGroups.push(dg);
|
|
|
|
return Promise.resolve(dg);
|
|
};
|
|
|
|
const deleteDeploymentGroup = options => {
|
|
const dgId = options.id;
|
|
const deleteDeploymentGroup = getServices({ deploymentGroupId: dgId })
|
|
.then(services =>
|
|
Promise.all(
|
|
services.map(service =>
|
|
handleStatusUpdateRequest({
|
|
serviceId: service.id,
|
|
transitionalServiceStatus: 'DELETING',
|
|
transitionalInstancesStatus: 'STOPPING',
|
|
serviceStatus: 'DELETED',
|
|
instancesStatus: 'DELETED'
|
|
})
|
|
)
|
|
)
|
|
)
|
|
.then(() =>
|
|
Object.assign({}, deploymentGroups.filter(dg => dg.id === dgId).shift(), {
|
|
status: 'DELETING'
|
|
})
|
|
);
|
|
|
|
setTimeout(() => {
|
|
const deploymentGroup = deploymentGroups
|
|
.filter(dg => dg.id === dgId)
|
|
.shift();
|
|
|
|
deploymentGroup.status = 'DELETED';
|
|
}, 5000);
|
|
|
|
return Promise.resolve(deleteDeploymentGroup);
|
|
};
|
|
|
|
const createServicesFromManifest = ({
|
|
deploymentGroupId = '',
|
|
environment = '',
|
|
files = [],
|
|
type,
|
|
format,
|
|
raw = ''
|
|
}) => {
|
|
const _config = config({
|
|
environment,
|
|
files,
|
|
raw,
|
|
_plain: true
|
|
});
|
|
|
|
const manifest = yaml.safeLoad(raw);
|
|
|
|
const version = {
|
|
id: uuid(),
|
|
manifest: {
|
|
id: uuid(),
|
|
type,
|
|
format,
|
|
environment,
|
|
files,
|
|
raw
|
|
}
|
|
};
|
|
|
|
Object.keys(manifest).forEach(name => {
|
|
const _service = {
|
|
deploymentGroupId,
|
|
slug: paramCase(name),
|
|
name,
|
|
config: lfind(_config, ['name', name]).config
|
|
};
|
|
|
|
const service = Object.assign({}, _service, {
|
|
id: hasha(JSON.stringify(_service))
|
|
});
|
|
|
|
const _instance = {
|
|
name: camelCase(`${service.slug}_01`),
|
|
serviceId: service.id,
|
|
deploymentGroupId
|
|
};
|
|
|
|
const instance = Object.assign({}, _instance, {
|
|
id: hasha(JSON.stringify(_instance))
|
|
});
|
|
|
|
services.push(service);
|
|
instances.push(instance);
|
|
});
|
|
|
|
const dgIndex = findIndex(deploymentGroups, ['id', deploymentGroupId]);
|
|
deploymentGroups[dgIndex] = Object.assign(deploymentGroups[dgIndex], {
|
|
version,
|
|
history: forceArray(deploymentGroups[dgIndex].history).concat([version])
|
|
});
|
|
|
|
return Promise.resolve(undefined);
|
|
};
|
|
|
|
const scale = ({ serviceId, replicas }) => {
|
|
const serviceIndex = findIndex(services, ['id', serviceId]);
|
|
const currentScale = instances.filter(
|
|
find({
|
|
serviceId
|
|
})
|
|
).length;
|
|
|
|
const version = {
|
|
id: uuid()
|
|
};
|
|
|
|
if (currentScale === replicas) {
|
|
return version;
|
|
}
|
|
|
|
services[serviceIndex].status = 'SCALING';
|
|
|
|
const up = n => {
|
|
buildArray(n).forEach((_, i) => {
|
|
const instance = {
|
|
name: `${services[serviceIndex].slug}_${currentScale + i}`,
|
|
serviceId,
|
|
deploymentGroupId: services[serviceIndex].deploymentGroupId,
|
|
status: 'RUNNING',
|
|
healthy: 'UNKNOWN'
|
|
};
|
|
|
|
instances.push(
|
|
Object.assign({}, instance, {
|
|
id: hasha(JSON.stringify(instance))
|
|
})
|
|
);
|
|
});
|
|
};
|
|
|
|
const down = n => {
|
|
buildArray(n).forEach((_, i) => {
|
|
instances.splice(findIndex(instances, ['serviceId', serviceId]), 1);
|
|
});
|
|
};
|
|
|
|
setTimeout(() => {
|
|
const diff = replicas - currentScale;
|
|
|
|
if (diff >= 0) {
|
|
up(diff);
|
|
} else {
|
|
down(Math.abs(diff));
|
|
}
|
|
|
|
services[serviceIndex].status = 'ACTIVE';
|
|
}, random(1500, 3000));
|
|
|
|
return version;
|
|
};
|
|
|
|
const updateInstancesStatus = (is, status) => {
|
|
is.forEach(i => {
|
|
const instance = instances.filter(instance => instance.id === i.id)[0];
|
|
instance.status = status;
|
|
});
|
|
return null;
|
|
};
|
|
|
|
const updateServiceStatus = (ss, status) => {
|
|
ss.forEach(s => {
|
|
const service = services.filter(service => service.id === s.id)[0];
|
|
service.status = status;
|
|
});
|
|
return null;
|
|
};
|
|
|
|
const updateServiceAndInstancesStatus = (
|
|
serviceId,
|
|
serviceStatus,
|
|
instancesStatus
|
|
) => {
|
|
return Promise.all([
|
|
getServices({ id: serviceId })/* ,
|
|
getServices({ parentId: serviceId }) */
|
|
])
|
|
.then(services => {
|
|
return services.reduce((services, service) => services.concat(service), [])
|
|
})
|
|
.then(services => {
|
|
updateServiceStatus(services, serviceStatus);
|
|
return Promise.all(
|
|
services.reduce(
|
|
(instances, service) =>
|
|
service.instances
|
|
? instances.concat(service.instances())
|
|
: instances,
|
|
[]
|
|
)
|
|
).then(instances =>
|
|
updateInstancesStatus(
|
|
instances.reduce((is, i) => is.concat(i), []),
|
|
instancesStatus
|
|
)
|
|
);
|
|
})
|
|
.then(() =>
|
|
Promise.all([
|
|
getUnfilteredServices({ id: serviceId })/* ,
|
|
getUnfilteredServices({ parentId: serviceId }) */
|
|
])
|
|
)
|
|
.then(services =>
|
|
services.reduce((services, service) => services.concat(service), [])
|
|
);
|
|
};
|
|
|
|
const handleStatusUpdateRequest = ({
|
|
serviceId,
|
|
transitionalServiceStatus,
|
|
transitionalInstancesStatus,
|
|
serviceStatus,
|
|
instancesStatus
|
|
}) => {
|
|
// this is what we need to delay
|
|
setTimeout(() => {
|
|
updateServiceAndInstancesStatus(serviceId, serviceStatus, instancesStatus);
|
|
}, 5000);
|
|
|
|
// this is what we'll need to return
|
|
return updateServiceAndInstancesStatus(
|
|
serviceId,
|
|
transitionalServiceStatus,
|
|
transitionalInstancesStatus
|
|
);
|
|
};
|
|
|
|
const deleteServices = options => {
|
|
// service transitional 'DELETING'
|
|
// instances transitional 'STOPPING'
|
|
// service 'DELETED'
|
|
// instances 'DELETED'
|
|
const deleteService = handleStatusUpdateRequest({
|
|
serviceId: options.ids[0],
|
|
transitionalServiceStatus: 'DELETING',
|
|
transitionalInstancesStatus: 'STOPPING',
|
|
serviceStatus: 'DELETED',
|
|
instancesStatus: 'DELETED'
|
|
});
|
|
return Promise.resolve(deleteService);
|
|
};
|
|
|
|
const stopServices = options => {
|
|
// service transitional 'STOPPING'
|
|
// instances transitional 'STOPPING'
|
|
// service 'STOPPED'
|
|
// instances 'STOPPED'
|
|
const stopService = handleStatusUpdateRequest({
|
|
serviceId: options.ids[0],
|
|
transitionalServiceStatus: 'STOPPING',
|
|
transitionalInstancesStatus: 'STOPPING',
|
|
serviceStatus: 'STOPPED',
|
|
instancesStatus: 'STOPPED'
|
|
});
|
|
return Promise.resolve(stopService);
|
|
};
|
|
|
|
const startServices = options => {
|
|
// service transitional ...
|
|
// instances transitional ...
|
|
// service 'ACTIVE'
|
|
// instances 'RUNNING'
|
|
const startService = handleStatusUpdateRequest({
|
|
serviceId: options.ids[0],
|
|
transitionalServiceStatus: 'PROVISIONING',
|
|
transitionalInstancesStatus: 'PROVISIONING',
|
|
serviceStatus: 'ACTIVE',
|
|
instancesStatus: 'RUNNING'
|
|
});
|
|
return Promise.resolve(startService);
|
|
};
|
|
|
|
const restartServices = options => {
|
|
// service transitional 'RESTARTING'
|
|
// instances transitional 'STOPPING'
|
|
// service 'ACTIVE'
|
|
// instances 'RUNNING'
|
|
const restartService = handleStatusUpdateRequest({
|
|
serviceId: options.ids[0],
|
|
transitionalServiceStatus: 'RESTARTING',
|
|
transitionalInstancesStatus: 'STOPPING',
|
|
serviceStatus: 'ACTIVE',
|
|
instancesStatus: 'RUNNING'
|
|
});
|
|
return Promise.resolve(restartService);
|
|
};
|
|
|
|
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(/^\$/, '')));
|
|
|
|
const config = ({
|
|
environment = '',
|
|
files = [],
|
|
raw = '',
|
|
_plain = false
|
|
}) => {
|
|
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 => ({
|
|
name,
|
|
id: hasha(JSON.stringify(service.config.environment[name])),
|
|
value: service.config.environment[name]
|
|
})
|
|
)
|
|
})
|
|
})
|
|
);
|
|
|
|
return _plain ? config : Promise.resolve(config);
|
|
};
|
|
|
|
const getMetrics = () => {
|
|
return Promise.resolve(metricData);
|
|
};
|
|
|
|
module.exports = {
|
|
portal: getPortal,
|
|
deploymentGroups: getDeploymentGroups,
|
|
deploymentGroup: query => getDeploymentGroups(query).then(dgs => dgs.shift()),
|
|
services: getServices,
|
|
service: query => getServices(query).then(services => services.shift()),
|
|
instances: getInstances,
|
|
instance: query => getInstances(query).then(instances => instances.shift()),
|
|
createDeploymentGroup,
|
|
provisionManifest: options =>
|
|
createServicesFromManifest(options).then(() => ({
|
|
id: hasha(JSON.stringify(options)),
|
|
type: options.type,
|
|
format: options.format
|
|
})),
|
|
deleteDeploymentGroup: (options, request, fn) =>
|
|
fn(null, deleteDeploymentGroup(options)),
|
|
deleteServices: (options, request, fn) => fn(null, deleteServices(options)),
|
|
scale: (options, reguest, fn) => fn(null, scale(options)),
|
|
restartServices: (options, request, fn) => fn(null, restartServices(options)),
|
|
stopServices: (options, request, fn) => fn(null, stopServices(options)),
|
|
startServices: (options, request, fn) => fn(null, startServices(options)),
|
|
config,
|
|
metrics: getMetrics
|
|
};
|