├── .buildkite ├── hooks │ └── pre-command.sh ├── pipeline.yml └── steps │ ├── build.sh │ └── push.sh ├── .gitignore ├── configure ├── .babelrc ├── .dockerignore ├── .eslintrc ├── .gitignore ├── Dockerfile ├── fixtures │ ├── haproxy-no-health-check.cfg │ └── haproxy.cfg ├── package.json ├── src │ ├── args.js │ ├── cmds │ │ ├── build.js │ │ ├── deploy.js │ │ └── push.js │ ├── compose-file.js │ ├── compose-file.spec.js │ ├── config.js │ ├── config.spec.js │ ├── deploy.js │ ├── deploy.spec.js │ ├── docker-server.js │ ├── docker-server.spec.js │ ├── haproxy.js │ ├── haproxy.spec.js │ ├── index.js │ ├── log.js │ ├── ports.js │ ├── ports.spec.js │ ├── services.js │ └── services.spec.js └── yarn.lock ├── doc └── visualizer.png ├── example ├── .gitignore ├── _docker.yml ├── _stacks.yml ├── api │ ├── .babelrc │ ├── .dockerignore │ ├── Dockerfile │ ├── index.js │ ├── package.json │ ├── src │ │ ├── resolvers.js │ │ ├── schema.js │ │ └── server.js │ └── yarn.lock ├── app.yml ├── gateway │ ├── .dockerignore │ ├── Dockerfile │ └── main.go ├── rproxy │ ├── Dockerfile │ ├── docker-entrypoint.sh │ ├── haproxy.cfg │ └── rsyslog.conf ├── services.yml └── web │ ├── .dockerignore │ ├── Dockerfile │ ├── components │ └── layout.js │ ├── package.json │ ├── pages │ ├── _health.js │ ├── about.js │ └── index.js │ └── yarn.lock ├── provisioning ├── aws │ ├── .gitignore │ ├── assume-role.tf │ ├── container-linux-config │ │ ├── manager.sh │ │ ├── manager.yml │ │ ├── worker.sh │ │ └── worker.yml │ ├── doc │ │ ├── graph.pdf │ │ ├── vpc-3azs.png │ │ ├── vpc-nat-gateway.png │ │ └── vpc-ssh-bastion.png │ ├── main.tf │ ├── nodes-managers.tf │ ├── nodes-workers.tf │ ├── nodes.tf │ ├── readme.md │ ├── register-dns.tf │ ├── register-dns │ │ ├── .babelrc │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src │ │ │ └── index.js │ │ ├── webpack.config.js │ │ └── yarn.lock │ ├── tokens.tf │ └── variables.tf ├── gcp │ ├── .gitignore │ ├── README.md │ ├── cloud-init │ │ ├── coreos-manager.yml │ │ ├── coreos-worker.yml │ │ └── rancher-worker.yml │ ├── main.tf │ ├── managers.tf │ ├── networks.tf │ ├── storage.tf │ ├── variables.tf │ └── workers.tf └── osx │ ├── dns.sh │ ├── docker-compose-dns.yml │ ├── docker-compose-load-balancer.yml │ ├── docker-compose-registry-ambassador.yml │ ├── docker-compose-registry.yml │ ├── load-balancer.sh │ ├── local-cleanup.sh │ ├── on-local.sh │ ├── on-swarm.sh │ ├── point-to-local.sh │ ├── point-to-swarm.sh │ ├── registry.sh │ ├── swarm-cleanup.sh │ └── swarm.sh └── readme.md /.buildkite/hooks/pre-command.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | -------------------------------------------------------------------------------- /.buildkite/pipeline.yml: -------------------------------------------------------------------------------- 1 | env: 2 | registry: eu.gcr.io/microplatform-demo 3 | 4 | steps: 5 | 6 | - label: ":docker: Build" 7 | command: bash .buildkite/steps/build.sh 8 | 9 | - label: ":node: configure cli tests" 10 | command: cd ./configure && docker build . 11 | 12 | - wait: 13 | -------------------------------------------------------------------------------- /.buildkite/steps/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | env 6 | export registry=localhost:5000 7 | export tag=latest 8 | 9 | cd example 10 | 11 | cat <app-ports.yml 12 | version: "3.1" 13 | 14 | services: 15 | rproxy: 16 | ports: 17 | - 8001:3000 18 | EOF 19 | 20 | cat <services-ports.yml 21 | version: "3.1" 22 | 23 | services: 24 | visualizer: 25 | ports: 26 | - 8000:3000 27 | EOF 28 | 29 | for stack in "app" "services"; do 30 | docker-compose -f ${stack}.yml -f ${stack}-ports.yml config >${stack}-unresolved.yml 31 | docker-compose -f ${stack}-unresolved.yml build 32 | done 33 | -------------------------------------------------------------------------------- /.buildkite/steps/push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | env 6 | 7 | # install gcloud 8 | # credentials.json 9 | # gcloud docker -a 10 | 11 | 12 | for file in "docker-compose-app.yml" "docker-compose-services.yml"; do 13 | docker-compose -f $file push 14 | done 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /configure/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-object-rest-spread"], 3 | "presets": [ 4 | "babel-preset-power-assert", 5 | [ 6 | "env", 7 | { 8 | "targets": { 9 | "node": "current" 10 | } 11 | } 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /configure/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile 4 | -------------------------------------------------------------------------------- /configure/.eslintrc: -------------------------------------------------------------------------------- 1 | extends: "airbnb-base" 2 | env: 3 | es6: true 4 | mocha: true 5 | rules: 6 | arrow-parens: ["error", "as-needed"] 7 | no-await-in-loop: 0 8 | no-continue: 0 9 | no-plusplus: 0 10 | no-restricted-syntax: 0 11 | no-shadow: 0 12 | import/prefer-default-export: 0 13 | -------------------------------------------------------------------------------- /configure/.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | -------------------------------------------------------------------------------- /configure/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | WORKDIR /home/node/app 4 | 5 | COPY package.json yarn.lock ./ 6 | RUN yarn 7 | 8 | COPY . ./ 9 | RUN yarn test 10 | -------------------------------------------------------------------------------- /configure/fixtures/haproxy-no-health-check.cfg: -------------------------------------------------------------------------------- 1 | global 2 | maxconn 4096 3 | log 127.0.0.1:514 local2 debug 4 | 5 | defaults 6 | mode http 7 | log global 8 | option httplog 9 | # option logasap 10 | option dontlognull 11 | option log-health-checks 12 | option forwardfor 13 | option contstats 14 | option http-server-close 15 | retries 3 16 | option redispatch 17 | timeout connect 5s 18 | timeout client 30s 19 | timeout server 30s 20 | 21 | frontend http_front 22 | bind *:80 23 | stats enable 24 | stats uri /haproxy?stats 25 | use_backend %[req.hdr(host),lower] 26 | 27 | backend visualizer.services.local 28 | balance roundrobin 29 | 30 | server web wkr1:8000 check 31 | server web wkr2:8000 check 32 | server web wkr3:8000 check 33 | 34 | backend rproxy.app.local 35 | balance roundrobin 36 | option httpchk GET /status 37 | server web wkr1:8001 check 38 | server web wkr2:8001 check 39 | server web wkr3:8001 check 40 | 41 | backend web.app.local 42 | balance roundrobin 43 | option httpchk GET /status 44 | server web wkr1:8001 check 45 | server web wkr2:8001 check 46 | server web wkr3:8001 check 47 | -------------------------------------------------------------------------------- /configure/fixtures/haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | maxconn 4096 3 | log 127.0.0.1:514 local2 debug 4 | 5 | defaults 6 | mode http 7 | log global 8 | option httplog 9 | # option logasap 10 | option dontlognull 11 | option log-health-checks 12 | option forwardfor 13 | option contstats 14 | option http-server-close 15 | retries 3 16 | option redispatch 17 | timeout connect 5s 18 | timeout client 30s 19 | timeout server 30s 20 | 21 | frontend http_front 22 | bind *:80 23 | stats enable 24 | stats uri /haproxy?stats 25 | use_backend %[req.hdr(host),lower] 26 | 27 | backend visualizer.services.local 28 | balance roundrobin 29 | option httpchk GET /_health 30 | server web wkr1:8000 check 31 | server web wkr2:8000 check 32 | server web wkr3:8000 check 33 | 34 | backend rproxy.app.local 35 | balance roundrobin 36 | option httpchk GET /status 37 | server web wkr1:8001 check 38 | server web wkr2:8001 check 39 | server web wkr3:8001 check 40 | 41 | backend web.app.local 42 | balance roundrobin 43 | option httpchk GET /status 44 | server web wkr1:8001 check 45 | server web wkr2:8001 check 46 | server web wkr3:8001 check 47 | -------------------------------------------------------------------------------- /configure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "configure", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "babel src -d lib", 8 | "test": "yarn build && mocha \"lib/**/*.spec.js\"" 9 | }, 10 | "author": "Stuart Harris ", 11 | "license": "ISC", 12 | "dependencies": { 13 | "bluebird": "^3.5.0", 14 | "chalk": "^2.1.0", 15 | "docker-machine": "^2.1.0", 16 | "dockerode": "^2.5.1", 17 | "execa": "^0.8.0", 18 | "get-stream": "^3.0.0", 19 | "git-repo-info": "^1.4.1", 20 | "js-yaml": "^3.10.0", 21 | "mkdirp": "^0.5.1", 22 | "ramda": "^0.24.1", 23 | "split-ca": "^1.0.1", 24 | "yargs": "^9.0.1" 25 | }, 26 | "devDependencies": { 27 | "babel-cli": "^6.26.0", 28 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 29 | "babel-preset-env": "^1.6.0", 30 | "babel-preset-power-assert": "^1.0.0", 31 | "chai": "^4.1.2", 32 | "eslint": "^4.7.2", 33 | "eslint-config-airbnb": "^15.1.0", 34 | "eslint-config-airbnb-base": "^12.0.0", 35 | "eslint-plugin-import": "^2.7.0", 36 | "eslint-plugin-jsx-a11y": "^6.0.2", 37 | "eslint-plugin-react": "^7.4.0", 38 | "mocha": "^3.5.3", 39 | "power-assert": "^1.4.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /configure/src/args.js: -------------------------------------------------------------------------------- 1 | export default { 2 | file: { 3 | alias: 'f', 4 | demandOption: true, 5 | requiresArg: true, 6 | default: '_stacks.yml', 7 | describe: 'YAML file with stacks configuration', 8 | type: 'string', 9 | }, 10 | domain: { 11 | alias: 'd', 12 | demandOption: true, 13 | requiresArg: true, 14 | default: 'local', 15 | describe: 'The name of the top-level domain you want to use', 16 | type: 'string', 17 | }, 18 | update: { 19 | alias: 'u', 20 | demandOption: true, 21 | default: false, 22 | describe: 'If true, updates the load balancer with new ports', 23 | type: 'boolean', 24 | }, 25 | swarm: { 26 | alias: 's', 27 | demandOption: true, 28 | requiresArg: true, 29 | default: 'swarm', 30 | describe: 'The name of a Docker swarm as described in _docker.yml', 31 | type: 'string', 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /configure/src/cmds/build.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import yaml from 'js-yaml'; 4 | import { chain, map } from 'ramda'; 5 | 6 | import { err, step } from '../log'; 7 | import { getComposeFiles } from '../config'; 8 | import { execFn } from '../compose-file'; 9 | 10 | export const command = 'build '; 11 | export const desc = `Builds and tags Docker images, described in the compose files for the specified stacks. 12 | (Replaces \${tag} with current commit sha if no env variable named tag is set) 13 | `; 14 | export const builder = {}; 15 | 16 | export const handler = async argv => { 17 | const stepper = step(argv.stacks.length); 18 | let nextStep = 1; 19 | const logStep = msg => stepper(nextStep++)(msg); 20 | 21 | const stackConfigPath = path.resolve(argv.file); 22 | const stackConfig = yaml.safeLoad(fs.readFileSync(stackConfigPath, 'utf8')); 23 | const filenamesByStack = getComposeFiles(stackConfig.stacks); 24 | for (const stack of argv.stacks) { 25 | logStep(`Building ${stack}`); 26 | const files = filenamesByStack[stack]; 27 | if (files == null) { 28 | err(`Stack ${stack} was not found in stacks yaml file`); 29 | continue; 30 | } 31 | const args = chain(f => ['-f', f], map(path.resolve, files)); 32 | await execFn('local', 'docker-compose', [...args, 'build'], true); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /configure/src/cmds/deploy.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import yaml from 'js-yaml'; 4 | import { concat, join, map, mergeWith, pipe } from 'ramda'; 5 | 6 | import { step, err } from '../log'; 7 | import { 8 | create as createPortOverrides, 9 | merge as mergeComposeFiles, 10 | execFn as mergeComposeFilesFn, 11 | write as writeComposeFiles, 12 | writeFn, 13 | } from '../compose-file'; 14 | import { getServices, getComposeFiles } from '../config'; 15 | import { getDocker, getEnv } from '../docker-server'; 16 | import { create as createLBConfig, reload as reloadLB, write as writeLBConfig } from '../haproxy'; 17 | import { assign as assignPorts } from '../ports'; 18 | import { findWithPublishedPorts as findPublicServices } from '../services'; 19 | import { validate, deploy, execFn } from '../deploy'; 20 | 21 | export const command = 'deploy [stacks...]'; 22 | export const desc = `Deploys the specified stacks. 23 | If no stacks are specified, then just creates merged compose files. 24 | `; 25 | export const builder = {}; 26 | 27 | export const handler = async argv => { 28 | const stackConfigPath = path.resolve(argv.file); 29 | const stackConfig = yaml.safeLoad(fs.readFileSync(stackConfigPath, 'utf8')); 30 | 31 | const stepper = step(2 + (argv.update ? 1 : 0) + (argv.stacks.length ? 3 : 0)); 32 | let nextStep = 1; 33 | const logStep = msg => stepper(nextStep++)(msg); 34 | 35 | logStep('Scanning swarm and configuring ports'); 36 | const env = await getEnv(argv.swarm); 37 | const docker = getDocker(env); 38 | const existing = await docker.listServices(); 39 | const configured = getServices(stackConfig); 40 | const servicesWithPorts = pipe(findPublicServices, assignPorts(configured))(existing); 41 | 42 | const portOverrides = createPortOverrides(servicesWithPorts); 43 | const portOverrideFilesByStack = writeComposeFiles(writeFn, portOverrides, 'ports'); 44 | 45 | if (argv.update) { 46 | logStep('Updating load balancer'); 47 | const loadBalancerConfig = createLBConfig(servicesWithPorts, argv.domain); 48 | writeLBConfig(loadBalancerConfig); 49 | await reloadLB(); 50 | } 51 | 52 | logStep('Merging compose files'); 53 | const filenamesByStack = getComposeFiles(stackConfig.stacks); 54 | const unresolved = await mergeComposeFiles( 55 | mergeComposeFilesFn, 56 | 'local', 57 | mergeWith(concat, filenamesByStack, map(x => [x], portOverrideFilesByStack)), 58 | false, 59 | ); 60 | writeComposeFiles(writeFn, unresolved, 'unresolved'); 61 | 62 | if (argv.stacks.length > 0) { 63 | const validations = validate(argv.stacks, stackConfig); 64 | if (validations.messages.length) { 65 | err(join(', ', validations.messages)); 66 | } else { 67 | logStep('Pulling images'); 68 | for (const stack of validations.stacks) { 69 | await execFn(argv.swarm, 'docker-compose', ['-f', `${stack}-unresolved.yml`, 'pull']); 70 | } 71 | 72 | logStep('Resolving images'); 73 | const resolved = await mergeComposeFiles( 74 | mergeComposeFilesFn, 75 | argv.swarm, 76 | mergeWith(concat, filenamesByStack, map(x => [x], portOverrideFilesByStack)), 77 | true, 78 | ); 79 | writeComposeFiles(writeFn, resolved, 'resolved'); 80 | 81 | logStep('Deploying'); 82 | deploy(execFn, argv.swarm, validations.stacks); 83 | } 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /configure/src/cmds/push.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import yaml from 'js-yaml'; 4 | import { chain, map } from 'ramda'; 5 | 6 | import { err, step } from '../log'; 7 | import { getComposeFiles } from '../config'; 8 | import { execFn } from '../compose-file'; 9 | 10 | export const command = 'push '; 11 | export const desc = `Pushes Docker images, described in the compose files for the specified stacks. 12 | `; 13 | export const builder = {}; 14 | 15 | export const handler = async argv => { 16 | const stepper = step(argv.stacks.length); 17 | let nextStep = 1; 18 | const logStep = msg => stepper(nextStep++)(msg); 19 | 20 | const stackConfigPath = path.resolve(argv.file); 21 | const stackConfig = yaml.safeLoad(fs.readFileSync(stackConfigPath, 'utf8')); 22 | const filenamesByStack = getComposeFiles(stackConfig.stacks); 23 | for (const stack of argv.stacks) { 24 | logStep(`Pushing ${stack}`); 25 | const files = filenamesByStack[stack]; 26 | if (files == null) { 27 | err(`Stack ${stack} was not found in stacks yaml file`); 28 | continue; 29 | } 30 | const args = chain(f => ['-f', f], map(path.resolve, files)); 31 | await execFn('local', 'docker-compose', [...args, 'push'], true); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /configure/src/compose-file.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import getRepoInfo from 'git-repo-info'; 3 | import getStream from 'get-stream'; 4 | import path from 'path'; 5 | import { chain, forEach, fromPairs, groupBy, join, map, toPairs } from 'ramda'; 6 | 7 | import { log } from './log'; 8 | import { exec, getEnv } from './docker-server'; 9 | 10 | export const create = services => { 11 | const stackNameAndServices = toPairs(groupBy(service => service.stack, services)); 12 | 13 | const genService = service => ` ${service.name}: 14 | ports: 15 | - ${service.port}:3000 16 | `; 17 | 18 | const genStack = services => `version: "3.1" 19 | 20 | services: 21 | ${join('', map(genService, services))} 22 | `; 23 | 24 | const toServices = ([stackname, services]) => [stackname, genStack(services)]; 25 | return fromPairs(map(toServices, stackNameAndServices)); 26 | }; 27 | 28 | export const execFn = async (server, cmd, args, stdout = false, stderr = true) => { 29 | const env = await getEnv(server); 30 | env.tag = process.env.tag || getRepoInfo().abbreviatedSha; 31 | const cp = exec(env, cmd, args, stdout, stderr); 32 | return getStream(cp.stdout); 33 | }; 34 | 35 | export const merge = async (execFn, server, filesByStack, resolve) => { 36 | const output = {}; 37 | for (const [stack, files] of toPairs(filesByStack)) { 38 | let args = [...chain(f => ['-f', f], map(path.resolve, files)), 'config']; 39 | if (resolve) { 40 | args = [...args, '--resolve-image-digests']; 41 | } 42 | output[stack] = await execFn(server, 'docker-compose', args); 43 | } 44 | return output; 45 | }; 46 | 47 | export const writeFn = (filePath, content) => { 48 | log(`Writing ${filePath}`); 49 | fs.writeFileSync(filePath, content); 50 | }; 51 | 52 | export const write = (writeFn, filesByStack, stage) => { 53 | const paths = {}; 54 | forEach(([stack, content]) => { 55 | const file = `${stack}-${stage}.yml`; 56 | paths[stack] = file; 57 | writeFn(path.resolve(file), content); 58 | }, toPairs(filesByStack)); 59 | return paths; 60 | }; 61 | -------------------------------------------------------------------------------- /configure/src/compose-file.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { create, merge, write } from './compose-file'; 3 | 4 | describe('compose-file', () => { 5 | it('should create the correct port overlays', () => { 6 | const services = [ 7 | { 8 | stack: 'services', 9 | name: 'visualizer', 10 | aliases: [], 11 | port: 8080, 12 | }, 13 | { 14 | stack: 'app', 15 | name: 'rproxy', 16 | aliases: ['web'], 17 | port: 80, 18 | }, 19 | { 20 | stack: 'app', 21 | name: 'gateway', 22 | aliases: ['api'], 23 | port: 8000, 24 | }, 25 | { 26 | stack: 'app', 27 | name: 'gateway1', 28 | aliases: ['api1'], 29 | port: 8001, 30 | }, 31 | ]; 32 | const expected = { 33 | services: `version: "3.1" 34 | 35 | services: 36 | visualizer: 37 | ports: 38 | - 8080:3000 39 | 40 | `, 41 | app: `version: "3.1" 42 | 43 | services: 44 | rproxy: 45 | ports: 46 | - 80:3000 47 | gateway: 48 | ports: 49 | - 8000:3000 50 | gateway1: 51 | ports: 52 | - 8001:3000 53 | 54 | `, 55 | }; 56 | const actual = create(services); 57 | expect(actual).to.deep.equal(expected); 58 | }); 59 | 60 | it('should merge the files correctly', async () => { 61 | const filesByStack = { 62 | a: ['a.yml', 'b.yml', 'c.yml'], 63 | }; 64 | const expectedCall = [ 65 | 'mgr1', 66 | 'docker-compose', 67 | [ 68 | '-f', 69 | `${process.cwd()}/a.yml`, 70 | '-f', 71 | `${process.cwd()}/b.yml`, 72 | '-f', 73 | `${process.cwd()}/c.yml`, 74 | 'config', 75 | ], 76 | ]; 77 | let actualCall; 78 | const actual = { a: 'merged' }; 79 | const expected = await merge( 80 | async (server, cmd, args) => { 81 | actualCall = [server, cmd, args]; 82 | return 'merged'; 83 | }, 84 | 'mgr1', 85 | filesByStack, 86 | false, 87 | ); 88 | expect(actualCall).to.deep.equal(expectedCall); 89 | expect(actual).to.deep.equal(expected); 90 | }); 91 | 92 | it('should merge and resolve the files correctly', async () => { 93 | const filesByStack = { 94 | a: ['a.yml', 'b.yml', 'c.yml'], 95 | }; 96 | const expectedCall = [ 97 | 'mgr1', 98 | 'docker-compose', 99 | [ 100 | '-f', 101 | `${process.cwd()}/a.yml`, 102 | '-f', 103 | `${process.cwd()}/b.yml`, 104 | '-f', 105 | `${process.cwd()}/c.yml`, 106 | 'config', 107 | '--resolve-image-digests', 108 | ], 109 | ]; 110 | let actualCall; 111 | const actual = { a: 'merged' }; 112 | const expected = await merge( 113 | async (server, cmd, args) => { 114 | actualCall = [server, cmd, args]; 115 | return 'merged'; 116 | }, 117 | 'mgr1', 118 | filesByStack, 119 | true, 120 | ); 121 | expect(actualCall).to.deep.equal(expectedCall); 122 | expect(actual).to.deep.equal(expected); 123 | }); 124 | 125 | it('should write the files correctly', () => { 126 | const files = { 127 | a: 'a1', 128 | b: 'b1', 129 | }; 130 | const contents = {}; 131 | const paths = write( 132 | (filePath, content) => { 133 | contents[filePath] = content; 134 | }, 135 | files, 136 | 'ports', 137 | ); 138 | 139 | const expectedContents = { 140 | [`${process.cwd()}/a-ports.yml`]: 'a1', 141 | [`${process.cwd()}/b-ports.yml`]: 'b1', 142 | }; 143 | expect(contents).to.deep.equal(expectedContents); 144 | 145 | const expectedFiles = { 146 | a: 'a-ports.yml', 147 | b: 'b-ports.yml', 148 | }; 149 | expect(paths).to.deep.equal(expectedFiles); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /configure/src/config.js: -------------------------------------------------------------------------------- 1 | import { chain, map, reduce } from 'ramda'; 2 | 3 | export const getServices = config => 4 | chain( 5 | stack => 6 | map( 7 | service => ({ 8 | stack: stack.name, 9 | name: service.name, 10 | aliases: service.aliases || [], 11 | }), 12 | stack.services, 13 | ), 14 | config.stacks, 15 | ); 16 | 17 | export const getComposeFiles = reduce( 18 | (acc, stack) => ({ ...acc, [stack.name]: stack['compose-files'] }), 19 | {}, 20 | ); 21 | -------------------------------------------------------------------------------- /configure/src/config.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { getServices, getComposeFiles } from './config'; 3 | 4 | describe('config', () => { 5 | const config = { 6 | stacks: [ 7 | { 8 | name: 'services', 9 | 'compose-files': ['services.yml'], 10 | services: [{ name: 'visualizer' }], 11 | }, 12 | { 13 | name: 'app', 14 | 'compose-files': ['app.yml'], 15 | services: [{ name: 'rproxy', aliases: ['web'] }], 16 | }, 17 | ], 18 | }; 19 | it('should get services from the config', () => { 20 | const expected = [ 21 | { 22 | stack: 'services', 23 | name: 'visualizer', 24 | aliases: [], 25 | }, 26 | { 27 | stack: 'app', 28 | name: 'rproxy', 29 | aliases: ['web'], 30 | }, 31 | ]; 32 | const actual = getServices(config); 33 | expect(actual).to.deep.equal(expected); 34 | }); 35 | it('should get compose files from the config', () => { 36 | const expected = { 37 | services: ['services.yml'], 38 | app: ['app.yml'], 39 | }; 40 | const actual = getComposeFiles(config.stacks); 41 | expect(actual).to.deep.equal(expected); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /configure/src/deploy.js: -------------------------------------------------------------------------------- 1 | import { contains, lensProp, over, pluck, reduce } from 'ramda'; 2 | 3 | import { getEnv, exec } from './docker-server'; 4 | 5 | const stacksLens = lensProp('stacks'); 6 | const messageLens = lensProp('messages'); 7 | 8 | export const validate = (stacknames, stackconfig) => 9 | reduce( 10 | (accumulator, name) => 11 | (contains(name, pluck('name', stackconfig.stacks)) 12 | ? over(stacksLens, a => [...a, name], accumulator) 13 | : over( 14 | messageLens, 15 | a => [...a, `The stack called "${name}" is not declared in the configuration`], 16 | accumulator, 17 | )), 18 | { stacks: [], messages: [] }, 19 | stacknames, 20 | ); 21 | 22 | export const execFn = async (mgr, cmd, args) => exec(await getEnv(mgr), cmd, args, false, true); 23 | 24 | export const deploy = async (execFn, mgr, stacks) => { 25 | for (const stack of stacks) { 26 | await execFn(mgr, 'docker', [ 27 | 'stack', 28 | 'deploy', 29 | '--compose-file', 30 | `${stack}-resolved.yml`, 31 | '--with-registry-auth', 32 | stack, 33 | ]); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /configure/src/deploy.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'power-assert'; 2 | import { validate, deploy } from './deploy'; 3 | 4 | describe('deploy', () => { 5 | const stackconfig = { 6 | stacks: [ 7 | { 8 | name: 'services', 9 | 'compose-files': ['services.yml'], 10 | services: [{ name: 'visualizer', health: '/_health' }], 11 | }, 12 | { 13 | name: 'app', 14 | 'compose-files': ['app.yml'], 15 | services: [{ name: 'rproxy', health: '/status', aliases: ['web'] }], 16 | }, 17 | ], 18 | }; 19 | describe('parse and validate stack names', () => { 20 | it('when both valid', () => { 21 | const stacknames = ['app', 'services']; 22 | const expected = { 23 | stacks: ['app', 'services'], 24 | messages: [], 25 | }; 26 | const actual = validate(stacknames, stackconfig); 27 | assert.deepEqual(actual, expected); 28 | }); 29 | it('when one valid and one invalid', () => { 30 | const stacknames = ['app', 'service']; 31 | const expected = { 32 | stacks: ['app'], 33 | messages: ['The stack called "service" is not declared in the configuration'], 34 | }; 35 | const actual = validate(stacknames, stackconfig); 36 | assert.deepEqual(actual, expected); 37 | }); 38 | it('when neither valid', () => { 39 | const stacknames = ['app1', 'service']; 40 | const expected = { 41 | stacks: [], 42 | messages: [ 43 | 'The stack called "app1" is not declared in the configuration', 44 | 'The stack called "service" is not declared in the configuration', 45 | ], 46 | }; 47 | const actual = validate(stacknames, stackconfig); 48 | assert.deepEqual(actual, expected); 49 | }); 50 | }); 51 | it('calls deployment correctly', async () => { 52 | const stacks = ['app', 'services']; 53 | const actual = []; 54 | const execFn = (mgr, cmd, args) => { 55 | actual.push({ mgr, cmd, args }); 56 | }; 57 | await deploy(execFn, 'mgr1', stacks); 58 | const expected = [ 59 | { 60 | mgr: 'mgr1', 61 | cmd: 'docker', 62 | args: [ 63 | 'stack', 64 | 'deploy', 65 | '--compose-file', 66 | 'app-resolved.yml', 67 | '--with-registry-auth', 68 | 'app', 69 | ], 70 | }, 71 | { 72 | mgr: 'mgr1', 73 | cmd: 'docker', 74 | args: [ 75 | 'stack', 76 | 'deploy', 77 | '--compose-file', 78 | 'services-resolved.yml', 79 | '--with-registry-auth', 80 | 'services', 81 | ], 82 | }, 83 | ]; 84 | assert.deepEqual(actual, expected); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /configure/src/docker-server.js: -------------------------------------------------------------------------------- 1 | import Bluebird from 'bluebird'; 2 | import Docker from 'dockerode'; 3 | import DockerMachine from 'docker-machine'; 4 | import execa from 'execa'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import splitca from 'split-ca'; 8 | import url from 'url'; 9 | import yaml from 'js-yaml'; 10 | import { find, isNil, reject } from 'ramda'; 11 | 12 | const dockerEnv = Bluebird.promisify(DockerMachine.env); 13 | const stat = Bluebird.promisify(fs.stat); 14 | 15 | const readConfigFn = () => yaml.safeLoad(fs.readFileSync(path.resolve('_docker.yml'), 'utf8')); 16 | 17 | export const getEnv = async (serverName = 'local', readConfig = readConfigFn) => { 18 | const server = find(x => x.name === serverName, readConfig().docker); 19 | if (!server) { 20 | throw new Error(`Cannot find configuration for docker server ${serverName}!`); 21 | } 22 | switch (server.type) { 23 | case 'unix-socket': 24 | try { 25 | if ((await stat(server.address)).isSocket()) { 26 | return { DOCKER_HOST: `unix://${path.resolve(server.address)}` }; 27 | } 28 | throw new Error(`File ${server.addess} is not a UNIX socket!`); 29 | } catch (e) { 30 | throw new Error(`No UNIX socket found at ${server.addess}!`); 31 | } 32 | case 'docker-machine': 33 | return dockerEnv(server.address, { parse: true }); 34 | default: 35 | throw new Error(`Docker server connection type ${server.type} is not understood!`); 36 | } 37 | }; 38 | 39 | export const buildEnv = (processEnv, dockerServerEnv) => 40 | reject(isNil, { ...processEnv, ...dockerServerEnv }); 41 | 42 | export const exec = (env, cmd, args, showStdout, showStderr) => { 43 | const env1 = buildEnv(process.env, env); 44 | const cp = execa(cmd, args, { env: env1, extendEnv: false }); 45 | if (showStdout) cp.stdout.pipe(process.stdout); 46 | if (showStderr) cp.stderr.pipe(process.stderr); 47 | return cp; 48 | }; 49 | 50 | export const getDocker = env => { 51 | const opts = {}; 52 | const host = url.parse(env.DOCKER_HOST); 53 | 54 | switch (host.protocol) { 55 | case 'unix:': 56 | opts.socketPath = host.pathname; 57 | break; 58 | case 'tcp:': 59 | opts.port = host.port; 60 | opts.host = host.hostname; 61 | 62 | if (env.DOCKER_TLS_VERIFY === '1' || opts.port === '2376') { 63 | opts.protocol = 'https'; 64 | } else { 65 | opts.protocol = 'http'; 66 | } 67 | 68 | if (env.DOCKER_CERT_PATH) { 69 | opts.ca = splitca(path.join(env.DOCKER_CERT_PATH, 'ca.pem')); 70 | opts.cert = fs.readFileSync(path.join(env.DOCKER_CERT_PATH, 'cert.pem')); 71 | opts.key = fs.readFileSync(path.join(env.DOCKER_CERT_PATH, 'key.pem')); 72 | } 73 | 74 | if (env.DOCKER_CLIENT_TIMEOUT) { 75 | opts.timeout = env.DOCKER_CLIENT_TIMEOUT; 76 | } 77 | break; 78 | default: 79 | throw new Error('DOCKER_HOST env variable should be something like tcp://10.0.1.1:2376 or unix:///var/run/docker.sock'); 80 | } 81 | 82 | return new Docker(opts); 83 | }; 84 | -------------------------------------------------------------------------------- /configure/src/docker-server.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'power-assert'; 2 | 3 | import { buildEnv } from './docker-server'; 4 | 5 | describe('docker-server', () => { 6 | it('should build the right env for local docker server', () => { 7 | const processEnv = { 8 | x: 1, 9 | DOCKER_TLS_VERIFY: 'x', 10 | DOCKER_HOST: 'x', 11 | DOCKER_CERT_PATH: 'x', 12 | DOCKER_MACHINE_NAME: 'x', 13 | }; 14 | const dockerEnv = { 15 | DOCKER_TLS_VERIFY: null, 16 | DOCKER_HOST: null, 17 | DOCKER_CERT_PATH: null, 18 | DOCKER_MACHINE_NAME: null, 19 | }; 20 | const expected = { x: 1 }; 21 | const actual = buildEnv(processEnv, dockerEnv); 22 | assert.deepEqual(actual, expected); 23 | }); 24 | it('should build the right env for remote docker server, when local', () => { 25 | const processEnv = { 26 | x: 1, 27 | }; 28 | const dockerEnv = { 29 | DOCKER_TLS_VERIFY: 'x', 30 | DOCKER_HOST: 'x', 31 | DOCKER_CERT_PATH: 'x', 32 | DOCKER_MACHINE_NAME: 'x', 33 | }; 34 | const expected = { 35 | x: 1, 36 | DOCKER_TLS_VERIFY: 'x', 37 | DOCKER_HOST: 'x', 38 | DOCKER_CERT_PATH: 'x', 39 | DOCKER_MACHINE_NAME: 'x', 40 | }; 41 | const actual = buildEnv(processEnv, dockerEnv); 42 | assert.deepEqual(actual, expected); 43 | }); 44 | it('should build the right env for remote docker server, when remote', () => { 45 | const processEnv = { 46 | x: 1, 47 | DOCKER_TLS_VERIFY: 'x', 48 | DOCKER_HOST: 'x', 49 | DOCKER_CERT_PATH: 'x', 50 | DOCKER_MACHINE_NAME: 'x', 51 | }; 52 | const dockerEnv = { 53 | DOCKER_TLS_VERIFY: 'y', 54 | DOCKER_HOST: 'y', 55 | DOCKER_CERT_PATH: 'y', 56 | DOCKER_MACHINE_NAME: 'y', 57 | }; 58 | const expected = { 59 | x: 1, 60 | DOCKER_TLS_VERIFY: 'y', 61 | DOCKER_HOST: 'y', 62 | DOCKER_CERT_PATH: 'y', 63 | DOCKER_MACHINE_NAME: 'y', 64 | }; 65 | const actual = buildEnv(processEnv, dockerEnv); 66 | assert.deepEqual(actual, expected); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /configure/src/haproxy.js: -------------------------------------------------------------------------------- 1 | import Docker from 'dockerode'; 2 | import fs from 'fs'; 3 | import mkdirp from 'mkdirp'; 4 | import { concat, head, join, map } from 'ramda'; 5 | 6 | import { log, warn } from './log'; 7 | 8 | export const create = (services, domain) => `global 9 | maxconn 4096 10 | log 127.0.0.1:514 local2 debug 11 | 12 | defaults 13 | mode http 14 | log global 15 | option httplog 16 | # option logasap 17 | option dontlognull 18 | option log-health-checks 19 | option forwardfor 20 | option contstats 21 | option http-server-close 22 | retries 3 23 | option redispatch 24 | timeout connect 5s 25 | timeout client 30s 26 | timeout server 30s 27 | 28 | frontend http_front 29 | bind *:80 30 | stats enable 31 | stats uri /haproxy?stats 32 | use_backend %[req.hdr(host),lower] 33 | ${join( 34 | '', 35 | map( 36 | s => 37 | `${join( 38 | '', 39 | map( 40 | name => 41 | ` 42 | backend ${name}.${s.stack}.${domain} 43 | balance roundrobin 44 | ${s.health ? `option httpchk GET ${s.health}` : ''} 45 | server web wkr1:${s.port} check 46 | server web wkr2:${s.port} check 47 | server web wkr3:${s.port} check 48 | `, 49 | concat([s.name], s.aliases), 50 | ), 51 | )}`, 52 | services, 53 | ), 54 | )}`; 55 | 56 | export const write = contents => { 57 | mkdirp.sync('/tmp/haproxy'); 58 | const file = '/tmp/haproxy/haproxy.cfg'; 59 | log(`Writing ${file}`); 60 | fs.writeFileSync(file, contents); 61 | }; 62 | 63 | export const reload = async () => { 64 | const docker = new Docker({ socketPath: '/var/run/docker.sock' }); 65 | const opts = { 66 | all: false, 67 | filters: { label: ['com.docker.compose.service=load_balancer'] }, 68 | }; 69 | const containerInfo = head(await docker.listContainers(opts)); 70 | if (containerInfo) { 71 | const container = docker.getContainer(containerInfo.Id); 72 | await container.kill({ signal: 'SIGHUP' }); 73 | log('Load balancer has been signalled to reload config'); 74 | } else { 75 | warn('Cannot find a container with service name "load_balancer"'); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /configure/src/haproxy.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'power-assert'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | import { create } from './haproxy'; 6 | 7 | describe('haproxy', () => { 8 | it('should create the correct config', () => { 9 | const services = [ 10 | { 11 | stack: 'services', 12 | name: 'visualizer', 13 | health: '/_health', 14 | aliases: [], 15 | port: 8000, 16 | }, 17 | { 18 | stack: 'app', 19 | name: 'rproxy', 20 | health: '/status', 21 | aliases: ['web'], 22 | port: 8001, 23 | }, 24 | ]; 25 | const expected = fs.readFileSync(path.resolve(__dirname, '../fixtures/haproxy.cfg'), 'utf8'); 26 | const actual = create(services, 'local'); 27 | assert(actual === expected); 28 | }); 29 | it('should not add a health check if not specified', () => { 30 | const services = [ 31 | { 32 | stack: 'services', 33 | name: 'visualizer', 34 | aliases: [], 35 | port: 8000, 36 | }, 37 | { 38 | stack: 'app', 39 | name: 'rproxy', 40 | health: '/status', 41 | aliases: ['web'], 42 | port: 8001, 43 | }, 44 | ]; 45 | const expected = fs.readFileSync( 46 | path.resolve(__dirname, '../fixtures/haproxy-no-health-check.cfg'), 47 | 'utf8', 48 | ); 49 | const actual = create(services, 'local'); 50 | assert(actual === expected); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /configure/src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import yargs from 'yargs'; 3 | 4 | import args from './args'; 5 | import { err } from './log'; 6 | 7 | process.on('unhandledRejection', msg => { 8 | err(msg); 9 | }); 10 | 11 | yargs 12 | .options(args) 13 | .commandDir('cmds') 14 | .demandCommand() 15 | .parse(); 16 | -------------------------------------------------------------------------------- /configure/src/log.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { curry } from 'ramda'; 3 | 4 | export const log = txt => { 5 | process.stdout.write(`${txt}\n`); 6 | }; 7 | 8 | export const err = txt => { 9 | process.stderr.write(`\n${chalk`{red ERROR: ${txt}}`}\n`); 10 | }; 11 | 12 | export const warn = txt => { 13 | process.stderr.write(`\n${chalk`{yellow WARNING: ${txt}}`}\n`); 14 | }; 15 | 16 | export const step = curry((count, current, msg) => { 17 | log(`\n${chalk`[${current}/${count}] {white ${msg} ...}`}`); 18 | }); 19 | -------------------------------------------------------------------------------- /configure/src/ports.js: -------------------------------------------------------------------------------- 1 | import { concat, find, reduce } from 'ramda'; 2 | 3 | export const findNext = services => { 4 | const usedPorts = reduce((acc, s) => ({ ...acc, [s.port]: true }), {}, services); 5 | for (let i = 8000; i < Infinity; i++) { 6 | if (usedPorts[i]) { 7 | continue; 8 | } 9 | return i; 10 | } 11 | return 0; 12 | }; 13 | 14 | export const assign = desiredServices => existingServices => 15 | reduce( 16 | (acc, svc) => { 17 | const existing = find(s => s.stack === svc.stack && s.name === svc.name, existingServices); 18 | const port = existing ? existing.port : findNext(concat(existingServices, acc)); 19 | return acc.concat({ ...svc, port }); 20 | }, 21 | [], 22 | desiredServices, 23 | ); 24 | -------------------------------------------------------------------------------- /configure/src/ports.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { findNext, assign } from './ports'; 3 | 4 | describe('should find the first unused port above 8000', () => { 5 | it('when only one', () => { 6 | const expected = 8001; 7 | const actual = findNext([ 8 | { 9 | stack: 'services', 10 | name: 'visualizer', 11 | port: 8000, 12 | }, 13 | ]); 14 | expect(actual).to.equal(expected); 15 | }); 16 | it('when one but without port', () => { 17 | const expected = 8000; 18 | const actual = findNext([ 19 | { 20 | stack: 'services', 21 | name: 'fsdkflkdf', 22 | }, 23 | ]); 24 | expect(actual).to.equal(expected); 25 | }); 26 | it('when there is a gap', () => { 27 | const expected = 8000; 28 | const actual = findNext([ 29 | { 30 | stack: 'services', 31 | name: 'fsdkflkdf', 32 | }, 33 | { 34 | stack: 'app', 35 | name: 'rproxy', 36 | aliases: ['web'], 37 | port: 8001, 38 | }, 39 | ]); 40 | expect(actual).to.equal(expected); 41 | }); 42 | it('when multiple', () => { 43 | const expected = 8003; 44 | const actual = findNext([ 45 | { 46 | stack: 'services', 47 | name: 'fsdkflkdf', 48 | port: 8000, 49 | }, 50 | { 51 | stack: 'services', 52 | name: 'fsdkflkdf', 53 | port: 8002, 54 | }, 55 | { 56 | stack: 'app', 57 | name: 'rproxy', 58 | aliases: ['web'], 59 | port: 8001, 60 | }, 61 | ]); 62 | expect(actual).to.equal(expected); 63 | }); 64 | }); 65 | 66 | describe('assignPorts', () => { 67 | it('should assign ports to those without', () => { 68 | const desiredServices = [{ stack: 'app', name: 'rproxy', aliases: ['web'] }]; 69 | const existingServices = [{ stack: 'services', name: 'visualizer', port: 8000 }]; 70 | const expected = [{ 71 | stack: 'app', name: 'rproxy', aliases: ['web'], port: 8001, 72 | }]; 73 | const actual = assign(desiredServices)(existingServices); 74 | expect(actual).to.deep.equal(expected); 75 | }); 76 | it('should not change port numbers already assigned', () => { 77 | const desiredServices = [{ stack: 'services', name: 'visualizer', port: 8000 }]; 78 | const existingServices = [{ stack: 'services', name: 'visualizer', port: 8000 }]; 79 | const expected = [{ stack: 'services', name: 'visualizer', port: 8000 }]; 80 | const actual = assign(desiredServices)(existingServices); 81 | expect(actual).to.deep.equal(expected); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /configure/src/services.js: -------------------------------------------------------------------------------- 1 | import { filter, head, map, pipe, pluck, replace } from 'ramda'; 2 | 3 | export const findWithPublishedPorts = pipe( 4 | map(s => { 5 | const stack = s.Spec.Labels['com.docker.stack.namespace']; 6 | const name = replace(new RegExp(`^${stack}_`), '', s.Spec.Name); 7 | const ports = s.Endpoint.Ports && pluck('PublishedPort', s.Endpoint.Ports); 8 | const port = head(ports || []); 9 | return { name, stack, port }; 10 | }), 11 | filter(s => s.port), 12 | ); 13 | -------------------------------------------------------------------------------- /configure/src/services.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { findWithPublishedPorts } from './services'; 3 | 4 | describe('services', () => { 5 | it('should find public services', () => { 6 | const services = [ 7 | { 8 | ID: '14grfo1mbeno4tsxspfmxy84p', 9 | Version: { Index: 1037 }, 10 | CreatedAt: '2017-07-20T13:02:43.967715338Z', 11 | UpdatedAt: '2017-07-20T13:02:43.969316219Z', 12 | Spec: { 13 | Name: 'services_visualizer', 14 | Labels: { 15 | 'com.docker.stack.image': 'charypar/swarm-dashboard:latest', 16 | 'com.docker.stack.namespace': 'services', 17 | }, 18 | TaskTemplate: { 19 | ContainerSpec: { 20 | Image: 21 | 'charypar/swarm-dashboard:latest@sha256:43e8e22732b1bcd9654b8672bb3e57e21c5a22f695bd8b8e65df14cdbe933528', 22 | Labels: { 'com.docker.stack.namespace': 'services' }, 23 | Env: ['PORT=3000'], 24 | Privileges: { CredentialSpec: null, SELinuxContext: null }, 25 | Mounts: [ 26 | { 27 | Type: 'bind', 28 | Source: '/var/run/docker.sock', 29 | Target: '/var/run/docker.sock', 30 | }, 31 | ], 32 | }, 33 | Resources: {}, 34 | RestartPolicy: { Condition: 'on-failure', MaxAttempts: 0 }, 35 | Placement: { 36 | Constraints: ['node.role == manager'], 37 | Platforms: [{ Architecture: 'amd64', OS: 'linux' }], 38 | }, 39 | Networks: [{ Target: 'sgxihewf68xh8pggc44snqkey', Aliases: ['visualizer'] }], 40 | ForceUpdate: 0, 41 | Runtime: 'container', 42 | }, 43 | Mode: { Replicated: { Replicas: 1 } }, 44 | EndpointSpec: { 45 | Mode: 'vip', 46 | Ports: [ 47 | { 48 | Protocol: 'tcp', 49 | TargetPort: 3000, 50 | PublishedPort: 8000, 51 | PublishMode: 'ingress', 52 | }, 53 | ], 54 | }, 55 | }, 56 | Endpoint: { 57 | Spec: { 58 | Mode: 'vip', 59 | Ports: [ 60 | { 61 | Protocol: 'tcp', 62 | TargetPort: 3000, 63 | PublishedPort: 8000, 64 | PublishMode: 'ingress', 65 | }, 66 | ], 67 | }, 68 | Ports: [ 69 | { 70 | Protocol: 'tcp', 71 | TargetPort: 3000, 72 | PublishedPort: 8000, 73 | PublishMode: 'ingress', 74 | }, 75 | ], 76 | VirtualIPs: [ 77 | { NetworkID: 'rqhyz2zmtmiz129akzfpb8u8m', Addr: '10.255.0.8/16' }, 78 | { NetworkID: 'sgxihewf68xh8pggc44snqkey', Addr: '10.0.1.2/24' }, 79 | ], 80 | }, 81 | }, 82 | { 83 | ID: '2vw9i2viun7rv274jo25o4lzb', 84 | Version: { Index: 1053 }, 85 | CreatedAt: '2017-07-20T13:09:14.303570219Z', 86 | UpdatedAt: '2017-07-20T13:09:14.304156653Z', 87 | Spec: { 88 | Name: 'app_rproxy', 89 | Labels: { 90 | 'com.docker.stack.image': 'localhost:5000/app_rproxy', 91 | 'com.docker.stack.namespace': 'app', 92 | }, 93 | TaskTemplate: { 94 | ContainerSpec: { 95 | Image: 96 | 'localhost:5000/app_rproxy:latest@sha256:961321670c35519992017eaba8857ebdf9d1be8cda7d7d502b088575ffc8783a', 97 | Labels: { 'com.docker.stack.namespace': 'app' }, 98 | Env: ['API_HOST=gateway:3000', 'PORT=3000', 'WEB_HOST=web:3000'], 99 | Privileges: { CredentialSpec: null, SELinuxContext: null }, 100 | }, 101 | Resources: {}, 102 | RestartPolicy: { Condition: 'on-failure', MaxAttempts: 0 }, 103 | Placement: { 104 | Constraints: ['node.role == worker'], 105 | Platforms: [{ Architecture: 'amd64', OS: 'linux' }], 106 | }, 107 | Networks: [{ Target: 'k8kd7mjwinx2kzquki19ref1c', Aliases: ['rproxy'] }], 108 | ForceUpdate: 0, 109 | Runtime: 'container', 110 | }, 111 | Mode: { Replicated: { Replicas: 3 } }, 112 | UpdateConfig: { 113 | Parallelism: 1, 114 | Delay: 2000000000, 115 | FailureAction: 'pause', 116 | MaxFailureRatio: 0, 117 | Order: 'stop-first', 118 | }, 119 | EndpointSpec: { 120 | Mode: 'vip', 121 | Ports: [ 122 | { 123 | Protocol: 'tcp', 124 | TargetPort: 3000, 125 | PublishedPort: 8001, 126 | PublishMode: 'ingress', 127 | }, 128 | ], 129 | }, 130 | }, 131 | Endpoint: { 132 | Spec: { 133 | Mode: 'vip', 134 | Ports: [ 135 | { 136 | Protocol: 'tcp', 137 | TargetPort: 3000, 138 | PublishedPort: 8001, 139 | PublishMode: 'ingress', 140 | }, 141 | ], 142 | }, 143 | Ports: [ 144 | { 145 | Protocol: 'tcp', 146 | TargetPort: 3000, 147 | PublishedPort: 8001, 148 | PublishMode: 'ingress', 149 | }, 150 | ], 151 | VirtualIPs: [ 152 | { NetworkID: 'rqhyz2zmtmiz129akzfpb8u8m', Addr: '10.255.0.10/16' }, 153 | { NetworkID: 'k8kd7mjwinx2kzquki19ref1c', Addr: '192.168.31.6/24' }, 154 | ], 155 | }, 156 | }, 157 | { 158 | ID: 'caj6kqaottahrrfpkd5ezp3jj', 159 | Version: { Index: 29 }, 160 | CreatedAt: '2017-07-18T13:38:38.298341959Z', 161 | UpdatedAt: '2017-07-18T13:38:38.298992516Z', 162 | Spec: { 163 | Name: 'swarm_registry_ambassador', 164 | Labels: { 165 | 'com.docker.stack.image': 'svendowideit/ambassador', 166 | 'com.docker.stack.namespace': 'swarm', 167 | }, 168 | TaskTemplate: { 169 | ContainerSpec: { 170 | Image: 171 | 'svendowideit/ambassador:latest@sha256:bb60fceae45493a7ce17c19958a38caf8d5b6869958fc9c7f78885c75f1881cf', 172 | Labels: { 'com.docker.stack.namespace': 'swarm' }, 173 | Env: ['DOCKER_PORT_5000_TCP=tcp://10.0.2.2:5000'], 174 | Privileges: { CredentialSpec: null, SELinuxContext: null }, 175 | }, 176 | Resources: {}, 177 | Placement: { 178 | Constraints: ['node.role == manager'], 179 | Platforms: [{ Architecture: 'amd64', OS: 'linux' }], 180 | }, 181 | Networks: [ 182 | { 183 | Target: 'ul1oczuiet0oykyxgo4xbp3u9', 184 | Aliases: ['registry_ambassador'], 185 | }, 186 | ], 187 | ForceUpdate: 0, 188 | Runtime: 'container', 189 | }, 190 | Mode: { Replicated: { Replicas: 1 } }, 191 | EndpointSpec: { 192 | Mode: 'vip', 193 | Ports: [ 194 | { 195 | Protocol: 'tcp', 196 | TargetPort: 5000, 197 | PublishedPort: 5000, 198 | PublishMode: 'ingress', 199 | }, 200 | ], 201 | }, 202 | }, 203 | Endpoint: { 204 | Spec: { 205 | Mode: 'vip', 206 | Ports: [ 207 | { 208 | Protocol: 'tcp', 209 | TargetPort: 5000, 210 | PublishedPort: 5000, 211 | PublishMode: 'ingress', 212 | }, 213 | ], 214 | }, 215 | Ports: [ 216 | { 217 | Protocol: 'tcp', 218 | TargetPort: 5000, 219 | PublishedPort: 5000, 220 | PublishMode: 'ingress', 221 | }, 222 | ], 223 | VirtualIPs: [ 224 | { NetworkID: 'rqhyz2zmtmiz129akzfpb8u8m', Addr: '10.255.0.6/16' }, 225 | { NetworkID: 'ul1oczuiet0oykyxgo4xbp3u9', Addr: '10.0.0.2/24' }, 226 | ], 227 | }, 228 | }, 229 | { 230 | ID: 'lsa7bp80iu4ux43i3mk5wo7wd', 231 | Version: { Index: 1059 }, 232 | CreatedAt: '2017-07-20T13:09:14.377785279Z', 233 | UpdatedAt: '2017-07-20T13:09:14.379609639Z', 234 | Spec: { 235 | Name: 'app_web', 236 | Labels: { 237 | 'com.docker.stack.image': 'localhost:5000/web', 238 | 'com.docker.stack.namespace': 'app', 239 | }, 240 | TaskTemplate: { 241 | ContainerSpec: { 242 | Image: 243 | 'localhost:5000/web:latest@sha256:dde7d576282758e76adfee4edc1f895ae3f751cae9c7f2165098b40538c391ae', 244 | Labels: { 'com.docker.stack.namespace': 'app' }, 245 | Env: ['API_HOST=gateway:3000', 'PORT=3000'], 246 | Privileges: { CredentialSpec: null, SELinuxContext: null }, 247 | }, 248 | Resources: {}, 249 | RestartPolicy: { Condition: 'on-failure', MaxAttempts: 0 }, 250 | Placement: { 251 | Constraints: ['node.role == worker'], 252 | Platforms: [{ Architecture: 'amd64', OS: 'linux' }], 253 | }, 254 | Networks: [{ Target: 'k8kd7mjwinx2kzquki19ref1c', Aliases: ['web'] }], 255 | ForceUpdate: 0, 256 | Runtime: 'container', 257 | }, 258 | Mode: { Replicated: { Replicas: 3 } }, 259 | UpdateConfig: { 260 | Parallelism: 1, 261 | Delay: 2000000000, 262 | FailureAction: 'pause', 263 | MaxFailureRatio: 0, 264 | Order: 'stop-first', 265 | }, 266 | EndpointSpec: { Mode: 'vip' }, 267 | }, 268 | Endpoint: { 269 | Spec: { Mode: 'vip' }, 270 | VirtualIPs: [ 271 | { 272 | NetworkID: 'k8kd7mjwinx2kzquki19ref1c', 273 | Addr: '192.168.31.10/24', 274 | }, 275 | ], 276 | }, 277 | }, 278 | { 279 | ID: 'o2bir5gicujsy0e2c38prqyix', 280 | Version: { Index: 1048 }, 281 | CreatedAt: '2017-07-20T13:09:14.227106059Z', 282 | UpdatedAt: '2017-07-20T13:09:14.227639957Z', 283 | Spec: { 284 | Name: 'app_gateway', 285 | Labels: { 286 | 'com.docker.stack.image': 'localhost:5000/proxy', 287 | 'com.docker.stack.namespace': 'app', 288 | }, 289 | TaskTemplate: { 290 | ContainerSpec: { 291 | Image: 292 | 'localhost:5000/proxy:latest@sha256:35966190ac3b4cfb758a4e12f7c25c547b4babb0e60994e6660207f080c7b81a', 293 | Labels: { 'com.docker.stack.namespace': 'app' }, 294 | Env: ['API_HOST=api:3000', 'PORT=3000'], 295 | Privileges: { CredentialSpec: null, SELinuxContext: null }, 296 | }, 297 | Resources: {}, 298 | RestartPolicy: { Condition: 'on-failure', MaxAttempts: 0 }, 299 | Placement: { 300 | Constraints: ['node.role == worker'], 301 | Platforms: [{ Architecture: 'amd64', OS: 'linux' }], 302 | }, 303 | Networks: [ 304 | { Target: 'k8kd7mjwinx2kzquki19ref1c', Aliases: ['gateway'] }, 305 | { Target: 'ro66sxffzd428njh9s01rlahs', Aliases: ['gateway'] }, 306 | ], 307 | ForceUpdate: 0, 308 | Runtime: 'container', 309 | }, 310 | Mode: { Replicated: { Replicas: 3 } }, 311 | UpdateConfig: { 312 | Parallelism: 1, 313 | Delay: 2000000000, 314 | FailureAction: 'pause', 315 | MaxFailureRatio: 0, 316 | Order: 'stop-first', 317 | }, 318 | EndpointSpec: { Mode: 'vip' }, 319 | }, 320 | Endpoint: { 321 | Spec: { Mode: 'vip' }, 322 | VirtualIPs: [ 323 | { NetworkID: 'k8kd7mjwinx2kzquki19ref1c', Addr: '192.168.31.2/24' }, 324 | { NetworkID: 'ro66sxffzd428njh9s01rlahs', Addr: '192.168.32.2/24' }, 325 | ], 326 | }, 327 | }, 328 | { 329 | ID: 'wq58pqv0f5i61yc4nud1am09p', 330 | Version: { Index: 1064 }, 331 | CreatedAt: '2017-07-20T13:09:14.453160073Z', 332 | UpdatedAt: '2017-07-20T13:09:14.456623928Z', 333 | Spec: { 334 | Name: 'app_api', 335 | Labels: { 336 | 'com.docker.stack.image': 'localhost:5000/api', 337 | 'com.docker.stack.namespace': 'app', 338 | }, 339 | TaskTemplate: { 340 | ContainerSpec: { 341 | Image: 342 | 'localhost:5000/api:latest@sha256:4089d1869f6ea1ee10b15bd3f509a3b1008b105dbee7f0d2a1c12eaa904817cb', 343 | Labels: { 'com.docker.stack.namespace': 'app' }, 344 | Env: ['PORT=3000'], 345 | Privileges: { CredentialSpec: null, SELinuxContext: null }, 346 | Secrets: [ 347 | { 348 | File: { 349 | Name: 'my_secret', UID: '0', GID: '0', Mode: 292, 350 | }, 351 | SecretID: 'vsjvhlaitn6fwn7v7epuaggzy', 352 | SecretName: 'my_secret', 353 | }, 354 | ], 355 | }, 356 | Resources: {}, 357 | RestartPolicy: { Condition: 'on-failure', MaxAttempts: 0 }, 358 | Placement: { 359 | Constraints: ['node.role == worker'], 360 | Platforms: [{ Architecture: 'amd64', OS: 'linux' }], 361 | }, 362 | Networks: [{ Target: 'ro66sxffzd428njh9s01rlahs', Aliases: ['api'] }], 363 | ForceUpdate: 0, 364 | Runtime: 'container', 365 | }, 366 | Mode: { Replicated: { Replicas: 3 } }, 367 | UpdateConfig: { 368 | Parallelism: 1, 369 | Delay: 5000000000, 370 | FailureAction: 'pause', 371 | MaxFailureRatio: 0, 372 | Order: 'stop-first', 373 | }, 374 | EndpointSpec: { Mode: 'vip' }, 375 | }, 376 | Endpoint: { 377 | Spec: { Mode: 'vip' }, 378 | VirtualIPs: [{ NetworkID: 'ro66sxffzd428njh9s01rlahs', Addr: '192.168.32.6/24' }], 379 | }, 380 | }, 381 | ]; 382 | const expected = [ 383 | { name: 'visualizer', stack: 'services', port: 8000 }, 384 | { name: 'rproxy', stack: 'app', port: 8001 }, 385 | { name: 'registry_ambassador', stack: 'swarm', port: 5000 }, 386 | ]; 387 | const actual = findWithPublishedPorts(services); 388 | expect(JSON.stringify(actual)).to.equal(JSON.stringify(expected)); 389 | }); 390 | 391 | it('should still work even if the stackname has an underscore', () => { 392 | const services = [ 393 | { 394 | Spec: { 395 | Name: 'my_stack_my_service', 396 | Labels: { 397 | 'com.docker.stack.namespace': 'my_stack', 398 | }, 399 | }, 400 | Endpoint: { 401 | Ports: [ 402 | { 403 | PublishedPort: 8000, 404 | }, 405 | ], 406 | }, 407 | }, 408 | ]; 409 | const expected = [{ name: 'my_service', stack: 'my_stack', port: 8000 }]; 410 | const actual = findWithPublishedPorts(services); 411 | expect(JSON.stringify(actual)).to.equal(JSON.stringify(expected)); 412 | }); 413 | }); 414 | -------------------------------------------------------------------------------- /doc/visualizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redbadger/stack/71059d2ab24cfa9e711884445d8cab90bdaa8173/doc/visualizer.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | *-ports.yml 3 | *-unresolved.yml 4 | *-resolved.yml 5 | -------------------------------------------------------------------------------- /example/_docker.yml: -------------------------------------------------------------------------------- 1 | docker: 2 | - name: local 3 | type: unix-socket 4 | address: /var/run/docker.sock 5 | - name: swarm 6 | type: docker-machine 7 | address: mgr1 8 | - name: aws 9 | type: unix-socket 10 | address: docker.sock 11 | -------------------------------------------------------------------------------- /example/_stacks.yml: -------------------------------------------------------------------------------- 1 | stacks: 2 | - name: services 3 | compose-files: 4 | - services.yml 5 | services: 6 | - name: visualizer 7 | health: /_health 8 | - name: app 9 | compose-files: 10 | - app.yml 11 | services: 12 | - name: rproxy 13 | health: /haproxy?stats 14 | aliases: 15 | - web 16 | -------------------------------------------------------------------------------- /example/api/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /example/api/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile 4 | -------------------------------------------------------------------------------- /example/api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:6-alpine 2 | 3 | RUN apk add --update tini curl \ 4 | && rm -r /var/cache 5 | ENTRYPOINT ["/sbin/tini", "--"] 6 | 7 | WORKDIR /home/node/app 8 | ENV NODE_ENV production 9 | 10 | COPY package.json yarn.lock ./ 11 | RUN yarn install --production 12 | 13 | COPY . ./ 14 | RUN chown -R node:node . 15 | 16 | USER node 17 | 18 | HEALTHCHECK --interval=5s --timeout=3s \ 19 | CMD curl --fail http://localhost:$PORT/_health || exit 1 20 | 21 | # Run under Tini 22 | CMD ["node", "index.js"] 23 | -------------------------------------------------------------------------------- /example/api/index.js: -------------------------------------------------------------------------------- 1 | require('babel-register')(); 2 | require('./src/server'); 3 | -------------------------------------------------------------------------------- /example/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "babel-preset-env": "^1.4.0", 8 | "babel-register": "^6.24.1", 9 | "bluebird": "^3.5.0", 10 | "express": "^4.15.2", 11 | "express-graphql": "^0.6.4", 12 | "express-winston": "^2.4.0", 13 | "graphql": "^0.9.3", 14 | "graphql-tools": "^0.11.0", 15 | "winston": "^2.3.1" 16 | }, 17 | "devDependencies": { 18 | "babel-cli": "^6.24.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/api/src/resolvers.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import Path from 'path'; 3 | import Promise from 'bluebird'; 4 | 5 | const readdir = Promise.promisify(fs.readdir); 6 | const readFile = Promise.promisify(fs.readFile); 7 | 8 | export default { 9 | Query: { 10 | server() { 11 | return process.env['HOSTNAME'] || 'localhost'; 12 | }, 13 | async secrets() { 14 | const path = '/run/secrets'; 15 | const files = await readdir(path); 16 | return files.map(async file => ({ 17 | name: file, 18 | value: await readFile(Path.resolve(path, file), 'utf8'), 19 | })); 20 | }, 21 | headers(_1, _2, req) { 22 | return JSON.stringify(req && req.headers); 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /example/api/src/schema.js: -------------------------------------------------------------------------------- 1 | import { makeExecutableSchema } from 'graphql-tools'; 2 | 3 | import resolvers from './resolvers'; 4 | 5 | const typeDefs = ` 6 | type Secret { 7 | name: String 8 | value: String 9 | } 10 | type Query { 11 | server: String 12 | secrets: [Secret] 13 | headers: String 14 | } 15 | schema { 16 | query: Query 17 | } 18 | `; 19 | 20 | const schema = makeExecutableSchema({ typeDefs, resolvers }); 21 | export default schema; 22 | -------------------------------------------------------------------------------- /example/api/src/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import graphqlHTTP from 'express-graphql'; 3 | import winston from 'winston'; 4 | import expressWinston from 'express-winston'; 5 | 6 | import schema from './schema'; 7 | 8 | const port = process.env.PORT; 9 | const app = express(); 10 | 11 | app.use( 12 | expressWinston.logger({ 13 | transports: [ 14 | new winston.transports.Console({ 15 | json: true, 16 | colorize: true, 17 | }), 18 | ], 19 | meta: true, // optional: control whether you want to log the meta data about the request (default to true) 20 | msg: 'HTTP {{req.method}} {{req.url}}', // optional: customize the default logging message. E.g. "{{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}" 21 | expressFormat: true, // Use the default Express/morgan request formatting. Enabling this will override any msg if true. Will only output colors with colorize set to true 22 | colorize: false, // Color the text and status code, using the Express/morgan color palette (text: gray, status: default green, 3XX cyan, 4XX yellow, 5XX red). 23 | ignoreRoute: function(req, res) { 24 | return req.path === '/_health'; 25 | }, // optional: allows to skip some log messages based on request and/or response 26 | }), 27 | ); 28 | 29 | app.use( 30 | '/graphql', 31 | graphqlHTTP({ 32 | schema, 33 | graphiql: true, 34 | }), 35 | ); 36 | 37 | app.get('/_health', (req, res) => res.end()); 38 | 39 | app.listen(port, () => { 40 | console.log(`listening on port ${port}`); 41 | }); 42 | 43 | export default app; 44 | -------------------------------------------------------------------------------- /example/api/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/graphql@^0.9.0": 6 | version "0.9.0" 7 | resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.9.0.tgz#fccf859f0d2817687f210737dc3be48a18b1d754" 8 | 9 | abbrev@1: 10 | version "1.1.0" 11 | resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" 12 | 13 | accepts@^1.3.0, accepts@~1.3.3: 14 | version "1.3.3" 15 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" 16 | dependencies: 17 | mime-types "~2.1.11" 18 | negotiator "0.6.1" 19 | 20 | ajv@^4.9.1: 21 | version "4.11.6" 22 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.6.tgz#947e93049790942b2a2d60a8289b28924d39f987" 23 | dependencies: 24 | co "^4.6.0" 25 | json-stable-stringify "^1.0.1" 26 | 27 | ansi-regex@^2.0.0: 28 | version "2.1.1" 29 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 30 | 31 | ansi-styles@^2.2.1: 32 | version "2.2.1" 33 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" 34 | 35 | ansi-styles@~1.0.0: 36 | version "1.0.0" 37 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" 38 | 39 | anymatch@^1.3.0: 40 | version "1.3.0" 41 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507" 42 | dependencies: 43 | arrify "^1.0.0" 44 | micromatch "^2.1.5" 45 | 46 | aproba@^1.0.3: 47 | version "1.1.1" 48 | resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.1.tgz#95d3600f07710aa0e9298c726ad5ecf2eacbabab" 49 | 50 | are-we-there-yet@~1.1.2: 51 | version "1.1.2" 52 | resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz#80e470e95a084794fe1899262c5667c6e88de1b3" 53 | dependencies: 54 | delegates "^1.0.0" 55 | readable-stream "^2.0.0 || ^1.1.13" 56 | 57 | arr-diff@^2.0.0: 58 | version "2.0.0" 59 | resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" 60 | dependencies: 61 | arr-flatten "^1.0.1" 62 | 63 | arr-flatten@^1.0.1: 64 | version "1.0.1" 65 | resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.1.tgz#e5ffe54d45e19f32f216e91eb99c8ce892bb604b" 66 | 67 | array-flatten@1.1.1: 68 | version "1.1.1" 69 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 70 | 71 | array-unique@^0.2.1: 72 | version "0.2.1" 73 | resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" 74 | 75 | arrify@^1.0.0: 76 | version "1.0.1" 77 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" 78 | 79 | asn1@~0.2.3: 80 | version "0.2.3" 81 | resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" 82 | 83 | assert-plus@1.0.0, assert-plus@^1.0.0: 84 | version "1.0.0" 85 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 86 | 87 | assert-plus@^0.2.0: 88 | version "0.2.0" 89 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" 90 | 91 | async-each@^1.0.0: 92 | version "1.0.1" 93 | resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" 94 | 95 | async@~1.0.0: 96 | version "1.0.0" 97 | resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" 98 | 99 | asynckit@^0.4.0: 100 | version "0.4.0" 101 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 102 | 103 | aws-sign2@~0.6.0: 104 | version "0.6.0" 105 | resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" 106 | 107 | aws4@^1.2.1: 108 | version "1.6.0" 109 | resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" 110 | 111 | babel-cli@^6.24.1: 112 | version "6.24.1" 113 | resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.24.1.tgz#207cd705bba61489b2ea41b5312341cf6aca2283" 114 | dependencies: 115 | babel-core "^6.24.1" 116 | babel-polyfill "^6.23.0" 117 | babel-register "^6.24.1" 118 | babel-runtime "^6.22.0" 119 | commander "^2.8.1" 120 | convert-source-map "^1.1.0" 121 | fs-readdir-recursive "^1.0.0" 122 | glob "^7.0.0" 123 | lodash "^4.2.0" 124 | output-file-sync "^1.1.0" 125 | path-is-absolute "^1.0.0" 126 | slash "^1.0.0" 127 | source-map "^0.5.0" 128 | v8flags "^2.0.10" 129 | optionalDependencies: 130 | chokidar "^1.6.1" 131 | 132 | babel-code-frame@^6.22.0: 133 | version "6.22.0" 134 | resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" 135 | dependencies: 136 | chalk "^1.1.0" 137 | esutils "^2.0.2" 138 | js-tokens "^3.0.0" 139 | 140 | babel-core@^6.24.1: 141 | version "6.24.1" 142 | resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.24.1.tgz#8c428564dce1e1f41fb337ec34f4c3b022b5ad83" 143 | dependencies: 144 | babel-code-frame "^6.22.0" 145 | babel-generator "^6.24.1" 146 | babel-helpers "^6.24.1" 147 | babel-messages "^6.23.0" 148 | babel-register "^6.24.1" 149 | babel-runtime "^6.22.0" 150 | babel-template "^6.24.1" 151 | babel-traverse "^6.24.1" 152 | babel-types "^6.24.1" 153 | babylon "^6.11.0" 154 | convert-source-map "^1.1.0" 155 | debug "^2.1.1" 156 | json5 "^0.5.0" 157 | lodash "^4.2.0" 158 | minimatch "^3.0.2" 159 | path-is-absolute "^1.0.0" 160 | private "^0.1.6" 161 | slash "^1.0.0" 162 | source-map "^0.5.0" 163 | 164 | babel-generator@^6.24.1: 165 | version "6.24.1" 166 | resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.24.1.tgz#e715f486c58ded25649d888944d52aa07c5d9497" 167 | dependencies: 168 | babel-messages "^6.23.0" 169 | babel-runtime "^6.22.0" 170 | babel-types "^6.24.1" 171 | detect-indent "^4.0.0" 172 | jsesc "^1.3.0" 173 | lodash "^4.2.0" 174 | source-map "^0.5.0" 175 | trim-right "^1.0.1" 176 | 177 | babel-helper-builder-binary-assignment-operator-visitor@^6.22.0: 178 | version "6.22.0" 179 | resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.22.0.tgz#29df56be144d81bdeac08262bfa41d2c5e91cdcd" 180 | dependencies: 181 | babel-helper-explode-assignable-expression "^6.22.0" 182 | babel-runtime "^6.22.0" 183 | babel-types "^6.22.0" 184 | 185 | babel-helper-call-delegate@^6.22.0: 186 | version "6.22.0" 187 | resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.22.0.tgz#119921b56120f17e9dae3f74b4f5cc7bcc1b37ef" 188 | dependencies: 189 | babel-helper-hoist-variables "^6.22.0" 190 | babel-runtime "^6.22.0" 191 | babel-traverse "^6.22.0" 192 | babel-types "^6.22.0" 193 | 194 | babel-helper-define-map@^6.23.0: 195 | version "6.23.0" 196 | resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.23.0.tgz#1444f960c9691d69a2ced6a205315f8fd00804e7" 197 | dependencies: 198 | babel-helper-function-name "^6.23.0" 199 | babel-runtime "^6.22.0" 200 | babel-types "^6.23.0" 201 | lodash "^4.2.0" 202 | 203 | babel-helper-explode-assignable-expression@^6.22.0: 204 | version "6.22.0" 205 | resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.22.0.tgz#c97bf76eed3e0bae4048121f2b9dae1a4e7d0478" 206 | dependencies: 207 | babel-runtime "^6.22.0" 208 | babel-traverse "^6.22.0" 209 | babel-types "^6.22.0" 210 | 211 | babel-helper-function-name@^6.22.0, babel-helper-function-name@^6.23.0: 212 | version "6.23.0" 213 | resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.23.0.tgz#25742d67175c8903dbe4b6cb9d9e1fcb8dcf23a6" 214 | dependencies: 215 | babel-helper-get-function-arity "^6.22.0" 216 | babel-runtime "^6.22.0" 217 | babel-template "^6.23.0" 218 | babel-traverse "^6.23.0" 219 | babel-types "^6.23.0" 220 | 221 | babel-helper-get-function-arity@^6.22.0: 222 | version "6.22.0" 223 | resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.22.0.tgz#0beb464ad69dc7347410ac6ade9f03a50634f5ce" 224 | dependencies: 225 | babel-runtime "^6.22.0" 226 | babel-types "^6.22.0" 227 | 228 | babel-helper-hoist-variables@^6.22.0: 229 | version "6.22.0" 230 | resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.22.0.tgz#3eacbf731d80705845dd2e9718f600cfb9b4ba72" 231 | dependencies: 232 | babel-runtime "^6.22.0" 233 | babel-types "^6.22.0" 234 | 235 | babel-helper-optimise-call-expression@^6.23.0: 236 | version "6.23.0" 237 | resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.23.0.tgz#f3ee7eed355b4282138b33d02b78369e470622f5" 238 | dependencies: 239 | babel-runtime "^6.22.0" 240 | babel-types "^6.23.0" 241 | 242 | babel-helper-regex@^6.22.0: 243 | version "6.22.0" 244 | resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.22.0.tgz#79f532be1647b1f0ee3474b5f5c3da58001d247d" 245 | dependencies: 246 | babel-runtime "^6.22.0" 247 | babel-types "^6.22.0" 248 | lodash "^4.2.0" 249 | 250 | babel-helper-remap-async-to-generator@^6.22.0: 251 | version "6.22.0" 252 | resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.22.0.tgz#2186ae73278ed03b8b15ced089609da981053383" 253 | dependencies: 254 | babel-helper-function-name "^6.22.0" 255 | babel-runtime "^6.22.0" 256 | babel-template "^6.22.0" 257 | babel-traverse "^6.22.0" 258 | babel-types "^6.22.0" 259 | 260 | babel-helper-replace-supers@^6.22.0, babel-helper-replace-supers@^6.23.0: 261 | version "6.23.0" 262 | resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.23.0.tgz#eeaf8ad9b58ec4337ca94223bacdca1f8d9b4bfd" 263 | dependencies: 264 | babel-helper-optimise-call-expression "^6.23.0" 265 | babel-messages "^6.23.0" 266 | babel-runtime "^6.22.0" 267 | babel-template "^6.23.0" 268 | babel-traverse "^6.23.0" 269 | babel-types "^6.23.0" 270 | 271 | babel-helpers@^6.24.1: 272 | version "6.24.1" 273 | resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" 274 | dependencies: 275 | babel-runtime "^6.22.0" 276 | babel-template "^6.24.1" 277 | 278 | babel-messages@^6.23.0: 279 | version "6.23.0" 280 | resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" 281 | dependencies: 282 | babel-runtime "^6.22.0" 283 | 284 | babel-plugin-check-es2015-constants@^6.22.0: 285 | version "6.22.0" 286 | resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" 287 | dependencies: 288 | babel-runtime "^6.22.0" 289 | 290 | babel-plugin-syntax-async-functions@^6.8.0: 291 | version "6.13.0" 292 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" 293 | 294 | babel-plugin-syntax-exponentiation-operator@^6.8.0: 295 | version "6.13.0" 296 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" 297 | 298 | babel-plugin-syntax-trailing-function-commas@^6.22.0: 299 | version "6.22.0" 300 | resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" 301 | 302 | babel-plugin-transform-async-to-generator@^6.22.0: 303 | version "6.22.0" 304 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.22.0.tgz#194b6938ec195ad36efc4c33a971acf00d8cd35e" 305 | dependencies: 306 | babel-helper-remap-async-to-generator "^6.22.0" 307 | babel-plugin-syntax-async-functions "^6.8.0" 308 | babel-runtime "^6.22.0" 309 | 310 | babel-plugin-transform-es2015-arrow-functions@^6.22.0: 311 | version "6.22.0" 312 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" 313 | dependencies: 314 | babel-runtime "^6.22.0" 315 | 316 | babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: 317 | version "6.22.0" 318 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" 319 | dependencies: 320 | babel-runtime "^6.22.0" 321 | 322 | babel-plugin-transform-es2015-block-scoping@^6.23.0: 323 | version "6.23.0" 324 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.23.0.tgz#e48895cf0b375be148cd7c8879b422707a053b51" 325 | dependencies: 326 | babel-runtime "^6.22.0" 327 | babel-template "^6.23.0" 328 | babel-traverse "^6.23.0" 329 | babel-types "^6.23.0" 330 | lodash "^4.2.0" 331 | 332 | babel-plugin-transform-es2015-classes@^6.23.0: 333 | version "6.23.0" 334 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.23.0.tgz#49b53f326202a2fd1b3bbaa5e2edd8a4f78643c1" 335 | dependencies: 336 | babel-helper-define-map "^6.23.0" 337 | babel-helper-function-name "^6.23.0" 338 | babel-helper-optimise-call-expression "^6.23.0" 339 | babel-helper-replace-supers "^6.23.0" 340 | babel-messages "^6.23.0" 341 | babel-runtime "^6.22.0" 342 | babel-template "^6.23.0" 343 | babel-traverse "^6.23.0" 344 | babel-types "^6.23.0" 345 | 346 | babel-plugin-transform-es2015-computed-properties@^6.22.0: 347 | version "6.22.0" 348 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.22.0.tgz#7c383e9629bba4820c11b0425bdd6290f7f057e7" 349 | dependencies: 350 | babel-runtime "^6.22.0" 351 | babel-template "^6.22.0" 352 | 353 | babel-plugin-transform-es2015-destructuring@^6.23.0: 354 | version "6.23.0" 355 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" 356 | dependencies: 357 | babel-runtime "^6.22.0" 358 | 359 | babel-plugin-transform-es2015-duplicate-keys@^6.22.0: 360 | version "6.22.0" 361 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.22.0.tgz#672397031c21610d72dd2bbb0ba9fb6277e1c36b" 362 | dependencies: 363 | babel-runtime "^6.22.0" 364 | babel-types "^6.22.0" 365 | 366 | babel-plugin-transform-es2015-for-of@^6.23.0: 367 | version "6.23.0" 368 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" 369 | dependencies: 370 | babel-runtime "^6.22.0" 371 | 372 | babel-plugin-transform-es2015-function-name@^6.22.0: 373 | version "6.22.0" 374 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.22.0.tgz#f5fcc8b09093f9a23c76ac3d9e392c3ec4b77104" 375 | dependencies: 376 | babel-helper-function-name "^6.22.0" 377 | babel-runtime "^6.22.0" 378 | babel-types "^6.22.0" 379 | 380 | babel-plugin-transform-es2015-literals@^6.22.0: 381 | version "6.22.0" 382 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" 383 | dependencies: 384 | babel-runtime "^6.22.0" 385 | 386 | babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.0: 387 | version "6.24.0" 388 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.0.tgz#a1911fb9b7ec7e05a43a63c5995007557bcf6a2e" 389 | dependencies: 390 | babel-plugin-transform-es2015-modules-commonjs "^6.24.0" 391 | babel-runtime "^6.22.0" 392 | babel-template "^6.22.0" 393 | 394 | babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.0: 395 | version "6.24.0" 396 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.0.tgz#e921aefb72c2cc26cb03d107626156413222134f" 397 | dependencies: 398 | babel-plugin-transform-strict-mode "^6.22.0" 399 | babel-runtime "^6.22.0" 400 | babel-template "^6.23.0" 401 | babel-types "^6.23.0" 402 | 403 | babel-plugin-transform-es2015-modules-systemjs@^6.23.0: 404 | version "6.23.0" 405 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.23.0.tgz#ae3469227ffac39b0310d90fec73bfdc4f6317b0" 406 | dependencies: 407 | babel-helper-hoist-variables "^6.22.0" 408 | babel-runtime "^6.22.0" 409 | babel-template "^6.23.0" 410 | 411 | babel-plugin-transform-es2015-modules-umd@^6.23.0: 412 | version "6.24.0" 413 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.0.tgz#fd5fa63521cae8d273927c3958afd7c067733450" 414 | dependencies: 415 | babel-plugin-transform-es2015-modules-amd "^6.24.0" 416 | babel-runtime "^6.22.0" 417 | babel-template "^6.23.0" 418 | 419 | babel-plugin-transform-es2015-object-super@^6.22.0: 420 | version "6.22.0" 421 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.22.0.tgz#daa60e114a042ea769dd53fe528fc82311eb98fc" 422 | dependencies: 423 | babel-helper-replace-supers "^6.22.0" 424 | babel-runtime "^6.22.0" 425 | 426 | babel-plugin-transform-es2015-parameters@^6.23.0: 427 | version "6.23.0" 428 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.23.0.tgz#3a2aabb70c8af945d5ce386f1a4250625a83ae3b" 429 | dependencies: 430 | babel-helper-call-delegate "^6.22.0" 431 | babel-helper-get-function-arity "^6.22.0" 432 | babel-runtime "^6.22.0" 433 | babel-template "^6.23.0" 434 | babel-traverse "^6.23.0" 435 | babel-types "^6.23.0" 436 | 437 | babel-plugin-transform-es2015-shorthand-properties@^6.22.0: 438 | version "6.22.0" 439 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.22.0.tgz#8ba776e0affaa60bff21e921403b8a652a2ff723" 440 | dependencies: 441 | babel-runtime "^6.22.0" 442 | babel-types "^6.22.0" 443 | 444 | babel-plugin-transform-es2015-spread@^6.22.0: 445 | version "6.22.0" 446 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" 447 | dependencies: 448 | babel-runtime "^6.22.0" 449 | 450 | babel-plugin-transform-es2015-sticky-regex@^6.22.0: 451 | version "6.22.0" 452 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.22.0.tgz#ab316829e866ee3f4b9eb96939757d19a5bc4593" 453 | dependencies: 454 | babel-helper-regex "^6.22.0" 455 | babel-runtime "^6.22.0" 456 | babel-types "^6.22.0" 457 | 458 | babel-plugin-transform-es2015-template-literals@^6.22.0: 459 | version "6.22.0" 460 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" 461 | dependencies: 462 | babel-runtime "^6.22.0" 463 | 464 | babel-plugin-transform-es2015-typeof-symbol@^6.23.0: 465 | version "6.23.0" 466 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" 467 | dependencies: 468 | babel-runtime "^6.22.0" 469 | 470 | babel-plugin-transform-es2015-unicode-regex@^6.22.0: 471 | version "6.22.0" 472 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.22.0.tgz#8d9cc27e7ee1decfe65454fb986452a04a613d20" 473 | dependencies: 474 | babel-helper-regex "^6.22.0" 475 | babel-runtime "^6.22.0" 476 | regexpu-core "^2.0.0" 477 | 478 | babel-plugin-transform-exponentiation-operator@^6.22.0: 479 | version "6.22.0" 480 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.22.0.tgz#d57c8335281918e54ef053118ce6eb108468084d" 481 | dependencies: 482 | babel-helper-builder-binary-assignment-operator-visitor "^6.22.0" 483 | babel-plugin-syntax-exponentiation-operator "^6.8.0" 484 | babel-runtime "^6.22.0" 485 | 486 | babel-plugin-transform-regenerator@^6.22.0: 487 | version "6.22.0" 488 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.22.0.tgz#65740593a319c44522157538d690b84094617ea6" 489 | dependencies: 490 | regenerator-transform "0.9.8" 491 | 492 | babel-plugin-transform-strict-mode@^6.22.0: 493 | version "6.22.0" 494 | resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.22.0.tgz#e008df01340fdc87e959da65991b7e05970c8c7c" 495 | dependencies: 496 | babel-runtime "^6.22.0" 497 | babel-types "^6.22.0" 498 | 499 | babel-polyfill@^6.23.0: 500 | version "6.23.0" 501 | resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.23.0.tgz#8364ca62df8eafb830499f699177466c3b03499d" 502 | dependencies: 503 | babel-runtime "^6.22.0" 504 | core-js "^2.4.0" 505 | regenerator-runtime "^0.10.0" 506 | 507 | babel-preset-env@^1.4.0: 508 | version "1.4.0" 509 | resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.4.0.tgz#c8e02a3bcc7792f23cded68e0355b9d4c28f0f7a" 510 | dependencies: 511 | babel-plugin-check-es2015-constants "^6.22.0" 512 | babel-plugin-syntax-trailing-function-commas "^6.22.0" 513 | babel-plugin-transform-async-to-generator "^6.22.0" 514 | babel-plugin-transform-es2015-arrow-functions "^6.22.0" 515 | babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" 516 | babel-plugin-transform-es2015-block-scoping "^6.23.0" 517 | babel-plugin-transform-es2015-classes "^6.23.0" 518 | babel-plugin-transform-es2015-computed-properties "^6.22.0" 519 | babel-plugin-transform-es2015-destructuring "^6.23.0" 520 | babel-plugin-transform-es2015-duplicate-keys "^6.22.0" 521 | babel-plugin-transform-es2015-for-of "^6.23.0" 522 | babel-plugin-transform-es2015-function-name "^6.22.0" 523 | babel-plugin-transform-es2015-literals "^6.22.0" 524 | babel-plugin-transform-es2015-modules-amd "^6.22.0" 525 | babel-plugin-transform-es2015-modules-commonjs "^6.23.0" 526 | babel-plugin-transform-es2015-modules-systemjs "^6.23.0" 527 | babel-plugin-transform-es2015-modules-umd "^6.23.0" 528 | babel-plugin-transform-es2015-object-super "^6.22.0" 529 | babel-plugin-transform-es2015-parameters "^6.23.0" 530 | babel-plugin-transform-es2015-shorthand-properties "^6.22.0" 531 | babel-plugin-transform-es2015-spread "^6.22.0" 532 | babel-plugin-transform-es2015-sticky-regex "^6.22.0" 533 | babel-plugin-transform-es2015-template-literals "^6.22.0" 534 | babel-plugin-transform-es2015-typeof-symbol "^6.23.0" 535 | babel-plugin-transform-es2015-unicode-regex "^6.22.0" 536 | babel-plugin-transform-exponentiation-operator "^6.22.0" 537 | babel-plugin-transform-regenerator "^6.22.0" 538 | browserslist "^1.4.0" 539 | invariant "^2.2.2" 540 | 541 | babel-register@^6.24.1: 542 | version "6.24.1" 543 | resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.24.1.tgz#7e10e13a2f71065bdfad5a1787ba45bca6ded75f" 544 | dependencies: 545 | babel-core "^6.24.1" 546 | babel-runtime "^6.22.0" 547 | core-js "^2.4.0" 548 | home-or-tmp "^2.0.0" 549 | lodash "^4.2.0" 550 | mkdirp "^0.5.1" 551 | source-map-support "^0.4.2" 552 | 553 | babel-runtime@^6.18.0, babel-runtime@^6.22.0: 554 | version "6.23.0" 555 | resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" 556 | dependencies: 557 | core-js "^2.4.0" 558 | regenerator-runtime "^0.10.0" 559 | 560 | babel-template@^6.22.0, babel-template@^6.23.0, babel-template@^6.24.1: 561 | version "6.24.1" 562 | resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.24.1.tgz#04ae514f1f93b3a2537f2a0f60a5a45fb8308333" 563 | dependencies: 564 | babel-runtime "^6.22.0" 565 | babel-traverse "^6.24.1" 566 | babel-types "^6.24.1" 567 | babylon "^6.11.0" 568 | lodash "^4.2.0" 569 | 570 | babel-traverse@^6.22.0, babel-traverse@^6.23.0, babel-traverse@^6.24.1: 571 | version "6.24.1" 572 | resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.24.1.tgz#ab36673fd356f9a0948659e7b338d5feadb31695" 573 | dependencies: 574 | babel-code-frame "^6.22.0" 575 | babel-messages "^6.23.0" 576 | babel-runtime "^6.22.0" 577 | babel-types "^6.24.1" 578 | babylon "^6.15.0" 579 | debug "^2.2.0" 580 | globals "^9.0.0" 581 | invariant "^2.2.0" 582 | lodash "^4.2.0" 583 | 584 | babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23.0, babel-types@^6.24.1: 585 | version "6.24.1" 586 | resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.24.1.tgz#a136879dc15b3606bda0d90c1fc74304c2ff0975" 587 | dependencies: 588 | babel-runtime "^6.22.0" 589 | esutils "^2.0.2" 590 | lodash "^4.2.0" 591 | to-fast-properties "^1.0.1" 592 | 593 | babylon@^6.11.0, babylon@^6.15.0: 594 | version "6.16.1" 595 | resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3" 596 | 597 | balanced-match@^0.4.1: 598 | version "0.4.2" 599 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" 600 | 601 | bcrypt-pbkdf@^1.0.0: 602 | version "1.0.1" 603 | resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" 604 | dependencies: 605 | tweetnacl "^0.14.3" 606 | 607 | binary-extensions@^1.0.0: 608 | version "1.8.0" 609 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774" 610 | 611 | block-stream@*: 612 | version "0.0.9" 613 | resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" 614 | dependencies: 615 | inherits "~2.0.0" 616 | 617 | bluebird@^3.5.0: 618 | version "3.5.0" 619 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" 620 | 621 | boom@2.x.x: 622 | version "2.10.1" 623 | resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" 624 | dependencies: 625 | hoek "2.x.x" 626 | 627 | brace-expansion@^1.0.0: 628 | version "1.1.6" 629 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" 630 | dependencies: 631 | balanced-match "^0.4.1" 632 | concat-map "0.0.1" 633 | 634 | braces@^1.8.2: 635 | version "1.8.5" 636 | resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" 637 | dependencies: 638 | expand-range "^1.8.1" 639 | preserve "^0.2.0" 640 | repeat-element "^1.1.2" 641 | 642 | browserslist@^1.4.0: 643 | version "1.7.7" 644 | resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" 645 | dependencies: 646 | caniuse-db "^1.0.30000639" 647 | electron-to-chromium "^1.2.7" 648 | 649 | buffer-shims@~1.0.0: 650 | version "1.0.0" 651 | resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" 652 | 653 | bytes@2.4.0: 654 | version "2.4.0" 655 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" 656 | 657 | caniuse-db@^1.0.30000639: 658 | version "1.0.30000649" 659 | resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000649.tgz#1ee1754a6df235450c8b7cd15e0ebf507221a86a" 660 | 661 | caseless@~0.12.0: 662 | version "0.12.0" 663 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 664 | 665 | chalk@^1.1.0: 666 | version "1.1.3" 667 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" 668 | dependencies: 669 | ansi-styles "^2.2.1" 670 | escape-string-regexp "^1.0.2" 671 | has-ansi "^2.0.0" 672 | strip-ansi "^3.0.0" 673 | supports-color "^2.0.0" 674 | 675 | chalk@~0.4.0: 676 | version "0.4.0" 677 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" 678 | dependencies: 679 | ansi-styles "~1.0.0" 680 | has-color "~0.1.0" 681 | strip-ansi "~0.1.0" 682 | 683 | chokidar@^1.6.1: 684 | version "1.6.1" 685 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2" 686 | dependencies: 687 | anymatch "^1.3.0" 688 | async-each "^1.0.0" 689 | glob-parent "^2.0.0" 690 | inherits "^2.0.1" 691 | is-binary-path "^1.0.0" 692 | is-glob "^2.0.0" 693 | path-is-absolute "^1.0.0" 694 | readdirp "^2.0.0" 695 | optionalDependencies: 696 | fsevents "^1.0.0" 697 | 698 | co@^4.6.0: 699 | version "4.6.0" 700 | resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" 701 | 702 | code-point-at@^1.0.0: 703 | version "1.1.0" 704 | resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" 705 | 706 | colors@1.0.x: 707 | version "1.0.3" 708 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" 709 | 710 | combined-stream@^1.0.5, combined-stream@~1.0.5: 711 | version "1.0.5" 712 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" 713 | dependencies: 714 | delayed-stream "~1.0.0" 715 | 716 | commander@^2.8.1: 717 | version "2.9.0" 718 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" 719 | dependencies: 720 | graceful-readlink ">= 1.0.0" 721 | 722 | concat-map@0.0.1: 723 | version "0.0.1" 724 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 725 | 726 | console-control-strings@^1.0.0, console-control-strings@~1.1.0: 727 | version "1.1.0" 728 | resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" 729 | 730 | content-disposition@0.5.2: 731 | version "0.5.2" 732 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 733 | 734 | content-type@^1.0.2, content-type@~1.0.2: 735 | version "1.0.2" 736 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" 737 | 738 | convert-source-map@^1.1.0: 739 | version "1.5.0" 740 | resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" 741 | 742 | cookie-signature@1.0.6: 743 | version "1.0.6" 744 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 745 | 746 | cookie@0.3.1: 747 | version "0.3.1" 748 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 749 | 750 | core-js@^2.4.0: 751 | version "2.4.1" 752 | resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" 753 | 754 | core-util-is@~1.0.0: 755 | version "1.0.2" 756 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 757 | 758 | cryptiles@2.x.x: 759 | version "2.0.5" 760 | resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" 761 | dependencies: 762 | boom "2.x.x" 763 | 764 | cycle@1.0.x: 765 | version "1.0.3" 766 | resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" 767 | 768 | dashdash@^1.12.0: 769 | version "1.14.1" 770 | resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" 771 | dependencies: 772 | assert-plus "^1.0.0" 773 | 774 | debug@2.6.1, debug@^2.1.1: 775 | version "2.6.1" 776 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.1.tgz#79855090ba2c4e3115cc7d8769491d58f0491351" 777 | dependencies: 778 | ms "0.7.2" 779 | 780 | debug@2.6.3, debug@^2.2.0: 781 | version "2.6.3" 782 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" 783 | dependencies: 784 | ms "0.7.2" 785 | 786 | deep-extend@~0.4.0: 787 | version "0.4.1" 788 | resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253" 789 | 790 | delayed-stream@~1.0.0: 791 | version "1.0.0" 792 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 793 | 794 | delegates@^1.0.0: 795 | version "1.0.0" 796 | resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" 797 | 798 | depd@1.1.0, depd@~1.1.0: 799 | version "1.1.0" 800 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" 801 | 802 | deprecated-decorator@^0.1.6: 803 | version "0.1.6" 804 | resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37" 805 | 806 | destroy@~1.0.4: 807 | version "1.0.4" 808 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 809 | 810 | detect-indent@^4.0.0: 811 | version "4.0.0" 812 | resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" 813 | dependencies: 814 | repeating "^2.0.0" 815 | 816 | ecc-jsbn@~0.1.1: 817 | version "0.1.1" 818 | resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" 819 | dependencies: 820 | jsbn "~0.1.0" 821 | 822 | ee-first@1.1.1: 823 | version "1.1.1" 824 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 825 | 826 | electron-to-chromium@^1.2.7: 827 | version "1.3.2" 828 | resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.2.tgz#b8ce5c93b308db0e92f6d0435c46ddec8f6363ab" 829 | 830 | encodeurl@~1.0.1: 831 | version "1.0.1" 832 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" 833 | 834 | escape-html@~1.0.3: 835 | version "1.0.3" 836 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 837 | 838 | escape-string-regexp@^1.0.2: 839 | version "1.0.5" 840 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 841 | 842 | esutils@^2.0.2: 843 | version "2.0.2" 844 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" 845 | 846 | etag@~1.8.0: 847 | version "1.8.0" 848 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" 849 | 850 | expand-brackets@^0.1.4: 851 | version "0.1.5" 852 | resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" 853 | dependencies: 854 | is-posix-bracket "^0.1.0" 855 | 856 | expand-range@^1.8.1: 857 | version "1.8.2" 858 | resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" 859 | dependencies: 860 | fill-range "^2.1.0" 861 | 862 | express-graphql@^0.6.4: 863 | version "0.6.4" 864 | resolved "https://registry.yarnpkg.com/express-graphql/-/express-graphql-0.6.4.tgz#e51c6281d075613feac72b3fb569440602d3dfe4" 865 | dependencies: 866 | accepts "^1.3.0" 867 | content-type "^1.0.2" 868 | http-errors "^1.3.0" 869 | raw-body "^2.1.0" 870 | 871 | express-winston@^2.4.0: 872 | version "2.4.0" 873 | resolved "https://registry.yarnpkg.com/express-winston/-/express-winston-2.4.0.tgz#27ab6cd93053e2dfdc35bceea14a077dc7d52e49" 874 | dependencies: 875 | chalk "~0.4.0" 876 | lodash "~4.11.1" 877 | 878 | express@^4.15.2: 879 | version "4.15.2" 880 | resolved "https://registry.yarnpkg.com/express/-/express-4.15.2.tgz#af107fc148504457f2dca9a6f2571d7129b97b35" 881 | dependencies: 882 | accepts "~1.3.3" 883 | array-flatten "1.1.1" 884 | content-disposition "0.5.2" 885 | content-type "~1.0.2" 886 | cookie "0.3.1" 887 | cookie-signature "1.0.6" 888 | debug "2.6.1" 889 | depd "~1.1.0" 890 | encodeurl "~1.0.1" 891 | escape-html "~1.0.3" 892 | etag "~1.8.0" 893 | finalhandler "~1.0.0" 894 | fresh "0.5.0" 895 | merge-descriptors "1.0.1" 896 | methods "~1.1.2" 897 | on-finished "~2.3.0" 898 | parseurl "~1.3.1" 899 | path-to-regexp "0.1.7" 900 | proxy-addr "~1.1.3" 901 | qs "6.4.0" 902 | range-parser "~1.2.0" 903 | send "0.15.1" 904 | serve-static "1.12.1" 905 | setprototypeof "1.0.3" 906 | statuses "~1.3.1" 907 | type-is "~1.6.14" 908 | utils-merge "1.0.0" 909 | vary "~1.1.0" 910 | 911 | extend@~3.0.0: 912 | version "3.0.0" 913 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" 914 | 915 | extglob@^0.3.1: 916 | version "0.3.2" 917 | resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" 918 | dependencies: 919 | is-extglob "^1.0.0" 920 | 921 | extsprintf@1.0.2: 922 | version "1.0.2" 923 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" 924 | 925 | eyes@0.1.x: 926 | version "0.1.8" 927 | resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" 928 | 929 | filename-regex@^2.0.0: 930 | version "2.0.0" 931 | resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.0.tgz#996e3e80479b98b9897f15a8a58b3d084e926775" 932 | 933 | fill-range@^2.1.0: 934 | version "2.2.3" 935 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" 936 | dependencies: 937 | is-number "^2.1.0" 938 | isobject "^2.0.0" 939 | randomatic "^1.1.3" 940 | repeat-element "^1.1.2" 941 | repeat-string "^1.5.2" 942 | 943 | finalhandler@~1.0.0: 944 | version "1.0.1" 945 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.1.tgz#bcd15d1689c0e5ed729b6f7f541a6df984117db8" 946 | dependencies: 947 | debug "2.6.3" 948 | encodeurl "~1.0.1" 949 | escape-html "~1.0.3" 950 | on-finished "~2.3.0" 951 | parseurl "~1.3.1" 952 | statuses "~1.3.1" 953 | unpipe "~1.0.0" 954 | 955 | for-in@^1.0.1: 956 | version "1.0.2" 957 | resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" 958 | 959 | for-own@^0.1.4: 960 | version "0.1.5" 961 | resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" 962 | dependencies: 963 | for-in "^1.0.1" 964 | 965 | forever-agent@~0.6.1: 966 | version "0.6.1" 967 | resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" 968 | 969 | form-data@~2.1.1: 970 | version "2.1.4" 971 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" 972 | dependencies: 973 | asynckit "^0.4.0" 974 | combined-stream "^1.0.5" 975 | mime-types "^2.1.12" 976 | 977 | forwarded@~0.1.0: 978 | version "0.1.0" 979 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" 980 | 981 | fresh@0.5.0: 982 | version "0.5.0" 983 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" 984 | 985 | fs-readdir-recursive@^1.0.0: 986 | version "1.0.0" 987 | resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" 988 | 989 | fs.realpath@^1.0.0: 990 | version "1.0.0" 991 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 992 | 993 | fsevents@^1.0.0: 994 | version "1.1.1" 995 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.1.tgz#f19fd28f43eeaf761680e519a203c4d0b3d31aff" 996 | dependencies: 997 | nan "^2.3.0" 998 | node-pre-gyp "^0.6.29" 999 | 1000 | fstream-ignore@^1.0.5: 1001 | version "1.0.5" 1002 | resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" 1003 | dependencies: 1004 | fstream "^1.0.0" 1005 | inherits "2" 1006 | minimatch "^3.0.0" 1007 | 1008 | fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: 1009 | version "1.0.11" 1010 | resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" 1011 | dependencies: 1012 | graceful-fs "^4.1.2" 1013 | inherits "~2.0.0" 1014 | mkdirp ">=0.5 0" 1015 | rimraf "2" 1016 | 1017 | gauge@~2.7.1: 1018 | version "2.7.3" 1019 | resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.3.tgz#1c23855f962f17b3ad3d0dc7443f304542edfe09" 1020 | dependencies: 1021 | aproba "^1.0.3" 1022 | console-control-strings "^1.0.0" 1023 | has-unicode "^2.0.0" 1024 | object-assign "^4.1.0" 1025 | signal-exit "^3.0.0" 1026 | string-width "^1.0.1" 1027 | strip-ansi "^3.0.1" 1028 | wide-align "^1.1.0" 1029 | 1030 | getpass@^0.1.1: 1031 | version "0.1.6" 1032 | resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6" 1033 | dependencies: 1034 | assert-plus "^1.0.0" 1035 | 1036 | glob-base@^0.3.0: 1037 | version "0.3.0" 1038 | resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" 1039 | dependencies: 1040 | glob-parent "^2.0.0" 1041 | is-glob "^2.0.0" 1042 | 1043 | glob-parent@^2.0.0: 1044 | version "2.0.0" 1045 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" 1046 | dependencies: 1047 | is-glob "^2.0.0" 1048 | 1049 | glob@^7.0.0, glob@^7.0.5: 1050 | version "7.1.1" 1051 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" 1052 | dependencies: 1053 | fs.realpath "^1.0.0" 1054 | inflight "^1.0.4" 1055 | inherits "2" 1056 | minimatch "^3.0.2" 1057 | once "^1.3.0" 1058 | path-is-absolute "^1.0.0" 1059 | 1060 | globals@^9.0.0: 1061 | version "9.17.0" 1062 | resolved "https://registry.yarnpkg.com/globals/-/globals-9.17.0.tgz#0c0ca696d9b9bb694d2e5470bd37777caad50286" 1063 | 1064 | graceful-fs@^4.1.2, graceful-fs@^4.1.4: 1065 | version "4.1.11" 1066 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 1067 | 1068 | "graceful-readlink@>= 1.0.0": 1069 | version "1.0.1" 1070 | resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" 1071 | 1072 | graphql-tools@^0.11.0: 1073 | version "0.11.0" 1074 | resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-0.11.0.tgz#14c372f6ddad7e63a757094d541a937d6b31b7da" 1075 | dependencies: 1076 | deprecated-decorator "^0.1.6" 1077 | lodash "^4.3.0" 1078 | uuid "^3.0.1" 1079 | optionalDependencies: 1080 | "@types/graphql" "^0.9.0" 1081 | 1082 | graphql@^0.9.3: 1083 | version "0.9.3" 1084 | resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.9.3.tgz#71fc0fa331bffb9c20678485861cfb370803118e" 1085 | dependencies: 1086 | iterall "1.0.3" 1087 | 1088 | har-schema@^1.0.5: 1089 | version "1.0.5" 1090 | resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" 1091 | 1092 | har-validator@~4.2.1: 1093 | version "4.2.1" 1094 | resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" 1095 | dependencies: 1096 | ajv "^4.9.1" 1097 | har-schema "^1.0.5" 1098 | 1099 | has-ansi@^2.0.0: 1100 | version "2.0.0" 1101 | resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" 1102 | dependencies: 1103 | ansi-regex "^2.0.0" 1104 | 1105 | has-color@~0.1.0: 1106 | version "0.1.7" 1107 | resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" 1108 | 1109 | has-unicode@^2.0.0: 1110 | version "2.0.1" 1111 | resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" 1112 | 1113 | hawk@~3.1.3: 1114 | version "3.1.3" 1115 | resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" 1116 | dependencies: 1117 | boom "2.x.x" 1118 | cryptiles "2.x.x" 1119 | hoek "2.x.x" 1120 | sntp "1.x.x" 1121 | 1122 | hoek@2.x.x: 1123 | version "2.16.3" 1124 | resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" 1125 | 1126 | home-or-tmp@^2.0.0: 1127 | version "2.0.0" 1128 | resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" 1129 | dependencies: 1130 | os-homedir "^1.0.0" 1131 | os-tmpdir "^1.0.1" 1132 | 1133 | http-errors@^1.3.0, http-errors@~1.6.1: 1134 | version "1.6.1" 1135 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" 1136 | dependencies: 1137 | depd "1.1.0" 1138 | inherits "2.0.3" 1139 | setprototypeof "1.0.3" 1140 | statuses ">= 1.3.1 < 2" 1141 | 1142 | http-signature@~1.1.0: 1143 | version "1.1.1" 1144 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" 1145 | dependencies: 1146 | assert-plus "^0.2.0" 1147 | jsprim "^1.2.2" 1148 | sshpk "^1.7.0" 1149 | 1150 | iconv-lite@0.4.15: 1151 | version "0.4.15" 1152 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" 1153 | 1154 | inflight@^1.0.4: 1155 | version "1.0.6" 1156 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 1157 | dependencies: 1158 | once "^1.3.0" 1159 | wrappy "1" 1160 | 1161 | inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1: 1162 | version "2.0.3" 1163 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 1164 | 1165 | ini@~1.3.0: 1166 | version "1.3.4" 1167 | resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" 1168 | 1169 | invariant@^2.2.0, invariant@^2.2.2: 1170 | version "2.2.2" 1171 | resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" 1172 | dependencies: 1173 | loose-envify "^1.0.0" 1174 | 1175 | ipaddr.js@1.3.0: 1176 | version "1.3.0" 1177 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec" 1178 | 1179 | is-binary-path@^1.0.0: 1180 | version "1.0.1" 1181 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" 1182 | dependencies: 1183 | binary-extensions "^1.0.0" 1184 | 1185 | is-buffer@^1.0.2: 1186 | version "1.1.5" 1187 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" 1188 | 1189 | is-dotfile@^1.0.0: 1190 | version "1.0.2" 1191 | resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d" 1192 | 1193 | is-equal-shallow@^0.1.3: 1194 | version "0.1.3" 1195 | resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" 1196 | dependencies: 1197 | is-primitive "^2.0.0" 1198 | 1199 | is-extendable@^0.1.1: 1200 | version "0.1.1" 1201 | resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" 1202 | 1203 | is-extglob@^1.0.0: 1204 | version "1.0.0" 1205 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" 1206 | 1207 | is-finite@^1.0.0: 1208 | version "1.0.2" 1209 | resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" 1210 | dependencies: 1211 | number-is-nan "^1.0.0" 1212 | 1213 | is-fullwidth-code-point@^1.0.0: 1214 | version "1.0.0" 1215 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" 1216 | dependencies: 1217 | number-is-nan "^1.0.0" 1218 | 1219 | is-glob@^2.0.0, is-glob@^2.0.1: 1220 | version "2.0.1" 1221 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" 1222 | dependencies: 1223 | is-extglob "^1.0.0" 1224 | 1225 | is-number@^2.0.2, is-number@^2.1.0: 1226 | version "2.1.0" 1227 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" 1228 | dependencies: 1229 | kind-of "^3.0.2" 1230 | 1231 | is-posix-bracket@^0.1.0: 1232 | version "0.1.1" 1233 | resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" 1234 | 1235 | is-primitive@^2.0.0: 1236 | version "2.0.0" 1237 | resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" 1238 | 1239 | is-typedarray@~1.0.0: 1240 | version "1.0.0" 1241 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 1242 | 1243 | isarray@1.0.0, isarray@~1.0.0: 1244 | version "1.0.0" 1245 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 1246 | 1247 | isobject@^2.0.0: 1248 | version "2.1.0" 1249 | resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" 1250 | dependencies: 1251 | isarray "1.0.0" 1252 | 1253 | isstream@0.1.x, isstream@~0.1.2: 1254 | version "0.1.2" 1255 | resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 1256 | 1257 | iterall@1.0.3: 1258 | version "1.0.3" 1259 | resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.0.3.tgz#e0b31958f835013c323ff0b10943829ac69aa4b7" 1260 | 1261 | jodid25519@^1.0.0: 1262 | version "1.0.2" 1263 | resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" 1264 | dependencies: 1265 | jsbn "~0.1.0" 1266 | 1267 | js-tokens@^3.0.0: 1268 | version "3.0.1" 1269 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" 1270 | 1271 | jsbn@~0.1.0: 1272 | version "0.1.1" 1273 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" 1274 | 1275 | jsesc@^1.3.0: 1276 | version "1.3.0" 1277 | resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" 1278 | 1279 | jsesc@~0.5.0: 1280 | version "0.5.0" 1281 | resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" 1282 | 1283 | json-schema@0.2.3: 1284 | version "0.2.3" 1285 | resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" 1286 | 1287 | json-stable-stringify@^1.0.1: 1288 | version "1.0.1" 1289 | resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" 1290 | dependencies: 1291 | jsonify "~0.0.0" 1292 | 1293 | json-stringify-safe@~5.0.1: 1294 | version "5.0.1" 1295 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 1296 | 1297 | json5@^0.5.0: 1298 | version "0.5.1" 1299 | resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" 1300 | 1301 | jsonify@~0.0.0: 1302 | version "0.0.0" 1303 | resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" 1304 | 1305 | jsprim@^1.2.2: 1306 | version "1.4.0" 1307 | resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918" 1308 | dependencies: 1309 | assert-plus "1.0.0" 1310 | extsprintf "1.0.2" 1311 | json-schema "0.2.3" 1312 | verror "1.3.6" 1313 | 1314 | kind-of@^3.0.2: 1315 | version "3.1.0" 1316 | resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.1.0.tgz#475d698a5e49ff5e53d14e3e732429dc8bf4cf47" 1317 | dependencies: 1318 | is-buffer "^1.0.2" 1319 | 1320 | lodash@^4.2.0, lodash@^4.3.0: 1321 | version "4.17.4" 1322 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" 1323 | 1324 | lodash@~4.11.1: 1325 | version "4.11.2" 1326 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.11.2.tgz#d6b4338b110a58e21dae5cebcfdbbfd2bc4cdb3b" 1327 | 1328 | loose-envify@^1.0.0: 1329 | version "1.3.1" 1330 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" 1331 | dependencies: 1332 | js-tokens "^3.0.0" 1333 | 1334 | media-typer@0.3.0: 1335 | version "0.3.0" 1336 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 1337 | 1338 | merge-descriptors@1.0.1: 1339 | version "1.0.1" 1340 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 1341 | 1342 | methods@~1.1.2: 1343 | version "1.1.2" 1344 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 1345 | 1346 | micromatch@^2.1.5: 1347 | version "2.3.11" 1348 | resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" 1349 | dependencies: 1350 | arr-diff "^2.0.0" 1351 | array-unique "^0.2.1" 1352 | braces "^1.8.2" 1353 | expand-brackets "^0.1.4" 1354 | extglob "^0.3.1" 1355 | filename-regex "^2.0.0" 1356 | is-extglob "^1.0.0" 1357 | is-glob "^2.0.1" 1358 | kind-of "^3.0.2" 1359 | normalize-path "^2.0.1" 1360 | object.omit "^2.0.0" 1361 | parse-glob "^3.0.4" 1362 | regex-cache "^0.4.2" 1363 | 1364 | mime-db@~1.27.0: 1365 | version "1.27.0" 1366 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" 1367 | 1368 | mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7: 1369 | version "2.1.15" 1370 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" 1371 | dependencies: 1372 | mime-db "~1.27.0" 1373 | 1374 | mime@1.3.4: 1375 | version "1.3.4" 1376 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" 1377 | 1378 | minimatch@^3.0.0, minimatch@^3.0.2: 1379 | version "3.0.3" 1380 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" 1381 | dependencies: 1382 | brace-expansion "^1.0.0" 1383 | 1384 | minimist@0.0.8: 1385 | version "0.0.8" 1386 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 1387 | 1388 | minimist@^1.2.0: 1389 | version "1.2.0" 1390 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 1391 | 1392 | "mkdirp@>=0.5 0", mkdirp@^0.5.1: 1393 | version "0.5.1" 1394 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 1395 | dependencies: 1396 | minimist "0.0.8" 1397 | 1398 | ms@0.7.2: 1399 | version "0.7.2" 1400 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" 1401 | 1402 | nan@^2.3.0: 1403 | version "2.6.1" 1404 | resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.1.tgz#8c84f7b14c96b89f57fbc838012180ec8ca39a01" 1405 | 1406 | negotiator@0.6.1: 1407 | version "0.6.1" 1408 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 1409 | 1410 | node-pre-gyp@^0.6.29: 1411 | version "0.6.34" 1412 | resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.34.tgz#94ad1c798a11d7fc67381b50d47f8cc18d9799f7" 1413 | dependencies: 1414 | mkdirp "^0.5.1" 1415 | nopt "^4.0.1" 1416 | npmlog "^4.0.2" 1417 | rc "^1.1.7" 1418 | request "^2.81.0" 1419 | rimraf "^2.6.1" 1420 | semver "^5.3.0" 1421 | tar "^2.2.1" 1422 | tar-pack "^3.4.0" 1423 | 1424 | nopt@^4.0.1: 1425 | version "4.0.1" 1426 | resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" 1427 | dependencies: 1428 | abbrev "1" 1429 | osenv "^0.1.4" 1430 | 1431 | normalize-path@^2.0.1: 1432 | version "2.1.1" 1433 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" 1434 | dependencies: 1435 | remove-trailing-separator "^1.0.1" 1436 | 1437 | npmlog@^4.0.2: 1438 | version "4.0.2" 1439 | resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.2.tgz#d03950e0e78ce1527ba26d2a7592e9348ac3e75f" 1440 | dependencies: 1441 | are-we-there-yet "~1.1.2" 1442 | console-control-strings "~1.1.0" 1443 | gauge "~2.7.1" 1444 | set-blocking "~2.0.0" 1445 | 1446 | number-is-nan@^1.0.0: 1447 | version "1.0.1" 1448 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 1449 | 1450 | oauth-sign@~0.8.1: 1451 | version "0.8.2" 1452 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" 1453 | 1454 | object-assign@^4.1.0: 1455 | version "4.1.1" 1456 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 1457 | 1458 | object.omit@^2.0.0: 1459 | version "2.0.1" 1460 | resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" 1461 | dependencies: 1462 | for-own "^0.1.4" 1463 | is-extendable "^0.1.1" 1464 | 1465 | on-finished@~2.3.0: 1466 | version "2.3.0" 1467 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 1468 | dependencies: 1469 | ee-first "1.1.1" 1470 | 1471 | once@^1.3.0, once@^1.3.3: 1472 | version "1.4.0" 1473 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 1474 | dependencies: 1475 | wrappy "1" 1476 | 1477 | os-homedir@^1.0.0: 1478 | version "1.0.2" 1479 | resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" 1480 | 1481 | os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: 1482 | version "1.0.2" 1483 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 1484 | 1485 | osenv@^0.1.4: 1486 | version "0.1.4" 1487 | resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" 1488 | dependencies: 1489 | os-homedir "^1.0.0" 1490 | os-tmpdir "^1.0.0" 1491 | 1492 | output-file-sync@^1.1.0: 1493 | version "1.1.2" 1494 | resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76" 1495 | dependencies: 1496 | graceful-fs "^4.1.4" 1497 | mkdirp "^0.5.1" 1498 | object-assign "^4.1.0" 1499 | 1500 | parse-glob@^3.0.4: 1501 | version "3.0.4" 1502 | resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" 1503 | dependencies: 1504 | glob-base "^0.3.0" 1505 | is-dotfile "^1.0.0" 1506 | is-extglob "^1.0.0" 1507 | is-glob "^2.0.0" 1508 | 1509 | parseurl@~1.3.1: 1510 | version "1.3.1" 1511 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" 1512 | 1513 | path-is-absolute@^1.0.0: 1514 | version "1.0.1" 1515 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 1516 | 1517 | path-to-regexp@0.1.7: 1518 | version "0.1.7" 1519 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 1520 | 1521 | performance-now@^0.2.0: 1522 | version "0.2.0" 1523 | resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" 1524 | 1525 | preserve@^0.2.0: 1526 | version "0.2.0" 1527 | resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" 1528 | 1529 | private@^0.1.6: 1530 | version "0.1.7" 1531 | resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" 1532 | 1533 | process-nextick-args@~1.0.6: 1534 | version "1.0.7" 1535 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" 1536 | 1537 | proxy-addr@~1.1.3: 1538 | version "1.1.4" 1539 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3" 1540 | dependencies: 1541 | forwarded "~0.1.0" 1542 | ipaddr.js "1.3.0" 1543 | 1544 | punycode@^1.4.1: 1545 | version "1.4.1" 1546 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" 1547 | 1548 | qs@6.4.0, qs@~6.4.0: 1549 | version "6.4.0" 1550 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" 1551 | 1552 | randomatic@^1.1.3: 1553 | version "1.1.6" 1554 | resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb" 1555 | dependencies: 1556 | is-number "^2.0.2" 1557 | kind-of "^3.0.2" 1558 | 1559 | range-parser@~1.2.0: 1560 | version "1.2.0" 1561 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 1562 | 1563 | raw-body@^2.1.0: 1564 | version "2.2.0" 1565 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96" 1566 | dependencies: 1567 | bytes "2.4.0" 1568 | iconv-lite "0.4.15" 1569 | unpipe "1.0.0" 1570 | 1571 | rc@^1.1.7: 1572 | version "1.2.1" 1573 | resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" 1574 | dependencies: 1575 | deep-extend "~0.4.0" 1576 | ini "~1.3.0" 1577 | minimist "^1.2.0" 1578 | strip-json-comments "~2.0.1" 1579 | 1580 | "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, readable-stream@^2.1.4: 1581 | version "2.2.9" 1582 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.9.tgz#cf78ec6f4a6d1eb43d26488cac97f042e74b7fc8" 1583 | dependencies: 1584 | buffer-shims "~1.0.0" 1585 | core-util-is "~1.0.0" 1586 | inherits "~2.0.1" 1587 | isarray "~1.0.0" 1588 | process-nextick-args "~1.0.6" 1589 | string_decoder "~1.0.0" 1590 | util-deprecate "~1.0.1" 1591 | 1592 | readdirp@^2.0.0: 1593 | version "2.1.0" 1594 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" 1595 | dependencies: 1596 | graceful-fs "^4.1.2" 1597 | minimatch "^3.0.2" 1598 | readable-stream "^2.0.2" 1599 | set-immediate-shim "^1.0.1" 1600 | 1601 | regenerate@^1.2.1: 1602 | version "1.3.2" 1603 | resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" 1604 | 1605 | regenerator-runtime@^0.10.0: 1606 | version "0.10.3" 1607 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.3.tgz#8c4367a904b51ea62a908ac310bf99ff90a82a3e" 1608 | 1609 | regenerator-transform@0.9.8: 1610 | version "0.9.8" 1611 | resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.9.8.tgz#0f88bb2bc03932ddb7b6b7312e68078f01026d6c" 1612 | dependencies: 1613 | babel-runtime "^6.18.0" 1614 | babel-types "^6.19.0" 1615 | private "^0.1.6" 1616 | 1617 | regex-cache@^0.4.2: 1618 | version "0.4.3" 1619 | resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.3.tgz#9b1a6c35d4d0dfcef5711ae651e8e9d3d7114145" 1620 | dependencies: 1621 | is-equal-shallow "^0.1.3" 1622 | is-primitive "^2.0.0" 1623 | 1624 | regexpu-core@^2.0.0: 1625 | version "2.0.0" 1626 | resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" 1627 | dependencies: 1628 | regenerate "^1.2.1" 1629 | regjsgen "^0.2.0" 1630 | regjsparser "^0.1.4" 1631 | 1632 | regjsgen@^0.2.0: 1633 | version "0.2.0" 1634 | resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" 1635 | 1636 | regjsparser@^0.1.4: 1637 | version "0.1.5" 1638 | resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" 1639 | dependencies: 1640 | jsesc "~0.5.0" 1641 | 1642 | remove-trailing-separator@^1.0.1: 1643 | version "1.0.1" 1644 | resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.0.1.tgz#615ebb96af559552d4bf4057c8436d486ab63cc4" 1645 | 1646 | repeat-element@^1.1.2: 1647 | version "1.1.2" 1648 | resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" 1649 | 1650 | repeat-string@^1.5.2: 1651 | version "1.6.1" 1652 | resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" 1653 | 1654 | repeating@^2.0.0: 1655 | version "2.0.1" 1656 | resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" 1657 | dependencies: 1658 | is-finite "^1.0.0" 1659 | 1660 | request@^2.81.0: 1661 | version "2.81.0" 1662 | resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" 1663 | dependencies: 1664 | aws-sign2 "~0.6.0" 1665 | aws4 "^1.2.1" 1666 | caseless "~0.12.0" 1667 | combined-stream "~1.0.5" 1668 | extend "~3.0.0" 1669 | forever-agent "~0.6.1" 1670 | form-data "~2.1.1" 1671 | har-validator "~4.2.1" 1672 | hawk "~3.1.3" 1673 | http-signature "~1.1.0" 1674 | is-typedarray "~1.0.0" 1675 | isstream "~0.1.2" 1676 | json-stringify-safe "~5.0.1" 1677 | mime-types "~2.1.7" 1678 | oauth-sign "~0.8.1" 1679 | performance-now "^0.2.0" 1680 | qs "~6.4.0" 1681 | safe-buffer "^5.0.1" 1682 | stringstream "~0.0.4" 1683 | tough-cookie "~2.3.0" 1684 | tunnel-agent "^0.6.0" 1685 | uuid "^3.0.0" 1686 | 1687 | rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1: 1688 | version "2.6.1" 1689 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" 1690 | dependencies: 1691 | glob "^7.0.5" 1692 | 1693 | safe-buffer@^5.0.1: 1694 | version "5.0.1" 1695 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" 1696 | 1697 | semver@^5.3.0: 1698 | version "5.3.0" 1699 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" 1700 | 1701 | send@0.15.1: 1702 | version "0.15.1" 1703 | resolved "https://registry.yarnpkg.com/send/-/send-0.15.1.tgz#8a02354c26e6f5cca700065f5f0cdeba90ec7b5f" 1704 | dependencies: 1705 | debug "2.6.1" 1706 | depd "~1.1.0" 1707 | destroy "~1.0.4" 1708 | encodeurl "~1.0.1" 1709 | escape-html "~1.0.3" 1710 | etag "~1.8.0" 1711 | fresh "0.5.0" 1712 | http-errors "~1.6.1" 1713 | mime "1.3.4" 1714 | ms "0.7.2" 1715 | on-finished "~2.3.0" 1716 | range-parser "~1.2.0" 1717 | statuses "~1.3.1" 1718 | 1719 | serve-static@1.12.1: 1720 | version "1.12.1" 1721 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.1.tgz#7443a965e3ced647aceb5639fa06bf4d1bbe0039" 1722 | dependencies: 1723 | encodeurl "~1.0.1" 1724 | escape-html "~1.0.3" 1725 | parseurl "~1.3.1" 1726 | send "0.15.1" 1727 | 1728 | set-blocking@~2.0.0: 1729 | version "2.0.0" 1730 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 1731 | 1732 | set-immediate-shim@^1.0.1: 1733 | version "1.0.1" 1734 | resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" 1735 | 1736 | setprototypeof@1.0.3: 1737 | version "1.0.3" 1738 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 1739 | 1740 | signal-exit@^3.0.0: 1741 | version "3.0.2" 1742 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 1743 | 1744 | slash@^1.0.0: 1745 | version "1.0.0" 1746 | resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" 1747 | 1748 | sntp@1.x.x: 1749 | version "1.0.9" 1750 | resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" 1751 | dependencies: 1752 | hoek "2.x.x" 1753 | 1754 | source-map-support@^0.4.2: 1755 | version "0.4.14" 1756 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.14.tgz#9d4463772598b86271b4f523f6c1f4e02a7d6aef" 1757 | dependencies: 1758 | source-map "^0.5.6" 1759 | 1760 | source-map@^0.5.0, source-map@^0.5.6: 1761 | version "0.5.6" 1762 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" 1763 | 1764 | sshpk@^1.7.0: 1765 | version "1.11.0" 1766 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.11.0.tgz#2d8d5ebb4a6fab28ffba37fa62a90f4a3ea59d77" 1767 | dependencies: 1768 | asn1 "~0.2.3" 1769 | assert-plus "^1.0.0" 1770 | dashdash "^1.12.0" 1771 | getpass "^0.1.1" 1772 | optionalDependencies: 1773 | bcrypt-pbkdf "^1.0.0" 1774 | ecc-jsbn "~0.1.1" 1775 | jodid25519 "^1.0.0" 1776 | jsbn "~0.1.0" 1777 | tweetnacl "~0.14.0" 1778 | 1779 | stack-trace@0.0.x: 1780 | version "0.0.10" 1781 | resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" 1782 | 1783 | "statuses@>= 1.3.1 < 2", statuses@~1.3.1: 1784 | version "1.3.1" 1785 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 1786 | 1787 | string-width@^1.0.1: 1788 | version "1.0.2" 1789 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" 1790 | dependencies: 1791 | code-point-at "^1.0.0" 1792 | is-fullwidth-code-point "^1.0.0" 1793 | strip-ansi "^3.0.0" 1794 | 1795 | string_decoder@~1.0.0: 1796 | version "1.0.0" 1797 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.0.tgz#f06f41157b664d86069f84bdbdc9b0d8ab281667" 1798 | dependencies: 1799 | buffer-shims "~1.0.0" 1800 | 1801 | stringstream@~0.0.4: 1802 | version "0.0.5" 1803 | resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" 1804 | 1805 | strip-ansi@^3.0.0, strip-ansi@^3.0.1: 1806 | version "3.0.1" 1807 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 1808 | dependencies: 1809 | ansi-regex "^2.0.0" 1810 | 1811 | strip-ansi@~0.1.0: 1812 | version "0.1.1" 1813 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" 1814 | 1815 | strip-json-comments@~2.0.1: 1816 | version "2.0.1" 1817 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 1818 | 1819 | supports-color@^2.0.0: 1820 | version "2.0.0" 1821 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" 1822 | 1823 | tar-pack@^3.4.0: 1824 | version "3.4.0" 1825 | resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984" 1826 | dependencies: 1827 | debug "^2.2.0" 1828 | fstream "^1.0.10" 1829 | fstream-ignore "^1.0.5" 1830 | once "^1.3.3" 1831 | readable-stream "^2.1.4" 1832 | rimraf "^2.5.1" 1833 | tar "^2.2.1" 1834 | uid-number "^0.0.6" 1835 | 1836 | tar@^2.2.1: 1837 | version "2.2.1" 1838 | resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" 1839 | dependencies: 1840 | block-stream "*" 1841 | fstream "^1.0.2" 1842 | inherits "2" 1843 | 1844 | to-fast-properties@^1.0.1: 1845 | version "1.0.2" 1846 | resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" 1847 | 1848 | tough-cookie@~2.3.0: 1849 | version "2.3.2" 1850 | resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" 1851 | dependencies: 1852 | punycode "^1.4.1" 1853 | 1854 | trim-right@^1.0.1: 1855 | version "1.0.1" 1856 | resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" 1857 | 1858 | tunnel-agent@^0.6.0: 1859 | version "0.6.0" 1860 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 1861 | dependencies: 1862 | safe-buffer "^5.0.1" 1863 | 1864 | tweetnacl@^0.14.3, tweetnacl@~0.14.0: 1865 | version "0.14.5" 1866 | resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 1867 | 1868 | type-is@~1.6.14: 1869 | version "1.6.15" 1870 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" 1871 | dependencies: 1872 | media-typer "0.3.0" 1873 | mime-types "~2.1.15" 1874 | 1875 | uid-number@^0.0.6: 1876 | version "0.0.6" 1877 | resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" 1878 | 1879 | unpipe@1.0.0, unpipe@~1.0.0: 1880 | version "1.0.0" 1881 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1882 | 1883 | user-home@^1.1.1: 1884 | version "1.1.1" 1885 | resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" 1886 | 1887 | util-deprecate@~1.0.1: 1888 | version "1.0.2" 1889 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 1890 | 1891 | utils-merge@1.0.0: 1892 | version "1.0.0" 1893 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" 1894 | 1895 | uuid@^3.0.0, uuid@^3.0.1: 1896 | version "3.0.1" 1897 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" 1898 | 1899 | v8flags@^2.0.10: 1900 | version "2.0.12" 1901 | resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.0.12.tgz#73235d9f7176f8e8833fb286795445f7938d84e5" 1902 | dependencies: 1903 | user-home "^1.1.1" 1904 | 1905 | vary@~1.1.0: 1906 | version "1.1.1" 1907 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" 1908 | 1909 | verror@1.3.6: 1910 | version "1.3.6" 1911 | resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" 1912 | dependencies: 1913 | extsprintf "1.0.2" 1914 | 1915 | wide-align@^1.1.0: 1916 | version "1.1.0" 1917 | resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.0.tgz#40edde802a71fea1f070da3e62dcda2e7add96ad" 1918 | dependencies: 1919 | string-width "^1.0.1" 1920 | 1921 | winston@^2.3.1: 1922 | version "2.3.1" 1923 | resolved "https://registry.yarnpkg.com/winston/-/winston-2.3.1.tgz#0b48420d978c01804cf0230b648861598225a119" 1924 | dependencies: 1925 | async "~1.0.0" 1926 | colors "1.0.x" 1927 | cycle "1.0.x" 1928 | eyes "0.1.x" 1929 | isstream "0.1.x" 1930 | stack-trace "0.0.x" 1931 | 1932 | wrappy@1: 1933 | version "1.0.2" 1934 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1935 | -------------------------------------------------------------------------------- /example/app.yml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | rproxy: 5 | image: ${registry}/rproxy:${tag} 6 | build: rproxy 7 | environment: 8 | PORT: 3000 9 | WEB_HOST: web:3000 10 | API_HOST: gateway:3000 11 | networks: 12 | - dmz 13 | deploy: 14 | replicas: 2 15 | update_config: 16 | parallelism: 1 17 | delay: 2s 18 | restart_policy: 19 | condition: on-failure 20 | placement: 21 | constraints: 22 | - node.role == worker 23 | web: 24 | image: ${registry}/web:${tag} 25 | build: web 26 | environment: 27 | PORT: 3000 28 | API_HOST: gateway:3000 29 | networks: 30 | - dmz 31 | deploy: 32 | replicas: 2 33 | update_config: 34 | parallelism: 1 35 | delay: 2s 36 | restart_policy: 37 | condition: on-failure 38 | placement: 39 | constraints: 40 | - node.role == worker 41 | gateway: 42 | image: ${registry}/gateway:${tag} 43 | build: gateway 44 | environment: 45 | PORT: 3000 46 | API_HOST: api:3000 47 | networks: 48 | - dmz 49 | - private 50 | deploy: 51 | replicas: 2 52 | update_config: 53 | parallelism: 1 54 | delay: 2s 55 | restart_policy: 56 | condition: on-failure 57 | placement: 58 | constraints: 59 | - node.role == worker 60 | api: 61 | image: ${registry}/api:${tag} 62 | build: api 63 | environment: 64 | PORT: 3000 65 | networks: 66 | - private 67 | deploy: 68 | replicas: 2 69 | update_config: 70 | parallelism: 1 71 | delay: 5s 72 | restart_policy: 73 | condition: on-failure 74 | placement: 75 | constraints: 76 | - node.role == worker 77 | secrets: 78 | - my_secret 79 | secrets: 80 | my_secret: 81 | external: true 82 | networks: 83 | dmz: 84 | ipam: 85 | driver: default 86 | config: 87 | - subnet: 192.168.31.0/24 88 | private: 89 | ipam: 90 | driver: default 91 | config: 92 | - subnet: 192.168.32.0/24 93 | -------------------------------------------------------------------------------- /example/gateway/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | -------------------------------------------------------------------------------- /example/gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.8.1-alpine 2 | 3 | RUN apk add --update tini curl \ 4 | && rm -r /var/cache 5 | ENTRYPOINT ["/sbin/tini", "--"] 6 | 7 | RUN mkdir -p /go/src/app 8 | WORKDIR /go/src/app 9 | 10 | COPY . /go/src/app 11 | RUN go-wrapper download 12 | RUN go-wrapper install 13 | 14 | HEALTHCHECK --interval=5s --timeout=3s \ 15 | CMD curl --fail http://localhost:$PORT/_health || exit 1 16 | 17 | # Run under Tini 18 | CMD ["go-wrapper", "run", "main.go"] 19 | -------------------------------------------------------------------------------- /example/gateway/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httputil" 6 | "net/url" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | func handler(w http.ResponseWriter, r *http.Request) { 12 | } 13 | 14 | func singleJoiningSlash(a, b string) string { 15 | aslash := strings.HasSuffix(a, "/") 16 | bslash := strings.HasPrefix(b, "/") 17 | switch { 18 | case aslash && bslash: 19 | return a + b[1:] 20 | case !aslash && !bslash: 21 | return a + "/" + b 22 | } 23 | return a + b 24 | } 25 | 26 | // CreateProxy creates a new reverse proxy 27 | func CreateProxy(target *url.URL) *httputil.ReverseProxy { 28 | targetQuery := target.RawQuery 29 | director := func(req *http.Request) { 30 | req.URL.Scheme = target.Scheme 31 | req.URL.Host = target.Host 32 | req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) 33 | if targetQuery == "" || req.URL.RawQuery == "" { 34 | req.URL.RawQuery = targetQuery + req.URL.RawQuery 35 | } else { 36 | req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery 37 | } 38 | // set custom headers 39 | req.Header.Set("X-Hostname", os.Getenv("HOSTNAME")) 40 | req.Header.Set("X-Extra", "hello") 41 | } 42 | return &httputil.ReverseProxy{Director: director} 43 | } 44 | 45 | func main() { 46 | port := os.Getenv("PORT") 47 | apiHost := os.Getenv("API_HOST") 48 | 49 | proxy := CreateProxy(&url.URL{Scheme: "http", Host: apiHost}) 50 | mux := http.NewServeMux() 51 | mux.HandleFunc("/_health", handler) 52 | mux.Handle("/", proxy) 53 | http.ListenAndServe(":"+port, mux) 54 | } 55 | -------------------------------------------------------------------------------- /example/rproxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM haproxy:1.7.8-alpine 2 | 3 | # NOTE: Alpine doesn't have a syslog daemon so install one here if you want one 4 | # RUN apk add --no-cache rsyslog 5 | # 6 | # COPY rsyslog.conf /etc/rsyslog.conf 7 | 8 | COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg 9 | 10 | # NOTE: In order to start the rsyslog daemon, we need a new ENTRYPOINT (and therefor a new CMD) 11 | # COPY docker-entrypoint.sh / 12 | # ENTRYPOINT ["/docker-entrypoint.sh"] 13 | # CMD ["haproxy", "-f", "/usr/local/etc/haproxy/haproxy.cfg"] 14 | -------------------------------------------------------------------------------- /example/rproxy/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # first arg is `-f` or `--some-option` 5 | if [ "${1#-}" != "$1" ]; then 6 | set -- haproxy "$@" 7 | fi 8 | 9 | if [ "$1" = 'haproxy' ]; then 10 | # if the user wants "haproxy", let's use "haproxy-systemd-wrapper" instead so we can have proper reloadability implemented by upstream 11 | shift # "haproxy" 12 | set -- "$(which haproxy-systemd-wrapper)" -p /run/haproxy.pid "$@" 13 | fi 14 | 15 | # rsyslogd -n & 16 | exec "$@" 17 | -------------------------------------------------------------------------------- /example/rproxy/haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | debug 3 | maxconn 4096 4 | log 127.0.0.1:514 local2 debug 5 | 6 | defaults 7 | mode http 8 | log global 9 | option httplog 10 | # option logasap 11 | option dontlognull 12 | option log-health-checks 13 | option forwardfor 14 | option contstats 15 | option http-server-close 16 | retries 3 17 | option redispatch 18 | timeout connect 5s 19 | timeout client 30s 20 | timeout server 30s 21 | 22 | frontend http_front 23 | bind *:${PORT} 24 | stats enable 25 | stats uri /rproxy?stats 26 | 27 | acl api path_beg -i /api/ 28 | use_backend api_back if api 29 | 30 | default_backend web_back 31 | 32 | backend web_back 33 | balance roundrobin 34 | option httpchk GET /_health 35 | default-server inter 3s fall 3 rise 2 36 | server web ${WEB_HOST} check 37 | 38 | backend api_back 39 | balance roundrobin 40 | option httpchk GET /_health 41 | http-request set-path %[path,regsub(^/api/,/,)] 42 | default-server inter 3s fall 3 rise 2 43 | server api ${API_HOST} check 44 | -------------------------------------------------------------------------------- /example/rproxy/rsyslog.conf: -------------------------------------------------------------------------------- 1 | # rsyslogd.conf 2 | # 3 | # if you experience problems, check: 4 | # http://www.rsyslog.com/troubleshoot 5 | 6 | #### MODULES #### 7 | 8 | module(load="imuxsock") # local system logging support (e.g. via logger command) 9 | #module(load="imklog") # kernel logging support (previously done by rklogd) 10 | module(load="immark") # --MARK-- message support 11 | module(load="imudp") # UDP listener support 12 | 13 | 14 | input(type="imudp" port="514") 15 | 16 | # Log all kernel messages to the console. 17 | # Logging much else clutters up the screen. 18 | #kern.* action(type="omfile" file="/dev/console") 19 | 20 | # Log anything (except mail) of level info or higher. 21 | # Don't log private authentication messages! 22 | *.info;mail.none;authpriv.none;cron.none action(type="omfile" file="/var/log/messages") 23 | 24 | # The authpriv file has restricted access. 25 | authpriv.* action(type="omfile" file="/var/log/secure") 26 | 27 | # Log all the mail messages in one place. 28 | mail.* action(type="omfile" file="/var/log/maillog") 29 | 30 | # Log cron stuff 31 | cron.* action(type="omfile" file="/var/log/cron") 32 | 33 | # Everybody gets emergency messages 34 | *.emerg action(type="omusrmsg" users="*") 35 | 36 | # Save news errors of level crit and higher in a special file. 37 | uucp,news.crit action(type="omfile" file="/var/log/spooler") 38 | 39 | # Save boot messages also to boot.log 40 | local7.* action(type="omfile" file="/var/log/boot.log") 41 | 42 | # log every host in its own directory 43 | # if $fromhost-ip then /var/log/$fromhost-ip/messages 44 | 45 | # Include all .conf files in /etc/rsyslog.d 46 | $IncludeConfig /etc/rsyslog.d/*.conf 47 | $template GRAYLOGRFC5424,"<%PRI%>%PROTOCOL-VERSION% %TIMESTAMP:::date-rfc3339% %HOSTNAME% %APP-NAME% %PROCID% %MSGID% %STRUCTURED-DATA% %msg%\n" 48 | *.info;mail.none;authpriv.none;cron.none;*.* @@graylog:514;GRAYLOGRFC5424 # forward everything to remote server 49 | -------------------------------------------------------------------------------- /example/services.yml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | visualizer: 5 | image: charypar/swarm-dashboard:latest 6 | environment: 7 | PORT: 3000 8 | volumes: 9 | - "/var/run/docker.sock:/var/run/docker.sock" 10 | deploy: 11 | replicas: 1 12 | restart_policy: 13 | condition: on-failure 14 | placement: 15 | constraints: 16 | - node.role == manager 17 | -------------------------------------------------------------------------------- /example/web/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile 4 | .next 5 | -------------------------------------------------------------------------------- /example/web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:6-alpine 2 | 3 | RUN apk add --update tini curl \ 4 | && rm -r /var/cache 5 | ENTRYPOINT ["/sbin/tini", "--"] 6 | 7 | WORKDIR /home/node/app 8 | ENV NODE_ENV production 9 | 10 | COPY package.json yarn.lock ./ 11 | RUN yarn 12 | 13 | COPY . ./ 14 | RUN yarn run build \ 15 | && npm prune --production --silent \ 16 | && chown -R node:node . 17 | 18 | HEALTHCHECK --interval=5s --timeout=3s \ 19 | CMD curl --fail http://localhost:$PORT/_health || exit 1 20 | 21 | USER node 22 | # Run under Tini 23 | CMD ["./node_modules/.bin/next", "start"] 24 | -------------------------------------------------------------------------------- /example/web/components/layout.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import Head from 'next/head'; 3 | 4 | export default ({ host, children, title = '!!!' }) => ( 5 |
6 | 7 | {title} 8 | 9 | 10 | 11 |
12 | 22 |
23 | 24 | {children} 25 | 26 |
27 | (rendered by {host}) 28 | 35 |
36 |
37 | ); 38 | -------------------------------------------------------------------------------- /example/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "next", 8 | "build": "next build", 9 | "start": "next start -p $PORT" 10 | }, 11 | "dependencies": { 12 | "isomorphic-fetch": "^2.2.1", 13 | "next": "^2.4.0", 14 | "react": "^15.5.4", 15 | "react-dom": "^15.5.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/web/pages/_health.js: -------------------------------------------------------------------------------- 1 | export default () =>
; 2 | -------------------------------------------------------------------------------- /example/web/pages/about.js: -------------------------------------------------------------------------------- 1 | import Layout from '../components/layout'; 2 | 3 | const Page = ({ host }) => ( 4 | 5 |

6 | About us ... 7 |

8 |
9 | ); 10 | 11 | Page.getInitialProps = ({ req }) => ({ 12 | host: process.env['HOSTNAME'] || (req ? 'localhost' : 'client'), 13 | }); 14 | 15 | export default Page; 16 | -------------------------------------------------------------------------------- /example/web/pages/index.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | import Layout from '../components/layout'; 3 | 4 | const Page = ({ host, data }) => ( 5 | 6 |

7 | Welcome! 8 |

9 |

10 | from api: 11 |

12 |

13 | {JSON.stringify(data)} 14 |

15 |
16 | ); 17 | 18 | Page.getInitialProps = async ({ req }) => { 19 | const apiHost = process.env.API_HOST; 20 | const endpoint = req ? `http://${apiHost}/graphql` : '/api/graphql'; 21 | const response = await fetch(endpoint, { 22 | method: 'POST', 23 | body: JSON.stringify({ 24 | query: 'query {server, secrets { name, value }, headers}', 25 | variables: null, 26 | }), 27 | headers: { 'Content-Type': 'application/json' }, 28 | }); 29 | const data = await response.json(); 30 | return { 31 | host: process.env['HOSTNAME'] || (req ? 'localhost' : 'client'), 32 | data, 33 | }; 34 | }; 35 | 36 | export default Page; 37 | -------------------------------------------------------------------------------- /provisioning/aws/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform 2 | *.tfstate* 3 | lambda.zip 4 | -------------------------------------------------------------------------------- /provisioning/aws/assume-role.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "autoscaling_assume_role" { 2 | statement { 3 | actions = ["sts:AssumeRole"] 4 | 5 | principals { 6 | type = "Service" 7 | identifiers = ["autoscaling.amazonaws.com"] 8 | } 9 | } 10 | } 11 | 12 | data "aws_iam_policy_document" "lambda_assume_role" { 13 | statement { 14 | actions = ["sts:AssumeRole"] 15 | 16 | principals { 17 | type = "Service" 18 | identifiers = ["lambda.amazonaws.com"] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /provisioning/aws/container-linux-config/manager.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | readonly tokensDir='/var/tokens' 6 | readonly managerTokenFile="${tokensDir}/manager" 7 | readonly workerTokenFile="${tokensDir}/worker" 8 | readonly lockDir="${tokensDir}/init.lock" 9 | 10 | isManagerListening() { 11 | local ip="$1" 12 | wget -q --spider -t 1 --connect-timeout 3 $ip:2377 13 | } 14 | 15 | findManagers() { 16 | local registeredNodes='' 17 | local listeningNodes='' 18 | while [[ -z $registeredNodes ]]; do 19 | registeredNodes="$(getent hosts swarm.local | awk '{ print $1 }')" 20 | for node in $registeredNodes; do 21 | if isManagerListening $node; then 22 | listeningNodes="${listeningNodes} ${node}" 23 | fi 24 | done 25 | sleep 2 26 | done 27 | echo $listeningNodes 28 | } 29 | 30 | joinSwarm() { 31 | echo "Trying to join a swarm as a manager..." 32 | local joinToken 33 | local managers 34 | managers=$(findManagers) 35 | if [[ -z $managers ]]; then 36 | initSwarm 37 | else 38 | for mgrIP in $managers; do 39 | echo "Trying to join a swarm managed by $mgrIP..." 40 | if isManagerListening $mgrIP; then 41 | joinToken=$(cat $managerTokenFile) 42 | docker swarm join --token $joinToken $mgrIP:2377 43 | joined='true' 44 | break 45 | fi 46 | echo "...Timeout" 47 | done 48 | fi 49 | } 50 | 51 | initSwarm() { 52 | echo "Swarm does not exist yet, initializing..." 53 | local privateIpAddress 54 | privateIpAddress="$(curl -s http://169.254.169.254/latest/meta-data/local-ipv4)" 55 | docker swarm init --advertise-addr $privateIpAddress 56 | 57 | docker swarm join-token -q manager >$managerTokenFile 58 | docker swarm join-token -q worker >$workerTokenFile 59 | joined='true' 60 | } 61 | 62 | createLock() { 63 | mkdir $lockDir &>/dev/null # atomic 64 | } 65 | 66 | removeLock() { 67 | if [[ -e $lockDir ]]; then rmdir $lockDir; fi 68 | } 69 | 70 | trap 'removeLock; exit 0' INT TERM EXIT 71 | joined='false' 72 | while true; do 73 | if createLock; then 74 | joinSwarm 75 | if [[ $joined = 'true' ]]; then break; else removeLock; fi 76 | fi 77 | sleep 2 78 | done 79 | -------------------------------------------------------------------------------- /provisioning/aws/container-linux-config/manager.yml: -------------------------------------------------------------------------------- 1 | # This config is meant to be consumed by the config transpiler, which will 2 | # generate the corresponding Ignition config. Do not pass this config directly 3 | # to instances of Container Linux. 4 | systemd: 5 | units: 6 | - name: var-tokens.mount 7 | enable: true 8 | contents: | 9 | [Mount] 10 | What=${efs-mount-target}:/ 11 | Where=/var/tokens 12 | Type=nfs 13 | [Install] 14 | WantedBy=multi-user.target 15 | - name: swarm.service 16 | enable: true 17 | contents: | 18 | [Unit] 19 | Description="Swarm initialisation" 20 | After=docker.service var-tokens.mount 21 | Requires=docker.service var-tokens.mount 22 | 23 | [Service] 24 | Type=oneshot 25 | RemainAfterExit=yes 26 | ExecStart=/bin/sh /home/core/swarm-init.sh 27 | 28 | [Install] 29 | WantedBy=multi-user.target 30 | storage: 31 | files: 32 | - path: /home/core/swarm-init.sh 33 | filesystem: root 34 | mode: 0755 35 | contents: 36 | inline: ${swarm-init-script} 37 | locksmith: 38 | reboot_strategy: off 39 | -------------------------------------------------------------------------------- /provisioning/aws/container-linux-config/worker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | readonly tokensDir='/var/tokens' 6 | readonly workerTokenFile="${tokensDir}/worker" 7 | readonly lockDir="${tokensDir}/init.lock" 8 | 9 | isManagerListening() { 10 | local ip="$1" 11 | wget -q --spider -t 1 --connect-timeout 3 $ip:2377 12 | } 13 | 14 | findManagers() { 15 | local registeredNodes='' 16 | local listeningNodes='' 17 | while [[ -z $registeredNodes ]]; do 18 | registeredNodes="$(getent hosts swarm.local | awk '{ print $1 }')" 19 | for node in $registeredNodes; do 20 | if isManagerListening $node; then 21 | listeningNodes="${listeningNodes} ${node}" 22 | fi 23 | done 24 | sleep 2 25 | done 26 | echo $listeningNodes 27 | } 28 | 29 | joinSwarm() { 30 | echo "Trying to join a swarm as a worker..." 31 | local joinToken 32 | local managers 33 | managers=$(findManagers) 34 | for mgrIP in $managers; do 35 | echo "Trying to join a swarm managed by $mgrIP..." 36 | if isManagerListening $mgrIP; then 37 | joinToken=$(cat $workerTokenFile) 38 | docker swarm join --token $joinToken $mgrIP:2377 39 | joined='true' 40 | break 41 | fi 42 | echo "...Timeout" 43 | done 44 | } 45 | 46 | createLock() { 47 | mkdir $lockDir &>/dev/null # atomic 48 | } 49 | 50 | removeLock() { 51 | if [[ -e $lockDir ]]; then rmdir $lockDir; fi 52 | } 53 | 54 | trap 'removeLock; exit 0' INT TERM EXIT 55 | joined='false' 56 | while true; do 57 | if createLock; then 58 | joinSwarm 59 | if [[ $joined = 'true' ]]; then break; else removeLock; fi 60 | fi 61 | sleep 2 62 | done 63 | -------------------------------------------------------------------------------- /provisioning/aws/container-linux-config/worker.yml: -------------------------------------------------------------------------------- 1 | # This config is meant to be consumed by the config transpiler, which will 2 | # generate the corresponding Ignition config. Do not pass this config directly 3 | # to instances of Container Linux. 4 | systemd: 5 | units: 6 | - name: var-tokens.mount 7 | enable: true 8 | contents: | 9 | [Mount] 10 | What=${efs-mount-target}:/ 11 | Where=/var/tokens 12 | Type=nfs 13 | [Install] 14 | WantedBy=multi-user.target 15 | - name: swarm.service 16 | enable: true 17 | contents: | 18 | [Unit] 19 | Description="Swarm initialisation" 20 | After=docker.service var-tokens.mount 21 | Requires=docker.service var-tokens.mount 22 | 23 | [Service] 24 | Type=oneshot 25 | RemainAfterExit=yes 26 | ExecStart=/bin/sh /home/core/swarm-init.sh 27 | 28 | [Install] 29 | WantedBy=multi-user.target 30 | storage: 31 | files: 32 | - path: /home/core/swarm-init.sh 33 | filesystem: root 34 | mode: 0755 35 | contents: 36 | inline: ${swarm-init-script} 37 | 38 | locksmith: 39 | reboot_strategy: off 40 | -------------------------------------------------------------------------------- /provisioning/aws/doc/graph.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redbadger/stack/71059d2ab24cfa9e711884445d8cab90bdaa8173/provisioning/aws/doc/graph.pdf -------------------------------------------------------------------------------- /provisioning/aws/doc/vpc-3azs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redbadger/stack/71059d2ab24cfa9e711884445d8cab90bdaa8173/provisioning/aws/doc/vpc-3azs.png -------------------------------------------------------------------------------- /provisioning/aws/doc/vpc-nat-gateway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redbadger/stack/71059d2ab24cfa9e711884445d8cab90bdaa8173/provisioning/aws/doc/vpc-nat-gateway.png -------------------------------------------------------------------------------- /provisioning/aws/doc/vpc-ssh-bastion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redbadger/stack/71059d2ab24cfa9e711884445d8cab90bdaa8173/provisioning/aws/doc/vpc-ssh-bastion.png -------------------------------------------------------------------------------- /provisioning/aws/main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | profile = "microplatform" 3 | region = "${var.region}" 4 | } 5 | -------------------------------------------------------------------------------- /provisioning/aws/nodes-managers.tf: -------------------------------------------------------------------------------- 1 | data "template_file" "ignition_manager" { 2 | template = "${file("${path.module}/container-linux-config/manager.yml")}" 3 | 4 | vars { 5 | efs-mount-target = "${aws_efs_file_system.tokens.id}.efs.${var.region}.amazonaws.com" 6 | swarm-init-script = "${jsonencode(file("${path.module}/container-linux-config/manager.sh"))}" 7 | } 8 | } 9 | 10 | data "ct_config" "ignition_manager" { 11 | pretty_print = false 12 | content = "${data.template_file.ignition_manager.rendered}" 13 | } 14 | 15 | resource "aws_launch_configuration" "manager" { 16 | name_prefix = "manager-" 17 | image_id = "${var.ami}" 18 | instance_type = "t2.micro" 19 | associate_public_ip_address = false 20 | 21 | security_groups = [ 22 | "${aws_security_group.nodes.id}", 23 | "${aws_security_group.ssh.id}", 24 | ] 25 | 26 | key_name = "${aws_key_pair.node.key_name}" 27 | user_data = "${data.ct_config.ignition_manager.rendered}" 28 | 29 | lifecycle { 30 | create_before_destroy = true 31 | } 32 | } 33 | 34 | resource "aws_autoscaling_group" "managers" { 35 | availability_zones = ["${var.zones}"] 36 | vpc_zone_identifier = ["${var.private_subnets}"] 37 | name = "microplatform-managers" 38 | 39 | min_size = 0 40 | max_size = 3 41 | desired_capacity = "${var.manager_count}" 42 | wait_for_capacity_timeout = 0 43 | 44 | health_check_grace_period = 300 45 | health_check_type = "ELB" 46 | launch_configuration = "${aws_launch_configuration.manager.name}" 47 | termination_policies = ["OldestInstance", "ClosestToNextInstanceHour"] 48 | default_cooldown = 300 49 | 50 | depends_on = [ 51 | "aws_efs_mount_target.tokens", 52 | "aws_route53_zone.local", 53 | ] 54 | 55 | initial_lifecycle_hook { 56 | name = "register_dns" 57 | lifecycle_transition = "autoscaling:EC2_INSTANCE_LAUNCHING" 58 | 59 | default_result = "ABANDON" 60 | heartbeat_timeout = 2000 61 | 62 | notification_metadata = <", 8 | "scripts": { 9 | "build": "webpack", 10 | "test": "yarn build && mocha \"lib/**/*.spec.js\"" 11 | }, 12 | "dependencies": { 13 | "ramda": "^0.24.1" 14 | }, 15 | "devDependencies": { 16 | "aws-sdk": "^2.95.0", 17 | "babel-cli": "^6.24.1", 18 | "babel-core": "^6.25.0", 19 | "babel-loader": "^7.1.1", 20 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 21 | "babel-preset-env": "^1.6.0", 22 | "babel-preset-power-assert": "^1.0.0", 23 | "eslint": "^4.3.0", 24 | "eslint-config-airbnb": "^15.1.0", 25 | "eslint-config-airbnb-base": "^11.3.0", 26 | "eslint-plugin-import": "^2.7.0", 27 | "eslint-plugin-jsx-a11y": "^5.1.1", 28 | "eslint-plugin-react": "^7.1.0", 29 | "mocha": "^3.4.2", 30 | "power-assert": "^1.4.4", 31 | "webpack": "^3.5.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /provisioning/aws/register-dns/src/index.js: -------------------------------------------------------------------------------- 1 | // To use this function, create an Auto Scaling 2 | // lifecycle hook on instance creation notifying a SNS topic, and 3 | // subscribe the lambda function to the SNS topic. 4 | // Sane values for Memory and Timeout are 128MB and 30s respectively. 5 | 6 | // eslint-disable-next-line import/no-extraneous-dependencies 7 | import AWS from 'aws-sdk'; 8 | import { path } from 'ramda'; 9 | 10 | const ec2 = new AWS.EC2(); 11 | const as = new AWS.AutoScaling(); 12 | const route53 = new AWS.Route53(); 13 | 14 | const pluckIpAddress = path(['Reservations', '0', 'Instances', '0', 'PrivateIpAddress']); 15 | const pluckMessage = path(['Records', '0', 'Sns', 'Message']); 16 | 17 | // eslint-disable-next-line no-console 18 | const log = console.log.bind(console); 19 | 20 | const getIpAddress = async instanceId => 21 | pluckIpAddress(await ec2.describeInstances({ InstanceIds: [instanceId] }).promise()); 22 | 23 | const updateDns = (action, hostedZoneId, instanceId, ipAddress) => { 24 | const params = { 25 | ChangeBatch: { 26 | Changes: [ 27 | { 28 | Action: action, 29 | ResourceRecordSet: { 30 | MultiValueAnswer: true, 31 | Name: 'swarm.local', 32 | ResourceRecords: [{ Value: ipAddress }], 33 | SetIdentifier: `Swarm manager ${instanceId}`, 34 | TTL: 60, 35 | Type: 'A', 36 | }, 37 | }, 38 | ], 39 | Comment: 'swarm managers', 40 | }, 41 | HostedZoneId: hostedZoneId, 42 | }; 43 | return route53.changeResourceRecordSets(params).promise(); 44 | }; 45 | 46 | const completeAsLifecycleAction = async lifecycleParams => { 47 | // returns true on success or false on failure 48 | // notifies AutoScaling that it should either continue or abandon the instance 49 | try { 50 | const data = await as.completeLifecycleAction(lifecycleParams).promise(); 51 | log(`INFO: CompleteLifecycleAction Successful.\nReported:\n${JSON.stringify(data)}`); 52 | return data; 53 | } catch (e) { 54 | log(`ERROR: AS lifecycle completion failed.\nDetails:\n${e}`); 55 | log(`DEBUG: CompleteLifecycleAction\nParams:\n${JSON.stringify(lifecycleParams)}`); 56 | return e; 57 | } 58 | }; 59 | 60 | const handlerImpl = async notification => { 61 | log(`INFO: request Recieved.\nDetails:\n${JSON.stringify(notification)}`); 62 | const message = JSON.parse(pluckMessage(notification)); 63 | log(`DEBUG: SNS message contents. \nMessage:\n${JSON.stringify(message)}`); 64 | if (!message.LifecycleHookName) { 65 | return null; 66 | } 67 | 68 | const lifecycleParams = { 69 | AutoScalingGroupName: message.AutoScalingGroupName, 70 | LifecycleHookName: message.LifecycleHookName, 71 | LifecycleActionToken: message.LifecycleActionToken, 72 | }; 73 | 74 | try { 75 | const metadata = JSON.parse(message.NotificationMetadata); 76 | log(`DEBUG: \nMetadata:\n${JSON.stringify(metadata)}`); 77 | const ipAddress = await getIpAddress(message.EC2InstanceId); 78 | log(`DEBUG: \nipAddress:\n${ipAddress}`); 79 | const response = await updateDns( 80 | metadata.action, 81 | metadata.hostedZoneId, 82 | message.EC2InstanceId, 83 | ipAddress, 84 | ); 85 | log(`DEBUG: response:\n${JSON.stringify(response)}`); 86 | lifecycleParams.LifecycleActionResult = 'CONTINUE'; 87 | log('INFO: Lambda function reporting success to AutoScaling'); 88 | } catch (e) { 89 | lifecycleParams.LifecycleActionResult = 'ABANDON'; 90 | log(`ERROR: Lambda function reporting failure to AutoScaling with error: ${e}`); 91 | } 92 | 93 | return completeAsLifecycleAction(lifecycleParams); 94 | }; 95 | 96 | export const handler = (notification, context, callback) => { 97 | handlerImpl(notification).then(x => callback(null, x), callback); 98 | }; 99 | -------------------------------------------------------------------------------- /provisioning/aws/register-dns/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | output: { 7 | filename: 'index.js', 8 | path: path.resolve(__dirname, 'lib'), 9 | library: 'index', 10 | libraryTarget: 'commonjs2', 11 | }, 12 | externals: [ 13 | function (context, request, callback) { 14 | if (/^aws-sdk/.test(request)) { 15 | return callback(null, `commonjs ${request}`); 16 | } 17 | callback(); 18 | }, 19 | ], // plugins: [new webpack.IgnorePlugin(/aws-sdk/)], 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.js$/, 24 | exclude: /node_modules/, 25 | use: { 26 | loader: 'babel-loader', 27 | }, 28 | }, 29 | ], 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /provisioning/aws/tokens.tf: -------------------------------------------------------------------------------- 1 | resource "aws_efs_file_system" "tokens" { 2 | tags { 3 | Name = "Tokens" 4 | } 5 | } 6 | 7 | resource "aws_efs_mount_target" "tokens" { 8 | count = "${length(var.private_subnets)}" 9 | file_system_id = "${aws_efs_file_system.tokens.id}" 10 | subnet_id = "${element(var.private_subnets, count.index)}" 11 | security_groups = ["${aws_security_group.efs_mount_target.id}"] 12 | } 13 | 14 | resource "aws_security_group" "efs_mount_target" { 15 | name = "efs_mount_target" 16 | description = "For EFS Mount targets" 17 | vpc_id = "${var.vpc_id}" 18 | 19 | ingress { 20 | from_port = 2049 21 | to_port = 2049 22 | protocol = "tcp" 23 | security_groups = ["${aws_security_group.nodes.id}"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /provisioning/aws/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | default = "eu-west-1" 3 | } 4 | 5 | variable "vpc_id" { 6 | default = "vpc-235fdf44" 7 | } 8 | 9 | variable "zones" { 10 | default = ["eu-west-1a", "eu-west-1b", "eu-west-1c"] 11 | } 12 | 13 | variable "private_subnets" { 14 | default = ["subnet-f49e2493", "subnet-981390d1", "subnet-01c7015a"] 15 | } 16 | 17 | variable "manager_count" { 18 | default = 3 19 | } 20 | 21 | variable "worker_count" { 22 | default = 3 23 | } 24 | 25 | variable "ami" { 26 | # coreos alpha channel, Container Linux 1520.1.0, Docker 17.06.1 27 | default = "ami-6383441a" 28 | } 29 | 30 | variable "ssh_bastion_sg" { 31 | default = ["sg-89129ef1"] 32 | } 33 | 34 | variable "ssh_public_key_file" { 35 | default = "~/.ssh/id_rsa.pub" 36 | } 37 | -------------------------------------------------------------------------------- /provisioning/gcp/.gitignore: -------------------------------------------------------------------------------- 1 | credentials.json 2 | *.tfstate* 3 | -------------------------------------------------------------------------------- /provisioning/gcp/README.md: -------------------------------------------------------------------------------- 1 | # Automated provisioning for Google Cloud Platform 2 | 3 | Create a self-healing, auto-scaling Docker cluster in GCP using Terraform. 4 | 5 | ## Architecture 6 | 7 | The cluster consists of six Compute Instance Groups: three for managers, three workers, 8 | spread across three availability zones. 9 | 10 | The managers groups have one machine each. The worker groups are auto-scaling and 11 | you can configure the minimums and maximums. 12 | 13 | In both groups the instance template used is configured to automatically join 14 | into a Docker Swarm using a small bit of bookkeeping stored in a Storage bucket. 15 | 16 | The scripts also configure network security to allow the Swarm to communicate and 17 | allow incoming SSH connections to the managers. 18 | 19 | ### Left for the user 20 | 21 | These scripts don't automatically set up a load balancer, although they do 22 | create a Target pool from the workers. 23 | 24 | ## Prerequisites 25 | 26 | You will need: 27 | 28 | * Terraform on your machine 29 | * A Google Cloud Platform project with a VPC network containing at least one 30 | subnetwork (we recommend having two networks - one public, one private) 31 | * A service account for which you've generated a JSON credentials file 32 | (In the Google Cloud Console, under API Manager, Credentials, click 33 | Create Credentials and pick "Service account key" from the choices). The scripts 34 | Assume this is stored in `credentials.json` in the current directory. 35 | 36 | ## Usage 37 | 38 | 1. Update `variables.tf` to match your project 39 | 1. Run `terraform plan` to see what will be created 40 | 1. Run `terraform apply`. Once terraform is done, it'll take a few minutes for all 41 | the compute instances to start and join the swarm. 42 | 1. You should now be able to ssh onto one of the managers, run `docker node ls` 43 | and you should see the cluster nodes. 44 | 45 | ## To do 46 | 47 | * Limit SSH access to only non-interactive only to only allow tunnelling to the 48 | manager nodes to issue docker commands 49 | * Create an SSH bastion that is the only machine accessible from the public 50 | internet and the only machine allowed to connect to the swarm managers 51 | * Add conditional load balancer creation 52 | -------------------------------------------------------------------------------- /provisioning/gcp/cloud-init/coreos-manager.yml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | 3 | coreos: 4 | units: 5 | - name: "docker.service" 6 | drop-ins: 7 | - name: "50-insecure-registry.conf" 8 | content: | 9 | [Service] 10 | Environment=DOCKER_OPTS='--insecure-registry="localhost:5000' 11 | 12 | - name: "swarm.service" 13 | enable: true 14 | command: start 15 | content: | 16 | [Unit] 17 | Description="Swarm initialisation" 18 | After=docker.service 19 | Requires=docker.service 20 | 21 | [Service] 22 | Type=oneshot 23 | RemainAfterExit=yes 24 | ExecStart=/bin/sh /home/core/swarm-init.sh 25 | 26 | [Install] 27 | WantedBy=multi-user.target 28 | 29 | write_files: 30 | - path: /home/core/swarm-init.sh 31 | permissions: "0755" 32 | owner: "core" 33 | content: | 34 | #!/bin/sh 35 | 36 | touch /home/core/the-script-has-run 37 | 38 | ACCESS_TOKEN=$(wget -qO - --header 'Metadata-Flavor: Google' 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token' | jq -r '.access_token') 39 | MY_IP=$(wget -qO - --header 'Metadata-Flavor: Google' 'http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ip') 40 | 41 | MANAGER_IPS=$(wget -qO - --header "Authorization: Bearer $ACCESS_TOKEN" 'https://www.googleapis.com/storage/v1/b/${BUCKET_NAME}/o?prefix=manager-ips/' | jq -r '.items[].name' 2>/dev/null | grep -o '[0-9.]*') 42 | 43 | # We're a manager, write our IP into Storage 44 | 45 | echo "Adding $MY_IP into the managers list" 46 | wget -q \ 47 | --header "Authorization: Bearer $ACCESS_TOKEN" \ 48 | --header 'Content-Type: binary/octet-stream' \ 49 | --header 'Content-Length: 0' \ 50 | --post-data '' \ 51 | "https://www.googleapis.com/upload/storage/v1/b/${BUCKET_NAME}/o?uploadType=media&name=manager-ips/$MY_IP" 52 | 53 | 54 | if [ -z $MANAGER_IPS ]; then 55 | # Initialize a new swarm 56 | 57 | echo "Swarm does not exist yet, initializing..." 58 | docker swarm init --advertise-addr $MY_IP > /dev/null 59 | 60 | MANAGER_TOKEN=$(docker swarm join-token -q manager) 61 | WORKER_TOKEN=$(docker swarm join-token -q worker) 62 | 63 | echo "Writing manager join token" 64 | wget -q \ 65 | --header "Authorization: Bearer $ACCESS_TOKEN" \ 66 | --header 'Content-Type: binary/octet-stream' \ 67 | --header "Content-Length: $(printf $MANAGER_TOKEN | wc -c)" \ 68 | --post-data $MANAGER_TOKEN \ 69 | "https://www.googleapis.com/upload/storage/v1/b/${BUCKET_NAME}/o?uploadType=media&name=manager-token" 70 | 71 | echo "Writing worker join token" 72 | wget -q \ 73 | --header "Authorization: Bearer $ACCESS_TOKEN" \ 74 | --header 'Content-Type: binary/octet-stream' \ 75 | --header "Content-Length: $(printf $WORKER_TOKEN | wc -c)" \ 76 | --post-data $WORKER_TOKEN \ 77 | "https://www.googleapis.com/upload/storage/v1/b/${BUCKET_NAME}/o?uploadType=media&name=worker-token" 78 | else 79 | # Join an existing swarm 80 | 81 | while [ -z $JOIN_TOKEN ]; do 82 | JOIN_TOKEN=$(wget -qO - --header "Authorization: Bearer $ACCESS_TOKEN" 'https://www.googleapis.com/download/storage/v1/b/${BUCKET_NAME}/o/manager-token?alt=media') 83 | sleep 10 84 | done 85 | 86 | for IP in $MANAGER_IPS; do 87 | echo "Trying to join a swarm managed by $IP..." 88 | if wget -q --spider -t 1 --connect-timeout 3 $IP:2377; then 89 | docker swarm join --token $JOIN_TOKEN $IP:2377 90 | break 91 | fi 92 | echo "...Timeout" 93 | done 94 | fi 95 | -------------------------------------------------------------------------------- /provisioning/gcp/cloud-init/coreos-worker.yml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | 3 | coreos: 4 | units: 5 | - name: "docker.service" 6 | drop-ins: 7 | - name: "50-insecure-registry.conf" 8 | content: | 9 | [Service] 10 | Environment=DOCKER_OPTS='--insecure-registry="localhost:5000' 11 | 12 | - name: "swarm.service" 13 | enable: true 14 | command: start 15 | content: | 16 | [Unit] 17 | Description="Swarm initialisation" 18 | After=docker.service 19 | Requires=docker.service 20 | 21 | [Service] 22 | Type=oneshot 23 | RemainAfterExit=yes 24 | ExecStart=/bin/sh /home/core/swarm-init.sh 25 | 26 | [Install] 27 | WantedBy=multi-user.target 28 | 29 | write_files: 30 | - path: /home/core/swarm-init.sh 31 | permissions: "0755" 32 | owner: "core" 33 | content: | 34 | #!/bin/sh 35 | 36 | touch /home/core/the-script-has-run 37 | 38 | ACCESS_TOKEN=$(wget -qO - --header 'Metadata-Flavor: Google' 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token' | jq -r '.access_token') 39 | 40 | MANAGER_IPS=$(wget -qO - --header "Authorization: Bearer $ACCESS_TOKEN" 'https://www.googleapis.com/storage/v1/b/${BUCKET_NAME}/o?prefix=manager-ips/' | jq -r '.items[].name' 2>/dev/null | grep -o '[0-9.]*') 41 | 42 | while [ -z $JOIN_TOKEN ]; do 43 | JOIN_TOKEN=$(wget -qO - --header "Authorization: Bearer $ACCESS_TOKEN" 'https://www.googleapis.com/download/storage/v1/b/${BUCKET_NAME}/o/worker-token?alt=media') 44 | sleep 10 45 | done 46 | 47 | for IP in $MANAGER_IPS; do 48 | echo "Trying to join a swarm managed by $IP..." 49 | if wget -q --spider -t 1 --connect-timeout 3 $IP:2377; then 50 | docker swarm join --token $JOIN_TOKEN $IP:2377 51 | break 52 | fi 53 | echo "...Timeout" 54 | done 55 | -------------------------------------------------------------------------------- /provisioning/gcp/cloud-init/rancher-worker.yml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | 3 | rancher: 4 | docker: 5 | insecure_registry: 6 | - 'localhost:5000' 7 | 8 | write_files: 9 | - path: /etc/rc.local 10 | permissions: "0755" 11 | owner: "root" 12 | content: | 13 | #!/bin/sh 14 | 15 | touch /home/docker/the-script-has-run 16 | 17 | export MANAGER_IP='${MANAGER_IP}' 18 | export JOIN_TOKEN='${JOIN_TOKEN}' 19 | 20 | wait-for-docker 21 | docker swarm join --token $JOIN_TOKEN $MANAGER_IP:2377 22 | -------------------------------------------------------------------------------- /provisioning/gcp/main.tf: -------------------------------------------------------------------------------- 1 | provider "google" { 2 | project = "${var.project_name}" 3 | region = "${var.region}" 4 | credentials = "${file("./credentials.json")}" 5 | } 6 | 7 | resource "google_service_account" "swarm_state" { 8 | account_id = "swarm-state" 9 | display_name = "Swarm state reader" 10 | } 11 | 12 | resource "google_compute_target_pool" "swarm_workers" { 13 | name = "swarm-workers" 14 | session_affinity = "NONE" 15 | } 16 | -------------------------------------------------------------------------------- /provisioning/gcp/managers.tf: -------------------------------------------------------------------------------- 1 | resource "google_compute_instance_group_manager" "swarm_managers" { 2 | name = "swarm-managers-${element(keys(var.zones), count.index)}" 3 | zone = "${lookup(var.zones, element(keys(var.zones), count.index))}" 4 | count = "${length(keys(var.zones))}" 5 | 6 | instance_template = "${google_compute_instance_template.swarm_manager.self_link}" 7 | target_pools = [] 8 | base_instance_name = "swarm-manager" 9 | target_size = 1 10 | 11 | depends_on = ["google_compute_instance_template.swarm_manager"] 12 | } 13 | 14 | resource "google_compute_instance_template" "swarm_manager" { 15 | name_prefix = "${var.env}-swarm-manager-" 16 | machine_type = "g1-small" 17 | can_ip_forward = false 18 | region = "${var.region}" 19 | 20 | tags = ["swarm-node", "swarm-manager"] 21 | 22 | service_account { 23 | email = "${google_service_account.swarm_state.email}" 24 | scopes = ["https://www.googleapis.com/auth/devstorage.read_write"] 25 | } 26 | 27 | metadata { 28 | user-data = "${data.template_file.metadata_manager.rendered}" 29 | } 30 | 31 | disk { 32 | source_image = "coreos-alpha-1409-0-0-v20170511" 33 | } 34 | 35 | network_interface { 36 | subnetwork = "${var.subnetwork}" 37 | 38 | access_config { 39 | // Ephemeral IP 40 | } 41 | } 42 | 43 | lifecycle { 44 | create_before_destroy = true 45 | } 46 | } 47 | 48 | data "template_file" "metadata_manager" { 49 | template = "${file("cloud-init/coreos-manager.yml")}" 50 | 51 | vars { 52 | BUCKET_NAME = "${google_storage_bucket.swarm_state.name}" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /provisioning/gcp/networks.tf: -------------------------------------------------------------------------------- 1 | resource "google_compute_firewall" "swarm_nodes" { 2 | name = "docker-swarm-node" 3 | network = "${var.network}" 4 | target_tags = ["swarm-node"] 5 | 6 | source_tags = ["swarm-node"] 7 | 8 | allow { 9 | protocol = "tcp" 10 | ports = ["7946"] 11 | } 12 | 13 | allow { 14 | protocol = "udp" 15 | ports = ["4789", "7946"] 16 | } 17 | } 18 | 19 | resource "google_compute_firewall" "web_servers" { 20 | name = "docker-swarm-web-servers" 21 | network = "${var.network}" 22 | target_tags = ["swarm-manager"] 23 | source_ranges = ["0.0.0.0/0"] 24 | 25 | allow { 26 | protocol = "tcp" 27 | ports = ["80", "8080"] 28 | } 29 | } 30 | 31 | resource "google_compute_firewall" "swarm_managers" { 32 | name = "docker-swarm-manager" 33 | network = "${var.network}" 34 | target_tags = ["swarm-manager"] 35 | 36 | source_tags = ["swarm-node"] 37 | 38 | allow { 39 | protocol = "tcp" 40 | ports = ["2377"] 41 | } 42 | } 43 | 44 | resource "google_compute_firewall" "swarm_managers_ssh" { 45 | name = "docker-swarm-manager-ssh" 46 | network = "${var.network}" 47 | target_tags = ["swarm-manager"] 48 | 49 | source_ranges = ["0.0.0.0/0"] 50 | 51 | allow { 52 | protocol = "tcp" 53 | ports = ["22"] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /provisioning/gcp/storage.tf: -------------------------------------------------------------------------------- 1 | resource "google_storage_bucket" "swarm_state" { 2 | name = "swarm-state" 3 | location = "EUROPE-WEST1" 4 | storage_class = "REGIONAL" 5 | force_destroy = true 6 | } 7 | 8 | resource "google_storage_bucket_acl" "swarm_state_acl" { 9 | bucket = "${google_storage_bucket.swarm_state.name}" 10 | 11 | role_entity = [ 12 | "WRITER:user-${google_service_account.swarm_state.email}", 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /provisioning/gcp/variables.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "gcs" { 3 | project = "microplatform-demo" 4 | bucket = "microplatform-tf-state" 5 | 6 | # path should be provided by running init e.g.: 7 | # terraform init --backend-config="path=dev.state" 8 | } 9 | } 10 | 11 | variable "project_name" { 12 | default = "microplatform-demo" 13 | } 14 | 15 | variable "region" { 16 | default = "europe-west1" 17 | } 18 | 19 | variable "zones" { 20 | default = { 21 | b = "europe-west1-b" 22 | c = "europe-west1-c" 23 | d = "europe-west1-d" 24 | } 25 | } 26 | 27 | variable "network" { 28 | default = "demo" 29 | } 30 | 31 | variable "subnetwork" { 32 | default = "private" 33 | } 34 | -------------------------------------------------------------------------------- /provisioning/gcp/workers.tf: -------------------------------------------------------------------------------- 1 | resource "google_compute_instance_group_manager" "swarm_workers" { 2 | name = "swarm-workers-${element(keys(var.zones), count.index)}" 3 | zone = "${lookup(var.zones, element(keys(var.zones), count.index))}" 4 | count = "${length(keys(var.zones))}" 5 | 6 | instance_template = "${google_compute_instance_template.swarm_worker.self_link}" 7 | target_pools = ["${google_compute_target_pool.swarm_workers.self_link}"] 8 | base_instance_name = "swarm-worker" 9 | 10 | depends_on = ["google_compute_instance_template.swarm_worker"] 11 | } 12 | 13 | resource "google_compute_instance_template" "swarm_worker" { 14 | name_prefix = "swarm-worker-" 15 | machine_type = "g1-small" 16 | can_ip_forward = false 17 | region = "${var.region}" 18 | 19 | tags = ["swarm-node"] 20 | 21 | service_account { 22 | email = "${google_service_account.swarm_state.email}" 23 | scopes = ["https://www.googleapis.com/auth/devstorage.read_only"] 24 | } 25 | 26 | metadata { 27 | user-data = "${data.template_file.metadata_worker.rendered}" 28 | block-project-ssh-keys = "TRUE" 29 | } 30 | 31 | disk { 32 | source_image = "coreos-alpha-1409-0-0-v20170511" 33 | } 34 | 35 | network_interface { 36 | subnetwork = "${var.subnetwork}" 37 | } 38 | 39 | lifecycle { 40 | create_before_destroy = true 41 | } 42 | } 43 | 44 | data "template_file" "metadata_worker" { 45 | template = "${file("cloud-init/coreos-worker.yml")}" 46 | 47 | vars { 48 | BUCKET_NAME = "${google_storage_bucket.swarm_state.name}" 49 | } 50 | } 51 | 52 | resource "google_compute_autoscaler" "swarm_workers" { 53 | name = "swarm-workers-${element(keys(var.zones), count.index)}" 54 | zone = "${lookup(var.zones, element(keys(var.zones), count.index))}" 55 | target = "${element(google_compute_instance_group_manager.swarm_workers.*.self_link, count.index)}" 56 | count = "${length(keys(var.zones))}" 57 | 58 | autoscaling_policy { 59 | min_replicas = 1 60 | max_replicas = 2 61 | cooldown_period = 60 # 60 is default 62 | 63 | cpu_utilization { 64 | target = 0.9 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /provisioning/osx/dns.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | scriptDir=$( 6 | cd "$(dirname "$0")" 7 | pwd 8 | ) 9 | 10 | cd "$scriptDir" 11 | 12 | compose="./on-local.sh docker-compose" 13 | 14 | $compose -f docker-compose-dns.yml -p dns up -d 15 | -------------------------------------------------------------------------------- /provisioning/osx/docker-compose-dns.yml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | dns: 5 | image: andyshinn/dnsmasq 6 | command: -d -A /local/127.0.0.1 7 | ports: 8 | - 53:53/tcp 9 | - 53:53/udp 10 | -------------------------------------------------------------------------------- /provisioning/osx/docker-compose-load-balancer.yml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | load_balancer: 5 | image: haproxy:1.7.8-alpine 6 | ports: 7 | - 80:80 8 | networks: 9 | - load_balancer 10 | extra_hosts: 11 | - "mgr1:${mgr1}" 12 | - "wkr1:${wkr1}" 13 | - "wkr2:${wkr2}" 14 | - "wkr3:${wkr3}" 15 | volumes: 16 | - /tmp/haproxy/:/usr/local/etc/haproxy/ 17 | networks: 18 | load_balancer: 19 | -------------------------------------------------------------------------------- /provisioning/osx/docker-compose-registry-ambassador.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | registry_ambassador: 5 | image: svendowideit/ambassador 6 | environment: 7 | DOCKER_PORT_5000_TCP: tcp://10.0.2.2:5000 8 | ports: 9 | - 5000:5000 10 | deploy: 11 | replicas: 1 12 | placement: 13 | constraints: 14 | - node.role == manager 15 | -------------------------------------------------------------------------------- /provisioning/osx/docker-compose-registry.yml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | registry: 5 | restart: always 6 | image: registry:2 7 | ports: 8 | - 5000:5000 9 | volumes: 10 | - ${HOME}/.docker/registry:/var/lib/registry 11 | networks: 12 | - registry 13 | registry-mirror: 14 | restart: always 15 | image: registry:2 16 | ports: 17 | - 5001:5000 18 | environment: 19 | REGISTRY_PROXY_REMOTEURL: https://registry-1.docker.io 20 | volumes: 21 | - ${HOME}/.docker/registry-mirror:/var/lib/registry 22 | networks: 23 | - registry 24 | networks: 25 | registry: 26 | -------------------------------------------------------------------------------- /provisioning/osx/load-balancer.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | scriptDir=$( 6 | cd "$(dirname "$0")" 7 | pwd 8 | ) 9 | 10 | cd "$scriptDir" 11 | 12 | compose="./on-local.sh docker-compose" 13 | 14 | for node in mgr1 wkr1 wkr2 wkr3; do 15 | eval "export ${node}=$(docker-machine ip $node)" 16 | done 17 | 18 | config=/tmp/haproxy/haproxy.cfg 19 | 20 | mkdir -p /tmp/haproxy 21 | if [ ! -f $config ]; then 22 | cat > $config </dev/null 47 | 48 | for node in $manager $workers; do 49 | createMachine $node 50 | done 51 | 52 | MGR_IP=$(docker-machine ip $manager) 53 | initSwarm $MGR_IP 54 | 55 | for node in $workers; do 56 | joinNode $MGR_IP $node 57 | done 58 | 59 | docker-machine ls 60 | $docker node ls 61 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://badge.buildkite.com/5d85736c5c70e3acecf5dc048195b85754df59febb84ddd824.svg?branch=master)](https://buildkite.com/red-badger-1/stack) 2 | 3 | This repository contains automation for provisioning 'Docker in Swarm Mode' clusters on your Mac (OSX), Amazon Web Services and Google Cloud Platform. It also has an example proxied web ui, gateway and api (on segregated networks) that you can use to test your deployments. 4 | 5 | For local use, there are a number of supporting services that can be used to simulate cloud-native infrastructure such as Docker registries, DNS, load balancers etc: 6 | 1. private Docker registry (for pushing local images) 7 | 1. registry mirror (for caching public images) 8 | 1. `haproxy` load balancer (for hostname to published port mapping) 9 | 1. `dnsmasq` dns server (for `.local` tld resolution) 10 | 11 | The example application has 3 stacks running inside the cluster: 12 | 1. the swarm visualizer (`services` stack, deployed using `configure` utility from the `example` directory) 13 | 1. the registry ambassador (`swarm` stack, deployed by `./provisioning/osx/registry.sh`) 14 | 1. the web and the api, and their associated reverse proxy and gateway (`app` stack, also deployed using `configure` utility from the `example` directory) 15 | 16 | ![Swarm Visualizer](doc/visualizer.png) 17 | 18 | Incoming requests can hit any node of the swarm and will be routed to an instance of the service (that has published the port) by the swarm's mesh routing. 19 | 20 | You can describe stack configurations (published services) in a `yaml` file which is called `_stacks.yml` by default, and then use the configuration utility (`configure/lib/index.js --help`) to write deployable compose-files created by merging multiple compose files (including one for automatic port assignment). Combined with the local `dns` server and `haproxy` load balancer, this allows you to access published services by FQDNs in the form `http://service.stack.local` (in this case http://visualizer.services.local and http://web.app.local). See examples below. 21 | 22 | ## To set up the cluster locally 23 | 1. Install the latest VirtualBox and Docker for Mac. 24 | 25 | 1. There is a script to create a swarm, which provisions 4 local VMs and joins them into a cluster. Take a look at the script to see how straight forward it is. NOTE: this script starts by removing any VMs with the names `mgr1,wkr1,wkr2,wkr3`. 26 | 27 | ```bash 28 | ./provisioning/osx/swarm.sh 29 | ``` 30 | 31 | 1. There is a script to run a container with a local dns server (outside the swarm). This is an instance of `dnsmasq` and it used to resolve the tld `.local` to `localhost`. Unfortunately you will need `sudo` to add a resolver for the `local` tld. 32 | 33 | ```sh 34 | sudo mkdir -p /etc/resolver 35 | sudo sh -c 'echo "nameserver 127.0.0.1" > /etc/resolver/local' 36 | ./provisioning/osx/dns.sh 37 | ``` 38 | 39 | 1. There is a script to create containers for a local private Docker registry and a local mirror of Docker hub. It also deploys a registry ambassador inside the swarm to route requests to the registry on the host (`localhost:5000`). 40 | 41 | ```sh 42 | ./provisioning/osx/registry.sh 43 | ``` 44 | 45 | 1. There is a script to run a container with a load balancer (outside the swarm). Note that if the IP addresses of your VMs change, you'll need to run this script again, so that the load balancer points to the correct nodes. (The first time you run this it will use a default config which will be updated when we use the `configure -u` utility below). 46 | 47 | ```sh 48 | ./provisioning/osx/load-balancer.sh 49 | ``` 50 | 51 | 1. Run the `configure` utility to generate deployable compose-files with correct ports. This also updates the load balancer in case the ports have changed. It deploys the `visualizer`, so that we can see what's going on. 52 | 53 | Note: you will need to build it (and run tests) before you use it for the first time: 54 | 55 | ```sh 56 | cd configure 57 | yarn 58 | yarn test 59 | cd .. 60 | ``` 61 | 62 | ```sh 63 | cd example 64 | ../configure/lib/index.js deploy --update services 65 | cd .. 66 | ``` 67 | 68 | 1. Build and push the app stack. 69 | 70 | ```sh 71 | cd example 72 | export registry=localhost:5000 73 | ../configure/lib/index.js build app 74 | ../configure/lib/index.js push app 75 | cd .. 76 | ``` 77 | 78 | 1. Create a secret that the `api` service will use (note we use `printf` instead of `echo` to prevent a new-line being added). 79 | 80 | ```sh 81 | printf 'sssshhhh!' | on-swarm docker secret create my_secret - 82 | ``` 83 | 84 | 1. Run the `configure` utility to deploy the `app` stack. 85 | 86 | ```sh 87 | cd example 88 | ../configure/lib/index.js deploy --update app 89 | cd .. 90 | ``` 91 | 92 | 1. The following steps use aliases to make it easier to work with local and swarm docker servers (you might want to add them to your `.bash_profile`): 93 | 94 | ```sh 95 | alias on-local="/path/to/provisioning/osx/on-local.sh" 96 | alias on-swarm="/path/to/provisioning/osx/on-swarm.sh" 97 | alias to-local="source /path/to/provisioning/osx/point-to-local.sh" 98 | alias to-swarm="source /path/to/provisioning/osx/point-to-swarm.sh" 99 | ``` 100 | 101 | You should wait until all the services in the swarm are running: 102 | 103 | ```sh 104 | on-swarm docker service ls 105 | 106 | ID NAME MODE REPLICAS IMAGE PORTS 107 | 1yofqh0g1b9b services_visualizer replicated 1/1 charypar/swarm-dashboard:latest *:8000->3000/tcp 108 | fuhipdgtyvvd swarm_registry_ambassador replicated 1/1 svendowideit/ambassador:latest *:5000->5000/tcp 109 | hi21cmf1oi0x app_gateway replicated 2/2 localhost:5000/gateway@sha256:75035764b5ee55c35820aa38b4cf7b4d1742be8e7f47ef5379296978cff87eb5 110 | j3wmx8r2s2kv app_api replicated 2/2 localhost:5000/api@sha256:1b836f221052dace536425e34dc84714440a31a48a8d48594cdff97107121084 *:8002->3000/tcp 111 | ntuo4tmulyel app_web replicated 2/2 localhost:5000/web@sha256:10e40e7311a083371af387fc1d6d505a468e7750e401928a52d2a8aafd217aab 112 | qzxhfbrkp2vz app_rproxy replicated 2/2 localhost:5000/rproxy@sha256:7348f573df6ff4a4623c59ab2453de3cd8ae24d0c150f2373a9521e4117c47a1 *:8001->3000/tcp 113 | ``` 114 | 115 | You should also have the following local containers running: 116 | 117 | ```sh 118 | on-local docker ps 119 | 120 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 121 | 73f862aa53c9 haproxy:1.7.8-alpine "/docker-entrypoin..." 22 hours ago Up 22 hours 0.0.0.0:80->80/tcp loadbalancer_load_balancer_1 122 | fbff0358c773 andyshinn/dnsmasq "dnsmasq -k -d -A ..." 2 days ago Up 2 days 0.0.0.0:53->53/tcp, 0.0.0.0:53->53/udp dns_dns_1 123 | 6e2686acd14a registry:2 "/entrypoint.sh /e..." 3 days ago Up 3 days 0.0.0.0:5001->5000/tcp registry_registry-mirror_1 124 | a578c7c8703c registry:2 "/entrypoint.sh /e..." 3 days ago Up 3 days 0.0.0.0:5000->5000/tcp registry_registry_1 125 | ``` 126 | 127 | When all the services have started, the app should be available at http://web.app.local and the visualizer at http://visualizer.services.local 128 | 129 | Note: If you get a 503 Service Unavailable from `app_web`, you may need to restart `app_rproxy` (this is probably due to the startup order and it _should_ recover on its own, this needs fixing): 130 | 131 | ```sh 132 | on-swarm docker service update --force app_rproxy 133 | ``` 134 | 135 | ## Cleaning up 136 | 137 | ```sh 138 | ./provisioning/osx/local-cleanup.sh 139 | ./provisioning/osx/swarm-cleanup.sh 140 | ``` 141 | 142 | A note about overlay networks 143 | ----- 144 | 145 | Be careful of clashes between `Boot2Docker`'s networking and `docker swarm`'s overlay networks 146 | (they both use `10.0.n/24`). This is why we change the subnet for the `private` overlay network in 147 | [the compose file](./app.yml) (as we ended up looking for a DNS server on the 148 | `private` network rather than on the host) 149 | --------------------------------------------------------------------------------