From 1e157641b2ea72d4eb079a574e597e0fe4ad3d66 Mon Sep 17 00:00:00 2001 From: geek Date: Thu, 8 Jun 2017 13:43:24 -0500 Subject: [PATCH] feat(portal-data): can stop services --- packages/cp-gql-schema/schema.gql | 1 + packages/docker-compose-client/lib/index.js | 12 ++ packages/docker-compose-client/test/index.js | 48 ++++++++ packages/portal-data/lib/index.js | 88 ++++++++++++-- packages/portal-data/package.json | 1 + packages/portal-data/test/index.js | 53 ++++++++- packages/portal-data/yarn.lock | 114 ++++++++++++++++++- 7 files changed, 302 insertions(+), 15 deletions(-) diff --git a/packages/cp-gql-schema/schema.gql b/packages/cp-gql-schema/schema.gql index ce1ec907..20ea3eda 100644 --- a/packages/cp-gql-schema/schema.gql +++ b/packages/cp-gql-schema/schema.gql @@ -120,6 +120,7 @@ enum InstanceStatus { PAUSED EXITED DELETED + STOPPED } type Instance { diff --git a/packages/docker-compose-client/lib/index.js b/packages/docker-compose-client/lib/index.js index c897ccdd..33463243 100644 --- a/packages/docker-compose-client/lib/index.js +++ b/packages/docker-compose-client/lib/index.js @@ -39,4 +39,16 @@ module.exports = class DockerComposeClient extends EventEmitter { return this._invoke('scale', options, manifest, cb); } + + config({ projectName, services, manifest }, cb) { + const options = { + // eslint-disable-next-line camelcase + project_name: projectName, + services: services.map(service => ({ + name: service + })) + }; + + return this._invoke('config', options, manifest, cb); + } }; diff --git a/packages/docker-compose-client/test/index.js b/packages/docker-compose-client/test/index.js index 8cd8dd2f..e4688968 100644 --- a/packages/docker-compose-client/test/index.js +++ b/packages/docker-compose-client/test/index.js @@ -69,6 +69,30 @@ const server = new Server({ projectName: options.project_name, services: options.services }); + }, + // eslint-disable-next-line object-shorthand + config: function(options, manifest, fn) { + if (typeof options !== 'object') { + return fn(new Error('Expected options')); + } + + if (typeof options.project_name !== 'string') { + return fn(new Error('Expected project name')); + } + + if (typeof manifest !== 'string') { + return fn(new Error('Expected manifest')); + } + + try { + safeLoad(manifest); + } catch (err) { + return fn(err); + } + + fn(null, { + projectName: options.project_name + }); } }); @@ -123,6 +147,30 @@ it('scale()', done => { ); }); +it('config()', done => { + const manifest = ` + hello: + image: hello-world:latest + world: + image: consul:latest + node: + image: node:latest + `; + + client.config( + { + projectName, + services: ['hello'], + manifest + }, + (err, res) => { + expect(err).to.not.exist(); + expect(res).to.exist(); + done(); + } + ); +}); + it('handles errors', done => { client.once('error', err => { expect(err).to.exist(); diff --git a/packages/portal-data/lib/index.js b/packages/portal-data/lib/index.js index cdb80abf..7a97e0ae 100644 --- a/packages/portal-data/lib/index.js +++ b/packages/portal-data/lib/index.js @@ -2,6 +2,7 @@ const EventEmitter = require('events'); const DockerClient = require('docker-compose-client'); +const Dockerode = require('dockerode'); const Hoek = require('hoek'); const Penseur = require('penseur'); const VAsync = require('vasync'); @@ -14,7 +15,7 @@ const internals = { db: { test: false }, - dockerHost: 'tcp://0.0.0.0:4242' + dockerComposeHost: 'tcp://0.0.0.0:4242' }, tables: { 'portals': { id: { type: 'uuid' }, primary: 'id', secondary: false, purge: false }, @@ -36,9 +37,10 @@ module.exports = class Data extends EventEmitter { // Penseur will assert that the options are correct this._db = new Penseur.Db(settings.name, settings.db); - this._docker = new DockerClient(settings.dockerHost); + this._dockerCompose = new DockerClient(settings.dockerComposeHost); + this._docker = new Dockerode(settings.docker); - this._docker.on('error', (err) => { + this._dockerCompose.on('error', (err) => { this.emit('error', err); }); } @@ -489,7 +491,7 @@ module.exports = class Data extends EventEmitter { manifest: manifest.raw }; options.services[service.name] = replicas; - this._docker.scale(options, (err) => { + this._dockerCompose.scale(options, (err) => { if (err) { return cb(err); } @@ -523,7 +525,7 @@ module.exports = class Data extends EventEmitter { setImmediate(() => { let isHandled = false; - this._docker.provision({ projectName: deploymentGroup.name, manifest: clientManifest.raw }, (err, res) => { + this._dockerCompose.provision({ projectName: deploymentGroup.name, manifest: clientManifest.raw }, (err, res) => { if (err) { this.emit('error', err); return; @@ -538,7 +540,8 @@ module.exports = class Data extends EventEmitter { const options = { manifestServices: manifest.json.services || manifest.json, deploymentGroup, - manifestId: key + manifestId: key, + provisionRes: res }; this.provisionServices(options); }); @@ -575,7 +578,7 @@ module.exports = class Data extends EventEmitter { // services - provisionServices ({ manifestServices, deploymentGroup, manifestId }, cb) { + provisionServices ({ manifestServices, deploymentGroup, manifestId, provisionRes }, cb) { // insert instance information // insert service information // insert version information -- will update deploymentGroups @@ -591,7 +594,7 @@ module.exports = class Data extends EventEmitter { const manifestService = manifestServices[serviceName]; const clientInstance = { name: serviceName, - machineId: `${deploymentGroup.name}_${serviceName}_1`, + machineId: provisionRes[serviceName].plan.containers[0].id, status: 'CREATED' }; this.createInstance(clientInstance, (err, createdInstance) => { @@ -604,7 +607,8 @@ module.exports = class Data extends EventEmitter { name: serviceName, slug: serviceName, deploymentGroupId: deploymentGroup.id, - instances: [createdInstance] + instances: [createdInstance], + info: provisionRes[serviceName] }; this.createService(clientService, (err, createdService) => { @@ -750,6 +754,62 @@ module.exports = class Data extends EventEmitter { }); } + stopServices ({ 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.stop((err) => { + if (err) { + return next(err); + } + + this.updateInstance({ id: instance.id, status: 'STOPPED' }, next); + }); + }); + }, + inputs: instanceIds + }, (err, results) => { + if (err) { + return cb(err); + } + + this.getServices({ ids }, cb); + }); + }); + } + + startServices ({ ids }, cb) { + + } + + restartServices ({ ids }, cb) { + + } + + deleteServices ({ ids }, cb) { + + } + // instances @@ -774,6 +834,16 @@ module.exports = class Data extends EventEmitter { }); } + updateInstance ({ id, status }, cb) { + this._db.instances.update(id, { status }, (err, instance) => { + if (err) { + return cb(err); + } + + cb(null, instance ? Transform.fromInstance(instance) : {}); + }); + } + // packages diff --git a/packages/portal-data/package.json b/packages/portal-data/package.json index a201a92e..7992705b 100644 --- a/packages/portal-data/package.json +++ b/packages/portal-data/package.json @@ -17,6 +17,7 @@ "license": "MPL-2.0", "dependencies": { "docker-compose-client": "^1.0.7", + "dockerode": "^2.4.3", "hoek": "^4.1.1", "penseur": "^7.8.1", "vasync": "^1.6.4", diff --git a/packages/portal-data/test/index.js b/packages/portal-data/test/index.js index a893e5d9..b5efffdf 100644 --- a/packages/portal-data/test/index.js +++ b/packages/portal-data/test/index.js @@ -6,16 +6,32 @@ const Code = require('code'); const Lab = require('lab'); const PortalData = require('../'); + const lab = exports.lab = Lab.script(); +const afterEach = lab.afterEach; const it = lab.it; const describe = lab.describe; const expect = Code.expect; + const internals = { - options: { name: 'test', db: { test: true } }, + options: { + name: 'test', + db: { test: true } + }, composeFile: Fs.readFileSync(Path.join(__dirname, 'docker-compose.yml')).toString() }; + +afterEach((done) => { + const data = new PortalData({ name: 'test', db: { test: true } }); + data.connect(() => { + data._db.r.dbDrop('test').run(data._db._connection, () => { + done(); + }); + }); +}); + describe('connect()', () => { it('connects to the database', (done) => { const data = new PortalData(internals.options); @@ -688,3 +704,38 @@ describe.skip('scale()', () => { }); }); }); + + +// skipping by default since it takes so long +describe.skip('stopServices()', () => { + it('stops 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.stopServices({ ids: [deploymentGroupServices[0].id] }, (err, services) => { + expect(err).to.not.exist(); + expect(services).to.exist(); + done(); + }); + }); + }); + }, 80000); + }); + }); + }); + }); +}); diff --git a/packages/portal-data/yarn.lock b/packages/portal-data/yarn.lock index d1488e9b..cf86cf0f 100644 --- a/packages/portal-data/yarn.lock +++ b/packages/portal-data/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +JSONStream@0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-0.10.0.tgz#74349d0d89522b71f30f0a03ff9bd20ca6f12ac0" + dependencies: + jsonparse "0.0.5" + through ">=2.2.7 <3" + acorn-jsx@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" @@ -105,6 +112,12 @@ bindings@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" +bl@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" + dependencies: + readable-stream "^2.0.5" + "bluebird@>= 2.3.2 < 3": version "2.11.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" @@ -212,6 +225,14 @@ concat-stream@^1.5.2: readable-stream "^2.2.2" typedarray "^0.0.6" +concat-stream@~1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" + dependencies: + inherits "~2.0.1" + readable-stream "~2.0.0" + typedarray "~0.0.5" + core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -222,7 +243,7 @@ d@1: dependencies: es5-ext "^0.10.9" -debug@^2.1.1: +debug@^2.1.1, debug@^2.6.0: version "2.6.8" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" dependencies: @@ -259,6 +280,23 @@ docker-compose-client@^1.0.7: apr-awaitify "^1.0.4" zerorpc "^0.9.7" +docker-modem@0.3.x: + version "0.3.7" + resolved "https://registry.yarnpkg.com/docker-modem/-/docker-modem-0.3.7.tgz#3f510d09f5d334dc2134228f92bd344671227df4" + dependencies: + JSONStream "0.10.0" + debug "^2.6.0" + readable-stream "~1.0.26-4" + split-ca "^1.0.0" + +dockerode@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-2.4.3.tgz#7ec55b492fc9f289e77325ff07c6a3a96021b4f2" + dependencies: + concat-stream "~1.5.1" + docker-modem "0.3.x" + tar-fs "~1.12.0" + doctrine@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63" @@ -266,6 +304,12 @@ doctrine@^2.0.0: esutils "^2.0.2" isarray "^1.0.0" +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.0.tgz#7a90d833efda6cfa6eac0f4949dbb0fad3a63206" + dependencies: + once "^1.4.0" + es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: version "0.10.21" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.21.tgz#19a725f9e51d0300bbc1e8e821109fd9daf55925" @@ -636,6 +680,10 @@ is-resolvable@^1.0.0: dependencies: tryit "^1.0.1" +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -682,6 +730,10 @@ jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" +jsonparse@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-0.0.5.tgz#330542ad3f0a654665b778f3eb2d9a9fa507ac64" + jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" @@ -786,7 +838,7 @@ object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" -once@^1.3.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" dependencies: @@ -871,11 +923,18 @@ progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" +pump@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.2.tgz#3b3ee6512f94f0e575538c17995f9f16990a5d51" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + radix62@1.x.x: version "1.0.1" resolved "https://registry.yarnpkg.com/radix62/-/radix62-1.0.1.tgz#cc2f27a49543b44ddaac712380409354bbe009a5" -readable-stream@^2.2.2: +readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.2.2: version "2.2.9" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.9.tgz#cf78ec6f4a6d1eb43d26488cac97f042e74b7fc8" dependencies: @@ -887,6 +946,26 @@ readable-stream@^2.2.2: string_decoder "~1.0.0" util-deprecate "~1.0.1" +readable-stream@~1.0.26-4: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@~2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + readline2@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" @@ -993,6 +1072,10 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" +split-ca@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -1012,6 +1095,10 @@ string-width@^2.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^3.0.0" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + string_decoder@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.1.tgz#62e200f039955a6810d8df0a33ffc0f013662d98" @@ -1047,11 +1134,28 @@ table@^3.7.8: slice-ansi "0.0.4" string-width "^2.0.0" +tar-fs@~1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.12.0.tgz#a6a80553d8a54c73de1d0ae0e79de77035605e1d" + dependencies: + mkdirp "^0.5.0" + pump "^1.0.0" + tar-stream "^1.1.2" + +tar-stream@^1.1.2: + version "1.5.4" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.4.tgz#36549cf04ed1aee9b2a30c0143252238daf94016" + dependencies: + bl "^1.0.0" + end-of-stream "^1.0.0" + readable-stream "^2.0.0" + xtend "^4.0.0" + text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" -through@^2.3.6: +"through@>=2.2.7 <3", through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -1071,7 +1175,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -typedarray@^0.0.6: +typedarray@^0.0.6, typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"