diff --git a/Gopkg.lock b/Gopkg.lock index f514f4db5a..90ca743115 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -65,6 +65,17 @@ revision = "47565b4f722fb6ceae66b95f853feed578a4a51c" version = "v0.3.3" +[[projects]] + branch = "master" + digest = "1:0b9298e02a671b0ca5b94fa82c5820e0458006751e20cd74e75a14a0b4cc085c" + name = "github.com/garden-io/garden" + packages = [ + "garden-cli/dockerutil", + "garden-cli/util", + ] + pruneopts = "UT" + revision = "be8fed2658027bc70f8c2776376e0b17c7301b1d" + [[projects]] digest = "1:78bbb1ba5b7c3f2ed0ea1eab57bdd3859aec7e177811563edc41198a760b06af" name = "github.com/mitchellh/go-homedir" @@ -120,6 +131,8 @@ "github.com/docker/docker/api/types/mount", "github.com/docker/docker/api/types/volume", "github.com/docker/docker/client", + "github.com/garden-io/garden/garden-cli/dockerutil", + "github.com/garden-io/garden/garden-cli/util", "github.com/mitchellh/go-homedir", "gopkg.in/yaml.v2", ] diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 371e7e1268..19c8b7d09c 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -446,9 +446,9 @@ This is useful for debugging or ad-hoc experimentation with modules. Examples: - garden run module my-container # run an ad-hoc instance of a my-container container and attach to it - garden run module my-container /bin/sh # run an interactive shell in a new my-container container - garden run module my-container --i=false /some/script # execute a script in my-container and return the output + garden run module my-container # run an ad-hoc instance of a my-container container and attach to it + garden run module my-container /bin/sh # run an interactive shell in a new my-container container + garden run module my-container --interactive=false /some/script # execute a script in my-container and return the output ##### Usage @@ -494,6 +494,32 @@ Examples: | -------- | ----- | ---- | ----------- | | `--force-build` | | boolean | Force rebuild of module. +### garden run task + +Run a task (in the context of its parent module). + +This is useful for re-running tasks on the go, for example after writing/modifying database migrations. + +Examples: + + garden run task my-db-migration # run my-migration + +##### Usage + + garden run task [options] + +##### Arguments + +| Argument | Required | Description | +| -------- | -------- | ----------- | + | `task` | Yes | The name of the task to run. + +##### Options + +| Argument | Alias | Type | Description | +| -------- | ----- | ---- | ----------- | + | `--force-build` | | boolean | Force rebuild of module before running. + ### garden run test Run the specified module test. diff --git a/docs/reference/config.md b/docs/reference/config.md index fea62fa0e2..5de92c04fe 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -313,7 +313,8 @@ module: # Required. name: - # Names of the services that must be running before the test is run. + # The names of any services that must be running, and the names of any tasks that must be + # executed, before the test is run. # # Optional. dependencies: @@ -444,6 +445,36 @@ module: # Optional. image: + # When this field is used, the files or directories specified within are automatically synced + # into the running container when they're modified. Additionally, any of this module's services + # that define a `hotReloadCommand` will be run with that command instead of the one specified in + # their `command` field. Services are only deployed with hot reloading enabled when their names + # are passed to the `--hot-reload` option in a call to the `deploy` or `dev` command. + # + # Optional. + hotReload: + # Specify one or more source files or directories to automatically sync into the running + # container. + # + # Required. + sync: + - # POSIX-style path of the directory to sync to the target, relative to the module's + # top-level directory. Must be a relative path if provided. Defaults to the module's + # top-level directory if no value is provided. + # + # Example: "src" + # + # Optional. + source: . + + # POSIX-style absolute path to sync the directory to inside the container. The root path + # (i.e. "/") is not allowed. + # + # Example: "/app/src" + # + # Required. + target: + # POSIX-style name of Dockerfile, relative to project root. Defaults to $MODULE_ROOT/Dockerfile. # # Optional. @@ -463,7 +494,8 @@ module: # Required. name: - # The names of services that this service depends on at runtime. + # The names of any services that this service depends on at runtime, and the names of any + # tasks that should be executed before this service is deployed. # # Optional. dependencies: @@ -622,7 +654,8 @@ module: # Required. name: - # Names of the services that must be running before the test is run. + # The names of any services that must be running, and the names of any tasks that must be + # executed, before the test is run. # # Optional. dependencies: @@ -646,34 +679,35 @@ module: env: {} - # When this field is used, the files or directories specified within are automatically synced - # into the running container when they're modified. Additionally, any of this module's services - # that define a `hotReloadCommand` will be run with that command instead of the one specified in - # their `command` field. Services are only deployed with hot reloading enabled when their names - # are passed to the `--hot-reload` option in a call to the `deploy` or `dev` command. + # A list of tasks that can be run from this container module. These can be used as dependencies + # for services (executed before the service is deployed) or for other tasks. # # Optional. - hotReload: - # Specify one or more source files or directories to automatically sync into the running - # container. + tasks: + # The task specification for a generic module. # - # Required. - sync: - - # POSIX-style path of the directory to sync to the target, relative to the module's - # top-level directory. Must be a relative path if provided. Defaults to the module's - # top-level directory if no value is provided. - # - # Example: "src" - # - # Optional. - source: . + # Optional. + - # The name of the task. + # + # Required. + name: - # POSIX-style absolute path to sync the directory to inside the container. The root path - # (i.e. "/") is not allowed. - # - # Example: "/app/src" - # - # Required. - target: + # The names of any tasks that must be executed, and the names of any services that must be + # running, before this task is executed. + # + # Optional. + dependencies: + - + + # Maximum duration (in seconds) of the task's execution. + # + # Optional. + timeout: null + + # The command that the task should run inside the container. + # + # Optional. + command: + - ``` diff --git a/examples/hello-world/.garden/local-config.yml b/examples/hello-world/.garden/local-config.yml index bddd183713..e69de29bb2 100644 --- a/examples/hello-world/.garden/local-config.yml +++ b/examples/hello-world/.garden/local-config.yml @@ -1,5 +0,0 @@ -kubernetes: - previous-usernames: - - hello -linkedModuleSources: [] -linkedProjectSources: [] diff --git a/examples/tasks/garden.yml b/examples/tasks/garden.yml new file mode 100644 index 0000000000..f52bbfd5fa --- /dev/null +++ b/examples/tasks/garden.yml @@ -0,0 +1,6 @@ +project: + name: tasks + environments: + - name: local + providers: + - name: local-kubernetes diff --git a/examples/tasks/hello-service/.dockerignore b/examples/tasks/hello-service/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/examples/tasks/hello-service/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/examples/tasks/hello-service/Dockerfile b/examples/tasks/hello-service/Dockerfile new file mode 100644 index 0000000000..1c18f5453d --- /dev/null +++ b/examples/tasks/hello-service/Dockerfile @@ -0,0 +1,13 @@ +FROM node:9-alpine + +ENV PORT=8080 +EXPOSE ${PORT} +WORKDIR /app + +ADD package.json /app +RUN npm install knex -g +RUN npm install + +ADD . /app + +CMD ["npm", "start"] diff --git a/examples/tasks/hello-service/app.js b/examples/tasks/hello-service/app.js new file mode 100644 index 0000000000..12e765ea6f --- /dev/null +++ b/examples/tasks/hello-service/app.js @@ -0,0 +1,25 @@ +const express = require("express") +const knex = require("knex")({ + client: "postgresql", + connection: { + host: "postgres-service", + port: 5432, + database: "postgres", + user: "postgres", + }, + pool: { + min: 4, + max: 10 + }, +}) + +const app = express(); + +app.get("/hello", (req, res) => { + knex.select("name").from("users") + .then((rows) => { + res.send(`Hello from Node! Usernames: ${rows.map(r => r.name).join(', ')}`) + }) +}); + +module.exports = { app } diff --git a/examples/tasks/hello-service/garden.yml b/examples/tasks/hello-service/garden.yml new file mode 100644 index 0000000000..f3bf655df1 --- /dev/null +++ b/examples/tasks/hello-service/garden.yml @@ -0,0 +1,24 @@ +module: + name: hello-service + description: Greeting service + type: container + services: + - name: hello-service + command: [npm, start] + ports: + - name: http + containerPort: 8080 + ingresses: + - path: /hello + port: http + dependencies: + - node-migration + tests: + - name: unit + command: [npm, test] + tasks: + - name: node-migration + # sleep to give postgres time to get ready + command: ["sleep 5 && knex migrate:latest"] + dependencies: + - postgres-service diff --git a/examples/tasks/hello-service/knexfile.js b/examples/tasks/hello-service/knexfile.js new file mode 100644 index 0000000000..37939d4391 --- /dev/null +++ b/examples/tasks/hello-service/knexfile.js @@ -0,0 +1,18 @@ +module.exports = { + development: { + client: "postgresql", + connection: { + host: "postgres-service", + port: 5432, + database: "postgres", + user: "postgres", + }, + pool: { + min: 4, + max: 10 + }, + migrations: { + tableName: "knex_migrations" + } + } +} diff --git a/examples/tasks/hello-service/main.js b/examples/tasks/hello-service/main.js new file mode 100644 index 0000000000..06833ec64f --- /dev/null +++ b/examples/tasks/hello-service/main.js @@ -0,0 +1,3 @@ +const { app } = require('./app'); + +app.listen(process.env.PORT, '0.0.0.0', () => console.log('Node service started')); diff --git a/examples/tasks/hello-service/migrations/20181008125224_create_users_table.js b/examples/tasks/hello-service/migrations/20181008125224_create_users_table.js new file mode 100644 index 0000000000..ce5c5b0fe2 --- /dev/null +++ b/examples/tasks/hello-service/migrations/20181008125224_create_users_table.js @@ -0,0 +1,13 @@ + +exports.up = function(knex) { + return knex.schema.createTable("users", function(t) { + t.increments("id").unsigned().primary(); + t.dateTime("created_at").notNull(); + t.dateTime("updated_at").nullable(); + t.string("name").notNull(); + }); +}; + +exports.down = function(knex) { + return knex.schema.dropTable("users"); +}; diff --git a/examples/tasks/hello-service/package-lock.json b/examples/tasks/hello-service/package-lock.json new file mode 100644 index 0000000000..9bf1445a8f --- /dev/null +++ b/examples/tasks/hello-service/package-lock.json @@ -0,0 +1,2432 @@ +{ + "name": "node-service", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bluebird": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.2.tgz", + "integrity": "sha512-dhHTWMI7kMx5whMQntl7Vr9C6BvV10lFXDAasnqnrMYhXVCzzk6IO9Fo2L75jXHT07WrOngL1WDXOp+yYS91Yg==" + }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.1", + "http-errors": "~1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "~2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "~1.6.15" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer-writer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-1.0.1.tgz", + "integrity": "sha1-Iqk2kB4wKa/NdUfrRIfOtpejvwg=" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "2.3.2", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=" + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "express": { + "version": "4.16.3", + "resolved": "http://registry.npmjs.org/express/-/express-4.16.3.tgz", + "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.3", + "qs": "6.5.1", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + } + }, + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "fined": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz", + "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=", + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + } + }, + "flagged-respawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.0.tgz", + "integrity": "sha1-Tnmumy6zi/hrO7Vr8+ClaqX8q9c=" + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "requires": { + "for-in": "^1.0.1" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + }, + "dependencies": { + "combined-stream": { + "version": "1.0.6", + "resolved": "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "~1.0.0" + } + } + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", + "dev": true + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", + "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", + "requires": { + "ajv": "^5.3.0", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=" + }, + "ipaddr.js": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "knex": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/knex/-/knex-0.15.2.tgz", + "integrity": "sha1-YFm4dIlgX0zIdZmm0qnSZXCek0A=", + "requires": { + "babel-runtime": "^6.26.0", + "bluebird": "^3.5.1", + "chalk": "2.3.2", + "commander": "^2.16.0", + "debug": "3.1.0", + "inherits": "~2.0.3", + "interpret": "^1.1.0", + "liftoff": "2.5.0", + "lodash": "^4.17.10", + "minimist": "1.2.0", + "mkdirp": "^0.5.1", + "pg-connection-string": "2.0.0", + "tarn": "^1.1.4", + "tildify": "1.2.0", + "uuid": "^3.3.2", + "v8flags": "^3.1.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "liftoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", + "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", + "requires": { + "extend": "^3.0.0", + "findup-sync": "^2.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + }, + "make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "requires": { + "kind-of": "^6.0.2" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "mime-db": { + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", + "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" + }, + "mime-types": { + "version": "2.1.20", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", + "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", + "requires": { + "mime-db": "~1.36.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + } + }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "packet-reader": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-0.3.1.tgz", + "integrity": "sha1-zWLmCvjX/qinBexP+ZCHHEaHHyc=" + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "requires": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pg": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-7.5.0.tgz", + "integrity": "sha512-VFyAnp8xsMZp8nwZnMp7lmU5QcWDOZSI3IDNcWv6pblsiOXis5o7lD7/zzVK1Z1JTBiIDDGQAMbFMkiUzCL59A==", + "requires": { + "buffer-writer": "1.0.1", + "packet-reader": "0.3.1", + "pg-connection-string": "0.1.3", + "pg-pool": "~2.0.3", + "pg-types": "~1.12.1", + "pgpass": "1.x", + "semver": "4.3.2" + }, + "dependencies": { + "pg-connection-string": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz", + "integrity": "sha1-2hhHsglA5C7hSSvq9l1J2RskXfc=" + } + } + }, + "pg-connection-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.0.0.tgz", + "integrity": "sha1-Pu/lmX4G2Ugh5NUC5CtqHHP434I=" + }, + "pg-pool": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-2.0.3.tgz", + "integrity": "sha1-wCIDLIlJ8xKk+R+2QJzgQHa+Mlc=" + }, + "pg-types": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-1.12.1.tgz", + "integrity": "sha1-1kCH45A7WP+q0nnnWVxSIIoUw9I=", + "requires": { + "postgres-array": "~1.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.0", + "postgres-interval": "^1.1.0" + } + }, + "pgpass": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz", + "integrity": "sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=", + "requires": { + "split": "^1.0.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "postgres-array": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-1.0.3.tgz", + "integrity": "sha512-5wClXrAP0+78mcsNX3/ithQ5exKvCyK5lr5NEEEeGwwM6NJdQgzIJBVxLvRW+huFpX92F2QnZ5CcokH0VhK2qQ==" + }, + "postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" + }, + "postgres-date": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.3.tgz", + "integrity": "sha1-4tiXAu/bJY/52c7g/pG9BpdSV6g=" + }, + "postgres-interval": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.1.2.tgz", + "integrity": "sha512-fC3xNHeTskCxL1dC8KOtxXt7YeFmlbTYtn7ul8MkVERuTmf7pI4DrkAxcw3kh1fQ9uz4wQmd03a1mRiXUZChfQ==", + "requires": { + "xtend": "^4.0.0" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "proxy-addr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.8.0" + } + }, + "psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": ">= 1.3.1 < 2" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "requires": { + "resolve": "^1.1.6" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "request-promise": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz", + "integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=", + "requires": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.1", + "stealthy-require": "^1.1.0", + "tough-cookie": ">=2.3.3" + } + }, + "request-promise-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "requires": { + "lodash": "^4.13.1" + } + }, + "resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "requires": { + "path-parse": "^1.0.5" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz", + "integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=" + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "requires": { + "through": "2" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "supertest": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.3.0.tgz", + "integrity": "sha512-dMQSzYdaZRSANH5LL8kX3UpgK9G1LRh/jnggs/TI0W2Sz7rkMx9Y48uia3K9NgcaWEV28tYkBnXE4tiFC77ygQ==", + "dev": true, + "requires": { + "methods": "^1.1.2", + "superagent": "^3.8.3" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "tarn": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-1.1.4.tgz", + "integrity": "sha512-j4samMCQCP5+6Il9/cxCqBd3x4vvlLeVdoyGex0KixPKl4F8LpNbDSC6NDhjianZgUngElRr9UI1ryZqJDhwGg==" + }, + "through": { + "version": "2.3.8", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "tildify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz", + "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=", + "requires": { + "os-homedir": "^1.0.0" + } + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "v8flags": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.1.tgz", + "integrity": "sha512-iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==", + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + } + } +} diff --git a/examples/tasks/hello-service/package.json b/examples/tasks/hello-service/package.json new file mode 100644 index 0000000000..251ef4cf62 --- /dev/null +++ b/examples/tasks/hello-service/package.json @@ -0,0 +1,24 @@ +{ + "name": "node-service", + "version": "1.0.0", + "description": "Simple Node.js docker service", + "main": "main.js", + "scripts": { + "start": "node main.js", + "test": "echo OK", + "integ": "node_modules/mocha/bin/mocha test/integ.js" + }, + "author": "garden.io ", + "license": "ISC", + "dependencies": { + "express": "^4.16.2", + "knex": "^0.15.2", + "pg": "^7.5.0", + "request": "^2.83.0", + "request-promise": "^4.2.2" + }, + "devDependencies": { + "mocha": "^5.1.1", + "supertest": "^3.0.0" + } +} diff --git a/examples/tasks/hello-service/test/integ.js b/examples/tasks/hello-service/test/integ.js new file mode 100644 index 0000000000..5cf62a6a85 --- /dev/null +++ b/examples/tasks/hello-service/test/integ.js @@ -0,0 +1,17 @@ +const supertest = require("supertest") +const { app } = require("../app") + +describe('GET /call-go-service', () => { + const agent = supertest.agent(app) + + it('should respond with a message from go-service', (done) => { + agent + .get("/call-go-service") + .expect(200, { message: "Hello from Go!" }) + .end((err) => { + if (err) return done(err) + done() + }) + }) +}) + diff --git a/examples/tasks/postgres/garden.yml b/examples/tasks/postgres/garden.yml new file mode 100644 index 0000000000..0bcf2db98d --- /dev/null +++ b/examples/tasks/postgres/garden.yml @@ -0,0 +1,13 @@ +module: + description: postgres container + type: container + name: postgres + image: postgres:9.4 + services: + - name: postgres-service + volumes: + - name: data + containerPath: /db-data + ports: + - name: db + containerPort: 5432 diff --git a/examples/tasks/user-service/Dockerfile b/examples/tasks/user-service/Dockerfile new file mode 100644 index 0000000000..615775025a --- /dev/null +++ b/examples/tasks/user-service/Dockerfile @@ -0,0 +1,12 @@ +FROM ruby:2.5 + +ENV PORT=8084 +EXPOSE ${PORT} +WORKDIR /app + +ADD Gemfile /app +RUN bundle install + +ADD . /app + +CMD ["ruby", "app.rb"] diff --git a/examples/tasks/user-service/Gemfile b/examples/tasks/user-service/Gemfile new file mode 100644 index 0000000000..277e686bc7 --- /dev/null +++ b/examples/tasks/user-service/Gemfile @@ -0,0 +1,6 @@ +source 'https://rubygems.org' + +gem 'activerecord' +gem 'sinatra-activerecord' +gem 'pg' +gem 'rake' diff --git a/examples/tasks/user-service/Rakefile b/examples/tasks/user-service/Rakefile new file mode 100644 index 0000000000..ed1f9ada6c --- /dev/null +++ b/examples/tasks/user-service/Rakefile @@ -0,0 +1,2 @@ +require 'sinatra/activerecord/rake' +require './user_model.rb' diff --git a/examples/tasks/user-service/app.rb b/examples/tasks/user-service/app.rb new file mode 100644 index 0000000000..3ff76bce02 --- /dev/null +++ b/examples/tasks/user-service/app.rb @@ -0,0 +1,16 @@ +require 'sinatra' +require './user_model.rb' + +set :port, ENV['PORT'].to_i + +before do + content_type :json +end + +get '/users' do + {user_names: User.all.map{|u| u.name}}.to_json +end + +get '/' do + redirect '/users' +end diff --git a/examples/tasks/user-service/config.ru b/examples/tasks/user-service/config.ru new file mode 100644 index 0000000000..bcba2d601d --- /dev/null +++ b/examples/tasks/user-service/config.ru @@ -0,0 +1,3 @@ +require './app.rb' + +run App diff --git a/examples/tasks/user-service/config/database.yml b/examples/tasks/user-service/config/database.yml new file mode 100644 index 0000000000..f94e097281 --- /dev/null +++ b/examples/tasks/user-service/config/database.yml @@ -0,0 +1,6 @@ +development: + adapter: postgresql + port: 5432 + host: postgres-service + database: postgres + username: postgres diff --git a/examples/tasks/user-service/db/migrate/20181008135211_populate_users.rb b/examples/tasks/user-service/db/migrate/20181008135211_populate_users.rb new file mode 100644 index 0000000000..5a69982f6b --- /dev/null +++ b/examples/tasks/user-service/db/migrate/20181008135211_populate_users.rb @@ -0,0 +1,11 @@ +class PopulateUsers < ActiveRecord::Migration[5.2] + def up + ["John", "Paul", "George", "Ringo"].each do |name| + User.create(name: name) + end + end + + def down + User.delete_all + end +end diff --git a/examples/tasks/user-service/db/schema.rb b/examples/tasks/user-service/db/schema.rb new file mode 100644 index 0000000000..ce2d822864 --- /dev/null +++ b/examples/tasks/user-service/db/schema.rb @@ -0,0 +1,34 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2018_10_08_135211) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "knex_migrations", id: :serial, force: :cascade do |t| + t.string "name", limit: 255 + t.integer "batch" + t.datetime "migration_time" + end + + create_table "knex_migrations_lock", primary_key: "index", id: :serial, force: :cascade do |t| + t.integer "is_locked" + end + + create_table "users", id: :serial, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at" + t.string "name", limit: 255, null: false + end + +end diff --git a/examples/tasks/user-service/garden.yml b/examples/tasks/user-service/garden.yml new file mode 100644 index 0000000000..8be10f50b0 --- /dev/null +++ b/examples/tasks/user-service/garden.yml @@ -0,0 +1,19 @@ +module: + name: user-service + description: User-listing service written in Ruby + type: container + services: + - name: user-service + command: [ruby, app.rb] + ports: + - name: http + containerPort: 8084 + dependencies: + - ruby-migration + tasks: + - name: ruby-migration + command: [rake, db:migrate] + dependencies: + # node-migration creates the users table, which has to exist before we use + # ruby-migration to insert records into it. + - node-migration diff --git a/examples/tasks/user-service/user_model.rb b/examples/tasks/user-service/user_model.rb new file mode 100644 index 0000000000..50168ffb59 --- /dev/null +++ b/examples/tasks/user-service/user_model.rb @@ -0,0 +1,4 @@ +require 'sinatra/activerecord' + +class User < ActiveRecord::Base +end diff --git a/garden-service/.gitignore b/garden-service/.gitignore index 7e9ff56e97..143e87c022 100644 --- a/garden-service/.gitignore +++ b/garden-service/.gitignore @@ -11,3 +11,4 @@ support/**/*.d.ts test/*.js test/src/**/*.js test/integ/**/*.js +test/data/plugins/**/*.js diff --git a/garden-service/gulpfile.ts b/garden-service/gulpfile.ts index dc0be4ed38..818853fd1c 100644 --- a/garden-service/gulpfile.ts +++ b/garden-service/gulpfile.ts @@ -60,6 +60,18 @@ module.exports = (gulp) => { .pipe(gulp.dest(destDir)), ) + gulp.task("tsc-watch", () => + spawn(npmBinPath("tsc"), [ + "-w", + "--pretty", + "--declaration", + "-p", tsConfigPath, + "--outDir", destDir, + "--preserveWatchOutput", + ], + ), + ) + gulp.task("tsfmt", () => spawn(npmBinPath("tsfmt"), ["--verify"])) gulp.task("tslint", () => @@ -98,12 +110,25 @@ module.exports = (gulp) => { .on("change", verify) }) + gulp.task("watch-code-light", () => { + const verify = (path) => { + try { + _spawn(npmBinPath("tsfmt"), ["--verify", path], { stdio: "inherit" }) + } catch (_) { } + } + + return gulp.watch([tsSources, testTsSources], gulp.parallel("generate-docs", "tslint", "tslint-tests")) + .on("add", verify) + .on("change", verify) + }) + gulp.task("build", gulp.series( gulp.parallel("add-version-files", "generate-docs", "pegjs", "tsc"), "build-container", )) gulp.task("test", gulp.parallel("build", "mocha")) gulp.task("watch", gulp.parallel("pegjs-watch", "watch-code")) + gulp.task("watch-light", gulp.parallel("pegjs-watch", "tsc-watch", "watch-code-light")) gulp.task("default", gulp.series("watch")) } diff --git a/garden-service/package.json b/garden-service/package.json index 659ae17314..3a4a5d4b7a 100644 --- a/garden-service/package.json +++ b/garden-service/package.json @@ -143,6 +143,7 @@ "check-package-lock": "git diff-index --quiet HEAD -- package-lock.json || (echo 'package-lock.json is dirty!' && exit 1)", "clean": "shx rm -rf build && git clean -X -f", "dev": "npm run clean && gulp build && npm link && gulp watch", + "dev-light": "npm run clean && gulp build && npm link && gulp watch-light", "dist": "npm run build", "generate-docs": "gulp generate-docs", "integ": "./test/integ/run", diff --git a/garden-service/src/actions.ts b/garden-service/src/actions.ts index 0912a4d200..4b26806ef4 100644 --- a/garden-service/src/actions.ts +++ b/garden-service/src/actions.ts @@ -11,7 +11,7 @@ import chalk from "chalk" import { Garden } from "./garden" import { PrimitiveMap } from "./config/common" import { Module, ModuleMap } from "./types/module" -import { ModuleActions, ServiceActions, PluginActions } from "./types/plugin/plugin" +import { ModuleActions, ServiceActions, PluginActions, WorkflowActions } from "./types/plugin/plugin" import { BuildResult, BuildStatus, @@ -28,6 +28,8 @@ import { TestResult, PluginActionOutputs, PublishResult, + RunWorkflowResult, + WorkflowActionOutputs, HotReloadResult, } from "./types/plugin/outputs" import { @@ -57,6 +59,9 @@ import { GetEnvironmentStatusParams, PluginModuleActionParamsBase, PublishModuleParams, + PluginWorkflowActionParamsBase, + RunWorkflowParams, + WorkflowActionParams, } from "./types/plugin/params" import { Service, @@ -67,7 +72,7 @@ import { mapValues, values, keyBy, omit } from "lodash" import { Omit } from "./util/util" import { RuntimeContext } from "./types/service" import { processServices, ProcessResults } from "./process" -import { getDeployTasks } from "./tasks/helpers" +import { getTasksForModule } from "./tasks/helpers" import { LogEntry } from "./logger/log-entry" import { createPluginContext } from "./plugin-context" import { CleanupEnvironmentParams } from "./types/plugin/params" @@ -91,13 +96,19 @@ export interface DeployServicesParams { // avoid having to specify common params on each action helper call type ActionHelperParams = Omit & { pluginName?: string } + type ModuleActionHelperParams = Omit & { pluginName?: string } // additionally make runtimeContext param optional + type ServiceActionHelperParams = Omit & { runtimeContext?: RuntimeContext, pluginName?: string } +type WorkflowActionHelperParams = + Omit + & { runtimeContext?: RuntimeContext, pluginName?: string } + type RequirePluginName = T & { pluginName: string } export class ActionHelper implements TypeGuard { @@ -300,12 +311,22 @@ export class ActionHelper implements TypeGuard { //endregion + //=========================================================================== + //region Workflow Methods + //=========================================================================== + + async runWorkflow(params: WorkflowActionHelperParams): Promise { + return this.callWorkflowHandler({ params, actionType: "runWorkflow" }) + } + + //endregion + //=========================================================================== //region Helper Methods //=========================================================================== private async getBuildDependencies(module: Module): Promise { - const dependencies = await this.garden.resolveModuleDependencies(module.build.dependencies, []) + const dependencies = await this.garden.resolveDependencyModules(module.build.dependencies, []) return keyBy(dependencies, "name") } @@ -334,10 +355,9 @@ export class ActionHelper implements TypeGuard { services, garden: this.garden, watch: false, - handler: async (module) => getDeployTasks({ + handler: async (module) => getTasksForModule({ garden: this.garden, module, - serviceNames, hotReloadServiceNames: [], force, forceBuild, @@ -431,6 +451,37 @@ export class ActionHelper implements TypeGuard { return (handler)(handlerParams) } + + private async callWorkflowHandler( + { params, actionType, defaultHandler }: + { + params: WorkflowActionHelperParams, actionType: T, + defaultHandler?: WorkflowActions[T], + }, + ): Promise { + + const { workflow } = params + const module = workflow.module + + const handler = await this.garden.getModuleActionHandler({ + moduleType: module.type, + actionType, + pluginName: params.pluginName, + defaultHandler, + }) + + const buildDependencies = await this.getBuildDependencies(module) + + const handlerParams: any = { + ...this.commonParams(handler), + ...params, + module, + workflow, + buildDependencies, + } + + return (handler)(handlerParams) + } } const dummyLogStreamer = async ({ service, logEntry }: GetServiceLogsParams) => { diff --git a/garden-service/src/commands/build.ts b/garden-service/src/commands/build.ts index be2289ee35..03ded1a31a 100644 --- a/garden-service/src/commands/build.ts +++ b/garden-service/src/commands/build.ts @@ -18,9 +18,7 @@ import { BuildTask } from "../tasks/build" import { TaskResults } from "../task-graph" import dedent = require("dedent") import { processModules } from "../process" -import { computeAutoReloadDependants, withDependants } from "../watch" import { Module } from "../types/module" -import { hotReloadAndLog } from "./helpers" const buildArguments = { module: new StringsParameter({ @@ -61,8 +59,8 @@ export class BuildCommand extends Command { await garden.clearBuilds() - const autoReloadDependants = await computeAutoReloadDependants(garden) const modules = await garden.getModules(args.module) + const dependencyGraph = await garden.getDependencyGraph() const moduleNames = modules.map(m => m.name) garden.log.header({ emoji: "hammer", command: "Build" }) @@ -73,12 +71,8 @@ export class BuildCommand extends Command { watch: opts.watch, handler: async (module) => [new BuildTask({ garden, module, force: opts.force })], changeHandler: async (module: Module) => { - - if (module.spec.hotReload) { - await hotReloadAndLog(garden, module) - } - - return (await withDependants(garden, [module], autoReloadDependants)) + const dependantModules = (await dependencyGraph.getDependants("build", module.name, true)).build + return [module].concat(dependantModules) .filter(m => moduleNames.includes(m.name)) .map(m => new BuildTask({ garden, module: m, force: true })) }, diff --git a/garden-service/src/commands/deploy.ts b/garden-service/src/commands/deploy.ts index 7386706852..4808fca232 100644 --- a/garden-service/src/commands/deploy.ts +++ b/garden-service/src/commands/deploy.ts @@ -18,10 +18,9 @@ import { StringsParameter, } from "./base" import { hotReloadAndLog, validateHotReloadOpt } from "./helpers" -import { getDeployTasks, getTasksForHotReload, getHotReloadModuleNames } from "../tasks/helpers" +import { getTasksForModule, getHotReloadModuleNames } from "../tasks/helpers" import { TaskResults } from "../task-graph" import { processServices } from "../process" -import { getNames } from "../util/util" const deployArgs = { service: new StringsParameter({ @@ -71,7 +70,6 @@ export class DeployCommand extends Command { async action({ garden, args, opts }: CommandParams): Promise> { const services = await garden.getServices(args.service) - const serviceNames = getNames(services) if (services.length === 0) { garden.log.error({ msg: "No services found. Aborting." }) @@ -100,11 +98,10 @@ export class DeployCommand extends Command { garden, services, watch, - handler: async (module) => getDeployTasks({ + handler: async (module) => getTasksForModule({ garden, module, - serviceNames, - watch, + fromWatch: false, hotReloadServiceNames, force: opts.force, forceBuild: opts["force-build"], @@ -112,12 +109,11 @@ export class DeployCommand extends Command { changeHandler: async (module) => { if (hotReloadModuleNames.has(module.name)) { await hotReloadAndLog(garden, module) - return getTasksForHotReload({ garden, module, hotReloadServiceNames, serviceNames }) - } else { - return getDeployTasks({ - garden, module, serviceNames, hotReloadServiceNames, force: true, forceBuild: true, watch: true, - }) } + return getTasksForModule({ + garden, module, hotReloadServiceNames, force: true, forceBuild: opts["force-build"], + fromWatch: true, includeDependants: true, + }) }, }) diff --git a/garden-service/src/commands/dev.ts b/garden-service/src/commands/dev.ts index d508758f14..6ed007d5d4 100644 --- a/garden-service/src/commands/dev.ts +++ b/garden-service/src/commands/dev.ts @@ -18,7 +18,7 @@ import { join } from "path" import { BuildTask } from "../tasks/build" import { Task } from "../tasks/base" import { hotReloadAndLog, validateHotReloadOpt } from "./helpers" -import { getDeployTasks, getTasksForHotReload, getHotReloadModuleNames } from "../tasks/helpers" +import { getTasksForModule, getHotReloadModuleNames } from "../tasks/helpers" import { Command, CommandResult, @@ -28,9 +28,7 @@ import { import { STATIC_DIR } from "../constants" import { processModules } from "../process" import { Module } from "../types/module" -import { computeAutoReloadDependants, withDependants } from "../watch" import { getTestTasks } from "../tasks/test" -import { getNames } from "../util/util" const ansiBannerPath = join(STATIC_DIR, "garden-banner-2.txt") @@ -73,7 +71,6 @@ export class DevCommand extends Command { await garden.actions.prepareEnvironment({}) - const autoReloadDependants = await computeAutoReloadDependants(garden) const modules = await garden.getModules() if (modules.length === 0) { @@ -89,8 +86,7 @@ export class DevCommand extends Command { return {} } - const services = await garden.getServices() - const serviceNames = getNames(services) + const dependencyGraph = await garden.getDependencyGraph() const tasksForModule = (watch: boolean) => { return async (module: Module) => { @@ -102,27 +98,21 @@ export class DevCommand extends Command { } const testModules: Module[] = watch - ? (await withDependants(garden, [module], autoReloadDependants)) + ? (await dependencyGraph.withDependantModules([module])) : [module] const testTasks: Task[] = flatten(await Bluebird.map( testModules, m => getTestTasks({ garden, module: m }))) - let tasks - if (watch && hotReload) { - tasks = testTasks.concat(await getTasksForHotReload({ garden, module, hotReloadServiceNames, serviceNames })) - } else { - tasks = testTasks.concat(await getDeployTasks({ - garden, - module, - watch, - serviceNames, - hotReloadServiceNames, - force: watch, - forceBuild: watch, - includeDependants: watch, - })) - } + const tasks = testTasks.concat(await getTasksForModule({ + garden, + module, + fromWatch: watch, + hotReloadServiceNames, + force: watch, + forceBuild: watch, + includeDependants: watch, + })) if (tasks.length === 0) { return [new BuildTask({ garden, module, force: watch })] diff --git a/garden-service/src/commands/run/module.ts b/garden-service/src/commands/run/module.ts index 0551ad4b9c..1501258efa 100644 --- a/garden-service/src/commands/run/module.ts +++ b/garden-service/src/commands/run/module.ts @@ -58,9 +58,12 @@ export class RunModuleCommand extends Command { Examples: - garden run module my-container # run an ad-hoc instance of a my-container container and attach to it - garden run module my-container /bin/sh # run an interactive shell in a new my-container container - garden run module my-container --i=false /some/script # execute a script in my-container and return the output + garden run module my-container # run an ad-hoc instance of a my-container \ + container and attach to it + garden run module my-container /bin/sh # run an interactive shell in a new \ + my-container container + garden run module my-container --interactive=false /some/script # execute a script in my-container and \ + return the output ` arguments = runArgs diff --git a/garden-service/src/commands/run/run.ts b/garden-service/src/commands/run/run.ts index 455b89a85c..7783210c23 100644 --- a/garden-service/src/commands/run/run.ts +++ b/garden-service/src/commands/run/run.ts @@ -12,6 +12,7 @@ import { highlightYaml } from "../../util/util" import { Command } from "../base" import { RunModuleCommand } from "./module" import { RunServiceCommand } from "./service" +import { RunWorkflowCommand } from "./workflow" import { RunTestCommand } from "./test" import { Garden } from "../../garden" @@ -22,6 +23,7 @@ export class RunCommand extends Command { subCommands = [ RunModuleCommand, RunServiceCommand, + RunWorkflowCommand, RunTestCommand, ] diff --git a/garden-service/src/commands/run/workflow.ts b/garden-service/src/commands/run/workflow.ts new file mode 100644 index 0000000000..d468fe50f8 --- /dev/null +++ b/garden-service/src/commands/run/workflow.ts @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import chalk from "chalk" +import { RunResult } from "../../types/plugin/outputs" +import { + BooleanParameter, + Command, + CommandParams, + StringParameter, + CommandResult, +} from "../base" +import { + uniq, + flatten, +} from "lodash" +import { printRuntimeContext } from "./run" +import dedent = require("dedent") +import { prepareRuntimeContext } from "../../types/service" +import { WorkflowTask } from "../../tasks/workflow" + +const runArgs = { + task: new StringParameter({ + help: "The name of the task to run.", + required: true, + }), +} + +const runOpts = { + "force-build": new BooleanParameter({ help: "Force rebuild of module before running." }), +} + +type Args = typeof runArgs +type Opts = typeof runOpts + +export class RunWorkflowCommand extends Command { + name = "task" + alias = "t" + help = "Run a task (in the context of its parent module)." + + description = dedent` + This is useful for re-running tasks on the go, for example after writing/modifying database migrations. + + Examples: + + garden run task my-db-migration # run my-migration + ` + + arguments = runArgs + options = runOpts + + async action({ garden, args, opts }: CommandParams): Promise> { + const workflow = await garden.getWorkflow(args.task) + const module = workflow.module + + const msg = `Running task ${chalk.white(workflow.name)}` + + garden.log.header({ + emoji: "runner", + command: msg, + }) + + await garden.actions.prepareEnvironment({}) + + const workflowTask = new WorkflowTask({ garden, workflow, force: true, forceBuild: opts["force-build"] }) + for (const depTask of await workflowTask.getDependencies()) { + await garden.addTask(depTask) + } + await garden.processTasks() + + // combine all dependencies for all services in the module, to be sure we have all the context we need + const depNames = uniq(flatten(module.serviceConfigs.map(s => s.dependencies))) + const deps = await garden.getServices(depNames) + + const runtimeContext = await prepareRuntimeContext(garden, module, deps) + + printRuntimeContext(garden, runtimeContext) + + garden.log.info("") + + const result = await garden.actions.runWorkflow({ workflow, runtimeContext, interactive: true }) + + garden.log.info(chalk.white(result.output)) + + garden.log.info("") + garden.log.header({ emoji: "heavy_check_mark", command: `Done!` }) + + return { result } + } +} diff --git a/garden-service/src/commands/scan.ts b/garden-service/src/commands/scan.ts index 14f8c591bc..01bf77b20c 100644 --- a/garden-service/src/commands/scan.ts +++ b/garden-service/src/commands/scan.ts @@ -24,7 +24,10 @@ export class ScanCommand extends Command { const modules = (await garden.getModules()) .map(m => { m.services.forEach(s => delete s.module) - return omit(m, ["_ConfigType", "cacheContext", "serviceConfigs", "serviceNames"]) + m.workflows.forEach(w => delete w.module) + return omit(m, [ + "_ConfigType", "cacheContext", "serviceConfigs", "serviceNames", "workflowConfigs", "workflowNames", + ]) }) const output = { modules } diff --git a/garden-service/src/commands/test.ts b/garden-service/src/commands/test.ts index 058f75cd33..c46dcfe546 100644 --- a/garden-service/src/commands/test.ts +++ b/garden-service/src/commands/test.ts @@ -23,7 +23,6 @@ import { TaskResults } from "../task-graph" import { processModules } from "../process" import { Module } from "../types/module" import { getTestTasks } from "../tasks/test" -import { computeAutoReloadDependants, withDependants } from "../watch" const testArgs = { module: new StringsParameter({ @@ -69,10 +68,10 @@ export class TestCommand extends Command { options = testOpts async action({ garden, args, opts }: CommandParams): Promise> { - const autoReloadDependants = await computeAutoReloadDependants(garden) + const dependencyGraph = await garden.getDependencyGraph() let modules: Module[] if (args.module) { - modules = await withDependants(garden, await garden.getModules(args.module), autoReloadDependants) + modules = await dependencyGraph.withDependantModules(await garden.getModules(args.module)) } else { // All modules are included in this case, so there's no need to compute dependants. modules = await garden.getModules() @@ -95,7 +94,7 @@ export class TestCommand extends Command { watch: opts.watch, handler: async (module) => getTestTasks({ garden, module, name, force, forceBuild }), changeHandler: async (module) => { - const modulesToProcess = await withDependants(garden, [module], autoReloadDependants) + const modulesToProcess = await dependencyGraph.withDependantModules([module]) return flatten(await Bluebird.map( modulesToProcess, m => getTestTasks({ garden, module: m, name, force, forceBuild }))) diff --git a/garden-service/src/config/base.ts b/garden-service/src/config/base.ts index 752bb166eb..4cf0167829 100644 --- a/garden-service/src/config/base.ts +++ b/garden-service/src/config/base.ts @@ -124,6 +124,7 @@ export async function loadConfig(projectRoot: string, path: string): Promise + < + M extends ModuleSpec = any, + S extends ServiceSpec = any, + T extends TestSpec = any, + W extends WorkflowSpec = any, + > extends BaseModuleSpec { plugin?: string // used to identify modules that are bundled as part of a plugin serviceConfigs: ServiceConfig[] testConfigs: TestConfig[] + workflowConfigs: WorkflowConfig[] // Plugins can add custom fields that are kept here spec: M diff --git a/garden-service/src/config/service.ts b/garden-service/src/config/service.ts index 57b5ed24d3..18f20f761c 100644 --- a/garden-service/src/config/service.ts +++ b/garden-service/src/config/service.ts @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import deline = require("deline") import * as Joi from "joi" import { PrimitiveMap, joiIdentifier, joiIdentifierMap, joiPrimitive, joiArray } from "./common" @@ -23,7 +24,10 @@ export const baseServiceSchema = Joi.object() .keys({ name: joiIdentifier().required(), dependencies: joiArray(joiIdentifier()) - .description("The names of services that this service depends on at runtime."), + .description(deline` + The names of any services that this service depends on at runtime, and the names of any + tasks that should be executed before this service is deployed. + `), outputs: serviceOutputsSchema, }) .unknown(true) diff --git a/garden-service/src/config/test.ts b/garden-service/src/config/test.ts index 4a34ff9bdc..635574a46a 100644 --- a/garden-service/src/config/test.ts +++ b/garden-service/src/config/test.ts @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import deline = require("deline") import * as Joi from "joi" import { joiArray, @@ -26,7 +27,10 @@ export const baseTestSpecSchema = Joi.object() .required() .description("The name of the test."), dependencies: joiArray(Joi.string()) - .description("Names of the services that must be running before the test is run."), + .description(deline` + The names of any services that must be running, and the names of any + tasks that must be executed, before the test is run. + `), timeout: Joi.number() .allow(null) .default(null) diff --git a/garden-service/src/config/workflow.ts b/garden-service/src/config/workflow.ts new file mode 100644 index 0000000000..492e13d82f --- /dev/null +++ b/garden-service/src/config/workflow.ts @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import deline = require("deline") +import * as Joi from "joi" +import { + joiArray, + joiIdentifier, +} from "./common" + +export interface WorkflowSpec { } + +export interface BaseWorkflowSpec extends WorkflowSpec { + name: string + dependencies: string[] + timeout: number | null +} + +export const baseWorkflowSpecSchema = Joi.object() + .keys({ + name: joiIdentifier() + .required() + .description("The name of the task."), + dependencies: joiArray(Joi.string()) + .description(deline` + The names of any tasks that must be executed, and the names of any + services that must be running, before this task is executed. + `), + timeout: Joi.number() + .optional() + .allow(null) + .default(null) + .description("Maximum duration (in seconds) of the task's execution."), + }) + .description("Required configuration for module tasks.") + +export interface WorkflowConfig extends BaseWorkflowSpec { + // Plugins can add custom fields that are kept here + spec: T +} + +export const workflowConfigSchema = baseWorkflowSpecSchema + .keys({ + spec: Joi.object() + .meta({ extendable: true }) + .description("The task's specification, as defined by its provider plugin."), + }) + .description("The configuration for a module's task.") + +export const workflowSchema = Joi.object() + .options({ presence: "required" }) + .keys({ + name: joiIdentifier() + .description("The name of the task."), + module: Joi.object().unknown(true), + config: workflowConfigSchema, + spec: Joi.object() + .meta({ extendable: true }) + .description("The configuration of the task (specific to each plugin)."), + }) diff --git a/garden-service/src/dependency-graph.ts b/garden-service/src/dependency-graph.ts new file mode 100644 index 0000000000..ac85cfaebe --- /dev/null +++ b/garden-service/src/dependency-graph.ts @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import * as Bluebird from "bluebird" +import { flatten, fromPairs, pick, uniq } from "lodash" +import { Garden } from "./garden" +import { Module } from "./types/module" +import { Service } from "./types/service" +import { Workflow } from "./types/workflow" +import { TestConfig } from "./config/test" +import { uniqByName } from "./util/util" + +export type DependencyGraphNodeType = "build" | "service" | "workflow" | "test" + | "push" | "publish" // these two types are currently not represented in DependencyGraph + +// The primary output type (for dependencies and dependants). +export type DependencyRelations = { + build: Module[], + service: Service[], + workflow: Workflow[], + test: TestConfig[], +} + +type DependencyRelationNames = { + build: string[], + service: string[], + workflow: string[], + test: string[], +} + +export type DependencyRelationFilterFn = (DependencyGraphNode) => boolean + +export class DependencyGraph { + + index: { [key: string]: DependencyGraphNode } + private garden: Garden + private serviceMap: { [key: string]: Service } + private workflowMap: { [key: string]: Workflow } + private testConfigMap: { [key: string]: TestConfig } + private testConfigModuleMap: { [key: string]: Module } + + static async factory(garden: Garden) { + const { modules, services, workflows } = await Bluebird.props({ + modules: garden.getModules(), + services: garden.getServices(), + workflows: garden.getWorkflows(), + }) + + return new DependencyGraph(garden, modules, services, workflows) + } + + constructor(garden: Garden, modules: Module[], services: Service[], workflows: Workflow[]) { + + this.garden = garden + this.index = {} + + this.serviceMap = fromPairs(services.map(s => [s.name, s])) + this.workflowMap = fromPairs(workflows.map(w => [w.name, w])) + this.testConfigMap = {} + this.testConfigModuleMap = {} + + for (const module of modules) { + + // Build dependencies + const buildNode = this.getNode("build", module.name, module.name) + for (const buildDep of module.build.dependencies) { + this.addRelation(buildNode, "build", buildDep.name, buildDep.name) + } + + // Service dependencies + for (const serviceConfig of module.serviceConfigs) { + const serviceNode = this.getNode("service", serviceConfig.name, module.name) + this.addRelation(serviceNode, "build", module.name, module.name) + for (const depName of serviceConfig.dependencies) { + if (this.serviceMap[depName]) { + this.addRelation(serviceNode, "service", depName, this.serviceMap[depName].module.name) + } else { + this.addRelation(serviceNode, "workflow", depName, this.workflowMap[depName].module.name) + } + } + } + + // Workflow dependencies + for (const workflowConfig of module.workflowConfigs) { + const workflowNode = this.getNode("workflow", workflowConfig.name, module.name) + this.addRelation(workflowNode, "build", module.name, module.name) + for (const depName of workflowConfig.dependencies) { + if (this.serviceMap[depName]) { + this.addRelation(workflowNode, "service", depName, this.serviceMap[depName].module.name) + } else { + this.addRelation(workflowNode, "workflow", depName, this.workflowMap[depName].module.name) + } + } + } + + // Test dependencies + for (const testConfig of module.testConfigs) { + const testConfigName = `${module.name}.${testConfig.name}` + this.testConfigMap[testConfigName] = testConfig + this.testConfigModuleMap[testConfigName] = module + const testNode = this.getNode("test", testConfigName, module.name) + this.addRelation(testNode, "build", module.name, module.name) + for (const depName of testConfig.dependencies) { + if (this.serviceMap[depName]) { + this.addRelation(testNode, "service", depName, this.serviceMap[depName].module.name) + } else { + this.addRelation(testNode, "workflow", depName, this.workflowMap[depName].module.name) + } + } + } + + } + } + + /** + * If filterFn is provided to any of the methods below that accept it, matching nodes + * (and their dependencies/dependants, if recursive = true) are ignored. + */ + + /** + * Returns the set union of modules with the set union of their dependants (across all dependency types). + * Recursive. + */ + async withDependantModules(modules: Module[], filterFn?: DependencyRelationFilterFn): Promise { + const dependants = flatten(await Bluebird.map(modules, m => this.getDependantsForModule(m, filterFn))) + // We call getModules to ensure that the returned modules have up-to-date versions. + const dependantModules = await this.modulesForRelations( + await this.mergeRelations(...dependants)) + return this.garden.getModules(uniq(modules.concat(dependantModules).map(m => m.name))) + } + + // Recursive. + async getDependantsForModule(module: Module, filterFn?: DependencyRelationFilterFn): Promise { + const runtimeDependencies = uniq(module.serviceDependencyNames.concat(module.workflowDependencyNames)) + const serviceNames = runtimeDependencies.filter(d => this.serviceMap[d]) + const workflowNames = runtimeDependencies.filter(d => this.workflowMap[d]) + + return this.mergeRelations(... await Bluebird.all([ + this.getDependants("build", module.name, true, filterFn), + // this.getDependantsForMany("build", module.build.dependencies.map(d => d.name), true, filterFn), + this.getDependantsForMany("service", serviceNames, true, filterFn), + this.getDependantsForMany("workflow", workflowNames, true, filterFn), + ])) + } + + async getDependencies( + nodeType: DependencyGraphNodeType, name: string, recursive: boolean, filterFn?: DependencyRelationFilterFn, + ): Promise { + return this.toRelations(this.getDependencyNodes(nodeType, name, recursive, filterFn)) + } + + async getDependants( + nodeType: DependencyGraphNodeType, name: string, recursive: boolean, filterFn?: DependencyRelationFilterFn, + ): Promise { + return this.toRelations(this.getDependantNodes(nodeType, name, recursive, filterFn)) + } + + async getDependenciesForMany( + nodeType: DependencyGraphNodeType, names: string[], recursive: boolean, filterFn?: DependencyRelationFilterFn, + ): Promise { + return this.toRelations(flatten( + names.map(name => this.getDependencyNodes(nodeType, name, recursive, filterFn)))) + } + + async getDependantsForMany( + nodeType: DependencyGraphNodeType, names: string[], recursive: boolean, filterFn?: DependencyRelationFilterFn, + ): Promise { + return this.toRelations(flatten( + names.map(name => this.getDependantNodes(nodeType, name, recursive, filterFn)))) + } + + /** + * Computes the set union for each node type across relationArr (i.e. concatenates + * and deduplicates for each key). + */ + async mergeRelations(...relationArr: DependencyRelations[]): Promise { + const names = {} + for (const type of ["build", "service", "workflow", "test"]) { + names[type] = uniqByName(flatten(relationArr.map(r => r[type]))).map(r => r.name) + } + + return this.relationsFromNames({ + build: names["build"], + service: names["service"], + workflow: names["workflow"], + test: names["test"], + }) + } + + async modulesForRelations(relations: DependencyRelations): Promise { + const moduleNames = uniq(flatten([ + relations.build, + relations.service.map(s => s.module), + relations.workflow.map(w => w.module), + relations.test.map(t => this.testConfigModuleMap[t.name]), + ]).map(m => m.name)) + // We call getModules to ensure that the returned modules have up-to-date versions. + return this.garden.getModules(moduleNames) + } + + private async toRelations(nodes): Promise { + return this.relationsFromNames({ + build: this.uniqueNames(nodes, "build"), + service: this.uniqueNames(nodes, "service"), + workflow: this.uniqueNames(nodes, "workflow"), + test: this.uniqueNames(nodes, "test"), + }) + } + + private async relationsFromNames(names: DependencyRelationNames): Promise { + return Bluebird.props({ + build: this.garden.getModules(names.build), + service: this.garden.getServices(names.service), + workflow: this.garden.getWorkflows(names.workflow), + test: Object.values(pick(this.testConfigMap, names.test)), + }) + } + + private getDependencyNodes( + nodeType: DependencyGraphNodeType, name: string, recursive: boolean, filterFn?: DependencyRelationFilterFn, + ): DependencyGraphNode[] { + const node = this.index[nodeKey(nodeType, name)] + if (node) { + if (recursive) { + return node.recursiveDependencies(filterFn) + } else { + return filterFn ? node.dependencies.filter(filterFn) : node.dependencies + } + } else { + return [] + } + } + + private getDependantNodes( + nodeType: DependencyGraphNodeType, name: string, recursive: boolean, filterFn?: DependencyRelationFilterFn, + ): DependencyGraphNode[] { + const node = this.index[nodeKey(nodeType, name)] + if (node) { + if (recursive) { + return node.recursiveDependants(filterFn) + } else { + return filterFn ? node.dependants.filter(filterFn) : node.dependants + } + } else { + return [] + } + } + + private uniqueNames(nodes: DependencyGraphNode[], type: DependencyGraphNodeType) { + return uniq(nodes.filter(n => n.type === type).map(n => n.name)) + } + + // Idempotent. + private addRelation( + dependant: DependencyGraphNode, dependencyType: DependencyGraphNodeType, + dependencyName: string, dependencyModuleName: string, + ) { + const dependency = this.getNode(dependencyType, dependencyName, dependencyModuleName) + dependant.addDependency(dependency) + dependency.addDependant(dependant) + } + + // Idempotent. + private getNode(type: DependencyGraphNodeType, name: string, moduleName: string) { + const key = nodeKey(type, name) + const existingNode = this.index[key] + if (existingNode) { + return existingNode + } else { + const newNode = new DependencyGraphNode(type, name, moduleName) + this.index[key] = newNode + return newNode + } + } + +} + +export class DependencyGraphNode { + + type: DependencyGraphNodeType + name: string // same as a corresponding task's name + moduleName: string + dependencies: DependencyGraphNode[] + dependants: DependencyGraphNode[] + + constructor(type: DependencyGraphNodeType, name: string, moduleName: string) { + this.type = type + this.name = name + this.moduleName = moduleName + this.dependencies = [] + this.dependants = [] + } + + // Idempotent. + addDependency(node: DependencyGraphNode) { + const key = nodeKey(node.type, node.name) + if (!this.dependencies.find(d => nodeKey(d.type, d.name) === key)) { + this.dependencies.push(node) + } + } + + // Idempotent. + addDependant(node: DependencyGraphNode) { + const key = nodeKey(node.type, node.name) + if (!this.dependants.find(d => nodeKey(d.type, d.name) === key)) { + this.dependants.push(node) + } + } + + /** + * If filterFn is provided, ignores matching nodes and their dependencies. + * Note: May return duplicate entries (deduplicated in DependencyGraph#toRelations). + */ + recursiveDependencies(filterFn?: DependencyRelationFilterFn) { + const deps = filterFn ? this.dependencies.filter(filterFn) : this.dependencies + return flatten(deps.concat( + deps.map(d => d.recursiveDependencies(filterFn)))) + } + + /** + * If filterFn is provided, ignores matching nodes and their dependants. + * Note: May return duplicate entries (deduplicated in DependencyGraph#toRelations). + */ + recursiveDependants(filterFn?: DependencyRelationFilterFn) { + const dependants = filterFn ? this.dependants.filter(filterFn) : this.dependants + return flatten(dependants.concat( + dependants.map(d => d.recursiveDependants(filterFn)))) + } + +} + +function nodeKey(type: DependencyGraphNodeType, name: string) { + return `${type}.${name}` +} + +// for testing/debugging +export function renderGraph(graph: DependencyGraph) { + const nodes = Object.values(graph.index) + const edges: string[][] = [] + for (const node of nodes) { + for (const dep of node.dependencies) { + edges.push([nodeKey(node.type, node.name), nodeKey(dep.type, dep.name)]) + } + } + return edges +} diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index d86d4ad441..8b4b54b0c4 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -7,6 +7,7 @@ */ import Bluebird = require("bluebird") +import deline = require("deline") import { parse, relative, @@ -16,14 +17,16 @@ import { import { extend, flatten, + intersection, isString, fromPairs, merge, keyBy, cloneDeep, + pick, pickBy, sortBy, - uniqBy, + difference, } from "lodash" const AsyncLock = require("async-lock") @@ -47,6 +50,8 @@ import { getNames, scanDirectory, pickKeys, + throwOnMissingNames, + uniqByName, } from "./util/util" import { DEFAULT_NAMESPACE, @@ -63,6 +68,7 @@ import { VcsHandler, ModuleVersion } from "./vcs/base" import { GitHandler } from "./vcs/git" import { BuildDir } from "./build-dir" import { HotReloadHandler, HotReloadScheduler } from "./hotReloadScheduler" +import { DependencyGraph } from "./dependency-graph" import { TaskGraph, TaskResults, @@ -80,6 +86,7 @@ import { } from "./types/plugin/plugin" import { joiIdentifier, validate } from "./config/common" import { Service } from "./types/service" +import { Workflow } from "./types/workflow" import { resolveTemplateStrings } from "./template-string" import { configSchema, @@ -100,7 +107,7 @@ import { FileWriter } from "./logger/writers/file-writer" import { LogLevel } from "./logger/log-node" import { ActionHelper } from "./actions" import { createPluginContext } from "./plugin-context" -import { ModuleAndServiceActions, Plugins, RegisterPluginParam } from "./types/plugin/plugin" +import { ModuleAndRuntimeActions, Plugins, RegisterPluginParam } from "./types/plugin/plugin" import { SUPPORTED_PLATFORMS, SupportedPlatform } from "./constants" import { platform, arch } from "os" @@ -108,8 +115,8 @@ export interface ActionHandlerMap { [actionName: string]: PluginActions[T] } -export interface ModuleActionHandlerMap { - [actionName: string]: ModuleAndServiceActions[T] +export interface ModuleActionHandlerMap { + [actionName: string]: ModuleAndRuntimeActions[T] } export type PluginActionMap = { @@ -119,9 +126,9 @@ export type PluginActionMap = { } export type ModuleActionMap = { - [A in keyof ModuleAndServiceActions]: { + [A in keyof ModuleAndRuntimeActions]: { [moduleType: string]: { - [pluginName: string]: ModuleAndServiceActions[A], + [pluginName: string]: ModuleAndRuntimeActions[A], }, } } @@ -145,12 +152,14 @@ export class Garden { public readonly log: Logger public readonly actionHandlers: PluginActionMap public readonly moduleActionHandlers: ModuleActionMap + public dependencyGraph: DependencyGraph private readonly loadedPlugins: { [key: string]: GardenPlugin } private moduleConfigs: ModuleConfigMap private modulesScanned: boolean private readonly registeredPlugins: { [key: string]: PluginFactory } - private readonly serviceNameIndex: { [key: string]: string } + private readonly serviceNameIndex: { [key: string]: string } // service name -> module name + private readonly workflowNameIndex: { [key: string]: string } // workflow name -> module name private readonly hotReloadScheduler: HotReloadScheduler private readonly taskGraph: TaskGraph @@ -188,6 +197,7 @@ export class Garden { this.moduleConfigs = {} this.serviceNameIndex = {} + this.workflowNameIndex = {} this.loadedPlugins = {} this.registeredPlugins = {} this.actionHandlers = fromPairs(pluginActionNames.map(n => [n, {}])) @@ -566,28 +576,37 @@ export class Garden { return (await this.getModules([name], noScan))[0] } + async getDependencyGraph() { + if (!this.dependencyGraph) { + this.dependencyGraph = await DependencyGraph.factory(this) + } + + return this.dependencyGraph + } + /** - * Given the provided lists of build and service dependencies, return a list of all modules - * required to satisfy those dependencies. + * Given the provided lists of build and runtime (service/workflow) dependencies, return a list of all + * modules required to satisfy those dependencies. */ - async resolveModuleDependencies(buildDependencies: BuildDependencyConfig[], serviceDependencies: string[]) { - const buildDeps = await Bluebird.map(buildDependencies, async (dep) => { - const moduleKey = getModuleKey(dep.name, dep.plugin) - const module = await this.getModule(moduleKey) - return [module].concat(await this.resolveModuleDependencies(module.build.dependencies, [])) - }) + async resolveDependencyModules( + buildDependencies: BuildDependencyConfig[], runtimeDependencies: string[], + ): Promise { + const moduleNames = buildDependencies.map(d => getModuleKey(d.name, d.plugin)) + const dg = await this.getDependencyGraph() - const runtimeDeps = await Bluebird.map(serviceDependencies, async (serviceName) => { - const service = await this.getService(serviceName) - return this.resolveModuleDependencies( - [{ name: service.module.name, copy: [] }], - service.config.dependencies || [], - ) - }) + const serviceNames = runtimeDependencies.filter(d => this.serviceNameIndex[d]) + const workflowNames = runtimeDependencies.filter(d => this.workflowNameIndex[d]) - const deps = flatten(buildDeps).concat(flatten(runtimeDeps)) + const buildDeps = await dg.getDependenciesForMany("build", moduleNames, true) + const serviceDeps = await dg.getDependenciesForMany("service", serviceNames, true) + const workflowDeps = await dg.getDependenciesForMany("workflow", workflowNames, true) - return sortBy(uniqBy(deps, "name"), "name") + const modules = [ + ...(await this.getModules(moduleNames)), + ...(await dg.modulesForRelations(await dg.mergeRelations(buildDeps, serviceDeps, workflowDeps))), + ] + + return sortBy(uniqByName(modules), "name") } /** @@ -595,7 +614,7 @@ export class Garden { * The combined version is a either the latest dirty module version (if any), or the hash of the module version * and the versions of its dependencies (in sorted order). */ - async resolveVersion(moduleName: string, moduleDependencies: BuildDependencyConfig[], force = false) { + async resolveVersion(moduleName: string, moduleDependencies: (Module | BuildDependencyConfig)[], force = false) { const depModuleNames = moduleDependencies.map(m => m.name) depModuleNames.sort() const cacheKey = ["moduleVersions", moduleName, ...depModuleNames] @@ -619,35 +638,132 @@ export class Garden { return version } + async getServiceOrWorkflow(name: string, noScan?: boolean): Promise | Workflow> { + const service = (await this.getServices([name], noScan))[0] + const workflow = (await this.getWorkflows([name], noScan))[0] + + if (!service && !workflow) { + throw new ParameterError(`Could not find service or task named ${name}`, { + missing: [name], + availableServices: Object.keys(this.serviceNameIndex), + availableWorkflows: Object.keys(this.workflowNameIndex), + }) + } + + return service || workflow + } + + /** + * Returns the service with the specified name. Throws error if it doesn't exist. + */ + async getService(name: string, noScan?: boolean): Promise> { + const service = (await this.getServices([name], noScan))[0] + + if (!service) { + throw new ParameterError(`Could not find service ${name}`, { + missing: [name], + available: Object.keys(this.serviceNameIndex), + }) + } + + return service + } + + async getWorkflow(name: string, noScan?: boolean): Promise { + const workflow = (await this.getWorkflows([name], noScan))[0] + + if (!workflow) { + throw new ParameterError(`Could not find task ${name}`, { + missing: [name], + available: Object.keys(this.workflowNameIndex), + }) + } + + return workflow + } + /* Returns all services that are registered in this context, or the ones specified. - Scans for modules and services in the project root if it hasn't already been done. + If the names parameter is used and workflow names are included in it, they will be + ignored. Scans for modules and services in the project root if it hasn't already + been done. */ async getServices(names?: string[], noScan?: boolean): Promise { + const services = (await this.getServicesAndWorkflows(names, noScan)).services + if (names) { + const workflowNames = Object.keys(this.workflowNameIndex) + throwOnMissingNames(difference(names, workflowNames), services, "service") + } + return services + } + + /* + Returns all workflows that are registered in this context, or the ones specified. + If the names parameter is used and service names are included in it, they will be + ignored. Scans for modules and services in the project root if it hasn't already + been done. + */ + async getWorkflows(names?: string[], noScan?: boolean): Promise { + const workflows = (await this.getServicesAndWorkflows(names, noScan)).workflows + if (names) { + const serviceNames = Object.keys(this.serviceNameIndex) + throwOnMissingNames(difference(names, serviceNames), workflows, "task") + } + return workflows + } + + async getServicesAndWorkflows(names?: string[], noScan?: boolean) { if (!this.modulesScanned && !noScan) { await this.scanModules() } - const picked = names ? pickKeys(this.serviceNameIndex, names, "service") : this.serviceNameIndex + let pickedServices: { [key: string]: string } + let pickedWorkflows: { [key: string]: string } - return Bluebird.map(Object.entries(picked), async ([serviceName, moduleName]) => { - const module = await this.getModule(moduleName) - const config = findByName(module.serviceConfigs, serviceName)! + if (names) { + const serviceNames = Object.keys(this.serviceNameIndex) + const workflowNames = Object.keys(this.workflowNameIndex) + pickedServices = pick(this.serviceNameIndex, intersection(names, serviceNames)) + pickedWorkflows = pick(this.workflowNameIndex, intersection(names, workflowNames)) + } else { + pickedServices = this.serviceNameIndex + pickedWorkflows = this.workflowNameIndex + } + + return Bluebird.props({ + + services: Bluebird.map(Object.entries(pickedServices), async ([serviceName, moduleName]): + Promise => { + + const module = await this.getModule(moduleName) + const config = findByName(module.serviceConfigs, serviceName)! + + return { + name: serviceName, + config, + module, + spec: config.spec, + } + + }), + + workflows: Bluebird.map(Object.entries(pickedWorkflows), async ([workflowName, moduleName]): + Promise => { + + const module = await this.getModule(moduleName) + const config = findByName(module.workflowConfigs, workflowName)! + + return { + name: workflowName, + config, + module, + spec: config.spec, + } + + }), - return { - name: serviceName, - config, - module, - spec: config.spec, - } }) - } - /** - * Returns the service with the specified name. Throws error if it doesn't exist. - */ - async getService(name: string, noScan?: boolean): Promise> { - return (await this.getServices([name], noScan))[0] } /* @@ -713,10 +829,13 @@ export class Garden { } private async detectCircularDependencies() { - const modules = await this.getModules() - const services = await this.getServices() + const { modules, services, workflows } = await Bluebird.props({ + modules: this.getModules(), + services: this.getServices(), + workflows: this.getWorkflows(), + }) - return detectCircularDependencies(modules, services) + return detectCircularDependencies(modules, services, workflows) } /* @@ -750,9 +869,9 @@ export class Garden { const serviceName = serviceConfig.name if (!force && this.serviceNameIndex[serviceName]) { - throw new ConfigurationError( - `Service names must be unique - ${serviceName} is declared multiple times ` + - `(in '${this.serviceNameIndex[serviceName]}' and '${config.name}')`, + throw new ConfigurationError(deline` + Service names must be unique - the service name ${serviceName} is declared multiple times + (in '${this.serviceNameIndex[serviceName]}' and '${config.name}')`, { serviceName, moduleA: this.serviceNameIndex[serviceName], @@ -764,6 +883,40 @@ export class Garden { this.serviceNameIndex[serviceName] = config.name } + // Add to workflow-module map + for (const workflowConfig of config.workflowConfigs) { + const workflowName = workflowConfig.name + + if (!force) { + + if (this.serviceNameIndex[workflowName]) { + throw new ConfigurationError(deline` + Service and task names must be mutually unique - the task name ${workflowName} (declared in + '${config.name}') is also declared as a service name in '${this.serviceNameIndex[workflowName]}'`, + { + conflictingName: workflowName, + moduleA: config.name, + moduleB: this.serviceNameIndex[workflowName], + }) + } + + if (this.workflowNameIndex[workflowName]) { + throw new ConfigurationError(deline` + Task names must be unique - the task name ${workflowName} is declared multiple times (in + '${this.workflowNameIndex[workflowName]}' and '${config.name}')`, + { + taskName: workflowName, + moduleA: config.name, + moduleB: this.serviceNameIndex[workflowName], + }) + } + + } + + this.workflowNameIndex[workflowName] = config.name + + } + if (this.modulesScanned) { // need to re-run this if adding modules after initial scan await this.detectCircularDependencies() @@ -849,7 +1002,7 @@ export class Garden { /** * Get a handler for the specified module action. */ - public getModuleActionHandlers( + public getModuleActionHandlers( { actionType, moduleType, pluginName }: { actionType: T, moduleType: string, pluginName?: string }, ): ModuleActionHandlerMap { @@ -906,10 +1059,10 @@ export class Garden { /** * Get the last configured handler for the specified action. */ - public getModuleActionHandler( + public getModuleActionHandler( { actionType, moduleType, pluginName, defaultHandler }: - { actionType: T, moduleType: string, pluginName?: string, defaultHandler?: ModuleAndServiceActions[T] }, - ): ModuleAndServiceActions[T] { + { actionType: T, moduleType: string, pluginName?: string, defaultHandler?: ModuleAndRuntimeActions[T] }, + ): ModuleAndRuntimeActions[T] { const handlers = Object.values(this.getModuleActionHandlers({ actionType, moduleType, pluginName })) diff --git a/garden-service/src/plugins/container.ts b/garden-service/src/plugins/container.ts index 1a82dc7022..e2e0f21bb7 100644 --- a/garden-service/src/plugins/container.ts +++ b/garden-service/src/plugins/container.ts @@ -39,7 +39,12 @@ import { Service, ingressHostnameSchema } from "../types/service" import { DEFAULT_PORT_PROTOCOL } from "../constants" import { splitFirst } from "../util/util" import { keyBy } from "lodash" -import { genericTestSchema, GenericTestSpec } from "./generic" +import { + genericTestSchema, + GenericTestSpec, + GenericWorkflowSpec, + genericWorkflowSpecSchema, +} from "./generic" import { ModuleSpec, ModuleConfig } from "../config/module" import { BaseServiceSpec, ServiceConfig, baseServiceSchema } from "../config/service" @@ -255,13 +260,24 @@ export interface ContainerTestSpec extends GenericTestSpec { } export const containerTestSchema = genericTestSchema +export interface ContainerWorkflowSpec extends GenericWorkflowSpec { + command: string[], +} + +export const containerWorkflowSchema = genericWorkflowSpecSchema + .keys({ + command: Joi.array().items(Joi.string()) + .description("The command that the task should run inside the container."), + }) + export interface ContainerModuleSpec extends ModuleSpec { buildArgs: PrimitiveMap, image?: string, dockerfile?: string, + hotReload?: HotReloadConfigSpec, services: ContainerServiceSpec[], tests: ContainerTestSpec[], - hotReload?: HotReloadConfigSpec, + tasks: ContainerWorkflowSpec[], } export type ContainerModuleConfig = ModuleConfig @@ -281,6 +297,7 @@ export const containerModuleSpecSchema = Joi.object() Specify the image name for the container. Should be a valid Docker image identifier. If specified and the module does not contain a Dockerfile, this image will be used to deploy services for this module. If specified and the module does contain a Dockerfile, this identifier is used when pushing the built image.`), + hotReload: hotReloadConfigSchema, dockerfile: Joi.string().uri({ relativeOnly: true }) .description("POSIX-style name of Dockerfile, relative to project root. Defaults to $MODULE_ROOT/Dockerfile."), services: joiArray(serviceSchema) @@ -288,14 +305,19 @@ export const containerModuleSpecSchema = Joi.object() .description("The list of services to deploy from this container module."), tests: joiArray(containerTestSchema) .description("A list of tests to run in the module."), - hotReload: hotReloadConfigSchema, + // We use the user-facing term "tasks" as the key here, instead of "workflows". + tasks: joiArray(containerWorkflowSchema) + .description(deline` + A list of tasks that can be run from this container module. These can be used as dependencies for services + (executed before the service is deployed) or for other tasks. + `), }) .description("Configuration for a container module.") export interface ContainerModule< M extends ContainerModuleSpec = ContainerModuleSpec, S extends ContainerServiceSpec = ContainerServiceSpec, - T extends ContainerTestSpec = ContainerTestSpec, + T extends ContainerTestSpec = ContainerTestSpec > extends Module { } interface ParsedImageId { @@ -503,6 +525,13 @@ export async function validateContainerModule({ moduleConfig }: ValidateModulePa timeout: t.timeout, })) + moduleConfig.workflowConfigs = moduleConfig.spec.tasks.map(t => ({ + name: t.name, + dependencies: t.dependencies, + spec: t, + timeout: t.timeout, + })) + const hasDockerfile = await pathExists(helpers.getDockerfilePathFromConfig(moduleConfig)) if (moduleConfig.spec.dockerfile && !hasDockerfile) { diff --git a/garden-service/src/plugins/generic.ts b/garden-service/src/plugins/generic.ts index d2261332f2..faf277441f 100644 --- a/garden-service/src/plugins/generic.ts +++ b/garden-service/src/plugins/generic.ts @@ -22,13 +22,13 @@ import { BuildResult, BuildStatus, ValidateModuleResult, - TestResult, + TestResult, WorkflowStatus, RunWorkflowResult, } from "../types/plugin/outputs" import { BuildModuleParams, GetBuildStatusParams, ValidateModuleParams, - TestModuleParams, + TestModuleParams, RunWorkflowParams, } from "../types/plugin/params" import { BaseServiceSpec } from "../config/service" import { BaseTestSpec, baseTestSpecSchema } from "../config/test" @@ -36,6 +36,7 @@ import { readModuleVersionFile, writeModuleVersionFile, ModuleVersion } from ".. import { GARDEN_BUILD_VERSION_FILENAME } from "../constants" import { ModuleSpec, ModuleConfig } from "../config/module" import execa = require("execa") +import { BaseWorkflowSpec, baseWorkflowSpecSchema } from "../config/workflow" export const name = "generic" @@ -52,6 +53,17 @@ export const genericTestSchema = baseTestSpecSchema }) .description("The test specification of a generic module.") +export interface GenericWorkflowSpec extends BaseWorkflowSpec { + command: string[], +} + +export const genericWorkflowSpecSchema = baseWorkflowSpecSchema + .keys({ + command: Joi.array().items(Joi.string()) + .description("The command to run in the module build context."), + }) + .description("The task specification for a generic module.") + export interface GenericModuleSpec extends ModuleSpec { env: { [key: string]: string }, tests: GenericTestSpec[], @@ -155,12 +167,51 @@ export async function testGenericModule({ module, testConfig }: TestModuleParams } } +export async function runGenericWorkflow(params: RunWorkflowParams): Promise { + const { workflow } = params + const module = workflow.module + const command = workflow.spec.command + const startedAt = new Date() + + const result = { + moduleName: module.name, + workflowName: workflow.name, + command, + version: module.version, + success: true, + startedAt, + } + + if (command && command.length) { + const commandResult = await execa.shell( + command.join(" "), + { + cwd: module.buildPath, + env: { ...process.env, ...mapValues(module.spec.env, v => v.toString()) }, + }, + ) + + result["completedAt"] = new Date() + result["output"] = commandResult.stdout + commandResult.stderr + } else { + result["completedAt"] = startedAt + result["output"] = "" + } + + return { ...result } +} + +export async function getGenericWorkflowStatus(): Promise { + return { done: false } +} + export const genericPlugin: GardenPlugin = { moduleActions: { generic: { validate: parseGenericModule, getBuildStatus: getGenericModuleBuildStatus, build: buildGenericModule, + runWorkflow: runGenericWorkflow, testModule: testGenericModule, }, }, diff --git a/garden-service/src/plugins/kubernetes/actions.ts b/garden-service/src/plugins/kubernetes/actions.ts index d82c5a0f27..d5680fe29d 100644 --- a/garden-service/src/plugins/kubernetes/actions.ts +++ b/garden-service/src/plugins/kubernetes/actions.ts @@ -24,6 +24,7 @@ import { TestModuleParams, DeleteServiceParams, RunServiceParams, + RunWorkflowParams, } from "../../types/plugin/params" import { ModuleVersion } from "../../vcs/base" import { ContainerModule, helpers, validateContainerModule } from "../container" @@ -169,7 +170,9 @@ export async function hotReload( } export async function runModule( - { ctx, module, command, interactive, runtimeContext, timeout }: RunModuleParams, + { + ctx, module, command, ignoreError = true, interactive, runtimeContext, timeout, + }: RunModuleParams, ): Promise { const context = ctx.provider.config.context const namespace = await getAppNamespace(ctx, ctx.provider) @@ -202,7 +205,7 @@ export async function runModule( const startedAt = new Date() const res = await kubectl(context, namespace).call(kubecmd, { - ignoreError: true, + ignoreError, timeout, tty: interactive, }) @@ -211,10 +214,10 @@ export async function runModule( moduleName: module.name, command, version, - success: res.code === 0, startedAt, completedAt: new Date(), output: res.output, + success: res.code === 0, } } @@ -234,6 +237,28 @@ export async function runService( }) } +export async function runWorkflow( + { ctx, workflow, interactive, runtimeContext, logEntry, buildDependencies }: + RunWorkflowParams, +) { + const result = await runModule({ + ctx, + buildDependencies, + interactive, + logEntry, + runtimeContext, + module: workflow.module, + command: workflow.spec.command || [], + ignoreError: false, + timeout: workflow.spec.timeout || 9999, + }) + + return { + ...result, + workflowName: workflow.name, + } +} + export async function testModule( { ctx, interactive, module, runtimeContext, testConfig, logEntry, buildDependencies }: TestModuleParams, diff --git a/garden-service/src/plugins/kubernetes/helm.ts b/garden-service/src/plugins/kubernetes/helm.ts index 9670af6096..8f488c5f0d 100644 --- a/garden-service/src/plugins/kubernetes/helm.ts +++ b/garden-service/src/plugins/kubernetes/helm.ts @@ -22,7 +22,7 @@ import { validate, } from "../../config/common" import { Module } from "../../types/module" -import { ModuleAndServiceActions } from "../../types/plugin/plugin" +import { ModuleAndRuntimeActions } from "../../types/plugin/plugin" import { BuildModuleParams, DeployServiceParams, @@ -110,7 +110,7 @@ const helmStatusCodeMap: { [code: number]: ServiceState } = { 8: "deploying", // PENDING_ROLLBACK } -export const helmHandlers: Partial> = { +export const helmHandlers: Partial> = { async validate({ moduleConfig }: ValidateModuleParams): Promise { moduleConfig.spec = validate( moduleConfig.spec, diff --git a/garden-service/src/plugins/kubernetes/kubectl.ts b/garden-service/src/plugins/kubernetes/kubectl.ts index 76d47bc167..59d6f8ab28 100644 --- a/garden-service/src/plugins/kubernetes/kubectl.ts +++ b/garden-service/src/plugins/kubernetes/kubectl.ts @@ -52,25 +52,25 @@ export class Kubectl { } private prepareArgs(args: string[], { tty }: SpawnOpts) { - const ops: string[] = [] + const opts: string[] = [] if (this.namespace) { - ops.push(`--namespace=${this.namespace}`) + opts.push(`--namespace=${this.namespace}`) } if (this.context) { - ops.push(`--context=${this.context}`) + opts.push(`--context=${this.context}`) } if (this.configPath) { - ops.push(`--kubeconfig=${this.configPath}`) + opts.push(`--kubeconfig=${this.configPath}`) } if (tty) { - ops.push("--tty") + opts.push("--tty") } - return ops.concat(args) + return args.concat(opts) } } diff --git a/garden-service/src/plugins/kubernetes/kubernetes.ts b/garden-service/src/plugins/kubernetes/kubernetes.ts index 56af5b0759..a9f6171764 100644 --- a/garden-service/src/plugins/kubernetes/kubernetes.ts +++ b/garden-service/src/plugins/kubernetes/kubernetes.ts @@ -16,6 +16,7 @@ import { } from "../../config/common" import { GardenPlugin } from "../../types/plugin/plugin" import { Provider, providerConfigBaseSchema, ProviderConfig } from "../../config/project" +import { getGenericWorkflowStatus } from "../generic" import { deleteService, execInService, @@ -26,6 +27,7 @@ import { testModule, runModule, runService, + runWorkflow, } from "./actions" import { deployContainerService, getContainerServiceStatus, pushModule } from "./deployment" import { helmHandlers } from "./helm" @@ -171,6 +173,7 @@ export function gardenPlugin({ config }: { config: KubernetesConfig }): GardenPl container: { getServiceStatus: getContainerServiceStatus, deployService: deployContainerService, + getWorkflowStatus: getGenericWorkflowStatus, deleteService, getServiceOutputs, execInService, @@ -179,6 +182,7 @@ export function gardenPlugin({ config }: { config: KubernetesConfig }): GardenPl hotReload, testModule, runService, + runWorkflow, getTestResult, getServiceLogs, }, diff --git a/garden-service/src/plugins/local/local-google-cloud-functions.ts b/garden-service/src/plugins/local/local-google-cloud-functions.ts index abe6eb23bb..84f9c2ee15 100644 --- a/garden-service/src/plugins/local/local-google-cloud-functions.ts +++ b/garden-service/src/plugins/local/local-google-cloud-functions.ts @@ -102,6 +102,7 @@ export const gardenPlugin = (): GardenPlugin => ({ }, serviceConfigs, + workflowConfigs: [], testConfigs: parsed.testConfigs, } }, diff --git a/garden-service/src/tasks/base.ts b/garden-service/src/tasks/base.ts index 65ddf12dd8..084c0dd5c1 100644 --- a/garden-service/src/tasks/base.ts +++ b/garden-service/src/tasks/base.ts @@ -10,6 +10,7 @@ import { TaskResults } from "../task-graph" import { ModuleVersion } from "../vcs/base" import { v1 as uuidv1 } from "uuid" import { Garden } from "../garden" +import { DependencyGraphNodeType } from "../dependency-graph" export class TaskDefinitionError extends Error { } @@ -21,6 +22,7 @@ export interface TaskParams { export abstract class Task { abstract type: string + abstract depType: DependencyGraphNodeType garden: Garden id: string force: boolean diff --git a/garden-service/src/tasks/build.ts b/garden-service/src/tasks/build.ts index a0f31eb45a..64469beacb 100644 --- a/garden-service/src/tasks/build.ts +++ b/garden-service/src/tasks/build.ts @@ -12,30 +12,47 @@ import { Module } from "../types/module" import { BuildResult } from "../types/plugin/outputs" import { Task } from "../tasks/base" import { Garden } from "../garden" +import { DependencyGraphNodeType } from "../dependency-graph" +import { getHotReloadModuleNames } from "./helpers" export interface BuildTaskParams { garden: Garden module: Module force: boolean + fromWatch?: boolean + hotReloadServiceNames?: string[] } export class BuildTask extends Task { type = "build" + depType: DependencyGraphNodeType = "build" private module: Module + private fromWatch: boolean + private hotReloadServiceNames: string[] - constructor({ garden, force, module }: BuildTaskParams) { + constructor({ garden, module, force, fromWatch = false, hotReloadServiceNames = [] }: BuildTaskParams) { super({ garden, force, version: module.version }) this.module = module + this.fromWatch = fromWatch + this.hotReloadServiceNames = hotReloadServiceNames } async getDependencies(): Promise { - const deps = await this.garden.resolveModuleDependencies(this.module.build.dependencies, []) + const dg = await this.garden.getDependencyGraph() + const hotReloadModuleNames = await getHotReloadModuleNames(this.garden, this.hotReloadServiceNames) + + // We ignore build dependencies on modules with services deployed with hot reloading + const deps = (await dg.getDependencies(this.depType, this.getName(), false)).build + .filter(module => !hotReloadModuleNames.has(module.name)) + return Bluebird.map(deps, async (m: Module) => { return new BuildTask({ garden: this.garden, module: m, force: this.force, + fromWatch: this.fromWatch, + hotReloadServiceNames: this.hotReloadServiceNames, }) }) } diff --git a/garden-service/src/tasks/deploy.ts b/garden-service/src/tasks/deploy.ts index 9f5126e9f4..a9756bb672 100644 --- a/garden-service/src/tasks/deploy.ts +++ b/garden-service/src/tasks/deploy.ts @@ -18,6 +18,9 @@ import { } from "../types/service" import { Garden } from "../garden" import { PushTask } from "./push" +import { WorkflowTask } from "./workflow" +import { DependencyGraphNodeType } from "../dependency-graph" +// import { BuildTask } from "./build" export interface DeployTaskParams { garden: Garden @@ -25,51 +28,77 @@ export interface DeployTaskParams { force: boolean forceBuild: boolean logEntry?: LogEntry - //TODO: Move watch and hotReloadServiceNames to a new commandContext object? - watch?: boolean + fromWatch?: boolean hotReloadServiceNames?: string[] } export class DeployTask extends Task { type = "deploy" + depType: DependencyGraphNodeType = "service" private service: Service private forceBuild: boolean private logEntry?: LogEntry - private watch: boolean - private hotReloadServiceNames?: string[] + private fromWatch: boolean + private hotReloadServiceNames: string[] - constructor({ garden, service, force, forceBuild, logEntry, watch, hotReloadServiceNames }: DeployTaskParams) { + constructor( + { garden, service, force, forceBuild, logEntry, fromWatch = false, hotReloadServiceNames = [] }: DeployTaskParams, + ) { super({ garden, force, version: service.module.version }) this.service = service this.forceBuild = forceBuild this.logEntry = logEntry - this.watch = !!watch + this.fromWatch = fromWatch this.hotReloadServiceNames = hotReloadServiceNames } async getDependencies() { - const servicesToDeploy = (await this.garden.getServices(this.service.config.dependencies)) - .filter(s => !includes(this.hotReloadServiceNames, s.name)) - const deps: Task[] = await Bluebird.map(servicesToDeploy, async (service) => { + const dg = await this.garden.getDependencyGraph() + + // We filter out service dependencies on services configured for hot reloading (if any) + const deps = await dg.getDependencies(this.depType, this.getName(), false, + (depNode) => !(depNode.type === this.depType && includes(this.hotReloadServiceNames, depNode.name))) + + const deployTasks = await Bluebird.map(deps.service, async (service) => { return new DeployTask({ garden: this.garden, service, force: false, forceBuild: this.forceBuild, - watch: this.watch, + fromWatch: this.fromWatch, hotReloadServiceNames: this.hotReloadServiceNames, }) }) - deps.push(new PushTask({ - garden: this.garden, - module: this.service.module, - forceBuild: this.forceBuild, - })) + if (this.fromWatch && includes(this.hotReloadServiceNames, this.service.name)) { + return deployTasks + } else { + const workflowTasks = deps.workflow.map(workflow => { + return new WorkflowTask({ + workflow, + garden: this.garden, + force: false, + forceBuild: this.forceBuild, + }) + }) - return deps + // const buildTask = new BuildTask({ + // garden: this.garden, module: this.service.module, force: true + // }) + + const pushTask = new PushTask({ + garden: this.garden, + module: this.service.module, + force: this.forceBuild, + fromWatch: this.fromWatch, + hotReloadServiceNames: this.hotReloadServiceNames, + }) + + // return [ ...deployTasks, ...workflowTasks, buildTask] + return [...deployTasks, ...workflowTasks, pushTask] + } } protected getName() { diff --git a/garden-service/src/tasks/helpers.ts b/garden-service/src/tasks/helpers.ts index 498659e50c..cb4b9d79d5 100644 --- a/garden-service/src/tasks/helpers.ts +++ b/garden-service/src/tasks/helpers.ts @@ -6,64 +6,72 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import * as Bluebird from "bluebird" -import { flatten } from "lodash" -import { computeAutoReloadDependants, withDependants } from "../watch" +import { flatten, intersection } from "lodash" import { DeployTask } from "./deploy" -import { getNames } from "../util/util" +import { BuildTask } from "./build" +import { WorkflowTask } from "./workflow" import { Garden } from "../garden" import { Module } from "../types/module" +import { Service } from "../types/service" +import { Workflow } from "../types/workflow" +import { DependencyGraphNode } from "../dependency-graph" -/** - * @param hotReloadServiceNames - names of services with hot reloading enabled (should not be redeployed) - */ -export async function getTasksForHotReload( - { garden, module, hotReloadServiceNames, serviceNames }: - { garden: Garden, module: Module, hotReloadServiceNames: string[], serviceNames: string[] }, +export async function getTasksForModule( + { garden, module, hotReloadServiceNames, force = false, forceBuild = false, + fromWatch = false, includeDependants = false }: + { + garden: Garden, module: Module, hotReloadServiceNames: string[], force?: boolean, forceBuild?: boolean, + fromWatch?: boolean, includeDependants?: boolean, + }, ) { - const hotReloadModuleNames = await getHotReloadModuleNames(garden, hotReloadServiceNames) + let buildTasks: BuildTask[] = [] + let dependantBuildModules: Module[] = [] + let services: Service[] = [] + let workflows: Workflow[] = [] - const modulesForDeployment = (await withDependants(garden, [module], - await computeAutoReloadDependants(garden))) - .filter(m => !hotReloadModuleNames.has(m.name)) + if (!includeDependants) { + buildTasks.push(new BuildTask({ garden, module, force: true, fromWatch, hotReloadServiceNames })) + services = module.services + workflows = module.workflows + } else { + const hotReloadModuleNames = await getHotReloadModuleNames(garden, hotReloadServiceNames) + const dg = await garden.getDependencyGraph() - return (await servicesForModules(garden, modulesForDeployment, serviceNames)) - .map(service => new DeployTask({ - garden, service, force: true, forceBuild: true, watch: true, hotReloadServiceNames, - })) + const dependantFilterFn = (dependantNode: DependencyGraphNode) => { + return !hotReloadModuleNames.has(dependantNode.moduleName) + } -} + if (intersection(module.serviceNames, hotReloadServiceNames).length) { + // Hot reloading is enabled for one or more of module's services. + const serviceDeps = await dg.getDependantsForMany("service", module.serviceNames, true, dependantFilterFn) -export async function getHotReloadModuleNames(garden: Garden, hotReloadServiceNames: string[]): Promise> { - return new Set(flatten((await garden.getServices(hotReloadServiceNames || [])) - .map(s => s.module.name))) -} + dependantBuildModules = serviceDeps.build + services = serviceDeps.service + workflows = serviceDeps.workflow + } else { + const dependants = await dg.getDependantsForModule(module, dependantFilterFn) + buildTasks.push(new BuildTask({ garden, module, force: true, fromWatch, hotReloadServiceNames })) + dependantBuildModules = dependants.build + services = module.services.concat(dependants.service) + workflows = module.workflows.concat(dependants.workflow) + } + } -export async function getDeployTasks( - { garden, module, serviceNames, hotReloadServiceNames, force = false, forceBuild = false, - watch = false, includeDependants = false }: - { - garden: Garden, module: Module, serviceNames?: string[], hotReloadServiceNames: string[], - force?: boolean, forceBuild?: boolean, watch?: boolean, includeDependants?: boolean, - }, -) { + buildTasks.push(...dependantBuildModules + .map(m => new BuildTask({ garden, module: m, force: forceBuild, fromWatch, hotReloadServiceNames }))) - const modulesForDeployment = includeDependants - ? (await withDependants(garden, [module], await computeAutoReloadDependants(garden))) - : [module] + const deployTasks = services + .map(service => new DeployTask({ garden, service, force, forceBuild, fromWatch, hotReloadServiceNames })) - return (await servicesForModules(garden, modulesForDeployment, serviceNames)) - .map(service => new DeployTask({ garden, service, force, forceBuild, watch, hotReloadServiceNames })) + const workflowTasks = workflows + .map(workflow => new WorkflowTask({ garden, workflow, force, forceBuild })) -} + return [...buildTasks, ...deployTasks, ...workflowTasks] -async function servicesForModules(garden: Garden, modules: Module[], serviceNames?: string[]) { - const moduleServices = flatten(await Bluebird.map( - modules, - m => garden.getServices(getNames(m.serviceConfigs)))) +} - return serviceNames - ? moduleServices.filter(s => serviceNames.includes(s.name)) - : moduleServices +export async function getHotReloadModuleNames(garden: Garden, hotReloadServiceNames: string[]): Promise> { + return new Set(flatten((await garden.getServices(hotReloadServiceNames || [])) + .map(s => s.module.name))) } diff --git a/garden-service/src/tasks/publish.ts b/garden-service/src/tasks/publish.ts index 87652546e5..d76882c4bd 100644 --- a/garden-service/src/tasks/publish.ts +++ b/garden-service/src/tasks/publish.ts @@ -12,6 +12,7 @@ import { Module } from "../types/module" import { PublishResult } from "../types/plugin/outputs" import { Task } from "../tasks/base" import { Garden } from "../garden" +import { DependencyGraphNodeType } from "../dependency-graph" export interface PublishTaskParams { garden: Garden @@ -21,6 +22,7 @@ export interface PublishTaskParams { export class PublishTask extends Task { type = "publish" + depType: DependencyGraphNodeType = "publish" private module: Module private forceBuild: boolean diff --git a/garden-service/src/tasks/push.ts b/garden-service/src/tasks/push.ts index a96c0d2fb8..356c581688 100644 --- a/garden-service/src/tasks/push.ts +++ b/garden-service/src/tasks/push.ts @@ -12,30 +12,40 @@ import { Module } from "../types/module" import { PushResult } from "../types/plugin/outputs" import { Task } from "../tasks/base" import { Garden } from "../garden" +import { DependencyGraphNodeType } from "../dependency-graph" export interface PushTaskParams { garden: Garden module: Module - forceBuild: boolean + force: boolean + fromWatch?: boolean + hotReloadServiceNames?: string[] } export class PushTask extends Task { type = "push" + depType: DependencyGraphNodeType = "push" + force: boolean private module: Module - private forceBuild: boolean + private fromWatch: boolean + private hotReloadServiceNames: string[] - constructor({ garden, module, forceBuild }: PushTaskParams) { + constructor({ garden, module, force, fromWatch = false, hotReloadServiceNames = [] }: PushTaskParams) { super({ garden, version: module.version }) this.module = module - this.forceBuild = forceBuild + this.force = force + this.fromWatch = fromWatch + this.hotReloadServiceNames = hotReloadServiceNames } async getDependencies() { return [new BuildTask({ garden: this.garden, module: this.module, - force: this.forceBuild, + force: this.force, + fromWatch: this.fromWatch, + hotReloadServiceNames: this.hotReloadServiceNames, })] } diff --git a/garden-service/src/tasks/test.ts b/garden-service/src/tasks/test.ts index 8a587c8969..141c506620 100644 --- a/garden-service/src/tasks/test.ts +++ b/garden-service/src/tasks/test.ts @@ -17,6 +17,7 @@ import { TestResult } from "../types/plugin/outputs" import { Task, TaskParams } from "../tasks/base" import { prepareRuntimeContext } from "../types/service" import { Garden } from "../garden" +import { DependencyGraphNodeType } from "../dependency-graph" class TestError extends Error { toString() { @@ -34,6 +35,7 @@ export interface TestTaskParams { export class TestTask extends Task { type = "test" + depType: DependencyGraphNodeType = "test" private module: Module private testConfig: TestConfig @@ -60,7 +62,8 @@ export class TestTask extends Task { return [] } - const services = await this.garden.getServices(this.testConfig.dependencies) + const dg = await this.garden.getDependencyGraph() + const services = (await dg.getDependencies(this.depType, this.getName(), false)).service const deps: Task[] = [new BuildTask({ garden: this.garden, @@ -176,6 +179,6 @@ async function getTestDependencies(garden: Garden, testConfig: TestConfig) { * Determine the version of the test run, based on the version of the module and each of its dependencies. */ async function getTestVersion(garden: Garden, module: Module, testConfig: TestConfig): Promise { - const moduleDeps = await garden.resolveModuleDependencies(module.build.dependencies, testConfig.dependencies) + const moduleDeps = await garden.resolveDependencyModules(module.build.dependencies, testConfig.dependencies) return garden.resolveVersion(module.name, moduleDeps) } diff --git a/garden-service/src/tasks/workflow.ts b/garden-service/src/tasks/workflow.ts new file mode 100644 index 0000000000..872c85520b --- /dev/null +++ b/garden-service/src/tasks/workflow.ts @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import chalk from "chalk" +import { Task } from "../tasks/base" +import { Garden } from "../garden" +import { Workflow } from "../types/workflow" +import { BuildTask } from "./build" +import { DeployTask } from "./deploy" +import { LogEntry } from "../logger/log-entry" +import { RunWorkflowResult } from "../types/plugin/outputs" +import { prepareRuntimeContext } from "../types/service" +import { DependencyGraphNodeType } from "../dependency-graph" + +export interface WorkflowTaskParams { + garden: Garden + workflow: Workflow + force: boolean + forceBuild: boolean + logEntry?: LogEntry +} + +export class WorkflowTask extends Task { + type = "workflow" + depType: DependencyGraphNodeType = "workflow" + + private workflow: Workflow + private forceBuild: boolean + + constructor({ garden, workflow, force, forceBuild }: WorkflowTaskParams) { + super({ garden, force, version: workflow.module.version }) + this.workflow = workflow + this.forceBuild = forceBuild + } + + async getDependencies(): Promise { + + const buildTask = new BuildTask({ + garden: this.garden, + module: this.workflow.module, + force: this.forceBuild, + }) + + const dg = await this.garden.getDependencyGraph() + const deps = await dg.getDependencies(this.depType, this.getName(), false) + + const deployTasks = deps.service.map(service => { + return new DeployTask({ + service, + garden: this.garden, + force: false, + forceBuild: false, + }) + }) + + const workflowTasks = deps.workflow.map(workflow => { + return new WorkflowTask({ + workflow, + garden: this.garden, + force: false, + forceBuild: false, + }) + }) + + return [buildTask, ...deployTasks, ...workflowTasks] + + } + + protected getName() { + return this.workflow.name + } + + getDescription() { + return `running task ${this.workflow.name} in module ${this.workflow.module.name}` + } + + async process() { + + const workflow = this.workflow + const module = workflow.module + + // combine all dependencies for all services in the module, to be sure we have all the context we need + const dg = await this.garden.getDependencyGraph() + const serviceDeps = (await dg.getDependencies(this.depType, this.getName(), false)).service + const runtimeContext = await prepareRuntimeContext(this.garden, module, serviceDeps) + + const logEntry = this.garden.log.info({ + section: workflow.name, + msg: "Running", + status: "active", + }) + + let result: RunWorkflowResult + try { + result = await this.garden.actions.runWorkflow({ + workflow, + logEntry, + runtimeContext, + interactive: false, + }) + } catch (err) { + logEntry.setError() + throw err + } + + logEntry.setSuccess({ msg: chalk.green(`Done (took ${logEntry.getDuration(1)} sec)`), append: true }) + + return result + + } + +} diff --git a/garden-service/src/types/module.ts b/garden-service/src/types/module.ts index b06c70f98b..4078d0f7bd 100644 --- a/garden-service/src/types/module.ts +++ b/garden-service/src/types/module.ts @@ -11,6 +11,8 @@ import { getNames } from "../util/util" import { TestSpec } from "../config/test" import { ModuleSpec, ModuleConfig, moduleConfigSchema } from "../config/module" import { ServiceSpec } from "../config/service" +import { Workflow, workflowFromConfig } from "./workflow" +import { WorkflowSpec, workflowSchema } from "../config/workflow" import { ModuleVersion, moduleVersionSchema } from "../vcs/base" import { pathToCacheContext } from "../cache" import { Garden } from "../garden" @@ -27,15 +29,20 @@ export interface Module< M extends ModuleSpec = any, S extends ServiceSpec = any, T extends TestSpec = any, - > extends ModuleConfig { + W extends WorkflowSpec = any, + > extends ModuleConfig { buildPath: string version: ModuleVersion - services: Service>[] + services: Service>[] serviceNames: string[] serviceDependencyNames: string[] - _ConfigType: ModuleConfig + workflows: Workflow>[] + workflowNames: string[] + workflowDependencyNames: string[] + + _ConfigType: ModuleConfig } export const moduleSchema = moduleConfigSchema @@ -54,7 +61,16 @@ export const moduleSchema = moduleConfigSchema .description("The names of the services that the module provides."), serviceDependencyNames: joiArray(joiIdentifier()) .required() - .description("The names of all the services that the services in this module depend on."), + .description("The names of all the services and tasks that the services in this module depend on."), + workflows: joiArray(Joi.lazy(() => workflowSchema)) + .required() + .description("A list of all the tasks that the module provides."), + workflowNames: joiArray(joiIdentifier()) + .required() + .description("The names of the tasks that the module provides."), + workflowDependencyNames: joiArray(joiIdentifier()) + .required() + .description("The names of all the tasks and services that the tasks in this module depend on."), }) export interface ModuleMap { @@ -78,11 +94,19 @@ export async function moduleFromConfig(garden: Garden, config: ModuleConfig): Pr .map(serviceConfig => serviceConfig.dependencies) .filter(deps => !!deps))), + workflows: [], + workflowNames: getNames(config.workflowConfigs), + workflowDependencyNames: uniq(flatten(config.workflowConfigs + .map(workflowConfig => workflowConfig.dependencies) + .filter(deps => !!deps))), + _ConfigType: config, } module.services = config.serviceConfigs.map(serviceConfig => serviceFromConfig(module, serviceConfig)) + module.workflows = config.workflowConfigs.map(workflowConfig => workflowFromConfig(module, workflowConfig)) + return module } diff --git a/garden-service/src/types/plugin/outputs.ts b/garden-service/src/types/plugin/outputs.ts index ac33a18ba4..679c78ea4b 100644 --- a/garden-service/src/types/plugin/outputs.ts +++ b/garden-service/src/types/plugin/outputs.ts @@ -137,7 +137,12 @@ export const moduleTypeDescriptionSchema = Joi.object() }) export type ValidateModuleResult = - ModuleConfig + ModuleConfig< + T["spec"], + T["serviceConfigs"][0]["spec"], + T["testConfigs"][0]["spec"], + T["workflowConfigs"][0]["spec"] + > export const validateModuleResultSchema = moduleConfigSchema @@ -156,7 +161,7 @@ export const buildModuleResultSchema = Joi.object() fetched: Joi.boolean() .description("Set to true if the build was fetched from a remote registry."), fresh: Joi.boolean() - .description("Set to true if the build was perfomed, false if it was already built, or fetched from a registry"), + .description("Set to true if the build was performed, false if it was already built, or fetched from a registry"), version: Joi.string() .description("The version that was built."), details: Joi.object() @@ -249,6 +254,53 @@ export const buildStatusSchema = Joi.object() .description("Whether an up-to-date build is ready for the module."), }) +export interface RunWorkflowResult extends RunResult { + moduleName: string + workflowName: string + command: string[] + version: ModuleVersion + success: boolean + startedAt: Date + completedAt: Date + output: string +} + +export const runWorkflowResultSchema = Joi.object() + .keys({ + moduleName: Joi.string() + .description("The name of the module that the task belongs to."), + workflowName: Joi.string() + .description("The name of the task that was run."), + command: Joi.array().items(Joi.string()) + .required() + .description("The command that the task ran in the module."), + version: moduleVersionSchema, + success: Joi.boolean() + .required() + .description("Whether the task was successfully run."), + startedAt: Joi.date() + .required() + .description("When the task run was started."), + completedAt: Joi.date() + .required() + .description("When the task run was completed."), + output: Joi.string() + .required() + .allow("") + .description("The output log from the run."), + }) + +export interface WorkflowStatus { + done: boolean +} + +export const workflowStatusSchema = Joi.object() + .keys({ + done: Joi.boolean() + .required() + .description("Whether the task has been successfully executed for the module's current version."), + }) + export interface PluginActionOutputs { getEnvironmentStatus: Promise prepareEnvironment: Promise @@ -269,6 +321,11 @@ export interface ServiceActionOutputs { runService: Promise } +export interface WorkflowActionOutputs { + getWorkflowStatus: Promise + runWorkflow: Promise +} + export interface ModuleActionOutputs extends ServiceActionOutputs { describeType: Promise validate: Promise diff --git a/garden-service/src/types/plugin/params.ts b/garden-service/src/types/plugin/params.ts index 9b1f8c3053..5048da07ba 100644 --- a/garden-service/src/types/plugin/params.ts +++ b/garden-service/src/types/plugin/params.ts @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import * as Joi from "joi" import Stream from "ts-stream" import { LogEntry } from "../../logger/log-entry" import { PluginContext, pluginContextSchema } from "../../plugin-context" @@ -13,10 +14,11 @@ import { ModuleVersion, moduleVersionSchema } from "../../vcs/base" import { Primitive, joiPrimitive, joiArray, joiIdentifierMap } from "../../config/common" import { Module, moduleSchema } from "../module" import { RuntimeContext, Service, serviceSchema, runtimeContextSchema } from "../service" +import { Workflow } from "../workflow" import { EnvironmentStatus, ServiceLogEntry, environmentStatusSchema } from "./outputs" -import * as Joi from "joi" import { moduleConfigSchema } from "../../config/module" import { testConfigSchema } from "../../config/test" +import { workflowSchema } from "../../config/workflow" export interface PluginActionContextParams { ctx: PluginContext @@ -59,6 +61,14 @@ const serviceActionParamsSchema = moduleActionParamsSchema service: serviceSchema, }) +export interface PluginWorkflowActionParamsBase extends PluginModuleActionParamsBase { + workflow: Workflow +} +const workflowActionParamsSchema = moduleActionParamsSchema + .keys({ + workflow: workflowSchema, + }) + /** * Plugin actions */ @@ -157,6 +167,7 @@ export interface RunModuleParams extends PluginModule command: string[] interactive: boolean runtimeContext: RuntimeContext + ignoreError?: boolean timeout?: number } const runBaseParams = { @@ -277,6 +288,20 @@ export interface RunServiceParams extends PluginServi export const runServiceParamsSchema = serviceActionParamsSchema .keys(runBaseParams) +/** + * Workflow actions + */ +export interface GetWorkflowStatusParams extends PluginWorkflowActionParamsBase { } +export const getWorkflowStatusParamsSchema = workflowActionParamsSchema + +export interface RunWorkflowParams extends PluginWorkflowActionParamsBase { + interactive: boolean + runtimeContext: RuntimeContext + timeout?: number +} +export const runWorkflowParamsSchema = workflowActionParamsSchema + .keys(runBaseParams) + export interface ServiceActionParams { getServiceStatus: GetServiceStatusParams deployService: DeployServiceParams @@ -287,6 +312,11 @@ export interface ServiceActionParams { runService: RunServiceParams } +export interface WorkflowActionParams { + getWorkflowStatus: GetWorkflowStatusParams + runWorkflow: RunWorkflowParams +} + export interface ModuleActionParams { describeType: DescribeModuleTypeParams, validate: ValidateModuleParams diff --git a/garden-service/src/types/plugin/plugin.ts b/garden-service/src/types/plugin/plugin.ts index cc1efb3855..0a346cbb1e 100644 --- a/garden-service/src/types/plugin/plugin.ts +++ b/garden-service/src/types/plugin/plugin.ts @@ -20,8 +20,13 @@ import { serviceOutputsSchema } from "../../config/service" import { LogNode } from "../../logger/log-node" import { Provider } from "../../config/project" import { + ModuleActionParams, + PluginActionParams, + ServiceActionParams, + WorkflowActionParams, prepareEnvironmentParamsSchema, cleanupEnvironmentParamsSchema, + getEnvironmentStatusParamsSchema, getSecretParamsSchema, setSecretParamsSchema, deleteSecretParamsSchema, @@ -41,7 +46,7 @@ import { runModuleParamsSchema, testModuleParamsSchema, getTestResultParamsSchema, - publishModuleParamsSchema, + publishModuleParamsSchema, getWorkflowStatusParamsSchema, runWorkflowParamsSchema, } from "./params" import { buildModuleResultSchema, @@ -61,17 +66,12 @@ import { hotReloadResultSchema, runResultSchema, ServiceActionOutputs, + WorkflowActionOutputs, setSecretResultSchema, testResultSchema, validateModuleResultSchema, - publishModuleResultSchema, + publishModuleResultSchema, workflowStatusSchema, runWorkflowResultSchema, } from "./outputs" -import { - ModuleActionParams, - PluginActionParams, - ServiceActionParams, - getEnvironmentStatusParamsSchema, -} from "./params" export type PluginActions = { [P in keyof PluginActionParams]: (params: PluginActionParams[P]) => PluginActionOutputs[P] @@ -81,14 +81,20 @@ export type ServiceActions = { [P in keyof ServiceActionParams]: (params: ServiceActionParams[P]) => ServiceActionOutputs[P] } +export type WorkflowActions = { + [P in keyof WorkflowActionParams]: (params: WorkflowActionParams[P]) => WorkflowActionOutputs[P] +} + export type ModuleActions = { [P in keyof ModuleActionParams]: (params: ModuleActionParams[P]) => ModuleActionOutputs[P] } -export type ModuleAndServiceActions = ModuleActions & ServiceActions +export type ModuleAndRuntimeActions = + ModuleActions & ServiceActions & WorkflowActions export type PluginActionName = keyof PluginActions export type ServiceActionName = keyof ServiceActions +export type WorkflowActionName = keyof WorkflowActions export type ModuleActionName = keyof ModuleActions export interface PluginActionDescription { @@ -235,7 +241,28 @@ export const serviceActionDescriptions: { [P in ServiceActionName]: PluginAction }, } -export const moduleActionDescriptions: { [P in ModuleActionName | ServiceActionName]: PluginActionDescription } = { +export const workflowActionDescriptions: { [P in WorkflowActionName]: PluginActionDescription } = { + getWorkflowStatus: { + description: dedent` + Check and return the execution status of a task, i.e. whether the task has been successfully + completed for the module's current version. + `, + paramsSchema: getWorkflowStatusParamsSchema, + resultSchema: workflowStatusSchema, + }, + runWorkflow: { + description: dedent` + Runs a task within the context of its module. This should wait until execution completes, and + should ideally attach it to the terminal (i.e. pipe the output from the task to the console, + as well as pipe input from the console to the running task). + `, + paramsSchema: runWorkflowParamsSchema, + resultSchema: runWorkflowResultSchema, + }, +} + +export const moduleActionDescriptions: + { [P in ModuleActionName | ServiceActionName | WorkflowActionName]: PluginActionDescription } = { // TODO: implement this method (it is currently not defined or used) describeType: { description: dedent` @@ -369,10 +396,11 @@ export const moduleActionDescriptions: { [P in ModuleActionName | ServiceActionN }, ...serviceActionDescriptions, + + ...workflowActionDescriptions, } export const pluginActionNames: PluginActionName[] = Object.keys(pluginActionDescriptions) -export const serviceActionNames: ServiceActionName[] = Object.keys(serviceActionDescriptions) export const moduleActionNames: ModuleActionName[] = Object.keys(moduleActionDescriptions) export interface GardenPlugin { @@ -382,7 +410,7 @@ export interface GardenPlugin { modules?: string[] actions?: Partial - moduleActions?: { [moduleType: string]: Partial } + moduleActions?: { [moduleType: string]: Partial } } export interface PluginFactoryParams { diff --git a/garden-service/src/types/workflow.ts b/garden-service/src/types/workflow.ts new file mode 100644 index 0000000000..32babe9540 --- /dev/null +++ b/garden-service/src/types/workflow.ts @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Module } from "./module" +import { WorkflowConfig } from "../config/workflow" + +export interface Workflow { + name: string + module: M + config: M["workflowConfigs"][0] + spec: M["workflowConfigs"][0]["spec"] +} + +export function workflowFromConfig(module: M, config: WorkflowConfig): Workflow { + return { + name: config.name, + module, + config, + spec: config.spec, + } +} diff --git a/garden-service/src/util/detectCycles.ts b/garden-service/src/util/detectCycles.ts index b7c0846498..35bab7401d 100644 --- a/garden-service/src/util/detectCycles.ts +++ b/garden-service/src/util/detectCycles.ts @@ -6,12 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import dedent = require("dedent") import { get, isEqual, join, set, uniqWith } from "lodash" import { Module, getModuleKey } from "../types/module" import { ConfigurationError, } from "../exceptions" import { Service } from "../types/service" +import { Workflow } from "../types/workflow" export type Cycle = string[] @@ -22,10 +24,10 @@ export type Cycle = string[] Throws an error if cycles were found. */ -export async function detectCircularDependencies(modules: Module[], services: Service[]) { +export async function detectCircularDependencies(modules: Module[], services: Service[], workflows: Workflow[]) { // Sparse matrices const buildGraph = {} - const serviceGraph = {} + const runtimeGraph = {} /* There's no need to account for test dependencies here, since any circularities there @@ -38,30 +40,47 @@ export async function detectCircularDependencies(modules: Module[], services: Se set(buildGraph, [module.name, depName], { distance: 1, next: depName }) } - // Service dependencies + // Runtime (service & workflow) dependencies for (const service of module.serviceConfigs || []) { for (const depName of service.dependencies) { - set(serviceGraph, [service.name, depName], { distance: 1, next: depName }) + set(runtimeGraph, [service.name, depName], { distance: 1, next: depName }) + } + } + + for (const workflow of module.workflowConfigs || []) { + for (const depName of workflow.dependencies) { + set(runtimeGraph, [workflow.name, depName], { distance: 1, next: depName }) } } } const serviceNames = services.map(s => s.name) + const workflowNames = workflows.map(w => w.name) const buildCycles = detectCycles(buildGraph, modules.map(m => m.name)) - const serviceCycles = detectCycles(serviceGraph, serviceNames) + const runtimeCycles = detectCycles(runtimeGraph, serviceNames.concat(workflowNames)) - if (buildCycles.length > 0 || serviceCycles.length > 0) { + if (buildCycles.length > 0 || runtimeCycles.length > 0) { const detail = {} + let errMsg = "Circular dependencies detected." + if (buildCycles.length > 0) { - detail["circular-build-dependencies"] = cyclesToString(buildCycles) + const buildCyclesDescription = cyclesToString(buildCycles) + errMsg = errMsg.concat("\n\n" + dedent` + Circular build dependencies: ${buildCyclesDescription} + `) + detail["circular-build-dependencies"] = buildCyclesDescription } - if (serviceCycles.length > 0) { - detail["circular-service-dependencies"] = cyclesToString(serviceCycles) + if (runtimeCycles.length > 0) { + const runtimeCyclesDescription = cyclesToString(runtimeCycles) + errMsg = errMsg.concat("\n\n" + dedent` + Circular service/task dependencies: ${runtimeCyclesDescription} + `) + detail["circular-service-or-task-dependencies"] = runtimeCyclesDescription } - throw new ConfigurationError("Circular dependencies detected", detail) + throw new ConfigurationError(errMsg, detail) } } diff --git a/garden-service/src/util/util.ts b/garden-service/src/util/util.ts index 803a7dabb3..7944fb2529 100644 --- a/garden-service/src/util/util.ts +++ b/garden-service/src/util/util.ts @@ -15,7 +15,7 @@ import * as Cryo from "cryo" import * as _spawn from "cross-spawn" import { pathExists, readFile, writeFile } from "fs-extra" import { join, basename, win32, posix } from "path" -import { find, pick, difference, fromPairs } from "lodash" +import { find, pick, difference, fromPairs, uniqBy } from "lodash" import { TimeoutError, ParameterError, RuntimeError, GardenError } from "../exceptions" import { isArray, isPlainObject, extend, mapValues, pickBy } from "lodash" import highlight from "cli-highlight" @@ -335,10 +335,14 @@ export function getNames(array: T[]) { return array.map(v => v.name) } -export function findByName(array: T[], name: string): T | undefined { +export function findByName(array: T[], name: string): T | undefined { return find(array, ["name", name]) } +export function uniqByName(array: T[]): T[] { + return uniqBy(array, item => item.name) +} + /** * Converts a Windows-style path to a cygwin style path (e.g. C:\some\folder -> /cygdrive/c/some/folder). */ @@ -378,6 +382,15 @@ export function pickKeys(obj: T, keys: U[], return picked } +export function throwOnMissingNames(names: string[], entries: T[], description: string) { + const available = getNames(entries) + const missing = difference(names, available) + + if (missing.length) { + throw new ParameterError(`Could not find ${description}(s): ${missing.join(", ")}`, { available, missing }) + } +} + export function hashString(s: string, length: number) { const urlHash = createHash("sha256") urlHash.update(s) diff --git a/garden-service/src/watch.ts b/garden-service/src/watch.ts index d8aea84139..bf9671ed29 100644 --- a/garden-service/src/watch.ts +++ b/garden-service/src/watch.ts @@ -7,68 +7,15 @@ */ import { watch } from "chokidar" -import { - mapValues, - set, - uniq, - values, -} from "lodash" import { basename, parse, relative } from "path" import { pathToCacheContext } from "./cache" -import { Module, getModuleKey } from "./types/module" +import { Module } from "./types/module" import { getIgnorer, scanDirectory } from "./util/util" import { MODULE_CONFIG_FILENAME } from "./constants" import { Garden } from "./garden" -export type AutoReloadDependants = { [key: string]: Module[] } export type ChangeHandler = (module: Module | null, configChanged: boolean) => Promise -/* - Resolves to modules and their build & service dependant modules (recursively). - Each module is represented at most once in the output. -*/ -export async function withDependants( - garden: Garden, - modules: Module[], - autoReloadDependants: AutoReloadDependants, -): Promise { - const moduleSet = new Set() - - const scanner = (module: Module) => { - moduleSet.add(module.name) - for (const dependant of (autoReloadDependants[module.name] || [])) { - if (!moduleSet.has(dependant.name)) { - scanner(dependant) - } - } - } - for (const m of modules) { - scanner(m) - } - - // we retrieve the modules again to be sure we have the latest versions - return garden.getModules(Array.from(moduleSet)) -} - -export async function computeAutoReloadDependants(garden: Garden): Promise { - const dependants = {} - - for (const module of await garden.getModules()) { - const depModules: Module[] = await uniqueDependencyModules(garden, module) - for (const dep of depModules) { - set(dependants, [dep.name, module.name], module) - } - } - - return mapValues(dependants, values) -} - -async function uniqueDependencyModules(garden: Garden, module: Module): Promise { - const buildDeps = module.build.dependencies.map(d => getModuleKey(d.name, d.plugin)) - const serviceDeps = (await garden.getServices(module.serviceDependencyNames)).map(s => s.module.name) - return garden.getModules(uniq(buildDeps.concat(serviceDeps))) -} - export class FSWatcher { private watcher diff --git a/garden-service/test/data/test-project-a/module-a/garden.yml b/garden-service/test/data/test-project-a/module-a/garden.yml index e1d3a70ed8..0fa0bda628 100644 --- a/garden-service/test/data/test-project-a/module-a/garden.yml +++ b/garden-service/test/data/test-project-a/module-a/garden.yml @@ -8,3 +8,6 @@ module: tests: - name: unit command: [echo, OK] + tasks: + - name: workflow-a + command: [echo, OK] \ No newline at end of file diff --git a/garden-service/test/data/test-project-a/module-b/garden.yml b/garden-service/test/data/test-project-a/module-b/garden.yml index ffcb1af832..7c0d20a5d5 100644 --- a/garden-service/test/data/test-project-a/module-b/garden.yml +++ b/garden-service/test/data/test-project-a/module-b/garden.yml @@ -12,3 +12,6 @@ module: tests: - name: unit command: [echo, OK] + tasks: + - name: workflow-b + command: [echo, OK] diff --git a/garden-service/test/data/test-project-a/module-c/garden.yml b/garden-service/test/data/test-project-a/module-c/garden.yml index 09cb206afc..c242bc611b 100644 --- a/garden-service/test/data/test-project-a/module-c/garden.yml +++ b/garden-service/test/data/test-project-a/module-c/garden.yml @@ -9,3 +9,6 @@ module: tests: - name: unit command: [echo, OK] + tasks: + - name: workflow-c + command: [echo, OK] diff --git a/garden-service/test/data/test-project-b/module-a/garden.yml b/garden-service/test/data/test-project-b/module-a/garden.yml index 969831e05c..5970a3ecf9 100644 --- a/garden-service/test/data/test-project-b/module-a/garden.yml +++ b/garden-service/test/data/test-project-b/module-a/garden.yml @@ -9,5 +9,12 @@ module: ports: - name: http containerPort: 8080 + dependencies: + - workflow-a build: command: [echo, A] + tasks: + - name: workflow-a + command: [echo, A] + dependencies: + - workflow-c \ No newline at end of file diff --git a/garden-service/test/data/test-project-b/module-b/garden.yml b/garden-service/test/data/test-project-b/module-b/garden.yml index 664293b6b0..801dc810bd 100644 --- a/garden-service/test/data/test-project-b/module-b/garden.yml +++ b/garden-service/test/data/test-project-b/module-b/garden.yml @@ -14,4 +14,4 @@ module: build: command: [echo, B] dependencies: - - module-a + - module-a \ No newline at end of file diff --git a/garden-service/test/data/test-project-b/module-c/garden.yml b/garden-service/test/data/test-project-b/module-c/garden.yml index 94d0bdab75..b3fa683519 100644 --- a/garden-service/test/data/test-project-b/module-c/garden.yml +++ b/garden-service/test/data/test-project-b/module-c/garden.yml @@ -20,3 +20,6 @@ module: build: dependencies: - module-b + tasks: + - name: workflow-c + command: [echo, C] \ No newline at end of file diff --git a/garden-service/test/data/test-project-container/module-a/garden.yml b/garden-service/test/data/test-project-container/module-a/garden.yml index c87f80ea6e..5d0e99a1da 100644 --- a/garden-service/test/data/test-project-container/module-a/garden.yml +++ b/garden-service/test/data/test-project-container/module-a/garden.yml @@ -11,3 +11,9 @@ module: port: http healthCheck: tcpPort: http + tasks: + - name: workflow-a + command: [echo, A] + dependencies: + - workflow-b + - service-b \ No newline at end of file diff --git a/garden-service/test/data/test-project-dependants/build-dependant/.dockerignore b/garden-service/test/data/test-project-dependants/build-dependant/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/garden-service/test/data/test-project-dependants/build-dependant/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/garden-service/test/data/test-project-dependants/build-dependant/Dockerfile b/garden-service/test/data/test-project-dependants/build-dependant/Dockerfile new file mode 100644 index 0000000000..58c75b00f3 --- /dev/null +++ b/garden-service/test/data/test-project-dependants/build-dependant/Dockerfile @@ -0,0 +1,2 @@ +FROM scratch +ADD . / diff --git a/garden-service/test/data/test-project-dependants/build-dependant/garden.yml b/garden-service/test/data/test-project-dependants/build-dependant/garden.yml new file mode 100644 index 0000000000..845cce10ed --- /dev/null +++ b/garden-service/test/data/test-project-dependants/build-dependant/garden.yml @@ -0,0 +1,15 @@ +module: + description: Build dependant + name: build-dependant + type: container + services: + - name: build-dependant + ports: + - name: http + containerPort: 8080 + ingresses: + - path: /dependant + port: http + build: + dependencies: + - good-morning diff --git a/garden-service/test/data/test-project-dependants/build-dependency/.dockerignore b/garden-service/test/data/test-project-dependants/build-dependency/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/garden-service/test/data/test-project-dependants/build-dependency/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/garden-service/test/data/test-project-dependants/build-dependency/Dockerfile b/garden-service/test/data/test-project-dependants/build-dependency/Dockerfile new file mode 100644 index 0000000000..58c75b00f3 --- /dev/null +++ b/garden-service/test/data/test-project-dependants/build-dependency/Dockerfile @@ -0,0 +1,2 @@ +FROM scratch +ADD . / diff --git a/garden-service/test/data/test-project-dependants/build-dependency/garden.yml b/garden-service/test/data/test-project-dependants/build-dependency/garden.yml new file mode 100644 index 0000000000..fc17c757e8 --- /dev/null +++ b/garden-service/test/data/test-project-dependants/build-dependency/garden.yml @@ -0,0 +1,12 @@ +module: + description: Build dependency + name: build-dependency + type: container + services: + - name: build-dependency + ports: + - name: http + containerPort: 8080 + ingresses: + - path: /dependency + port: http diff --git a/garden-service/test/data/test-project-dependants/garden.yml b/garden-service/test/data/test-project-dependants/garden.yml new file mode 100644 index 0000000000..6f10744dc8 --- /dev/null +++ b/garden-service/test/data/test-project-dependants/garden.yml @@ -0,0 +1,8 @@ +project: + name: test-project-dependants + environmentDefaults: + providers: [] + environments: + - name: local + providers: + - name: test-plugin diff --git a/garden-service/test/data/test-project-dependants/good-evening/.dockerignore b/garden-service/test/data/test-project-dependants/good-evening/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/garden-service/test/data/test-project-dependants/good-evening/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/garden-service/test/data/test-project-dependants/good-evening/Dockerfile b/garden-service/test/data/test-project-dependants/good-evening/Dockerfile new file mode 100644 index 0000000000..58c75b00f3 --- /dev/null +++ b/garden-service/test/data/test-project-dependants/good-evening/Dockerfile @@ -0,0 +1,2 @@ +FROM scratch +ADD . / diff --git a/garden-service/test/data/test-project-dependants/good-evening/garden.yml b/garden-service/test/data/test-project-dependants/good-evening/garden.yml new file mode 100644 index 0000000000..37b711acee --- /dev/null +++ b/garden-service/test/data/test-project-dependants/good-evening/garden.yml @@ -0,0 +1,12 @@ +module: + description: Good evening greeting service + name: good-evening + type: container + services: + - name: good-evening + ports: + - name: http + containerPort: 8080 + ingresses: + - path: /evening + port: http diff --git a/garden-service/test/data/test-project-dependants/good-morning/.dockerignore b/garden-service/test/data/test-project-dependants/good-morning/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/garden-service/test/data/test-project-dependants/good-morning/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/garden-service/test/data/test-project-dependants/good-morning/Dockerfile b/garden-service/test/data/test-project-dependants/good-morning/Dockerfile new file mode 100644 index 0000000000..58c75b00f3 --- /dev/null +++ b/garden-service/test/data/test-project-dependants/good-morning/Dockerfile @@ -0,0 +1,2 @@ +FROM scratch +ADD . / diff --git a/garden-service/test/data/test-project-dependants/good-morning/garden.yml b/garden-service/test/data/test-project-dependants/good-morning/garden.yml new file mode 100644 index 0000000000..906ee553b0 --- /dev/null +++ b/garden-service/test/data/test-project-dependants/good-morning/garden.yml @@ -0,0 +1,20 @@ +module: + description: Good morning greeting service + name: good-morning + type: container + services: + - name: good-morning + ports: + - name: http + containerPort: 8080 + ingresses: + - path: /morning + port: http + dependencies: + - good-morning-task + build: + dependencies: + - build-dependency + tasks: + - name: good-morning-task + command: [echo, good-morning-task] diff --git a/garden-service/test/data/test-project-dependants/service-dependant/.dockerignore b/garden-service/test/data/test-project-dependants/service-dependant/.dockerignore new file mode 100644 index 0000000000..1cd4736667 --- /dev/null +++ b/garden-service/test/data/test-project-dependants/service-dependant/.dockerignore @@ -0,0 +1,4 @@ +node_modules +Dockerfile +garden.yml +app.yaml diff --git a/garden-service/test/data/test-project-dependants/service-dependant/Dockerfile b/garden-service/test/data/test-project-dependants/service-dependant/Dockerfile new file mode 100644 index 0000000000..58c75b00f3 --- /dev/null +++ b/garden-service/test/data/test-project-dependants/service-dependant/Dockerfile @@ -0,0 +1,2 @@ +FROM scratch +ADD . / diff --git a/garden-service/test/data/test-project-dependants/service-dependant/garden.yml b/garden-service/test/data/test-project-dependants/service-dependant/garden.yml new file mode 100644 index 0000000000..740c0ac120 --- /dev/null +++ b/garden-service/test/data/test-project-dependants/service-dependant/garden.yml @@ -0,0 +1,20 @@ +module: + description: Service dependant + name: service-dependant + type: container + services: + - name: service-dependant + ports: + - name: http + containerPort: 8080 + ingresses: + - path: /dependant + port: http + dependencies: + - good-morning + # - dependant-task + tasks: + - name: dependant-task + command: [echo, dependant-task] + dependencies: + - service-dependant diff --git a/garden-service/test/data/test-project-hot-reload/good-morning/garden.yml b/garden-service/test/data/test-project-hot-reload/good-morning/garden.yml index a286fc9192..b380de2c6d 100644 --- a/garden-service/test/data/test-project-hot-reload/good-morning/garden.yml +++ b/garden-service/test/data/test-project-hot-reload/good-morning/garden.yml @@ -19,6 +19,11 @@ module: httpGet: path: /_ah/health port: http + dependencies: + - good-morning-task build: dependencies: - build-dependency + tasks: + - name: good-morning-task + command: [echo, good-morning-task] diff --git a/garden-service/test/data/test-project-hot-reload/service-dependant/garden.yml b/garden-service/test/data/test-project-hot-reload/service-dependant/garden.yml index 219e5a809a..a35f6a09e9 100644 --- a/garden-service/test/data/test-project-hot-reload/service-dependant/garden.yml +++ b/garden-service/test/data/test-project-hot-reload/service-dependant/garden.yml @@ -21,3 +21,9 @@ module: port: http dependencies: - good-morning + # - dependant-task + tasks: + - name: dependant-task + command: [echo, dependant-task] + dependencies: + - service-dependant diff --git a/garden-service/test/helpers.ts b/garden-service/test/helpers.ts index 1a6e08c7fe..8203b11c3d 100644 --- a/garden-service/test/helpers.ts +++ b/garden-service/test/helpers.ts @@ -31,6 +31,7 @@ import { ValidateModuleParams, RunModuleParams, RunServiceParams, + RunWorkflowParams, SetSecretParams, } from "../src/types/plugin/params" import { @@ -125,6 +126,13 @@ export const testPlugin: PluginFactory = (): GardenPlugin => { spec, })) + moduleConfig.workflowConfigs = moduleConfig.spec.tasks.map(t => ({ + name: t.name, + dependencies: t.dependencies, + spec: t, + timeout: t.timeout, + })) + moduleConfig.testConfigs = moduleConfig.spec.tests.map(t => ({ name: t.name, dependencies: t.dependencies, @@ -138,6 +146,9 @@ export const testPlugin: PluginFactory = (): GardenPlugin => { build: buildGenericModule, runModule, + async getServiceStatus() { return {} }, + async deployService() { return {} }, + async runService( { ctx, service, interactive, runtimeContext, timeout, buildDependencies }: RunServiceParams, ) { @@ -152,8 +163,27 @@ export const testPlugin: PluginFactory = (): GardenPlugin => { }) }, - async getServiceStatus() { return {} }, - async deployService() { return {} }, + async runWorkflow( + { ctx, workflow, interactive, runtimeContext, logEntry, buildDependencies }: RunWorkflowParams, + ) { + const result = await runModule({ + ctx, + buildDependencies, + interactive, + logEntry, + runtimeContext, + module: workflow.module, + command: workflow.spec.command || [], + ignoreError: false, + timeout: workflow.spec.timeout || 9999, + }) + + return { + ...result, + workflowName: workflow.name, + } + }, + }, }, } @@ -192,6 +222,7 @@ export const defaultModuleConfig: ModuleConfig = { }, serviceConfigs: [], testConfigs: [], + workflowConfigs: [], } export const makeTestModule = (params: Partial = {}) => { diff --git a/garden-service/test/src/actions.ts b/garden-service/test/src/actions.ts index 469f5e8348..9e58d343eb 100644 --- a/garden-service/test/src/actions.ts +++ b/garden-service/test/src/actions.ts @@ -1,12 +1,13 @@ import { Garden } from "../../src/garden" import { makeTestGardenA } from "../helpers" -import { PluginFactory, PluginActions, ModuleAndServiceActions } from "../../src/types/plugin/plugin" +import { PluginFactory, PluginActions, ModuleAndRuntimeActions } from "../../src/types/plugin/plugin" import { validate } from "../../src/config/common" import { ActionHelper } from "../../src/actions" import { expect } from "chai" import { omit } from "lodash" import { Module } from "../../src/types/module" import { Service } from "../../src/types/service" +import { Workflow } from "../../src/types/workflow" import Stream from "ts-stream" import { ServiceLogEntry } from "../../src/types/plugin/outputs" import { @@ -26,6 +27,8 @@ import { execInServiceParamsSchema, getServiceLogsParamsSchema, runServiceParamsSchema, + getWorkflowStatusParamsSchema, + runWorkflowParamsSchema, getEnvironmentStatusParamsSchema, prepareEnvironmentParamsSchema, cleanupEnvironmentParamsSchema, @@ -42,6 +45,7 @@ describe("ActionHelper", () => { let actions: ActionHelper let module: Module let service: Service + let workflow: Workflow before(async () => { const plugins = { "test-plugin": testPlugin, "test-plugin-b": testPluginB } @@ -49,6 +53,7 @@ describe("ActionHelper", () => { actions = garden.actions module = await garden.getModule("module-a") service = await garden.getService("service-a") + workflow = await garden.getWorkflow("workflow-a") }) // Note: The test plugins below implicitly validate input params for each of the tests @@ -306,6 +311,29 @@ describe("ActionHelper", () => { }) }) }) + + describe("runWorkflow", () => { + it("should correctly call the corresponding plugin handler", async () => { + const result = await actions.runWorkflow({ + workflow, + interactive: true, + runtimeContext: { + envVars: { FOO: "bar" }, + dependencies: {}, + }, + }) + expect(result).to.eql({ + moduleName: workflow.module.name, + workflowName: workflow.name, + command: ["foo"], + completedAt: now, + output: "bla bla", + success: true, + startedAt: now, + version: workflow.module.version, + }) + }) + }) }) const testPlugin: PluginFactory = async () => ({ @@ -343,7 +371,7 @@ const testPlugin: PluginFactory = async () => ({ }, }, moduleActions: { - test: { + test: { describeType: async (params) => { validate(params, describeModuleTypeParamsSchema) return { @@ -362,9 +390,16 @@ const testPlugin: PluginFactory = async () => ({ spec, })) + const workflowConfigs = (params.moduleConfig.spec.tasks || []).map(spec => ({ + name: spec.name, + dependencies: spec.dependencies || [], + spec, + })) + return { ...params.moduleConfig, serviceConfigs, + workflowConfigs, } }, @@ -479,6 +514,28 @@ const testPlugin: PluginFactory = async () => ({ version: params.module.version, } }, + + getWorkflowStatus: async (params) => { + validate(params, getWorkflowStatusParamsSchema) + return { + done: true, + } + }, + + runWorkflow: async (params) => { + validate(params, runWorkflowParamsSchema) + const module = params.workflow.module + return { + moduleName: module.name, + workflowName: params.workflow.name, + command: ["foo"], + completedAt: now, + output: "bla bla", + success: true, + startedAt: now, + version: params.module.version, + } + }, }, }, }) diff --git a/garden-service/test/src/build-dir.ts b/garden-service/test/src/build-dir.ts index 9f8fbaae3c..508030c3d4 100644 --- a/garden-service/test/src/build-dir.ts +++ b/garden-service/test/src/build-dir.ts @@ -56,10 +56,8 @@ describe("BuildDir", () => { join(buildDirA, "some-dir", "some-file"), ] - const buildDirPrettyPrint = nodetree(garden.buildDir.buildDirPath) - for (const p of copiedPaths) { - expect(await pathExists(p)).to.eql(true, buildDirPrettyPrint) + expect(await pathExists(p)).to.eql(true) } }) @@ -98,8 +96,7 @@ describe("BuildDir", () => { const notCopiedPath = join(buildDirD, "B", "build", "unused.txt") expect(await pathExists(notCopiedPath)).to.eql(false) } catch (e) { - const buildDirPrettyPrint = nodetree(garden.buildDir.buildDirPath) - console.log(buildDirPrettyPrint) + console.log(nodetree(garden.buildDir.buildDirPath)) throw e } }) diff --git a/garden-service/test/src/commands/deploy.ts b/garden-service/test/src/commands/deploy.ts index 30e955fe6c..b795df2f7e 100644 --- a/garden-service/test/src/commands/deploy.ts +++ b/garden-service/test/src/commands/deploy.ts @@ -10,9 +10,31 @@ import { import { DeployServiceParams, GetServiceStatusParams, + RunWorkflowParams, } from "../../../src/types/plugin/params" import { ServiceState, ServiceStatus } from "../../../src/types/service" import { taskResultOutputs } from "../../helpers" +import { RunWorkflowResult } from "../../../src/types/plugin/outputs" + +const placeholderTimestamp = new Date() + +const placeholderWorkflowResult = (moduleName, workflowName, command) => ({ + moduleName, + workflowName, + command, + version: { + versionString: "1", + dirtyTimestamp: null, + dependencyVersions: {}, + }, + success: true, + startedAt: placeholderTimestamp, + completedAt: placeholderTimestamp, + output: "out", +}) + +const workflowResultA = placeholderWorkflowResult("module-a", "workflow-a", ["echo", "A"]) +const workflowResultC = placeholderWorkflowResult("module-c", "workflow-c", ["echo", "C"]) const testProvider: PluginFactory = () => { const testStatuses: { [key: string]: ServiceStatus } = { @@ -45,6 +67,10 @@ const testProvider: PluginFactory = () => { return newStatus } + const runWorkflow = async ({ workflow }: RunWorkflowParams): Promise => { + return placeholderWorkflowResult(workflow.module.name, workflow.name, workflow.spec.command) + } + return { moduleActions: { container: { @@ -52,6 +78,7 @@ const testProvider: PluginFactory = () => { build: buildGenericModule, deployService, getServiceStatus, + runWorkflow, }, }, } @@ -85,6 +112,8 @@ describe("DeployCommand", () => { "build.module-a": { fresh: true, buildLog: "A" }, "build.module-b": { fresh: true, buildLog: "B" }, "build.module-c": {}, + "workflow.workflow-a": workflowResultA, + "workflow.workflow-c": workflowResultC, "deploy.service-a": { version: "1", state: "ready" }, "deploy.service-b": { version: "1", state: "ready" }, "deploy.service-c": { version: "1", state: "ready" }, @@ -115,6 +144,9 @@ describe("DeployCommand", () => { expect(taskResultOutputs(result!)).to.eql({ "build.module-a": { fresh: true, buildLog: "A" }, "build.module-b": { fresh: true, buildLog: "B" }, + "build.module-c": {}, + "workflow.workflow-a": workflowResultA, + "workflow.workflow-c": workflowResultC, "deploy.service-a": { version: "1", state: "ready" }, "deploy.service-b": { version: "1", state: "ready" }, "push.module-a": { pushed: false }, diff --git a/garden-service/test/src/commands/run/run.ts b/garden-service/test/src/commands/run/run.ts index f4f0ed5fc9..a92caff4e5 100644 --- a/garden-service/test/src/commands/run/run.ts +++ b/garden-service/test/src/commands/run/run.ts @@ -11,6 +11,6 @@ describe("RunCommand", () => { it("should contain a set of subcommands", () => { const cmd = new RunCommand() const subcommandNames = cmd.subCommands.map(s => new s().name) - expect(subcommandNames).to.eql(["module", "service", "test"]) + expect(subcommandNames).to.eql(["module", "service", "task", "test"]) }) }) diff --git a/garden-service/test/src/commands/run/workflow.ts b/garden-service/test/src/commands/run/workflow.ts new file mode 100644 index 0000000000..c2c6ba77c2 --- /dev/null +++ b/garden-service/test/src/commands/run/workflow.ts @@ -0,0 +1,30 @@ +import { expect } from "chai" +import { omit } from "lodash" +import { RunWorkflowCommand } from "../../../../src/commands/run/workflow" +import { makeTestGardenA } from "../../../helpers" + +describe("RunWorkflowCommand", () => { + + it("should run a workflow", async () => { + const garden = await makeTestGardenA() + const cmd = new RunWorkflowCommand() + + const { result } = await cmd.action({ + garden, + args: { task: "workflow-a" }, + opts: { "force-build": false }, + }) + + const expected = { + command: ["echo", "OK"], + moduleName: "module-a", + output: "OK", + success: true, + workflowName: "workflow-a", + } + + const omittedKeys = ["completedAt", "startedAt", "version"] + expect(omit(result, omittedKeys)).to.eql(expected) + }) + +}) diff --git a/garden-service/test/src/config/base.ts b/garden-service/test/src/config/base.ts index 1913148823..13678be6a9 100644 --- a/garden-service/test/src/config/base.ts +++ b/garden-service/test/src/config/base.ts @@ -53,6 +53,10 @@ describe("loadConfig", async () => { spec: { services: [{ name: "service-a" }], + tasks: [{ + name: "workflow-a", + command: ["echo", "OK"], + }], tests: [{ name: "unit", command: ["echo", "OK"], @@ -60,6 +64,7 @@ describe("loadConfig", async () => { }, serviceConfigs: [], + workflowConfigs: [], testConfigs: [], }) }) diff --git a/garden-service/test/src/garden.ts b/garden-service/test/src/garden.ts index f7cc5046c1..c2e0ebeeb6 100644 --- a/garden-service/test/src/garden.ts +++ b/garden-service/test/src/garden.ts @@ -151,6 +151,31 @@ describe("Garden", () => { }) }) + describe("getServicesAndWorkflows", () => { + it("should scan for modules and return all registered services and workflows in the context", async () => { + const garden = await makeTestGardenA() + const { services, workflows } = await garden.getServicesAndWorkflows() + + expect(getNames(services).sort()).to.eql(["service-a", "service-b", "service-c"]) + expect(getNames(workflows).sort()).to.eql(["workflow-a", "workflow-b", "workflow-c"]) + }) + + it("should optionally return specified services and workflows in the context", async () => { + const garden = await makeTestGardenA() + const { services, workflows } = await garden.getServicesAndWorkflows(["service-b", "service-c", "workflow-a"]) + + expect(getNames(services).sort()).to.eql(["service-b", "service-c"]) + expect(getNames(workflows).sort()).to.eql(["workflow-a"]) + }) + + it("should not throw if a named service or workflow is missing", async () => { + const garden = await makeTestGardenA() + + await garden.getServicesAndWorkflows(["not", "real"]) + }) + + }) + describe("getServices", () => { it("should scan for modules and return all registered services in the context", async () => { const garden = await makeTestGardenA() @@ -202,6 +227,81 @@ describe("Garden", () => { }) }) + describe("getWorkflows", () => { + it("should scan for modules and return all registered workflows in the context", async () => { + const garden = await makeTestGardenA() + const workflows = await garden.getWorkflows() + + expect(getNames(workflows).sort()).to.eql(["workflow-a", "workflow-b", "workflow-c"]) + }) + + it("should optionally return specified workflows in the context", async () => { + const garden = await makeTestGardenA() + const workflows = await garden.getWorkflows(["workflow-b", "workflow-c"]) + + expect(getNames(workflows).sort()).to.eql(["workflow-b", "workflow-c"]) + }) + + it("should throw if named workflow is missing", async () => { + const garden = await makeTestGardenA() + + try { + await garden.getWorkflows(["bla"]) + } catch (err) { + expect(err.type).to.equal("parameter") + return + } + + throw new Error("Expected error") + }) + }) + + describe("getWorkflow", () => { + it("should return the specified workflow", async () => { + const garden = await makeTestGardenA() + const workflow = await garden.getWorkflow("workflow-b") + + expect(workflow.name).to.equal("workflow-b") + }) + + it("should throw if workflow is missing", async () => { + const garden = await makeTestGardenA() + + try { + await garden.getWorkflows(["bla"]) + } catch (err) { + expect(err.type).to.equal("parameter") + return + } + + throw new Error("Expected error") + }) + }) + + describe("getServiceOrWorkflow", () => { + it("should return the specified service or workflow", async () => { + const garden = await makeTestGardenA() + const service = await garden.getServiceOrWorkflow("service-a") + const workflow = await garden.getServiceOrWorkflow("workflow-a") + + expect(service.name).to.equal("service-a") + expect(workflow.name).to.equal("workflow-a") + }) + + it("should throw if no matching service or workflow was found", async () => { + const garden = await makeTestGardenA() + + try { + await garden.getServiceOrWorkflow("bla") + } catch (err) { + expect(err.type).to.equal("parameter") + return + } + + throw new Error("Expected error") + }) + }) + describe("scanModules", () => { // TODO: assert that gitignore in project root is respected it("should scan the project root for modules and add to the context", async () => { @@ -461,19 +561,19 @@ describe("Garden", () => { describe("resolveModuleDependencies", () => { it("should resolve build dependencies", async () => { const garden = await makeTestGardenA() - const modules = await garden.resolveModuleDependencies([{ name: "module-c", copy: [] }], []) + const modules = await garden.resolveDependencyModules([{ name: "module-c", copy: [] }], []) expect(getNames(modules)).to.eql(["module-a", "module-b", "module-c"]) }) it("should resolve service dependencies", async () => { const garden = await makeTestGardenA() - const modules = await garden.resolveModuleDependencies([], ["service-b"]) + const modules = await garden.resolveDependencyModules([], ["service-b"]) expect(getNames(modules)).to.eql(["module-a", "module-b"]) }) it("should combine module and service dependencies", async () => { const garden = await makeTestGardenA() - const modules = await garden.resolveModuleDependencies([{ name: "module-b", copy: [] }], ["service-c"]) + const modules = await garden.resolveDependencyModules([{ name: "module-b", copy: [] }], ["service-c"]) expect(getNames(modules)).to.eql(["module-a", "module-b", "module-c"]) }) }) diff --git a/garden-service/test/src/plugins/container.ts b/garden-service/test/src/plugins/container.ts index 648bc1ced9..8e06559b2a 100644 --- a/garden-service/test/src/plugins/container.ts +++ b/garden-service/test/src/plugins/container.ts @@ -44,10 +44,12 @@ describe("plugins.container", () => { spec: { buildArgs: {}, services: [], + tasks: [], tests: [], }, serviceConfigs: [], + workflowConfigs: [], testConfigs: [], } @@ -120,10 +122,12 @@ describe("plugins.container", () => { buildArgs: {}, image: "some/image", services: [], + tasks: [], tests: [], }, serviceConfigs: [], + workflowConfigs: [], testConfigs: [], }) @@ -221,6 +225,12 @@ describe("plugins.container", () => { outputs: {}, volumes: [], }], + tasks: [{ + name: "workflow-a", + command: ["echo", "OK"], + dependencies: [], + timeout: null, + }], tests: [{ name: "unit", command: ["echo", "OK"], @@ -231,6 +241,7 @@ describe("plugins.container", () => { }, serviceConfigs: [], + workflowConfigs: [], testConfigs: [], } @@ -265,6 +276,13 @@ describe("plugins.container", () => { outputs: {}, volumes: [], }], + tasks: + [{ + name: "workflow-a", + command: ["echo", "OK"], + dependencies: [], + timeout: null, + }], tests: [{ name: "unit", @@ -299,6 +317,21 @@ describe("plugins.container", () => { volumes: [], }, }], + workflowConfigs: + [{ + dependencies: [], + name: "workflow-a", + spec: { + command: [ + "echo", + "OK", + ], + dependencies: [], + name: "workflow-a", + timeout: null, + }, + timeout: null, + }], testConfigs: [{ name: "unit", @@ -356,6 +389,12 @@ describe("plugins.container", () => { outputs: {}, volumes: [], }], + tasks: [{ + name: "workflow-a", + command: ["echo"], + dependencies: [], + timeout: null, + }], tests: [{ name: "unit", command: ["echo", "OK"], @@ -366,6 +405,7 @@ describe("plugins.container", () => { }, serviceConfigs: [], + workflowConfigs: [], testConfigs: [], } @@ -406,10 +446,17 @@ describe("plugins.container", () => { outputs: {}, volumes: [], }], + tasks: [{ + name: "workflow-a", + command: ["echo"], + dependencies: [], + timeout: null, + }], tests: [], }, serviceConfigs: [], + workflowConfigs: [], testConfigs: [], } @@ -447,10 +494,17 @@ describe("plugins.container", () => { outputs: {}, volumes: [], }], + tasks: [{ + name: "workflow-a", + command: ["echo"], + dependencies: [], + timeout: null, + }], tests: [], }, serviceConfigs: [], + workflowConfigs: [], testConfigs: [], } diff --git a/garden-service/test/src/plugins/kubernetes/ingress.ts b/garden-service/test/src/plugins/kubernetes/ingress.ts index 9cb0250001..548d0dc9fc 100644 --- a/garden-service/test/src/plugins/kubernetes/ingress.ts +++ b/garden-service/test/src/plugins/kubernetes/ingress.ts @@ -339,10 +339,12 @@ describe("createIngresses", () => { buildArgs: {}, image: "some/image:1.1", services: [], + tasks: [], tests: [], }, serviceConfigs: [], + workflowConfigs: [], testConfigs: [], } diff --git a/garden-service/test/src/tasks/helpers.ts b/garden-service/test/src/tasks/helpers.ts new file mode 100644 index 0000000000..fb044cb4f8 --- /dev/null +++ b/garden-service/test/src/tasks/helpers.ts @@ -0,0 +1,230 @@ +import * as Bluebird from "bluebird" +import deline = require("deline") +import { expect } from "chai" +import { flatten, uniq } from "lodash" +import { resolve } from "path" +import { Garden } from "../../../src/garden" +import { makeTestGarden, dataDir } from "../../helpers" +import { getTasksForModule } from "../../../src/tasks/helpers" +import { Task } from "../../../src/tasks/base" + +async function sortedBaseKeysWithDependencies(tasks: Task[]): Promise { + return sortedBaseKeys(flatten([tasks].concat(await Bluebird.map(tasks, t => t.getDependencies())))) +} + +function sortedBaseKeys(tasks: Task[]): string[] { + return uniq(tasks.map(t => t.getBaseKey())).sort() +} + +describe("TaskHelpers", () => { + + let garden: Garden + + before(async () => { + garden = await makeTestGarden(resolve(dataDir, "test-project-dependants")) + }) + + /** + * Note: Since we also test with dependencies included in the task lists generated , these tests also check the + * getDependencies methods of the task classes in question. + */ + describe("getTasksForModule", () => { + + it("returns the correct set of tasks for the changed module", async () => { + const module = await garden.getModule("good-morning") + const tasks = await getTasksForModule({ + garden, module, hotReloadServiceNames: [], force: true, forceBuild: true, + fromWatch: false, includeDependants: false, + }) + + expect(sortedBaseKeys(tasks)).to.eql([ + "build.good-morning", + "deploy.good-morning", + "workflow.good-morning-task", + ]) + + expect(await sortedBaseKeysWithDependencies(tasks)).to.eql([ + "build.build-dependency", + "build.good-morning", + "deploy.good-morning", + "push.good-morning", + "workflow.good-morning-task", + ].sort()) + }) + + describe("returns the correct set of tasks for the changed module and its dependants", () => { + const expectedBaseKeysByChangedModule = [ + { + moduleName: "build-dependency", + withoutDependencies: [ + "build.build-dependency", + "deploy.build-dependency", + + "build.good-morning", + "deploy.good-morning", + "workflow.good-morning-task", + + "build.build-dependant", + "deploy.build-dependant", + + "deploy.service-dependant", + "workflow.dependant-task", + ].sort(), + withDependencies: [ + "build.build-dependency", + "push.build-dependency", + "deploy.build-dependency", + + "build.good-morning", + "push.good-morning", + "deploy.good-morning", + "workflow.good-morning-task", + + "build.build-dependant", + "push.build-dependant", + "deploy.build-dependant", + + "build.service-dependant", + "push.service-dependant", + "deploy.service-dependant", + "workflow.dependant-task", + ].sort(), + }, + { + moduleName: "good-morning", + withoutDependencies: [ + "build.good-morning", + "deploy.good-morning", + "workflow.good-morning-task", + + "build.build-dependant", + "deploy.build-dependant", + + "deploy.service-dependant", + "workflow.dependant-task", + ].sort(), + withDependencies: [ + "build.build-dependency", + + "build.good-morning", + "push.good-morning", + "deploy.good-morning", + "workflow.good-morning-task", + + "build.build-dependant", + "push.build-dependant", + "deploy.build-dependant", + + "build.service-dependant", + "push.service-dependant", + "deploy.service-dependant", + "workflow.dependant-task", + ].sort(), + }, + { + moduleName: "good-evening", + withoutDependencies: ["build.good-evening", "deploy.good-evening"], + withDependencies: ["build.good-evening", "push.good-evening", "deploy.good-evening"].sort(), + }, + { + moduleName: "build-dependant", + withoutDependencies: ["build.build-dependant", "deploy.build-dependant"], + withDependencies: [ + "build.good-morning", + + "build.build-dependant", + "push.build-dependant", + "deploy.build-dependant", + ].sort(), + }, + { + moduleName: "service-dependant", + withoutDependencies: ["build.service-dependant", "deploy.service-dependant", "workflow.dependant-task"], + withDependencies: [ + "deploy.good-morning", + + "build.service-dependant", + "push.service-dependant", + "deploy.service-dependant", + "workflow.dependant-task", + ].sort(), + }, + ] + + for (const { moduleName, withoutDependencies, withDependencies } of expectedBaseKeysByChangedModule) { + it(`returns the correct set of tasks for ${moduleName} and its dependants`, async () => { + const module = await garden.getModule(moduleName) + const tasks = await getTasksForModule({ + garden, module, hotReloadServiceNames: [], force: true, forceBuild: true, + fromWatch: true, includeDependants: true, + }) + expect(sortedBaseKeys(tasks)).to.eql(withoutDependencies) + expect(await sortedBaseKeysWithDependencies(tasks)).to.eql(withDependencies) + }) + + } + + }) + + describe(deline`returns the correct set of tasks for the changed module and its dependants + (with hot reloading)`, () => { + const expectedBaseKeysByChangedModule = [ + { + moduleName: "build-dependency", + withoutDependencies: ["build.build-dependency", "deploy.build-dependency"], + withDependencies: ["build.build-dependency", "push.build-dependency", "deploy.build-dependency"].sort(), + }, + { + moduleName: "good-morning", + withoutDependencies: ["deploy.service-dependant", "workflow.dependant-task"], + withDependencies: [ + "build.service-dependant", + "push.service-dependant", + "deploy.service-dependant", + "workflow.dependant-task", + ].sort(), + }, + { + moduleName: "good-evening", + withoutDependencies: ["build.good-evening", "deploy.good-evening"], + withDependencies: ["build.good-evening", "push.good-evening", "deploy.good-evening"].sort(), + }, + { + moduleName: "build-dependant", + withoutDependencies: ["build.build-dependant", "deploy.build-dependant"], + withDependencies: [ + "build.build-dependant", + "push.build-dependant", + "deploy.build-dependant", + ].sort(), + }, + { + moduleName: "service-dependant", + withoutDependencies: ["build.service-dependant", "deploy.service-dependant", "workflow.dependant-task"], + withDependencies: [ + "build.service-dependant", + "push.service-dependant", + "deploy.service-dependant", + "workflow.dependant-task", + ].sort(), + }, + ] + + for (const { moduleName, withoutDependencies, withDependencies } of expectedBaseKeysByChangedModule) { + it(`returns the correct set of tasks for ${moduleName} and its dependants`, async () => { + const module = await garden.getModule(moduleName) + const tasks = await getTasksForModule({ + garden, module, hotReloadServiceNames: ["good-morning"], force: true, forceBuild: true, + fromWatch: true, includeDependants: true, + }) + expect(sortedBaseKeys(tasks)).to.eql(withoutDependencies) + expect(await sortedBaseKeysWithDependencies(tasks)).to.eql(withDependencies) + }) + + } + + }) + + }) + +}) diff --git a/garden-service/test/src/watch.ts b/garden-service/test/src/watch.ts deleted file mode 100644 index f69d761ff5..0000000000 --- a/garden-service/test/src/watch.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { expect } from "chai" -import { mapValues } from "lodash" -import { join } from "path" -import { - AutoReloadDependants, - computeAutoReloadDependants, -} from "../../src/watch" -import { makeTestGarden } from "../helpers" - -export function dependantModuleNames(ard: AutoReloadDependants): { [key: string]: string[] } { - return mapValues(ard, dependants => { - return Array.from(dependants).map(d => d.name).sort() - }) -} - -describe("watch", () => { - - describe("computeAutoReloadDependants", () => { - it("should include build and service dependants of requested modules", async () => { - const projectRoot = join(__dirname, "..", "data", "test-project-auto-reload") - const garden = await makeTestGarden(projectRoot) - const dependants = dependantModuleNames( - await computeAutoReloadDependants(garden)) - - expect(dependants).to.eql({ - "module-a": ["module-b"], - "module-b": ["module-d", "module-e"], - "module-c": ["module-e", "module-f"], - }) - }) - }) - -})