diff --git a/packages/portal-data/lib/index.js b/packages/portal-data/lib/index.js index 7a97e0ae..b81de7f4 100644 --- a/packages/portal-data/lib/index.js +++ b/packages/portal-data/lib/index.js @@ -491,10 +491,11 @@ module.exports = class Data extends EventEmitter { manifest: manifest.raw }; options.services[service.name] = replicas; - this._dockerCompose.scale(options, (err) => { + this._dockerCompose.scale(options, (err, res) => { if (err) { return cb(err); } + console.log(JSON.stringify(res, null, ' ')); finish(); }); } @@ -592,9 +593,11 @@ module.exports = class Data extends EventEmitter { VAsync.forEachPipeline({ func: (serviceName, next) => { const manifestService = manifestServices[serviceName]; + const container = provisionRes[serviceName].plan.containers[0]; + const clientInstance = { name: serviceName, - machineId: provisionRes[serviceName].plan.containers[0].id, + machineId: container ? container.id : `${deploymentGroup.name}_${serviceName}_1`, status: 'CREATED' }; this.createInstance(clientInstance, (err, createdInstance) => { @@ -698,21 +701,12 @@ module.exports = class Data extends EventEmitter { return cb(null, null); } - VAsync.parallel({ - funcs: [ - (next) => { - this._db.instances.get(service.instance_ids, next); - }, - (next) => { - this._db.packages.single({ id: service.package_id }, next); - } - ] - }, (err, results) => { + this._db.packages.single({ id: service.package_id }, (err, packages) => { if (err) { return cb(err); } - cb(null, Transform.fromService({ service, instances: results.successes[0], package: results.successes[1] })); + cb(null, Transform.fromService({ service, instances: this._instancesFilter(service.instance_ids), packages })); }); }); } @@ -749,11 +743,45 @@ module.exports = class Data extends EventEmitter { } return cb(null, services.map((service) => { - return Transform.fromService({ service }); + return Transform.fromService({ service, instances: this._instancesFilter(service.instance_ids) }); })); }); } + _instancesFilter (instanceIds) { + return ({ name, machineId, status }) => { + return new Promise((resolve, reject) => { + const query = { + id: this._db.or(instanceIds) + }; + + if (name) { + query.name = name; + } + + if (machineId) { + query.machine_id = machineId; + } + + if (status) { + query.status = status; + } + + this._db.instances.query(query, (err, instances) => { + if (err) { + return reject(err); + } + + if (!instances || !instances.length) { + return resolve([]); + } + + resolve(instances.map(Transform.fromInstance)); + }); + }); + }; + } + stopServices ({ ids }, cb) { this._db.services.get(ids, (err, services) => { if (err) { @@ -799,15 +827,138 @@ module.exports = class Data extends EventEmitter { } startServices ({ ids }, cb) { + this._db.services.get(ids, (err, services) => { + if (err) { + return cb(err); + } + if (!services || !services.length) { + return cb(); + } + + let instanceIds = []; + services.forEach((service) => { + instanceIds = instanceIds.concat(service.instance_ids); + }); + + VAsync.forEachParallel({ + func: (instanceId, next) => { + this._db.instances.get(instanceId, (err, instance) => { + if (err) { + return next(err); + } + + const container = this._docker.getContainer(instance.machine_id); + + container.start((err) => { + if (err) { + return next(err); + } + + this.updateInstance({ id: instance.id, status: 'RUNNING' }, next); + }); + }); + }, + inputs: instanceIds + }, (err, results) => { + if (err) { + return cb(err); + } + + this.getServices({ ids }, cb); + }); + }); } restartServices ({ ids }, cb) { + this._db.services.get(ids, (err, services) => { + if (err) { + return cb(err); + } + if (!services || !services.length) { + return cb(); + } + + let instanceIds = []; + services.forEach((service) => { + instanceIds = instanceIds.concat(service.instance_ids); + }); + + VAsync.forEachParallel({ + func: (instanceId, next) => { + this._db.instances.get(instanceId, (err, instance) => { + if (err) { + return next(err); + } + + this.updateInstance({ id: instance.id, status: 'RESTARTING' }, () => { + const container = this._docker.getContainer(instance.machine_id); + + container.restart((err) => { + if (err) { + return next(err); + } + + this.updateInstance({ id: instance.id, status: 'RUNNING' }, next); + }); + }); + }); + }, + inputs: instanceIds + }, (err, results) => { + if (err) { + return cb(err); + } + + this.getServices({ ids }, cb); + }); + }); } deleteServices ({ ids }, cb) { + this._db.services.get(ids, (err, services) => { + if (err) { + return cb(err); + } + if (!services || !services.length) { + return cb(); + } + + let instanceIds = []; + services.forEach((service) => { + instanceIds = instanceIds.concat(service.instance_ids); + }); + + VAsync.forEachParallel({ + func: (instanceId, next) => { + this._db.instances.get(instanceId, (err, instance) => { + if (err) { + return next(err); + } + + const container = this._docker.getContainer(instance.machine_id); + + // Use force in case the container is running. TODO: should we keep force? + container.remove({ force: true }, (err) => { + if (err) { + return next(err); + } + + this.updateInstance({ id: instance.id, status: 'DELETED' }, next); + }); + }); + }, + inputs: instanceIds + }, (err, results) => { + if (err) { + return cb(err); + } + + this.getServices({ ids }, cb); + }); + }); } diff --git a/packages/portal-data/lib/transform.js b/packages/portal-data/lib/transform.js index 82111a80..291d74e2 100644 --- a/packages/portal-data/lib/transform.js +++ b/packages/portal-data/lib/transform.js @@ -53,7 +53,7 @@ exports.fromService = function ({ service, instances, packages }) { name: service.name, slug: service.slug, environment: service.environment || [], - instances: instances ? instances.map(exports.fromInstance) : [], + instances, currentMetrics: [], connections: service.service_dependency_ids, package: packages ? exports.fromPackage(packages) : {}, diff --git a/packages/portal-data/test/index.js b/packages/portal-data/test/index.js index b5efffdf..f04cd017 100644 --- a/packages/portal-data/test/index.js +++ b/packages/portal-data/test/index.js @@ -691,11 +691,13 @@ describe.skip('scale()', () => { setTimeout(() => { data.getDeploymentGroup({ id: deploymentGroup.id }, (err, deploymentGroup) => { expect(err).to.not.exist(); - data.scale({ id: deploymentGroup.services[0].id, replicas: 3 }, (err, version) => { - expect(err).to.not.exist(); - expect(version).to.exist(); - expect(version.scales[0].replicas).to.equal(3); - done(); + deploymentGroup.services().then((deploymentGroupServices) => { + data.scale({ id: deploymentGroupServices[0].id, replicas: 3 }, (err, version) => { + expect(err).to.not.exist(); + expect(version).to.exist(); + expect(version.scales[0].replicas).to.equal(3); + done(); + }); }); }); }, 80000); @@ -739,3 +741,105 @@ describe.skip('stopServices()', () => { }); }); }); + +// skipping by default since it takes so long +describe.skip('startServices()', () => { + it('starts all instances of a service', { timeout: 180000 }, (done) => { + const data = new PortalData(internals.options); + data.connect((err) => { + expect(err).to.not.exist(); + data.createDeploymentGroup({ name: 'something' }, (err, deploymentGroup) => { + expect(err).to.not.exist(); + const clientManifest = { + deploymentGroupId: deploymentGroup.id, + type: 'compose', + format: 'yml', + raw: internals.composeFile + }; + data.provisionManifest(clientManifest, (err, manifest) => { + expect(err).to.not.exist(); + setTimeout(() => { + data.getDeploymentGroup({ id: deploymentGroup.id }, (err, deploymentGroup) => { + expect(err).to.not.exist(); + deploymentGroup.services().then((deploymentGroupServices) => { + data.startServices({ ids: [deploymentGroupServices[0].id] }, (err, services) => { + expect(err).to.not.exist(); + expect(services).to.exist(); + done(); + }); + }); + }); + }, 80000); + }); + }); + }); + }); +}); + +// skipping by default since it takes so long +describe.skip('restartServices()', () => { + it('restarts all instances of a service', { timeout: 180000 }, (done) => { + const data = new PortalData(internals.options); + data.connect((err) => { + expect(err).to.not.exist(); + data.createDeploymentGroup({ name: 'something' }, (err, deploymentGroup) => { + expect(err).to.not.exist(); + const clientManifest = { + deploymentGroupId: deploymentGroup.id, + type: 'compose', + format: 'yml', + raw: internals.composeFile + }; + data.provisionManifest(clientManifest, (err, manifest) => { + expect(err).to.not.exist(); + setTimeout(() => { + data.getDeploymentGroup({ id: deploymentGroup.id }, (err, deploymentGroup) => { + expect(err).to.not.exist(); + deploymentGroup.services().then((deploymentGroupServices) => { + data.restartServices({ ids: [deploymentGroupServices[0].id] }, (err, services) => { + expect(err).to.not.exist(); + expect(services).to.exist(); + done(); + }); + }); + }); + }, 80000); + }); + }); + }); + }); +}); + +// skipping by default since it takes so long +describe.skip('deleteServices()', () => { + it('deletes all instances of a service', { timeout: 180000 }, (done) => { + const data = new PortalData(internals.options); + data.connect((err) => { + expect(err).to.not.exist(); + data.createDeploymentGroup({ name: 'something' }, (err, deploymentGroup) => { + expect(err).to.not.exist(); + const clientManifest = { + deploymentGroupId: deploymentGroup.id, + type: 'compose', + format: 'yml', + raw: internals.composeFile + }; + data.provisionManifest(clientManifest, (err, manifest) => { + expect(err).to.not.exist(); + setTimeout(() => { + data.getDeploymentGroup({ id: deploymentGroup.id }, (err, deploymentGroup) => { + expect(err).to.not.exist(); + deploymentGroup.services().then((deploymentGroupServices) => { + data.deleteServices({ ids: [deploymentGroupServices[0].id] }, (err, services) => { + expect(err).to.not.exist(); + expect(services).to.exist(); + done(); + }); + }); + }); + }, 80000); + }); + }); + }); + }); +});