├── test ├── index.js ├── npm.js └── fleet.js ├── lib ├── index.js ├── command.js ├── npm.js └── fleet.js ├── .travis.yml ├── bin └── deploy.js ├── .gitignore ├── LICENSE ├── package.json └── README.md /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./npm') 4 | require('./fleet') 5 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.command = require('./command') 4 | exports.npm = require('./npm') 5 | exports.fleet = require('./fleet') 6 | 7 | // Hack to silence brisky-hub issue 8 | 9 | process.on('uncaughtException', error => { 10 | if (error.message.indexOf('cannot call send() while not connected') < 0) { 11 | throw error 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const cp = require('child_process') 4 | const maxBuffer = 100 * 1024 5 | 6 | exports.run = (command, cwd, env) => { 7 | return new Promise((resolve, reject) => { 8 | cp.exec(command, {maxBuffer, cwd, env}, (error, stdout) => { 9 | if (error) { return reject(error) } 10 | 11 | resolve(stdout.toString().trim()) 12 | }) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # .travis.yml 2 | language: node_js 3 | git: 4 | depth: 1 5 | matrix: 6 | include: 7 | - node_js: '6.3' 8 | script: npm run travis 9 | before_install: 10 | - echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc 11 | notifications: 12 | email: false 13 | deploy: 14 | - provider: npm 15 | email: jim@vigour.io 16 | api_key: ${NPM_TOKEN} 17 | on: 18 | branch: master 19 | tags: true 20 | -------------------------------------------------------------------------------- /bin/deploy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const fleet = require('../lib/fleet') 6 | const dir = process.cwd() 7 | 8 | fleet.getServices(fleet.getPkg(dir), dir, process.argv[2]) 9 | .then(() => { 10 | console.info('Deployment successful. Services will discover each other soon.') 11 | }) 12 | .catch((error) => { 13 | console.error('Deployment failed due to error: %j, stack: %s', error, error ? error.stack : '(no stack)') 14 | }) 15 | -------------------------------------------------------------------------------- /lib/npm.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const command = require('./command') 4 | 5 | exports.getLastVersion = module => { 6 | return command.run(`npm v ${module} version`) 7 | .then(output => { 8 | return output 9 | .split('\n').pop() // last line 10 | .split(' ').pop() // after space 11 | .replace(/'/g, '') // without quotes 12 | }) 13 | } 14 | 15 | exports.getServices = module => { 16 | return command.run(`npm v ${module} services`).then(output => { 17 | output = output.replace(/'/g, '"') 18 | return JSON.parse(output.indexOf('{') === -1 ? '{}' : output) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Vigour.io 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /test/npm.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const sinon = require('sinon') 5 | const cp = require('child_process') 6 | 7 | const npm = require('../lib/npm') 8 | 9 | const exec = sinon.stub(cp, 'exec') 10 | 11 | test('npm client - catch cli error', t => { 12 | exec 13 | .withArgs('npm v sample@^1.0.0 version') 14 | .callsArgWith(2, 'cli error') 15 | 16 | npm.getLastVersion('sample@^1.0.0') 17 | .catch(error => { 18 | t.equal(error, 'cli error', 'cli error caught') 19 | t.end() 20 | }) 21 | }) 22 | 23 | test('npm client - get latest version', t => { 24 | exec 25 | .withArgs('npm v sample@^1.0.0 version') 26 | .callsArgWith(2, null, '\nsample@1.1.1 \'1.1.1\'\nsample@1.2.2 \'1.2.2\'\nsample@1.3.3 \'1.3.3\'\n') 27 | 28 | npm.getLastVersion('sample@^1.0.0') 29 | .then(version => { 30 | t.equal(version.constructor, String, 'latest version is a string') 31 | t.equal(version, '1.3.3', 'latest version is 1.3.3') 32 | t.end() 33 | }) 34 | }) 35 | 36 | test('npm client - get services', t => { 37 | exec 38 | .withArgs('npm v sample@1.3.3 services') 39 | .callsArgWith(2, null, "\n{'sample2': '^2.0.0'}\n") 40 | 41 | npm.getServices('sample@1.3.3') 42 | .then(services => { 43 | t.equal(services.constructor, Object, 'services is an object') 44 | t.equal(services.sample2, '^2.0.0', 'sample2@^2.0.0 is a service dependency') 45 | t.end() 46 | }) 47 | }) 48 | 49 | test.onFinish(() => { 50 | cp.exec.restore() 51 | }) 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "now-fleet", 3 | "version": "3.0.9", 4 | "description": "Helps with multiple microservice deployments", 5 | "main": "./lib/index.js", 6 | "bin": { 7 | "now-fleet-deploy": "bin/deploy.js" 8 | }, 9 | "scripts": { 10 | "test": "(ducktape; node test) | tap-difflet && standard", 11 | "watch": "nodemon test | tap-difflet", 12 | "cover": "istanbul cover --report none --print detail test", 13 | "docs": "node_modules/vigour-doc/bin/vdoc", 14 | "travis": "npm run cover -s && istanbul report lcov && ((cat coverage/lcov.info | coveralls) || exit 0) && standard", 15 | "perf-browser": "budo ./test/performance/index.js -p 8080 --live" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/vigour-io/now-fleet.git" 20 | }, 21 | "keywords": [ 22 | "now", 23 | "npm", 24 | "deploy", 25 | "discover" 26 | ], 27 | "author": "Mustafa Dokumacı", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/vigour-io/now-fleet/issues" 31 | }, 32 | "homepage": "https://github.com/vigour-io/now-fleet#readme", 33 | "dependencies": { 34 | "brisky-hub": "^1.1.0", 35 | "observe-now": "^3.0.0" 36 | }, 37 | "devDependencies": { 38 | "tape": "^4.4.0", 39 | "sinon": "^1.17.0", 40 | "ducktape": "^1.0.0", 41 | "tap-difflet": "0.4.0", 42 | "nodemon": "^1.9.1", 43 | "coveralls": "^2.11.9", 44 | "istanbul": "^0.4.3", 45 | "standard": "^7.0.1", 46 | "vigour-doc": "^1.1.5", 47 | "pre-commit": "^1.1.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # now-fleet 2 | 3 | 4 | [![Build Status](https://travis-ci.org/vigour-io/now-fleet.svg?branch=master)](https://travis-ci.org/vigour-io/now-fleet) 5 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 6 | [![npm version](https://badge.fury.io/js/now-fleet.svg)](https://badge.fury.io/js/now-fleet) 7 | [![Coverage Status](https://coveralls.io/repos/github/vigour-io/now-fleet/badge.svg?branch=master)](https://coveralls.io/github/vigour-io/now-fleet?branch=master) 8 | 9 | 10 | An api to make it easy to deploy complete infrastructures of microservices using now 11 | 12 | ## Installing 13 | 14 | ```bash 15 | npm install now-fleet --save 16 | ``` 17 | 18 | ## Service Dependency 19 | We define service dependencies in `package.json` of each service. Let's say we have a service A depending on service B and C, `package.json` for A should have a `services` field as following: 20 | ```json 21 | { 22 | "services": { 23 | "serviceB": "^2.0.0", 24 | "serviceC": "^3.0.0" 25 | } 26 | } 27 | ``` 28 | 29 | Let's say B also depends on C. Then `package.json` for B should have a `services` field as following: 30 | ```json 31 | { 32 | "services": { 33 | "serviceC": "^2.0.0" 34 | } 35 | } 36 | ``` 37 | 38 | Service dependencies need a version like npm module dependencies. This version should be a **published version of npm module** for dependency service. For our example, service A depends on `^3.0.0` of C but service B depends on `^2.0.0` of C. 39 | 40 | ## Fleet Deployment 41 | We start deployment from the topmost service which depends on other services, for our example service A. Let's call it **root service**. 42 | 43 | ```bash 44 | export NOW_TOKEN="YOUR-NOW-API-TOKEN" 45 | export REGISTRY_HOST="registery.host.sh" 46 | node_modules/.bin/now-fleet-deploy type=ENV_TYPE 47 | ``` 48 | 49 | This script walks through all the services we depend on and dependencies of them recursively. Deploys them to now and gives us now url of root service. 50 | 51 | ### Root Service Decision 52 | Deployment script should run on a service considering dependency tree. It can only walk down from top and can't discover dependants magically. If there is a service in the stack which is not a dependency of any other service, it won't be discovered and should be deployed separately. 53 | 54 | ### Circular Dependency 55 | Circular dependencies are taken care at deployment time and all fine. A can depend on B and B can depend on A at the same time or while A depends on B and B depends on C; C can depend on A. 56 | 57 | ### Limitations 58 | Another version of root service can't be a dependency of any service in the tree. For example `serviceA@2.0.0` is deployment root and depends on, service B and service C. This schema allows service B or C depending on `serviceA@2.0.0` but not on `serviceA@1.0.0`. 59 | 60 | ## Service Discovery 61 | Each service should discover dependency urls on the boot time. This module provides a method for discovery. 62 | 63 | ### getServices(pkg, delay) 64 | Discovers host names of dependencies defined in `package.json` by polling the latest deployed services from now API. Takes care of finding the host name for the right version of the dependency service. 65 | Second parameter is the delay in miliseconds between each polling from now API. It'll repeat until discovering all the deployments. 66 | 67 | ```js 68 | const fleet = require('now-fleet').fleet 69 | 70 | const pkg = require('./package.json') 71 | 72 | fleet.getServices(pkg, 2000) 73 | .then(pkg => { 74 | // pkg._services object has all the host names 75 | // pkg._services.serviceC is something like url-sdfsd.now.sh 76 | }) 77 | ``` 78 | -------------------------------------------------------------------------------- /lib/fleet.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const fs = require('fs') 5 | 6 | const now = require('observe-now') 7 | const Hub = require('brisky-hub') 8 | 9 | const npm = require('./npm') 10 | const command = require('./command') 11 | 12 | const hubId = +new Date() 13 | 14 | const getList = () => new Promise((resolve, reject) => { 15 | const registry = new Hub({ 16 | id: hubId, 17 | url: `wss://${process.env.REGISTRY_HOST}`, 18 | context: false 19 | }) 20 | 21 | const prepare = () => { 22 | const deployments = registry.get('deployments') 23 | 24 | var list = [] 25 | 26 | deployments.each(dep => { 27 | if (!dep.get([ 'pkg', 'version' ])) { 28 | return 29 | } 30 | 31 | const deployment = { 32 | name: dep.name.compute(), 33 | version: dep.pkg.version.compute(), 34 | env: dep.pkg.env.compute(), 35 | url: dep.url.compute(), 36 | created: dep.created.compute() 37 | } 38 | 39 | const found = list.find( 40 | d => d.name === deployment.name && d.version === deployment.version && d.env === deployment.env 41 | ) 42 | 43 | if (found && found.created < deployment.created) { 44 | found.url = deployment.url 45 | found.created = deployment.created 46 | } else if (!found) { 47 | list.push(deployment) 48 | } 49 | }) 50 | 51 | list.sort((a, b) => { 52 | return a.created > b.created ? -1 : b.created > a.created ? 1 : 0 53 | }) 54 | 55 | resolve(list) 56 | 57 | registry.set(null) 58 | } 59 | 60 | var timeout = setTimeout(() => { 61 | clearTimeout(timeout) 62 | reject(new Error('Failed to connect to registry hub and retrieve deployments.')) 63 | registry.set(null) 64 | }, 20e3) 65 | 66 | registry.subscribe({ deployments: { val: true } }, val => { 67 | clearTimeout(timeout) 68 | timeout = setTimeout(prepare, 500) 69 | registry.off('subscription', 'deps') 70 | }, null, null, null, 'deps') 71 | }) 72 | 73 | var data = exports.data = {} 74 | 75 | function resetData () { 76 | data.deployments = [] 77 | data.servicesFlat = [] 78 | data.dir = '' 79 | data.env = '' 80 | } 81 | 82 | exports.getServices = (pkg, dir, env) => { 83 | if (!process.env.REGISTRY_HOST) { 84 | console.log('env.REGISTRY_HOST is not in place!') 85 | process.exit(1) 86 | } 87 | 88 | resetData() 89 | 90 | var rootService 91 | 92 | if (pkg._services) { 93 | // is this already deployed? then we are discovering 94 | const delay = dir && Number(dir) > 0 ? Number(dir) : 3e3 95 | 96 | return getList() 97 | .then((deployments) => { 98 | const notDiscovered = Object.keys(pkg._services).find(name => { 99 | const service = pkg._services[name] 100 | 101 | if (service.constructor === String) { 102 | return false 103 | } 104 | 105 | const found = deployments.find( 106 | d => d.name === name && d.version === service.version && d.env === pkg._env && d.created > service.lastDeploy 107 | ) 108 | 109 | if (found) { 110 | pkg._services[name] = found.url 111 | } 112 | 113 | return !found 114 | }) 115 | 116 | // Check if we still have any services not discovered 117 | if (notDiscovered) { 118 | return (new Promise(resolve => setTimeout(resolve, delay))) 119 | .then(() => exports.getServices(pkg, dir)) 120 | } 121 | 122 | return pkg 123 | }) 124 | } else { 125 | // if not deployed yet then we are deploying 126 | data.dir = dir && dir.constructor === String ? dir : process.cwd() 127 | data.env = env || process.env.envSet || '' 128 | 129 | return getList() 130 | .then((list) => { data.deployments = list }) 131 | .then(() => exports.addDependencies(exports.addService(pkg.name, pkg.version), pkg.services || {})) 132 | .then(() => { 133 | var other = data.servicesFlat.find(service => service.name === pkg.name && service.version !== pkg.version) 134 | 135 | if (other) { 136 | throw new Error(`Can not depend on a different version of root module: ${other.name}@${other.version}`) 137 | } 138 | 139 | // take the root service out of flat array 140 | rootService = data.servicesFlat.shift() 141 | 142 | // prepare install string 143 | const install = data.servicesFlat.filter(s => s.deploy).map(s => `${s.name}@${s.version}`).join(' ') 144 | 145 | if (install.length) { 146 | // npm install all together 147 | console.log(`NowFleet: npm installing ${install}...`) 148 | return command.run(`npm install ${install}`, dir) 149 | } 150 | }) 151 | // then deploy all the other services in the tree 152 | .then(() => Promise.all(data.servicesFlat.map(deploy))) 153 | // prepare package for the root service last 154 | .then(() => preparePkg(rootService, Object.assign({}, pkg))) 155 | } 156 | } 157 | 158 | exports.addService = (name, version) => { 159 | var service = { 160 | name, version, 161 | dependants: [], dependencies: [], 162 | deploy: true, lastDeploy: 0, lastUrl: '' 163 | } 164 | data.servicesFlat.push(service) 165 | 166 | const found = data.deployments.find(d => d.name === name && d.version === version && d.env === data.env) 167 | 168 | if (found) { 169 | console.log(`NowFleet: skipped deploying ${found.name}@${found.version} for ${found.env} ${found.url}`) 170 | service.lastDeploy = found.created 171 | service.lastUrl = found.url 172 | service.deploy = false 173 | } 174 | 175 | return service 176 | } 177 | 178 | exports.addDependencies = (dependant, dependencies) => { 179 | return Promise.all(Object.keys(dependencies).map(name => { 180 | var version = dependencies[name] 181 | 182 | return npm.getLastVersion(`${name}@${version}`) 183 | .then(latest => { 184 | var dependency = data.servicesFlat.find(s => s.name === name && s.version === latest) 185 | 186 | if (dependency) { 187 | // just wire it, if same version of this service is already in flat list 188 | return wireDependency(dependant, dependency) 189 | } 190 | 191 | dependency = exports.addService(name, latest) 192 | 193 | wireDependency(dependant, dependency) 194 | 195 | // go for dependencies recursively 196 | return npm.getServices(`${name}@${latest}`) 197 | .then(services => exports.addDependencies(dependency, services)) 198 | }) 199 | })) 200 | } 201 | 202 | function deploy (service) { 203 | if (!service.deploy) { 204 | // skip service if it does not need a deploy 205 | return 206 | } 207 | 208 | // calculate the local directory of module 209 | const dir = path.join(data.dir, 'node_modules', service.name) 210 | 211 | exports.setPkg(dir, preparePkg(service, exports.getPkg(dir))) 212 | 213 | fs.writeFileSync(path.join(dir, '.npmrc'), `//registry.npmjs.org/:_authToken=${process.env.NPM_TOKEN}`) 214 | var env = { REGISTRY_HOST: process.env.REGISTRY_HOST } 215 | data.env.split('&').forEach(pair => { 216 | const [key, val] = pair.split('=') 217 | env[key] = val 218 | }) 219 | 220 | console.log(`NowFleet: deploying ${service.name}@${service.version} with ${data.env}`) 221 | 222 | return new Promise((resolve, reject) => { 223 | const sdep = now.deployment(process.env.NOW_TOKEN) 224 | .deploy(dir, env) 225 | .on('deployed', () => { 226 | console.log(`NowFleet: ${service.name}@${service.version} deployed:`, sdep.get('url').compute(), 'waiting until ready...') 227 | }) 228 | .on('ready', () => { 229 | console.log(`NowFleet: ${service.name}@${service.version} is ready.`) 230 | service.lastUrl = sdep.get('url').compute().replace(/^https:\/\//, '') 231 | service.deploy = false 232 | resolve() 233 | sdep.set(null) 234 | }) 235 | .on('error', reject) 236 | }) 237 | .then(() => command.run(`rm -r ${dir}`)) 238 | } 239 | 240 | function wireDependency (dependant, dependency) { 241 | dependency.dependants.push(dependant) 242 | dependant.dependencies.push(dependency) 243 | if (dependency.deploy) { 244 | deployDependant(dependant) 245 | } 246 | } 247 | 248 | function deployDependant (dependant) { 249 | // stop when a marked dependant found 250 | // this is necessary to avoid indefinite recursion 251 | if (dependant.deploy) { return } 252 | 253 | dependant.deploy = true 254 | 255 | // mark dependents of dependents to deploy recursively 256 | dependant.dependants.forEach(deployDependant) 257 | } 258 | 259 | function preparePkg (service, pkg) { 260 | var _services = {} 261 | service.dependencies.forEach(dependency => { 262 | _services[dependency.name] = dependency.deploy ? { 263 | // discovery will try finding this version of service 264 | // deployed after lastDeploy time 265 | version: dependency.version, 266 | lastDeploy: dependency.lastDeploy 267 | 268 | // set the url directly if we already know it 269 | } : dependency.lastUrl 270 | }) 271 | pkg._services = _services 272 | pkg._env = data.env 273 | delete pkg.devDependencies 274 | return pkg 275 | } 276 | 277 | exports.getPkg = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'package.json'))) 278 | exports.setPkg = (dir, pkg) => fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n') 279 | -------------------------------------------------------------------------------- /test/fleet.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('tape') 4 | const sinon = require('sinon') 5 | const path = require('path') 6 | const fs = require('fs') 7 | 8 | const Hub = require('brisky-hub') 9 | const now = require('observe-now') 10 | 11 | const fleet = require('../lib/fleet') 12 | const npm = require('../lib/npm') 13 | const command = require('../lib/command') 14 | 15 | process.env.REGISTRY_HOST = 'REGISTRY-HOST' 16 | process.env.NPM_TOKEN = 'NPM-TOKEN' 17 | process.env.NOW_TOKEN = 'NOW-TOKEN' 18 | 19 | const deployments = [ 20 | {name: 's1', version: '1', env: 'a=b', url: 'u3.sh', created: 13}, 21 | {name: 's1', version: '1', env: 'a=c', url: 'u2.sh', created: 12}, 22 | {name: 's1', version: '2', env: 'c=d', url: 'u4.sh', created: 21}, 23 | {name: 's1', version: '2', env: 'a=b&c=d', url: 'u5.sh', created: 22}, 24 | {name: 's2', version: '1', env: 'c=d', url: 'u6.sh', created: 11}, 25 | {name: 's2', version: '2', env: 'a=b', url: 'u7.sh', created: 21}, 26 | {name: 's2', version: '2', env: 'a=b&c=d', url: 'u8.sh', created: 22}, 27 | {name: 's3', version: '1', env: 'a=b&c=d', url: 'u9.sh', created: 11}, 28 | {name: 's4', version: '1', env: 'a=b&c=d', url: 'u10.sh', created: 11} 29 | ] 30 | 31 | const registryDeployments = new Hub({ 32 | 1: { name: 's1', url: 'u1.sh', created: 11, pkg: { version: '1', env: 'a=b', routes: {}, wrapper: {} } }, 33 | 2: { name: 's1', url: 'u2.sh', created: 12, pkg: { version: '1', env: 'a=c', routes: {}, wrapper: {} } }, 34 | 3: { name: 's1', url: 'u3.sh', created: 13, pkg: { version: '1', env: 'a=b', routes: {}, wrapper: {} } }, 35 | 4: { name: 's1', url: 'u4.sh', created: 21, pkg: { version: '2', env: 'c=d', routes: {}, wrapper: {} } }, 36 | 5: { name: 's1', url: 'u5.sh', created: 22, pkg: { version: '2', env: 'a=b&c=d', routes: {}, wrapper: {} } }, 37 | 6: { name: 's2', url: 'u6.sh', created: 11, pkg: { version: '1', env: 'c=d', routes: {}, wrapper: {} } }, 38 | 7: { name: 's2', url: 'u7.sh', created: 21, pkg: { version: '2', env: 'a=b', routes: {}, wrapper: {} } }, 39 | 8: { name: 's2', url: 'u8.sh', created: 22, pkg: { version: '2', env: 'a=b&c=d', routes: {}, wrapper: {} } }, 40 | 9: { name: 's3', url: 'u9.sh', created: 11, pkg: { version: '1', env: 'a=b&c=d', routes: {}, wrapper: {} } }, 41 | 10: { name: 's4', url: 'u10.sh', created: 11, pkg: { version: '1', env: 'a=b&c=d', routes: {}, wrapper: {} } }, 42 | 99: { name: 's10', url: 'u99.sh', created: 11, pkg: {} } 43 | }) 44 | 45 | test('services - prepare flat services list', t => { 46 | sinon.stub(npm, 'getLastVersion', v => Promise.resolve(v.split('@').pop().slice(1))) 47 | 48 | const getServices = sinon.stub(npm, 'getServices') 49 | getServices.withArgs('s2@2').returns(Promise.resolve({})) 50 | getServices.withArgs('s3@1').returns(Promise.resolve({ 's4': '^2' })) 51 | getServices.withArgs('s4@2').returns(Promise.resolve({ 's2': '^2' })) 52 | 53 | fleet.data.deployments = deployments 54 | fleet.data.servicesFlat = [] 55 | fleet.data.env = 'a=b&c=d' 56 | fleet.addDependencies(fleet.addService('s1', '2'), { 's2': '^2', 's3': '^1' }) 57 | .then(() => { 58 | var s1 = { 59 | name: 's1', version: '2', 60 | deploy: true, lastDeploy: 22, lastUrl: 'u5.sh' 61 | } 62 | var s2 = { 63 | name: 's2', version: '2', 64 | deploy: false, lastDeploy: 22, lastUrl: 'u8.sh' 65 | } 66 | var s3 = { 67 | name: 's3', version: '1', 68 | deploy: true, lastDeploy: 11, lastUrl: 'u9.sh' 69 | } 70 | var s4 = { 71 | name: 's4', version: '2', 72 | deploy: true, lastDeploy: 0, lastUrl: '' 73 | } 74 | 75 | var dependencies = {} 76 | fleet.data.servicesFlat.forEach(service => { 77 | dependencies[service.name] = { 78 | dependencies: service.dependencies.length, 79 | dependants: service.dependants.length 80 | } 81 | // can not check dependencies with deep equal 82 | // because it has self reference 83 | delete service.dependants 84 | delete service.dependencies 85 | }) 86 | t.deepEqual(fleet.data.servicesFlat, [s1, s2, s3, s4], 'flat services list is as expected') 87 | t.deepEqual(dependencies, { 88 | s1: {dependencies: 2, dependants: 0}, 89 | s2: {dependencies: 0, dependants: 2}, 90 | s3: {dependencies: 1, dependants: 1}, 91 | s4: {dependencies: 1, dependants: 1} 92 | }, 'dependency counts are as expected') 93 | t.end() 94 | 95 | npm.getLastVersion.restore() 96 | npm.getServices.restore() 97 | }) 98 | }) 99 | 100 | test('services - prepare flat services list for circular dependency', t => { 101 | sinon.stub(npm, 'getLastVersion', v => Promise.resolve(v.split('@').pop().slice(1))) 102 | 103 | const getServices = sinon.stub(npm, 'getServices') 104 | getServices.withArgs('s2@3').returns(Promise.resolve({ 's3': '^1' })) 105 | getServices.withArgs('s3@1').returns(Promise.resolve({ 's4': '^1' })) 106 | getServices.withArgs('s4@1').returns(Promise.resolve({ 's1': '^1' })) 107 | 108 | fleet.data.deployments = deployments 109 | fleet.data.servicesFlat = [] 110 | fleet.data.env = 'a=b&c=d' 111 | fleet.addDependencies(fleet.addService('s1', '1'), { 's2': '^3' }) 112 | .then(() => { 113 | var s1 = { 114 | name: 's1', version: '1', 115 | deploy: true, lastDeploy: 0, lastUrl: '' 116 | } 117 | var s2 = { 118 | name: 's2', version: '3', 119 | deploy: true, lastDeploy: 0, lastUrl: '' 120 | } 121 | var s3 = { 122 | name: 's3', version: '1', 123 | deploy: true, lastDeploy: 11, lastUrl: 'u9.sh' 124 | } 125 | var s4 = { 126 | name: 's4', version: '1', 127 | deploy: true, lastDeploy: 11, lastUrl: 'u10.sh' 128 | } 129 | 130 | var dependencies = {} 131 | fleet.data.servicesFlat.forEach(service => { 132 | dependencies[service.name] = { 133 | dependencies: service.dependencies.length, 134 | dependants: service.dependants.length 135 | } 136 | // can not check dependencies with deep equal 137 | // because it has self reference 138 | delete service.dependants 139 | delete service.dependencies 140 | }) 141 | t.deepEqual(fleet.data.servicesFlat, [s1, s2, s3, s4], 'flat services list is as expected') 142 | t.deepEqual(dependencies, { 143 | s1: {dependencies: 1, dependants: 1}, 144 | s2: {dependencies: 1, dependants: 1}, 145 | s3: {dependencies: 1, dependants: 1}, 146 | s4: {dependencies: 1, dependants: 1} 147 | }, 'dependency counts are as expected') 148 | t.end() 149 | 150 | npm.getLastVersion.restore() 151 | npm.getServices.restore() 152 | }) 153 | }) 154 | 155 | test('services - deploy all with error', t => { 156 | const subscribe = sinon.stub(Hub.prototype, 'subscribe') 157 | const get = sinon.stub(Hub.prototype, 'get') 158 | 159 | subscribe 160 | .withArgs({ deployments: { val: true } }) 161 | .callsArg(1) 162 | 163 | get 164 | .withArgs('deployments') 165 | .returns(registryDeployments) 166 | 167 | sinon.stub(npm, 'getLastVersion', v => Promise.resolve(v.split('@').pop().slice(1))) 168 | 169 | const getServices = sinon.stub(npm, 'getServices') 170 | getServices.withArgs('s2@2').returns(Promise.resolve({})) 171 | getServices.withArgs('s3@1').returns(Promise.resolve({ 's4': '^2' })) 172 | getServices.withArgs('s4@2').returns(Promise.resolve({ 's2': '^2', 's1': '^1' })) 173 | getServices.withArgs('s1@1').returns(Promise.resolve({})) 174 | 175 | const pkg = { 176 | name: 's1', version: 's2', 177 | services: { 's2': '^2', 's3': '^1' } 178 | } 179 | fleet.data.servicesFlat = [] 180 | fleet.getServices(pkg, 'directory') 181 | .catch((error) => { 182 | t.equal(error.message, 'Can not depend on a different version of root module: s1@1', 'error caught') 183 | t.end() 184 | 185 | subscribe.restore() 186 | get.restore() 187 | npm.getLastVersion.restore() 188 | npm.getServices.restore() 189 | }) 190 | }) 191 | 192 | test('services - deploy all successfuly', t => { 193 | const subscribe = sinon.stub(Hub.prototype, 'subscribe') 194 | const get = sinon.stub(Hub.prototype, 'get') 195 | 196 | subscribe 197 | .withArgs({ deployments: { val: true } }) 198 | .callsArg(1) 199 | 200 | get 201 | .withArgs('deployments') 202 | .returns(registryDeployments) 203 | 204 | const pkg = { 205 | name: 's1', version: '2', 206 | services: { 's2': '^2', 's3': '^1' } 207 | } 208 | 209 | const readFileSync = sinon.stub(fs, 'readFileSync') 210 | readFileSync.returns('{}') 211 | 212 | sinon.stub(npm, 'getLastVersion', v => Promise.resolve(v.split('@').pop().slice(1))) 213 | 214 | const getServices = sinon.stub(npm, 'getServices') 215 | getServices.withArgs('s2@2').returns(Promise.resolve({})) 216 | getServices.withArgs('s3@1').returns(Promise.resolve({ 's4': '^2' })) 217 | getServices.withArgs('s4@2').returns(Promise.resolve({ 's2': '^2' })) 218 | 219 | var writeFileSyncArgs = {} 220 | sinon.stub(fs, 'writeFileSync', (file, data) => { 221 | const parsed = path.parse(file) 222 | if (parsed.name === '.npmrc') { 223 | return 224 | } 225 | writeFileSyncArgs[parsed.dir] = JSON.parse(data) 226 | }) 227 | 228 | const run = sinon.stub(command, 'run') 229 | run.returns(Promise.resolve()) 230 | 231 | const nowDeploy = sinon.stub(now, 'deployment') 232 | const nowDeployment = nowDeploy.withArgs('NOW-TOKEN') 233 | 234 | nowDeployment 235 | .onFirstCall() 236 | .returns({ 237 | on (e, cb) { 238 | if (e !== 'error') { 239 | setImmediate(cb) 240 | } 241 | return this 242 | }, 243 | deploy (dir, env) { 244 | t.deepEqual(env, { a: 'b', c: 'd', REGISTRY_HOST: 'REGISTRY-HOST' }, 'deploys with right env') 245 | t.equal(dir, path.join('directory', 'node_modules', 's3'), 'deploys s3 folder') 246 | return this 247 | }, 248 | get (key) { 249 | return { 250 | compute () { 251 | return { 252 | url: 'u3.sh', 253 | id: 's3' 254 | }[ key ] 255 | } 256 | } 257 | }, 258 | set () {} 259 | }) 260 | 261 | nowDeployment 262 | .onSecondCall() 263 | .returns({ 264 | on (e, cb) { 265 | if (e !== 'error') { 266 | setImmediate(cb) 267 | } 268 | return this 269 | }, 270 | deploy (dir, env) { 271 | t.deepEqual(env, { a: 'b', c: 'd', REGISTRY_HOST: 'REGISTRY-HOST' }, 'deploys with right env') 272 | t.equal(dir, path.join('directory', 'node_modules', 's4'), 'deploys s4 folder') 273 | return this 274 | }, 275 | get (key) { 276 | return { 277 | compute () { 278 | return { 279 | url: 'u4.sh', 280 | id: 's4' 281 | }[ key ] 282 | } 283 | } 284 | }, 285 | set () {} 286 | }) 287 | 288 | fleet.data.servicesFlat = [] 289 | fleet.getServices(pkg, 'directory', 'a=b&c=d') 290 | .then(() => { 291 | t.deepEqual(writeFileSyncArgs, { 292 | 'directory/node_modules/s3': { 293 | _services: { 294 | 's4': { version: '2', lastDeploy: 0 } 295 | }, 296 | _env: 'a=b&c=d' 297 | }, 298 | 'directory/node_modules/s4': { 299 | _services: { 300 | 's2': 'u8.sh' 301 | }, 302 | _env: 'a=b&c=d' 303 | } 304 | }, 'package.json files are prepared') 305 | t.ok(run.getCall(0).calledWith('npm install s3@1 s4@2', 'directory'), 'npm installed services') 306 | t.ok(run.getCall(1).calledWith('rm -r directory/node_modules/s3'), 'removed s3') 307 | t.ok(run.getCall(2).calledWith('rm -r directory/node_modules/s4'), 'removed s4') 308 | t.end() 309 | 310 | subscribe.restore() 311 | get.restore() 312 | npm.getLastVersion.restore() 313 | npm.getServices.restore() 314 | fs.readFileSync.restore() 315 | fs.writeFileSync.restore() 316 | run.restore() 317 | nowDeploy.restore() 318 | }) 319 | }) 320 | 321 | test('services - discover services', t => { 322 | const subscribe = sinon.stub(Hub.prototype, 'subscribe') 323 | const get = sinon.stub(Hub.prototype, 'get') 324 | 325 | subscribe 326 | .withArgs({ deployments: { val: true } }) 327 | .callsArg(1) 328 | 329 | get 330 | .withArgs('deployments') 331 | .returns(registryDeployments) 332 | 333 | var pkg = { 334 | _services: { 335 | 's2': 'u8.sh', 336 | 's3': { version: '1', lastDeploy: 11 }, 337 | 's4': { version: '2', lastDeploy: 11 } 338 | }, 339 | _env: 'a=b&c=d' 340 | } 341 | 342 | fleet.getServices(pkg, 1) 343 | .then((pkg) => { 344 | t.deepEqual(pkg._services, { 345 | 's2': 'u8.sh', 346 | 's3': 'u12.sh', 347 | 's4': 'u11.sh' 348 | }, 'services should be discovered as expected') 349 | t.end() 350 | 351 | subscribe.restore() 352 | get.restore() 353 | }) 354 | 355 | const s4New = { 11: { name: 's4', url: 'u11.sh', created: 12, pkg: { version: '2', env: 'a=b&c=d', routes: {}, wrapper: {} } } } 356 | const s3New = { 12: { name: 's3', url: 'u12.sh', created: 12, pkg: { version: '1', env: 'a=b&c=d', routes: {}, wrapper: {} } } } 357 | 358 | setTimeout(() => { 359 | registryDeployments.set(s4New) 360 | setTimeout(() => { 361 | registryDeployments.set(s3New) 362 | }, 300) 363 | }, 300) 364 | }) 365 | --------------------------------------------------------------------------------