containership.core
Version:
The core functionality of Containership
502 lines (423 loc) • 21.2 kB
JavaScript
;
const crypto = require('./crypto');
const _ = require('lodash');
const constants = require('containership.core.constants');
const async = require('async');
class Applications {
constructor(core) {
this.list = {};
this.core = core;
}
// restores applications from myriad snapshot
bootstrap(bootstrap_callback) {
// retrieve applications from myriad and deserialize
this.core.cluster.myriad.persistence.keys(constants.myriad.APPLICATIONS, (err, applications) => {
if(_.isNull(err) && !_.isEmpty(applications)) {
this.core.loggers['containership.core'].log('info', 'Applications restored from disk snaphot');
}
async.series([
// unload all containers
(callback) => {
async.each(applications, (application_name, callback) => {
this.unload_containers(_.last(application_name.split(constants.myriad.DELIMITER)), callback);
}, callback);
},
// reconcile running containers
(callback) => {
this.core.cluster.legiond.send({
event: constants.events.RECONCILE
}, callback);
}
], bootstrap_callback);
});
}
add(config, add_callback) {
const self = this;
this.core.cluster.myriad.persistence.keys(constants.myriad.APPLICATIONS, (err, applications) => {
// add new app
if(!_.includes(applications, [constants.myriad.APPLICATION_PREFIX, config.id].join(constants.myriad.DELIMITER))) {
_.defaults(config, {
id: null,
tags: {},
env_vars: {},
cpus: 0.1,
memory: 128,
command: '',
image: 'containership/engine',
engine: 'docker',
network_mode: 'bridge',
respawn: true,
volumes: [],
privileged: false,
health_checks: []
});
self.core.scheduler.leader.application.get_loadbalancer_port(config.port, (err, discovery_port) => {
if(err) {
return add_callback(err);
} else {
config.discovery_port = discovery_port;
}
const containers = config.containers || [];
delete config.containers;
self.core.cluster.myriad.persistence.set([constants.myriad.APPLICATION_PREFIX, config.id].join(constants.myriad.DELIMITER), JSON.stringify(config), (err) => {
if(err) {
return add_callback(err);
}
const deployed_containers = [];
async.each(containers, (container, callback) => {
if(_.has(container.tags, 'constraints') && _.has(container.tags.constraints, 'per_host') && _.has(container.tags, 'host')) {
delete container.tags.host;
}
if(container.random_host_port) {
container.host_port = null;
}
container.status = 'unloaded';
container.host = null;
container.start_time = null;
self.deploy_container(config.id, container, () => {
deployed_containers.push(container);
return callback();
});
}, function (err) {
if (err) {
return add_callback(err);
}
config.containers = deployed_containers;
return self.core.scheduler.leader.application.start_health_check(config.id, (err) => {
// if health check is unable to be started remove application
if(err) {
return self.remove(config.id, () => {
return add_callback(err);
});
}
return add_callback(null, config);
});
});
});
});
} else {
// update existing app
self.core.cluster.myriad.persistence.get([constants.myriad.APPLICATION_PREFIX, config.id].join(constants.myriad.DELIMITER), (err, application) => {
if(err) {
return add_callback(err);
}
try {
application = JSON.parse(application);
} catch(err) {
return add_callback(err);
}
_.defaults(config, application);
self.core.cluster.myriad.persistence.set([constants.myriad.APPLICATION_PREFIX, config.id].join(constants.myriad.DELIMITER), JSON.stringify(config), (err) => {
if(err) {
return add_callback(err);
}
const application_name = config.id;
delete config.id;
const updated_containers = [];
self.get_containers(application_name, (err, containers) => {
async.each(containers, (container, callback) => {
_.merge(container, config);
self.core.cluster.myriad.persistence.set([constants.myriad.CONTAINERS_PREFIX, application_name, container.id].join(constants.myriad.DELIMITER), JSON.stringify(container), (/*err*/) => {
updated_containers.push(container);
return callback();
});
}, function(err) {
if (err) {
return add_callback(err);
}
config.containers = updated_containers;
config.id = application_name;
return add_callback(null, config);
});
});
});
});
}
});
}
remove(application_name, remove_callback) {
const self = this;
async.series([
(callback) => {
self.core.cluster.myriad.persistence.delete([constants.myriad.APPLICATION_PREFIX, application_name].join(constants.myriad.DELIMITER), callback);
},
(callback) => {
self.core.cluster.myriad.persistence.keys([constants.myriad.CONTAINERS_PREFIX, application_name, '*'].join(constants.myriad.DELIMITER), (err, containers) => {
async.each(containers, (container_id, callback) => {
self.remove_container(application_name, _.last(container_id.split(self.core.constants.myriad.DELIMITER)), callback);
}, callback);
});
},
(callback) => {
self.core.scheduler.leader.application.stop_health_check(application_name);
return callback();
}
], remove_callback);
}
get_containers(application_name, get_containers_callback) {
const self = this;
this.core.cluster.myriad.persistence.keys([constants.myriad.CONTAINERS_PREFIX, application_name, '*'].join(constants.myriad.DELIMITER), (err, containers) => {
async.map(containers, (container_id, callback) => {
self.core.cluster.myriad.persistence.get(container_id, (err, container) => {
if(err) {
return callback(err);
}
try {
container = JSON.parse(container);
const EMPTY_CONTAINER_ID = [constants.myriad.CONTAINERS_PREFIX, application_name, ''].join(constants.myriad.DELIMITER);
const isEmptyContainerId = EMPTY_CONTAINER_ID === container_id;
// If we have arrived at a state where the myriad-kv key contains an ID for the container, but the
// actual container data does not contain an ID, set the container data id to the value of the myriad kv id
if (!isEmptyContainerId && !container.id) {
container.id = container_id.substring(EMPTY_CONTAINER_ID.length);
}
return callback(null, container);
} catch(err) {
return callback(err);
}
});
}, get_containers_callback);
});
}
get_container(application_name, container_id, get_container_callback) {
this.core.cluster.myriad.persistence.get([constants.myriad.CONTAINERS_PREFIX, application_name, container_id].join(constants.myriad.DELIMITER), (err, container) => {
if(err) {
return get_container_callback(err);
}
try {
container = JSON.parse(container);
return get_container_callback(null, container);
} catch(err) {
return get_container_callback(err);
}
});
}
deploy_container(application_name, container, deploy_container_callback) {
const self = this;
async.waterfall([
(callback) => {
self.core.cluster.myriad.persistence.get([constants.myriad.APPLICATION_PREFIX, application_name].join(constants.myriad.DELIMITER), (err, application) => {
if(err) {
return callback(err);
}
try {
application = JSON.parse(application);
if(!_.isEmpty(application.health_checks)) {
const clear_health_check = {
consecutive_failures: 0,
consecutive_successes: 0,
is_healthy: false
};
const enforce_health_check_exists = {
tags: {
metadata : {
health_checks : {}
}
}
};
_.merge(container, enforce_health_check_exists);
// clear container health checks results from metadata
// in case it has data from old application health check
container.tags.metadata.health_checks = {};
_.each(application.health_checks, (health_check) => {
container.tags.metadata.health_checks[health_check.id] = clear_health_check;
});
} else if(container && container.tags && container.tags.metadata && container.tags.metadata.health_checks) {
delete container.tags.metadata.health_checks;
}
return callback(null, application);
} catch(err) {
return callback(err);
}
});
},
(configuration, callback) => {
delete container.health_checks;
configuration = _.omit(configuration, 'id');
const config = _.defaults(container, configuration);
const tags = _.merge(container.tags, configuration.tags);
config.tags = tags;
self.core.scheduler.leader.container.deploy(application_name, config, callback);
}
], (err, resource) => {
if(!_.has(container, 'id')) {
container.id = crypto.generate_uuid();
}
if(err) {
return deploy_container_callback(err);
}
if(_.isUndefined(resource)) {
container.status = 'unloaded';
self.core.cluster.myriad.persistence.set([constants.myriad.CONTAINERS_PREFIX, application_name, container.id].join(constants.myriad.DELIMITER), JSON.stringify(container), (/*err*/) => {
return deploy_container_callback(null, container);
});
} else {
const random_host_port = resource.host_port != container.host_port;
container.host = resource.host.id;
container.host_port = resource.host_port;
container.random_host_port = random_host_port;
container.status = 'loading';
container.start_time = new Date().valueOf();
self.core.loggers['containership.core'].log('verbose', `Deploying ${application_name} container ${container.id} to ${resource.host.host_name}`);
async.series([
(callback) => {
self.core.cluster.myriad.persistence.set([constants.myriad.CONTAINERS_PREFIX, application_name, container.id].join(constants.myriad.DELIMITER), JSON.stringify(container), callback);
},
(callback) => {
self.core.cluster.legiond.send({
event: constants.events.LOAD_CONTAINER,
data: {
application: application_name,
container: container
}
}, resource.host);
return callback();
}
], function(err) {
return deploy_container_callback(err, container);
});
}
});
}
redeploy_containers(application_name, redeploy_containers_callback) {
const self = this;
this.get_containers(application_name, (err, containers) => {
if(err) {
return redeploy_containers_callback(err);
}
const redeployed_containers = [];
async.each(containers, (container, callback) => {
async.series([
(callback) => {
self.deploy_container(application_name, _.omit(container, ['id', 'host', 'start_time']), (err, container) => {
redeployed_containers.push(container);
return callback();
});
},
(callback) => {
self.remove_container(application_name, container.id, () => {
return callback();
});
}
], callback);
}, function(err) {
if (err) {
return redeploy_containers_callback(err);
}
return redeploy_containers_callback(null, redeployed_containers);
});
});
}
redeploy_container(application_name, container_id, redeploy_container_callback) {
const self = this;
this.get_container(application_name, container_id, (err, container) => {
if(err) {
return redeploy_container_callback(err);
}
let redeployed_container;
async.series([
(callback) => {
self.deploy_container(application_name, _.omit(container, ['id', 'host', 'start_time']), (err, container) => {
redeployed_container = container;
return callback();
});
},
(callback) => {
self.remove_container(application_name, container.id, () => {
return callback();
});
}
], function(err) {
if (err) {
return redeploy_container_callback(err);
}
return redeploy_container_callback(null, redeployed_container);
});
});
}
remove_containers(application_name, num_containers, remove_containers_callback) {
const self = this;
const errors = [];
this.core.scheduler.leader.application.remove_containers(application_name, num_containers, (err, container_ids) => {
if(err) {
return remove_containers_callback(err);
}
async.each(container_ids, (container_id, callback) => {
self.remove_container(application_name, container_id, (err) => {
if(err) {
errors.push(container_id);
}
return callback();
});
}, () => {
if(errors.length) {
return remove_containers_callback(new Error(`Failed to remove containers: ${errors.join(' ')}`));
}
return remove_containers_callback();
});
});
}
remove_container(application_name, container_id, remove_container_callback) {
const self = this;
this.core.cluster.myriad.persistence.get([constants.myriad.CONTAINERS_PREFIX, application_name, container_id].join(constants.myriad.DELIMITER), (err, container) => {
if(err) {
return remove_container_callback(err);
}
try {
container = JSON.parse(container);
} catch(err) {
return remove_container_callback(err);
}
if(container.status != 'loaded') {
self.core.cluster.myriad.persistence.delete([constants.myriad.CONTAINERS_PREFIX, application_name, container_id].join(constants.myriad.DELIMITER), (err) => {
return remove_container_callback(err);
});
} else {
const hosts = _.keyBy(self.core.cluster.legiond.get_peers(), 'id');
const host = hosts[container.host];
self.core.cluster.legiond.send({
event: constants.events.UNLOAD_CONTAINER,
data: {
application: application_name,
container_id: container_id,
engine: container.engine
}
}, host);
return remove_container_callback();
}
});
}
unload_containers(application_name, containers, unload_containers_callback) {
const self = this;
this.core.cluster.myriad.persistence.keys([constants.myriad.CONTAINERS_PREFIX, application_name, '*'].join(constants.myriad.DELIMITER), (err, current_containers) => {
if(_.isString(containers)) {
containers = [containers];
} else if(_.isFunction(containers)) {
unload_containers_callback = containers;
containers = _.map(current_containers, (container) => {
return _.last(container.split(constants.myriad.DELIMITER));
});
}
async.each(containers, (container_id, callback) => {
self.core.cluster.myriad.persistence.get([constants.myriad.CONTAINERS_PREFIX, application_name, container_id].join(constants.myriad.DELIMITER), (err, configuration) => {
if(err) {
return callback(err);
}
try {
configuration = JSON.parse(configuration);
} catch(err) {
return callback(err);
}
if(configuration.random_host_port) {
configuration.host_port = null;
}
configuration.status = 'unloaded';
configuration.host = null;
configuration.start_time = null;
self.core.cluster.myriad.persistence.set([constants.myriad.CONTAINERS_PREFIX, application_name, container_id].join(constants.myriad.DELIMITER), JSON.stringify(configuration), callback);
});
}, unload_containers_callback);
});
}
}
module.exports = Applications;