From 5e7bec0aa795afcb616b41ddb9507a78cf0a640c Mon Sep 17 00:00:00 2001 From: geek Date: Thu, 15 Jun 2017 16:22:15 +0100 Subject: [PATCH] feat(portal-watch): initial implementation --- packages/portal-watch/.gitignore | 3 + packages/portal-watch/lib/index.js | 173 ++++++++++++++++++++++++++++ packages/portal-watch/package.json | 22 ++++ packages/portal-watch/test/index.js | 49 ++++++++ yarn.lock | 74 ++++++------ 5 files changed, 287 insertions(+), 34 deletions(-) create mode 100644 packages/portal-watch/.gitignore create mode 100644 packages/portal-watch/lib/index.js create mode 100644 packages/portal-watch/package.json create mode 100644 packages/portal-watch/test/index.js diff --git a/packages/portal-watch/.gitignore b/packages/portal-watch/.gitignore new file mode 100644 index 00000000..825fc67c --- /dev/null +++ b/packages/portal-watch/.gitignore @@ -0,0 +1,3 @@ +node_modules +.DS_Store +npm-debug.log diff --git a/packages/portal-watch/lib/index.js b/packages/portal-watch/lib/index.js new file mode 100644 index 00000000..6585640d --- /dev/null +++ b/packages/portal-watch/lib/index.js @@ -0,0 +1,173 @@ +'use strict'; + +// const Assert = require('assert'); +const TritonWatch = require('triton-watch'); + + +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; + + this._tritonWatch = new TritonWatch({ + frequency: 500, + triton: { + profile: { + url: options.url || process.env.SDC_URL, + account: options.account || process.env.SDC_ACCOUNT, + keyId: options.keyId || process.env.SDC_KEY_ID + } + } + }); + + this._tritonWatch.on('change', (container) => { return this.onChange(container); }); + } + + getDeploymentGroupId (name, cb) { + this._data.getDeploymentGroup({ name }, (err, deploymentGroup) => { + if (err) { + return cb(err); + } + + return cb(null, deploymentGroup && deploymentGroup.id); + }); + } + + getServiceId ({ serviceName, deploymentGroupId }, cb) { + this._data.getServices({ name: serviceName, deploymentGroupId }, (err, services) => { + if (err) { + return cb(err); + } + + if (!services || !services.length) { + return cb(); + } + + return cb(null, services.pop().id); + }); + } + + getInstance (machineId, cb) { + this._data.getInstances({ machineId }, (err, instances) => { + if (err) { + return cb(err); + } + + if (!instances || !instances.length) { + return cb(); + } + + return cb(null, instances.pop()); + }); + } + + resolveChanges ({ machine, deploymentGroupId, serviceId, instance }) { + const handleError = (err) => { + if (err) { + console.error(err); + } + }; + + const create = () => { + return this._data.updateInstance({ + name: machine.name, + status: machine.state.toUpperCase(), + machineId: machine.id + }, handleError); + }; + + const update = () => { + return this._data.updateInstance({ + id: instance.id, + status: machine.state.toUpperCase() + }, handleError); + }; + + return (!instance || !instance.id) ? + create() : + update(); + } + + onChange (machine) { + if (!machine) { + console.error('`change` event received without machine data'); + return; + } + + const { id, tags = [] } = machine; + + // assert id existence + if (!id) { + console.error('`change` event received for a machine without `id`'); + return; + } + + // assert that it's a docker-compose project + const isCompose = [DEPLOYMENT_GROUP, SERVICE, HASH].every( + (name) => { return tags[name]; } + ); + + if (!isCompose) { + console.error(`Changed machine ${id} was not provisioned by docker-compose`); + return; + } + + const deploymentGroupName = tags[DEPLOYMENT_GROUP]; + const serviceName = tags[SERVICE]; + + const handleError = (next) => { + return (err, item) => { + if (err) { + console.error(err); + return; + } + + next(item); + }; + }; + + const getInstance = (deploymentGroupId, serviceId) => { + this.getInstance(id, handleError((instance) => { + return this.resolveChanges({ + machine, + deploymentGroupId, + serviceId, + instance + }); + })); + }; + + // assert that service exists + const assertService = (deploymentGroupId) => { + this.getServiceId({ serviceName, deploymentGroupId }, handleError((serviceId) => { + if (!serviceId) { + console.error(`Service "${serviceName}" form DeploymentGroup "${deploymentGroupName}" for machine ${id} not found`); + return; + } + + getInstance(deploymentGroupId, serviceId); + })); + }; + + // 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(); + } +}; diff --git a/packages/portal-watch/package.json b/packages/portal-watch/package.json new file mode 100644 index 00000000..dcc783c2 --- /dev/null +++ b/packages/portal-watch/package.json @@ -0,0 +1,22 @@ +{ + "name": "portal-watch", + "version": "1.0.0", + "private": true, + "license": "MPL-2.0", + "description": "resolver of triton change events", + "repository": "github:yldio/joyent-portal", + "main": "lib", + "scripts": { + "lint": "belly-button --fix", + "lint-ci": "belly-button", + "test": "lab -c --ignore debug", + "test-ci": "echo 0 `# lab -c -r console -o stdout -r tap -o $CIRCLE_TEST_REPORTS/test/portal-watch.xml`" + }, + "dependencies": { + "triton-watch": "^1.0.1" + }, + "devDependencies": { + "belly-button": "^3.1.0", + "lab": "^13.1.0" + } +} diff --git a/packages/portal-watch/test/index.js b/packages/portal-watch/test/index.js new file mode 100644 index 00000000..2c2471e4 --- /dev/null +++ b/packages/portal-watch/test/index.js @@ -0,0 +1,49 @@ +'use strict'; + +const Lab = require('lab'); +const PortalWatch = require('../'); + + +const lab = exports.lab = Lab.script(); +const it = lab.it; +const expect = Lab.expect; + + +it('updates instances with the current status', (done) => { + const data = { + getDeploymentGroup: (options, next) => { + expect(options.name).to.equal('test-project'); + next(null, { id: 'deployment-group-id' }); + }, + getServices: (options, next) => { + expect(options.deploymentGroupId).to.equal('deployment-group-id'); + expect(options.name).to.equal('test-service'); + next(null, [{ id: 'service-id' }]); + }, + getInstances: (options, next) => { + expect(options.machineId).to.equal('test-id'); + next(null, [{ id: 'instance-id' }]); + }, + updateInstance: (options, next) => { + expect(options.id).to.equal('instance-id'); + expect(options.status).to.equal('DELETED'); + done(); + } + }; + + const machine = { + id: 'test-id', + tags: { + 'docker:label:com.docker.compose.project': 'test-project', + 'docker:label:com.docker.compose.service': 'test-service', + 'docker:label:com.docker.compose.config-hash': 'test-hash' + }, + state: 'deleted' + }; + + const portalOptions = { data, url: 'url', account: 'account', keyId: 'de:e7:73:9a:aa:91:bb:3e:72:8d:cc:62:ca:58:a2:ec' }; + const portalWatch = new PortalWatch(portalOptions); + portalWatch._tritonWatch.removeAllListeners('change'); + + portalWatch.onChange(machine); +}); diff --git a/yarn.lock b/yarn.lock index f8eebbe9..2839eb92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -439,10 +439,8 @@ array-includes@^3.0.3: es-abstract "^1.7.0" array-iterate@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/array-iterate/-/array-iterate-1.1.0.tgz#4f13148ffffa5f2756b50460e5eac8eed31a14e6" - dependencies: - has "^1.0.1" + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-iterate/-/array-iterate-1.1.1.tgz#865bf7f8af39d6b0982c60902914ac76bc0108f6" array-union@^1.0.1: version "1.0.2" @@ -1612,6 +1610,10 @@ browserslist@^2.1.2: caniuse-lite "^1.0.30000684" electron-to-chromium "^1.3.14" +brule@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/brule/-/brule-2.0.0.tgz#7020ecb04301f4e1f03f26fc00569edf81258699" + buf-compare@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/buf-compare/-/buf-compare-1.0.1.tgz#fef28da8b8113a0a0db4430b0b6467b69730b34a" @@ -3517,20 +3519,16 @@ esquery@^1.0.0: estraverse "^4.0.0" esrecurse@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" + version "4.2.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" dependencies: - estraverse "~4.1.0" + estraverse "^4.1.0" object-assign "^4.0.1" -estraverse@^4.0.0, estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" -estraverse@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" - estree-walker@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.2.1.tgz#bdafe8095383d8414d5dc2ecf4c9173b6db9412e" @@ -4393,10 +4391,11 @@ hash-base@^2.0.0: inherits "^2.0.1" hash.js@^1.0.0, hash.js@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.0.3.tgz#1332ff00156c0a0ffdd8236013d07b77a0451573" + version "1.1.1" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.1.tgz#5cb2e796499224e69fd0b00ed01d2d4a16e7a323" dependencies: - inherits "^2.0.1" + inherits "^2.0.3" + minimalistic-assert "^1.0.0" hawk@~3.1.3: version "3.1.3" @@ -4420,8 +4419,8 @@ hedges@^1.0.0: resolved "https://registry.yarnpkg.com/hedges/-/hedges-1.2.0.tgz#45183bd0f884e62261f3a2cfbed40fad409b340c" history@^4.5.1, history@^4.6.0: - version "4.6.2" - resolved "https://registry.yarnpkg.com/history/-/history-4.6.2.tgz#716e863e1da0e97a028eed6da644061dd1e1ed1d" + version "4.6.3" + resolved "https://registry.yarnpkg.com/history/-/history-4.6.3.tgz#6d723a8712c581d6bef37e8c26f4aedc6eb86967" dependencies: invariant "^2.2.1" loose-envify "^1.2.0" @@ -5209,8 +5208,8 @@ jest-snapshot@^20.0.3: pretty-format "^20.0.3" jest-styled-components@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/jest-styled-components/-/jest-styled-components-3.0.2.tgz#90047608075219dd0ee60e7c80d7c0ba90c6534d" + version "3.1.0" + resolved "https://registry.yarnpkg.com/jest-styled-components/-/jest-styled-components-3.1.0.tgz#3687b1a564b16241d19bb8ecea1e6b477d4e0c0c" dependencies: css "^2.2.1" @@ -6414,13 +6413,11 @@ npmlog@^4.0.2, npmlog@^4.1.0: set-blocking "~2.0.0" nspell@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nspell/-/nspell-1.0.2.tgz#834c1e46c39ca99b9d7f6d4c485ef76a8a711b8c" + version "1.0.3" + resolved "https://registry.yarnpkg.com/nspell/-/nspell-1.0.3.tgz#2ade9be62ae3769240d37b9d92fe946a817bca90" dependencies: - has "^1.0.1" is-buffer "^1.1.4" trim "0.0.1" - x-is-string "^0.1.0" num2fraction@^1.2.2: version "1.2.2" @@ -7063,8 +7060,8 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" promise@^7.1.1: - version "7.3.0" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.0.tgz#e7feec5aa87a2cbb81acf47d9a3adbd9d4642d7b" + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" dependencies: asap "~2.0.3" @@ -7342,8 +7339,8 @@ react-router@^4.1.1: warning "^3.0.0" react-styled-flexboxgrid@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/react-styled-flexboxgrid/-/react-styled-flexboxgrid-2.0.2.tgz#c1991bd6b249ee4b7cf0abbc35869a0d28e65620" + version "2.0.3" + resolved "https://registry.yarnpkg.com/react-styled-flexboxgrid/-/react-styled-flexboxgrid-2.0.3.tgz#308a8bbc80b1737a65f4ccf35d02afe20932a2f2" dependencies: lodash.isinteger "^4.0.4" @@ -7966,10 +7963,9 @@ retext-simplify@^4.0.0: unist-util-position "^3.0.0" retext-spell@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/retext-spell/-/retext-spell-2.3.0.tgz#09302b3697244d429a882bb87251e0626fef6e69" + version "2.3.1" + resolved "https://registry.yarnpkg.com/retext-spell/-/retext-spell-2.3.1.tgz#f52ee13797fc4eb52de1e8b9dc3f6fe85ca97bd7" dependencies: - has "^1.0.1" lodash.includes "^4.2.0" nlcst-is-literal "^1.0.0" nlcst-to-string "^2.0.0" @@ -8726,8 +8722,8 @@ stylis@2.0.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-2.0.0.tgz#6785a6546bd73478799a67d49d67086953b50ad5" stylis@^3.0.19: - version "3.1.8" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.1.8.tgz#59e643861f7e67f0c9d157d7dfa8cf25a752e836" + version "3.1.9" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.1.9.tgz#638370451f980437f57c59e58d2e296be29fafb7" subtext@4.x.x: version "4.4.1" @@ -9128,7 +9124,13 @@ trim@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" -triton@^5.2.0: +triton-watch@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/triton-watch/-/triton-watch-1.0.1.tgz#b1087f6a57383f1e83d0a308e65110ca9a2a38d0" + dependencies: + triton "5.2.x" + +triton@5.2.x, triton@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/triton/-/triton-5.2.0.tgz#16ce8cb155c37785b6818e403f5cf83934921d41" dependencies: @@ -9627,7 +9629,7 @@ window-size@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" -wordwrap@0.0.2, wordwrap@~0.0.2: +wordwrap@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" @@ -9635,6 +9637,10 @@ wordwrap@1.0.0, wordwrap@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"