2017-06-15 18:22:15 +03:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
// const Assert = require('assert');
|
2017-06-28 18:04:34 +03:00
|
|
|
const Throat = require('throat');
|
2017-06-15 18:22:15 +03:00
|
|
|
const TritonWatch = require('triton-watch');
|
2017-06-22 20:09:13 +03:00
|
|
|
const util = require('util');
|
2017-06-15 18:22:15 +03:00
|
|
|
|
|
|
|
|
|
|
|
const DEPLOYMENT_GROUP = 'docker:label:com.docker.compose.project';
|
|
|
|
const SERVICE = 'docker:label:com.docker.compose.service';
|
|
|
|
const HASH = 'docker:label:com.docker.compose.config-hash';
|
|
|
|
|
|
|
|
module.exports = class Watcher {
|
|
|
|
constructor (options) {
|
|
|
|
options = options || {};
|
|
|
|
|
|
|
|
// todo assert options
|
|
|
|
this._data = options.data;
|
2017-06-27 20:14:26 +03:00
|
|
|
this._frequency = 500;
|
2017-06-15 18:22:15 +03:00
|
|
|
|
|
|
|
this._tritonWatch = new TritonWatch({
|
2017-06-27 20:14:26 +03:00
|
|
|
frequency: this._frequency,
|
2017-06-15 18:22:15 +03:00
|
|
|
triton: {
|
|
|
|
profile: {
|
|
|
|
url: options.url || process.env.SDC_URL,
|
|
|
|
account: options.account || process.env.SDC_ACCOUNT,
|
|
|
|
keyId: options.keyId || process.env.SDC_KEY_ID
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-06-28 18:04:34 +03:00
|
|
|
this._queues = {};
|
|
|
|
|
2017-06-29 17:28:56 +03:00
|
|
|
this._tritonWatch.on('change', (container) => {
|
|
|
|
return this.onChange(container);
|
|
|
|
});
|
2017-06-28 18:04:34 +03:00
|
|
|
|
|
|
|
this._tritonWatch.on('all', (containers) => {
|
|
|
|
containers.forEach((container) => {
|
|
|
|
this.onChange(container);
|
|
|
|
});
|
|
|
|
});
|
2017-06-15 18:22:15 +03:00
|
|
|
}
|
|
|
|
|
2017-06-22 20:09:13 +03:00
|
|
|
poll () {
|
|
|
|
this._tritonWatch.poll();
|
|
|
|
}
|
|
|
|
|
2017-06-26 17:29:12 +03:00
|
|
|
getContainers () {
|
|
|
|
return this._tritonWatch.getContainers();
|
|
|
|
}
|
|
|
|
|
2017-06-28 18:04:34 +03:00
|
|
|
pushToQueue ({ serviceName, deploymentGroupId }, cb) {
|
|
|
|
const name = `${deploymentGroupId}-${serviceName}`;
|
|
|
|
|
|
|
|
if (this._queues[name]) {
|
|
|
|
this._queues[name](cb);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._queues[name] = Throat(1);
|
|
|
|
this._queues[name](cb);
|
|
|
|
}
|
|
|
|
|
2017-06-15 18:22:15 +03:00
|
|
|
getDeploymentGroupId (name, cb) {
|
|
|
|
this._data.getDeploymentGroup({ name }, (err, deploymentGroup) => {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
return cb(null, deploymentGroup && deploymentGroup.id);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-06-26 17:29:12 +03:00
|
|
|
getService ({ serviceName, serviceHash, deploymentGroupId }, cb) {
|
|
|
|
this._data.getServices({ name: serviceName, hash: serviceHash, deploymentGroupId }, (err, services) => {
|
2017-06-15 18:22:15 +03:00
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!services || !services.length) {
|
|
|
|
return cb();
|
|
|
|
}
|
|
|
|
|
2017-06-22 20:09:13 +03:00
|
|
|
return cb(null, services.pop());
|
2017-06-15 18:22:15 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-06-22 20:09:13 +03:00
|
|
|
getInstances (service, cb) {
|
|
|
|
service.instances()
|
|
|
|
.then((instances) => { return cb(null, instances); })
|
|
|
|
.catch((err) => { return cb(err); });
|
|
|
|
}
|
2017-06-15 18:22:15 +03:00
|
|
|
|
2017-06-28 18:04:34 +03:00
|
|
|
resolveChanges ({ machine, service, instances }, cb) {
|
2017-06-22 20:09:13 +03:00
|
|
|
// 1. if instance doesn't exist, create new
|
|
|
|
// 2. if instance exist, update status
|
2017-06-15 18:22:15 +03:00
|
|
|
|
2017-06-22 20:09:13 +03:00
|
|
|
const handleError = (cb) => {
|
|
|
|
return (err, data) => {
|
|
|
|
if (err) {
|
|
|
|
console.error(err);
|
|
|
|
return;
|
|
|
|
}
|
2017-06-15 18:22:15 +03:00
|
|
|
|
2017-06-22 20:09:13 +03:00
|
|
|
if (cb) {
|
|
|
|
cb(err, data);
|
|
|
|
}
|
|
|
|
};
|
2017-06-15 18:22:15 +03:00
|
|
|
};
|
|
|
|
|
2017-06-22 20:09:13 +03:00
|
|
|
const isNew = instances
|
|
|
|
.every(({ machineId }) => { return machine.id !== machineId; });
|
|
|
|
|
|
|
|
const instance = instances
|
|
|
|
.filter(({ machineId }) => { return machine.id === machineId; })
|
|
|
|
.pop();
|
|
|
|
|
2017-06-28 18:04:34 +03:00
|
|
|
const updateService = (updatedService, cb) => {
|
2017-06-27 20:14:26 +03:00
|
|
|
console.log('-> updating service', util.inspect(updatedService));
|
2017-06-28 18:04:34 +03:00
|
|
|
return this._data.updateService(updatedService, handleError(cb));
|
2017-06-27 20:14:26 +03:00
|
|
|
};
|
|
|
|
|
2017-06-28 18:04:34 +03:00
|
|
|
const create = (cb) => {
|
|
|
|
const status = (machine.state || '').toUpperCase();
|
|
|
|
|
|
|
|
if (status === 'DELETED') {
|
|
|
|
return cb();
|
|
|
|
}
|
|
|
|
|
2017-06-22 20:09:13 +03:00
|
|
|
const instance = {
|
2017-06-15 18:22:15 +03:00
|
|
|
name: machine.name,
|
2017-06-28 18:04:34 +03:00
|
|
|
status,
|
2017-06-29 01:44:14 +03:00
|
|
|
machineId: machine.id,
|
|
|
|
ips: machine.ips
|
2017-06-22 20:09:13 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
console.log('-> creating instance', util.inspect(instance));
|
|
|
|
return this._data.createInstance(instance, handleError((_, instance) => {
|
2017-06-27 20:14:26 +03:00
|
|
|
return updateService({
|
2017-06-22 20:09:13 +03:00
|
|
|
id: service.id,
|
|
|
|
instances: instances.concat(instance)
|
2017-06-28 18:04:34 +03:00
|
|
|
}, cb);
|
2017-06-22 20:09:13 +03:00
|
|
|
}));
|
2017-06-15 18:22:15 +03:00
|
|
|
};
|
|
|
|
|
2017-06-28 18:04:34 +03:00
|
|
|
const update = (cb) => {
|
2017-06-22 20:09:13 +03:00
|
|
|
const updatedInstance = {
|
2017-06-15 18:22:15 +03:00
|
|
|
id: instance.id,
|
2017-06-22 20:09:13 +03:00
|
|
|
status: (machine.state || '').toUpperCase()
|
|
|
|
};
|
|
|
|
|
|
|
|
console.log('-> updating instance', util.inspect(updatedInstance));
|
2017-06-27 20:14:26 +03:00
|
|
|
return this._data.updateInstance(updatedInstance, handleError(() => {
|
2017-06-27 20:45:51 +03:00
|
|
|
if (['DELETED', 'DESTROYED'].indexOf(machine.state.toUpperCase()) < 0) {
|
2017-06-28 18:04:34 +03:00
|
|
|
return cb();
|
2017-06-27 20:14:26 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return setTimeout(() => {
|
|
|
|
return updateService({
|
|
|
|
id: service.id,
|
2017-06-28 13:06:53 +03:00
|
|
|
instances: instances.filter(({ id }) => {
|
|
|
|
return id !== instance.id;
|
|
|
|
})
|
2017-06-28 18:04:34 +03:00
|
|
|
}, cb);
|
|
|
|
}, this._frequency * 3);
|
2017-06-27 20:14:26 +03:00
|
|
|
}));
|
2017-06-15 18:22:15 +03:00
|
|
|
};
|
|
|
|
|
2017-06-22 20:09:13 +03:00
|
|
|
return isNew ?
|
2017-06-28 18:04:34 +03:00
|
|
|
create(cb) :
|
|
|
|
update(cb);
|
2017-06-15 18:22:15 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
onChange (machine) {
|
|
|
|
if (!machine) {
|
2017-06-22 20:09:13 +03:00
|
|
|
console.error('-> `change` event received without machine data');
|
2017-06-15 18:22:15 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-06-22 20:09:13 +03:00
|
|
|
console.log('-> `change` event received', util.inspect(machine));
|
|
|
|
|
2017-06-26 17:29:12 +03:00
|
|
|
const { id, tags = {} } = machine;
|
2017-06-15 18:22:15 +03:00
|
|
|
|
|
|
|
// assert id existence
|
|
|
|
if (!id) {
|
2017-06-22 20:09:13 +03:00
|
|
|
console.error('-> `change` event received for a machine without `id`');
|
2017-06-15 18:22:15 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// assert that it's a docker-compose project
|
|
|
|
const isCompose = [DEPLOYMENT_GROUP, SERVICE, HASH].every(
|
|
|
|
(name) => { return tags[name]; }
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!isCompose) {
|
2017-06-22 20:09:13 +03:00
|
|
|
console.error(`-> Changed machine ${id} was not provisioned by docker-compose`);
|
2017-06-15 18:22:15 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const deploymentGroupName = tags[DEPLOYMENT_GROUP];
|
|
|
|
const serviceName = tags[SERVICE];
|
|
|
|
|
|
|
|
const handleError = (next) => {
|
|
|
|
return (err, item) => {
|
|
|
|
if (err) {
|
|
|
|
console.error(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
next(item);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2017-06-28 18:04:34 +03:00
|
|
|
const getInstances = (service, cb) => {
|
2017-06-22 20:09:13 +03:00
|
|
|
this.getInstances(service, handleError((instances) => {
|
2017-06-15 18:22:15 +03:00
|
|
|
return this.resolveChanges({
|
|
|
|
machine,
|
2017-06-22 20:09:13 +03:00
|
|
|
service,
|
|
|
|
instances
|
2017-06-28 18:04:34 +03:00
|
|
|
}, cb);
|
2017-06-15 18:22:15 +03:00
|
|
|
}));
|
|
|
|
};
|
|
|
|
|
|
|
|
// assert that service exists
|
|
|
|
const assertService = (deploymentGroupId) => {
|
2017-06-28 18:04:34 +03:00
|
|
|
this.pushToQueue({ serviceName, deploymentGroupId }, () => {
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
this.getService({ serviceName, deploymentGroupId }, handleError((service) => {
|
|
|
|
if (!service) {
|
|
|
|
console.error(`Service "${serviceName}" form DeploymentGroup "${deploymentGroupName}" for machine ${id} not found`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
getInstances(service, resolve);
|
|
|
|
}));
|
|
|
|
});
|
|
|
|
});
|
2017-06-15 18:22:15 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
// assert that project managed by this portal
|
|
|
|
const assertDeploymentGroup = () => {
|
|
|
|
this.getDeploymentGroupId(deploymentGroupName, handleError((deploymentGroupId) => {
|
|
|
|
if (!deploymentGroupId) {
|
|
|
|
console.error(`DeploymentGroup "${deploymentGroupName}" for machine ${id} not found`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
assertService(deploymentGroupId);
|
|
|
|
}));
|
|
|
|
};
|
|
|
|
|
|
|
|
assertDeploymentGroup();
|
|
|
|
}
|
|
|
|
};
|
2017-06-26 17:29:12 +03:00
|
|
|
|
|
|
|
module.exports.DEPLOYMENT_GROUP = DEPLOYMENT_GROUP;
|
|
|
|
module.exports.SERVICE = SERVICE;
|
|
|
|
module.exports.HASH = HASH;
|