diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 78a3b244ea..43220faf3c 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -36,13 +36,13 @@ Examples: ##### Usage - garden build [module] [options] + garden build [modules] [options] ##### Arguments | Argument | Required | Description | | -------- | -------- | ----------- | - | `module` | No | Specify module(s) to build. Use comma as a separator to specify multiple modules. + | `modules` | No | Specify module(s) to build. Use comma as a separator to specify multiple modules. ##### Options @@ -73,7 +73,7 @@ Note: Currently only supports simple GET requests for HTTP/HTTPS ingresses. | Argument | Required | Description | | -------- | -------- | ----------- | - | `serviceAndPath` | Yes | The name(s) of the service(s) to call followed by the ingress path (e.g. my-container/somepath). + | `serviceAndPath` | Yes | The name of the service to call followed by the ingress path (e.g. my-container/somepath). ### garden create project @@ -175,7 +175,7 @@ resources. ### garden delete service -Deletes a running service. +Deletes running services. Deletes (i.e. un-deploys) the specified services. Note that this command does not take into account any services depending on the deleted service, and might therefore leave the project in an unstable state. @@ -187,13 +187,13 @@ Examples: ##### Usage - garden delete service + garden delete service ##### Arguments | Argument | Required | Description | | -------- | -------- | ----------- | - | `service` | Yes | The name(s) of the service(s) to delete. Use comma as a separator to specify multiple services. + | `services` | Yes | The name(s) of the service(s) to delete. Use comma as a separator to specify multiple services. ### garden deploy @@ -209,6 +209,7 @@ Examples: garden deploy # deploy all modules in the project garden deploy my-service # only deploy my-service + garden deploy service-a,service-b # only deploy service-a and service-b garden deploy --force # force re-deploy of modules, even if they're already deployed garden deploy --watch # watch for changes to code garden deploy --hot-reload=my-service # deploys all services, with hot reloading enabled for my-service @@ -216,13 +217,13 @@ Examples: ##### Usage - garden deploy [service] [options] + garden deploy [services] [options] ##### Arguments | Argument | Required | Description | | -------- | -------- | ----------- | - | `service` | No | The name(s) of the service(s) to deploy (skip to deploy all services). Use comma as a separator to specify multiple services. + | `services` | No | The name(s) of the service(s) to deploy (skip to deploy all services). Use comma as a separator to specify multiple services. ##### Options @@ -408,13 +409,13 @@ Examples: ##### Usage - garden logs [service] [options] + garden logs [services] [options] ##### Arguments | Argument | Required | Description | | -------- | -------- | ----------- | - | `service` | No | The name(s) of the service(s) to log (skip to log all services). Use comma as a separator to specify multiple services. + | `services` | No | The name(s) of the service(s) to log (skip to log all services). Use comma as a separator to specify multiple services. ##### Options @@ -438,13 +439,13 @@ Examples: ##### Usage - garden publish [module] [options] + garden publish [modules] [options] ##### Arguments | Argument | Required | Description | | -------- | -------- | ----------- | - | `module` | No | The name(s) of the module(s) to publish (skip to publish all modules). Use comma as a separator to specify multiple modules. + | `modules` | No | The name(s) of the module(s) to publish (skip to publish all modules). Use comma as a separator to specify multiple modules. ##### Options @@ -638,13 +639,13 @@ Examples: ##### Usage - garden test [module] [options] + garden test [modules] [options] ##### Arguments | Argument | Required | Description | | -------- | -------- | ----------- | - | `module` | No | The name(s) of the module(s) to test (skip to test all modules). Use comma as a separator to specify multiple modules. + | `modules` | No | The name(s) of the module(s) to test (skip to test all modules). Use comma as a separator to specify multiple modules. ##### Options @@ -669,13 +670,13 @@ Examples: ##### Usage - garden unlink source [source] [options] + garden unlink source [sources] [options] ##### Arguments | Argument | Required | Description | | -------- | -------- | ----------- | - | `source` | No | The name(s) of the source(s) to unlink. Use comma as a separator to specify multiple sources. + | `sources` | No | The name(s) of the source(s) to unlink. Use comma as a separator to specify multiple sources. ##### Options @@ -697,13 +698,13 @@ Examples: ##### Usage - garden unlink module [module] [options] + garden unlink module [modules] [options] ##### Arguments | Argument | Required | Description | | -------- | -------- | ----------- | - | `module` | No | The name(s) of the module(s) to unlink. Use comma as a separator to specify multiple modules. + | `modules` | No | The name(s) of the module(s) to unlink. Use comma as a separator to specify multiple modules. ##### Options @@ -724,13 +725,13 @@ Examples: ##### Usage - garden update-remote sources [source] + garden update-remote sources [sources] ##### Arguments | Argument | Required | Description | | -------- | -------- | ----------- | - | `source` | No | The name(s) of the remote source(s) to update. Use comma as a separator to specify multiple sources. + | `sources` | No | The name(s) of the remote source(s) to update. Use comma as a separator to specify multiple sources. ### garden update-remote modules @@ -746,13 +747,13 @@ Examples: ##### Usage - garden update-remote modules [module] + garden update-remote modules [modules] ##### Arguments | Argument | Required | Description | | -------- | -------- | ----------- | - | `module` | No | The name(s) of the remote module(s) to update. Use comma as a separator to specify multiple modules. + | `modules` | No | The name(s) of the remote module(s) to update. Use comma as a separator to specify multiple modules. ### garden update-remote all diff --git a/garden-service/package-lock.json b/garden-service/package-lock.json index 52ced2b18f..b886469331 100644 --- a/garden-service/package-lock.json +++ b/garden-service/package-lock.json @@ -396,13 +396,14 @@ "version": "1.3.5", "resolved": "http://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "dev": true, "requires": { "@types/node": "*" } }, "@types/async-lock": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/@types/async-lock/-/async-lock-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.1.0.tgz", "integrity": "sha512-Eo8EXiqmChtkt0ETf6AQ8aiDHT3Tht6OuMSa3/9nfuyqFimp7ZwPMiufsA56A7ZUGBuwFzH860jO0d8n0lETtg==", "dev": true }, @@ -416,6 +417,7 @@ "version": "1.17.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "dev": true, "requires": { "@types/connect": "*", "@types/node": "*" @@ -429,7 +431,7 @@ }, "@types/chokidar": { "version": "1.7.5", - "resolved": "http://registry.npmjs.org/@types/chokidar/-/chokidar-1.7.5.tgz", + "resolved": "https://registry.npmjs.org/@types/chokidar/-/chokidar-1.7.5.tgz", "integrity": "sha512-PDkSRY7KltW3M60hSBlerxI8SFPXsO3AL/aRVsO4Kh9IHRW74Ih75gUuTd/aE4LSSFqypb10UIX3QzOJwBQMGQ==", "dev": true, "requires": { @@ -450,14 +452,22 @@ "version": "3.4.32", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "dev": true, "requires": { "@types/node": "*" } }, + "@types/cookiejar": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.0.tgz", + "integrity": "sha512-EIjmpvnHj+T4nMcKwHwxZKUfDmphIKJc2qnEMhSoOvr1lYEQpuRKRz8orWr//krYIIArS/KGGLfL2YGVUYXmIA==", + "dev": true + }, "@types/cookies": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.1.tgz", "integrity": "sha512-ku6IvbucEyuC6i4zAVK/KnuzWNXdbFd1HkXlNLg/zhWDGTtQT5VhumiPruB/BHW34PWVFwyfwGftDQHfWNxu3Q==", + "dev": true, "requires": { "@types/connect": "*", "@types/express": "*", @@ -498,12 +508,13 @@ }, "@types/events": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", - "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" + "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", + "dev": true }, "@types/execa": { "version": "0.9.0", - "resolved": "http://registry.npmjs.org/@types/execa/-/execa-0.9.0.tgz", + "resolved": "https://registry.npmjs.org/@types/execa/-/execa-0.9.0.tgz", "integrity": "sha512-mgfd93RhzjYBUHHV532turHC2j4l/qxsF/PbfDmprHDEUHmNZGlDn1CEsulGK3AfsPdhkWzZQT/S/k0UGhLGsA==", "dev": true, "requires": { @@ -514,6 +525,7 @@ "version": "4.16.0", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.0.tgz", "integrity": "sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w==", + "dev": true, "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "*", @@ -524,6 +536,7 @@ "version": "4.16.0", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz", "integrity": "sha512-lTeoCu5NxJU4OD9moCgm0ESZzweAx0YqsAcab6OB0EB3+As1OaHtKnaGJvcngQxYsi9UNv0abn4/DRavrRxt4w==", + "dev": true, "requires": { "@types/events": "*", "@types/node": "*", @@ -598,7 +611,8 @@ "@types/http-assert": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.3.0.tgz", - "integrity": "sha512-RObYTpPMo0IY+ZksPtKHsXlYFRxsYIvUqd68e89Y7otDrXsjBy1VgMd53kxVV0JMsNlkCASjllFOlLlhxEv0iw==" + "integrity": "sha512-RObYTpPMo0IY+ZksPtKHsXlYFRxsYIvUqd68e89Y7otDrXsjBy1VgMd53kxVV0JMsNlkCASjllFOlLlhxEv0iw==", + "dev": true }, "@types/inquirer": { "version": "0.0.43", @@ -631,7 +645,8 @@ "@types/keygrip": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.1.tgz", - "integrity": "sha1-/1QEYtL7TQqIRBzq8n0oewHD2Hg=" + "integrity": "sha1-/1QEYtL7TQqIRBzq8n0oewHD2Hg=", + "dev": true }, "@types/klaw": { "version": "2.1.1", @@ -646,6 +661,7 @@ "version": "2.0.47", "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.0.47.tgz", "integrity": "sha512-llhCaHNWKFDMx1GCrqwgsWgUO+C4Da0SccbgevHIYOKVxwegEjFzl0WaMWHk3wWx0P0AdqHR+gQYZ2ZAb0ez0Q==", + "dev": true, "requires": { "@types/accepts": "*", "@types/cookies": "*", @@ -659,6 +675,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/@types/koa-bodyparser/-/koa-bodyparser-4.2.1.tgz", "integrity": "sha512-dd6mVT30OmGYIOmNRF3269Bv+IJ68AVrvYcPViB7bYnzxk7nZyfeAsUx96lvXmaTpOGF4XZ7WDCuSOd7Npi6pw==", + "dev": true, "requires": { "@types/koa": "*" } @@ -666,7 +683,8 @@ "@types/koa-compose": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.2.tgz", - "integrity": "sha1-3BBuAAu/kqOskA91bfRzRIh+6Ec=" + "integrity": "sha1-3BBuAAu/kqOskA91bfRzRIh+6Ec=", + "dev": true }, "@types/koa-router": { "version": "7.0.35", @@ -677,10 +695,21 @@ "@types/koa": "*" } }, + "@types/koa-websocket": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/koa-websocket/-/koa-websocket-5.0.3.tgz", + "integrity": "sha512-VgDEoySsNJTi+g0niUaUlnzYGJTeJmNoAxX+2cUGsHrVg19wczEN76TkQY+KGGh8r8vOSyky3qNqMXv1ncBdoA==", + "dev": true, + "requires": { + "@types/koa": "*", + "@types/koa-compose": "*", + "@types/ws": "*" + } + }, "@types/lodash": { - "version": "4.14.117", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.117.tgz", - "integrity": "sha512-xyf2m6tRbz8qQKcxYZa7PA4SllYcay+eh25DN3jmNYY6gSTL7Htc/bttVdkqj2wfJGbeWlQiX8pIyJpKU+tubw==", + "version": "4.14.118", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.118.tgz", + "integrity": "sha512-iiJbKLZbhSa6FYRip/9ZDX6HXhayXLDGY2Fqws9cOkEQ6XeKfaxB0sC541mowZJueYyMnVUmmG+al5/4fCDrgw==", "dev": true }, "@types/log-symbols": { @@ -701,7 +730,8 @@ "@types/mime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz", - "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==" + "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==", + "dev": true }, "@types/minimatch": { "version": "3.0.3", @@ -749,20 +779,21 @@ }, "@types/path-is-inside": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/@types/path-is-inside/-/path-is-inside-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/@types/path-is-inside/-/path-is-inside-1.0.0.tgz", "integrity": "sha512-hfnXRGugz+McgX2jxyy5qz9sB21LRzlGn24zlwN2KEgoPtEvjzNRrLtUkOOebPDPZl3Rq7ywKxYvylVcEZDnEw==", "dev": true }, "@types/prettyjson": { "version": "0.0.28", - "resolved": "http://registry.npmjs.org/@types/prettyjson/-/prettyjson-0.0.28.tgz", + "resolved": "https://registry.npmjs.org/@types/prettyjson/-/prettyjson-0.0.28.tgz", "integrity": "sha1-ExqJDe1kIbG1RfRRCkrqvG1GUnU=", "dev": true }, "@types/range-parser": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.2.tgz", - "integrity": "sha512-HtKGu+qG1NPvYe1z7ezLsyIaXYyi8SoAVqWDZgDQ8dLrsZvSzUNCwZyfX33uhWxL/SU0ZDQZ3nwZ0nimt507Kw==" + "integrity": "sha512-HtKGu+qG1NPvYe1z7ezLsyIaXYyi8SoAVqWDZgDQ8dLrsZvSzUNCwZyfX33uhWxL/SU0ZDQZ3nwZ0nimt507Kw==", + "dev": true }, "@types/rx": { "version": "4.1.1", @@ -900,6 +931,7 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "dev": true, "requires": { "@types/express-serve-static-core": "*", "@types/mime": "*" @@ -911,6 +943,25 @@ "integrity": "sha512-dA5z2WlP7uurAiveIWTDRgfr1U58Qdmo6doDeAyJlYFQ3vnUOW7BqJ+tl+M8FaLcflhrVvwIfzxJJvlz6anx4A==", "dev": true }, + "@types/superagent": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.4.tgz", + "integrity": "sha512-Dnh0Iw6NO55z1beXvlsvUrfk4cd9eL2nuTmUk+rAhSVCk10PGGFbqCCTwbau9D0d2W3DITiXl4z8VCqppGkMPQ==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "@types/supertest": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.6.tgz", + "integrity": "sha512-qRvPP8dO7IBqJz8LaQ7/Lw2oo/geiDUPAMx/L+CQCkR9sN622O30XCH7RSyUmilyCSyjxyhJ7cEtd3hmwPwvhw==", + "dev": true, + "requires": { + "@types/superagent": "*" + } + }, "@types/tar": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/tar/-/tar-4.0.0.tgz", @@ -931,7 +982,7 @@ }, "@types/undertaker": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/@types/undertaker/-/undertaker-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/@types/undertaker/-/undertaker-1.2.0.tgz", "integrity": "sha512-bx/5nZCGkasXs6qaA3B6SVDjBZqdyk04UO12e0uEPSzjt5H8jEJw0DKe7O7IM0hM2bVHRh70pmOH7PEHqXwzOw==", "dev": true, "requires": { @@ -992,7 +1043,7 @@ }, "@types/wrap-ansi": { "version": "3.0.0", - "resolved": "http://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", "dev": true }, @@ -1075,7 +1126,7 @@ }, "ansi-colors": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dev": true, "requires": { @@ -1412,7 +1463,7 @@ }, "axios": { "version": "0.18.0", - "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", "requires": { "follow-redirects": "^1.3.0", @@ -1444,7 +1495,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -1466,7 +1517,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -1991,7 +2042,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "requires": { "string-width": "^1.0.1", @@ -2010,7 +2061,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" @@ -2311,6 +2362,11 @@ "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", "dev": true }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" + }, "cookies": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.3.tgz", @@ -2862,7 +2918,7 @@ }, "enabled": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", "requires": { "env-variable": "0.0.x" @@ -3051,6 +3107,11 @@ "es5-ext": "~0.10.14" } }, + "eventemitter2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", + "integrity": "sha1-YZegldX7a1folC9v1+qtY6CclFI=" + }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -3401,6 +3462,11 @@ "mime-types": "^2.1.12" } }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==" + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -3464,24 +3530,24 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "resolved": false, "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "resolved": false, "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "aproba": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "resolved": false, "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "optional": true }, "are-we-there-yet": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "resolved": false, "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", "optional": true, "requires": { @@ -3491,12 +3557,12 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "resolved": false, "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { "balanced-match": "^1.0.0", @@ -3505,34 +3571,34 @@ }, "chownr": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "resolved": false, "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", "optional": true }, "code-point-at": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "resolved": false, "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "console-control-strings": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "core-util-is": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "resolved": false, "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "optional": true }, "debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "resolved": false, "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "optional": true, "requires": { @@ -3541,25 +3607,25 @@ }, "deep-extend": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", + "resolved": false, "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", "optional": true }, "delegates": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "resolved": false, "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "resolved": false, "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "optional": true }, "fs-minipass": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "resolved": false, "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "optional": true, "requires": { @@ -3568,13 +3634,13 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "resolved": false, "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "optional": true }, "gauge": { "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "resolved": false, "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "optional": true, "requires": { @@ -3590,7 +3656,7 @@ }, "glob": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "resolved": false, "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "optional": true, "requires": { @@ -3604,13 +3670,13 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "resolved": false, "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "optional": true }, "iconv-lite": { "version": "0.4.21", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", + "resolved": false, "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", "optional": true, "requires": { @@ -3619,7 +3685,7 @@ }, "ignore-walk": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "resolved": false, "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "optional": true, "requires": { @@ -3628,7 +3694,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "resolved": false, "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "optional": true, "requires": { @@ -3638,18 +3704,18 @@ }, "inherits": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "resolved": false, "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { "number-is-nan": "^1.0.0" @@ -3657,13 +3723,13 @@ }, "isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "resolved": false, "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { "brace-expansion": "^1.1.7" @@ -3671,12 +3737,12 @@ }, "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minipass": { "version": "2.2.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", + "resolved": false, "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "requires": { "safe-buffer": "^5.1.1", @@ -3685,7 +3751,7 @@ }, "minizlib": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", + "resolved": false, "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", "optional": true, "requires": { @@ -3694,7 +3760,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" @@ -3702,13 +3768,13 @@ }, "ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "resolved": false, "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "optional": true }, "needle": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.0.tgz", + "resolved": false, "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", "optional": true, "requires": { @@ -3719,7 +3785,7 @@ }, "node-pre-gyp": { "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz", + "resolved": false, "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", "optional": true, "requires": { @@ -3737,7 +3803,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "resolved": false, "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "optional": true, "requires": { @@ -3747,13 +3813,13 @@ }, "npm-bundled": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz", + "resolved": false, "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", "optional": true }, "npm-packlist": { "version": "1.1.10", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.10.tgz", + "resolved": false, "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", "optional": true, "requires": { @@ -3763,7 +3829,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "resolved": false, "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "optional": true, "requires": { @@ -3775,18 +3841,18 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "resolved": false, "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "optional": true }, "once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" @@ -3794,19 +3860,19 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "resolved": false, "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": false, "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "optional": true }, "osenv": { "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "resolved": false, "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "optional": true, "requires": { @@ -3816,19 +3882,19 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": false, "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "optional": true }, "process-nextick-args": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "resolved": false, "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "optional": true }, "rc": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.7.tgz", + "resolved": false, "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", "optional": true, "requires": { @@ -3840,7 +3906,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": false, "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "optional": true } @@ -3848,7 +3914,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": false, "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "optional": true, "requires": { @@ -3863,7 +3929,7 @@ }, "rimraf": { "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "resolved": false, "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "optional": true, "requires": { @@ -3872,42 +3938,42 @@ }, "safe-buffer": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "resolved": false, "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, "safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "resolved": false, "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "optional": true }, "sax": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "resolved": false, "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "optional": true }, "semver": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "resolved": false, "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "resolved": false, "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "resolved": false, "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "optional": true }, "string-width": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { "code-point-at": "^1.0.0", @@ -3917,7 +3983,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": false, "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "optional": true, "requires": { @@ -3926,7 +3992,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" @@ -3934,13 +4000,13 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "resolved": false, "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "optional": true }, "tar": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.1.tgz", + "resolved": false, "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", "optional": true, "requires": { @@ -3955,13 +4021,13 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "resolved": false, "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "optional": true }, "wide-align": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "resolved": false, "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", "optional": true, "requires": { @@ -3970,12 +4036,12 @@ }, "wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "resolved": false, "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "yallist": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "resolved": false, "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" } } @@ -4349,7 +4415,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -4362,7 +4428,7 @@ }, "os-locale": { "version": "1.4.0", - "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, "requires": { @@ -4400,7 +4466,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -4438,7 +4504,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -4462,7 +4528,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { @@ -4567,7 +4633,7 @@ "dependencies": { "chalk": { "version": "2.3.1", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", "dev": true, "requires": { @@ -4640,7 +4706,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -4701,7 +4767,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -4719,7 +4785,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -5135,7 +5201,7 @@ }, "is-builtin-module": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "dev": true, "requires": { @@ -5737,6 +5803,40 @@ } } }, + "koa-websocket": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/koa-websocket/-/koa-websocket-5.0.1.tgz", + "integrity": "sha512-NRfUdC1fGH9P45lOEygdPxdrJZ1aHoB9Kcn06WST/g4NtqIggH+ZzfATMeDpF0unjsvwVzxjNJoHIRIP9wY7gg==", + "requires": { + "co": "^4.4.0", + "debug": "^3.1.0", + "koa-compose": "^4.0.0", + "ws": "^5.2.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "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==" + }, + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, "kuler": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", @@ -6296,6 +6396,11 @@ "to-regex": "^3.0.2" } }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, "mime-db": { "version": "1.37.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", @@ -6324,7 +6429,7 @@ }, "minimist": { "version": "0.0.8", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minimist-options": { @@ -6400,7 +6505,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" @@ -6713,7 +6818,7 @@ }, "lodash": { "version": "2.4.1", - "resolved": "http://registry.npmjs.org/lodash/-/lodash-2.4.1.tgz", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.1.tgz", "integrity": "sha1-W3cjA03aTSYuWkb7LFjXzCL3FCA=", "dev": true } @@ -8535,7 +8640,7 @@ }, "pegjs": { "version": "0.10.0", - "resolved": "http://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz", + "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz", "integrity": "sha1-z4uvrm7d/0tafvsYUmnqr0YQ3b0=", "dev": true }, @@ -8567,7 +8672,7 @@ }, "pkijs": { "version": "1.3.33", - "resolved": "http://registry.npmjs.org/pkijs/-/pkijs-1.3.33.tgz", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-1.3.33.tgz", "integrity": "sha1-ponvYhE7fDSOH/wJll0iOeW7TJI=" }, "plugin-error": { @@ -8594,7 +8699,7 @@ }, "pretty-hrtime": { "version": "1.0.3", - "resolved": "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true }, @@ -8786,7 +8891,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -9072,7 +9177,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "requires": { "ret": "~0.1.10" @@ -9209,7 +9314,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -10073,7 +10178,7 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "strip-indent": { @@ -10082,6 +10187,47 @@ "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", "dev": true }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "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==", + "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==" + } + } + }, + "supertest": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.3.0.tgz", + "integrity": "sha512-dMQSzYdaZRSANH5LL8kX3UpgK9G1LRh/jnggs/TI0W2Sz7rkMx9Y48uia3K9NgcaWEV28tYkBnXE4tiFC77ygQ==", + "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", @@ -10255,7 +10401,7 @@ }, "through": { "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { @@ -10290,6 +10436,12 @@ "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", "dev": true }, + "timekeeper": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.1.2.tgz", + "integrity": "sha512-fc1DDqbiyz5vxRO4xkiATwfWUw1FV7W20+FJYal1SnoIYgNuB4WNxYLtbG3zjUBwOSk3P4u1TgBAZYG/aqBWMw==", + "dev": true + }, "timers-ext": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.5.tgz", @@ -10467,7 +10619,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -11097,7 +11249,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "requires": { "cliui": "^4.0.0", diff --git a/garden-service/package.json b/garden-service/package.json index 4722115848..2db97c5db7 100644 --- a/garden-service/package.json +++ b/garden-service/package.json @@ -23,7 +23,6 @@ ], "dependencies": { "@drubin/client-node": "0.7.1-rc1", - "@types/koa-bodyparser": "^4.2.1", "ansi-escapes": "^3.1.0", "async-exit-hook": "^2.0.1", "async-lock": "^1.1.3", @@ -45,6 +44,7 @@ "dockerode": "^2.5.7", "elegant-spinner": "^1.0.1", "escape-string-regexp": "^1.0.5", + "eventemitter2": "^5.0.1", "execa": "^1.0.0", "fs-extra": "^7.0.0", "get-port": "^4.0.0", @@ -58,6 +58,7 @@ "koa": "^2.6.2", "koa-bodyparser": "^4.2.1", "koa-router": "^7.4.0", + "koa-websocket": "^5.0.1", "lodash": "^4.17.11", "log-symbols": "^2.2.0", "moment": "^2.22.2", @@ -68,6 +69,7 @@ "split": "^1.0.1", "string-width": "^2.1.1", "strip-ansi": "^5.0.0", + "supertest": "^3.3.0", "sywac": "^1.2.1", "tar": "^4.4.6", "terminal-link": "^1.1.0", @@ -101,7 +103,9 @@ "@types/json-stringify-safe": "^5.0.0", "@types/klaw": "^2.1.1", "@types/koa": "^2.0.47", + "@types/koa-bodyparser": "^4.2.1", "@types/koa-router": "^7.0.35", + "@types/koa-websocket": "^5.0.3", "@types/lodash": "^4.14.117", "@types/log-symbols": "^2.0.0", "@types/log-update": "^2.0.0", @@ -114,6 +118,7 @@ "@types/path-is-inside": "^1.0.0", "@types/prettyjson": "0.0.28", "@types/string-width": "^2.0.0", + "@types/supertest": "^2.0.6", "@types/tar": "^4.0.0", "@types/uniqid": "^4.1.2", "@types/unzip": "^0.1.1", @@ -138,6 +143,7 @@ "snyk": "^1.105.0", "testdouble": "^3.8.2", "testdouble-chai": "^0.5.0", + "timekeeper": "^2.1.2", "tmp-promise": "^1.0.5", "ts-node": "^7.0.1", "tslint": "^5.11.0", diff --git a/garden-service/src/cli/cli.ts b/garden-service/src/cli/cli.ts index 6ffcd9e55b..60fd919383 100644 --- a/garden-service/src/cli/cli.ts +++ b/garden-service/src/cli/cli.ts @@ -11,7 +11,6 @@ import { intersection, merge, range } from "lodash" import { resolve } from "path" import { safeDump } from "js-yaml" import { coreCommands } from "../commands/commands" - import { DeepPrimitiveMap } from "../config/common" import { getEnumKeys, shutdown, sleep } from "../util/util" import { @@ -24,8 +23,7 @@ import { StringParameter, } from "../commands/base" import { GardenError, InternalError, PluginError, toGardenError } from "../exceptions" -import { ContextOpts, Garden } from "../garden" - +import { Garden, GardenOpts } from "../garden" import { getLogger, Logger, LoggerType } from "../logger/logger" import { LogLevel } from "../logger/log-node" import { BasicTerminalWriter } from "../logger/writers/basic-terminal-writer" @@ -268,7 +266,7 @@ export class GardenCli { // entries (i.e. print new lines). const log = logger.placeholder() - const contextOpts: ContextOpts = { env, log } + const contextOpts: GardenOpts = { environmentName: env, log } if (command.noProject) { contextOpts.config = MOCK_CONFIG } diff --git a/garden-service/src/commands/build.ts b/garden-service/src/commands/build.ts index dffc2b2f30..a6bc4d351a 100644 --- a/garden-service/src/commands/build.ts +++ b/garden-service/src/commands/build.ts @@ -22,7 +22,7 @@ import { Module } from "../types/module" import { logHeader } from "../logger/util" const buildArguments = { - module: new StringsParameter({ + modules: new StringsParameter({ help: "Specify module(s) to build. Use comma as a separator to specify multiple modules.", }), } @@ -66,7 +66,8 @@ export class BuildCommand extends Command { { args, opts, garden, log }: CommandParams, ): Promise> { await garden.clearBuilds() - const modules = await garden.getModules(args.module) + + const modules = await garden.getModules(args.modules) const dependencyGraph = await garden.getDependencyGraph() const moduleNames = modules.map(m => m.name) diff --git a/garden-service/src/commands/call.ts b/garden-service/src/commands/call.ts index 154a891ab1..91913f2b15 100644 --- a/garden-service/src/commands/call.ts +++ b/garden-service/src/commands/call.ts @@ -24,7 +24,7 @@ import dedent = require("dedent") const callArgs = { serviceAndPath: new StringParameter({ - help: "The name(s) of the service(s) to call followed by the ingress path (e.g. my-container/somepath).", + help: "The name of the service to call followed by the ingress path (e.g. my-container/somepath).", required: true, }), } diff --git a/garden-service/src/commands/delete.ts b/garden-service/src/commands/delete.ts index 410da6aea8..6c4cefcf85 100644 --- a/garden-service/src/commands/delete.ts +++ b/garden-service/src/commands/delete.ts @@ -105,7 +105,7 @@ export class DeleteEnvironmentCommand extends Command { } const deleteServiceArgs = { - service: new StringsParameter({ + services: new StringsParameter({ help: "The name(s) of the service(s) to delete. Use comma as a separator to specify multiple services.", required: true, }), @@ -114,7 +114,8 @@ type DeleteServiceArgs = typeof deleteServiceArgs export class DeleteServiceCommand extends Command { name = "service" - help = "Deletes a running service." + alias = "services" + help = "Deletes running services." arguments = deleteServiceArgs description = dedent` @@ -128,7 +129,7 @@ export class DeleteServiceCommand extends Command { ` async action({ garden, log, args }: CommandParams): Promise { - const services = await garden.getServices(args.service) + const services = await garden.getServices(args.services) if (services.length === 0) { log.warn({ msg: "No services found. Aborting." }) diff --git a/garden-service/src/commands/deploy.ts b/garden-service/src/commands/deploy.ts index 8119ecf188..cbe354c932 100644 --- a/garden-service/src/commands/deploy.ts +++ b/garden-service/src/commands/deploy.ts @@ -24,7 +24,7 @@ import { processServices } from "../process" import { logHeader } from "../logger/util" const deployArgs = { - service: new StringsParameter({ + services: new StringsParameter({ help: deline`The name(s) of the service(s) to deploy (skip to deploy all services). Use comma as a separator to specify multiple services.`, }), @@ -64,6 +64,7 @@ export class DeployCommand extends Command { garden deploy # deploy all modules in the project garden deploy my-service # only deploy my-service + garden deploy service-a,service-b # only deploy service-a and service-b garden deploy --force # force re-deploy of modules, even if they're already deployed garden deploy --watch # watch for changes to code garden deploy --hot-reload=my-service # deploys all services, with hot reloading enabled for my-service @@ -78,7 +79,7 @@ export class DeployCommand extends Command { } async action({ garden, log, args, opts }: CommandParams): Promise> { - const services = await garden.getServices(args.service) + const services = await garden.getServices(args.services) if (services.length === 0) { log.error({ msg: "No services found. Aborting." }) diff --git a/garden-service/src/commands/get/get-config.ts b/garden-service/src/commands/get/get-config.ts index f376ff359d..002a521ef0 100644 --- a/garden-service/src/commands/get/get-config.ts +++ b/garden-service/src/commands/get/get-config.ts @@ -8,43 +8,15 @@ import * as yaml from "js-yaml" import { highlightYaml } from "../../util/util" -import { Provider } from "../../config/project" -import { PrimitiveMap } from "../../config/common" -import { Module } from "../../types/module" import { Command, CommandResult, CommandParams } from "../base" - -interface ConfigOutput { - environmentName: string - providers: Provider[] - variables: PrimitiveMap - modules: Module[] -} +import { ConfigDump } from "../../garden" export class GetConfigCommand extends Command { name = "config" help = "Outputs the fully resolved configuration for this project and environment." - async action({ garden, log }: CommandParams): Promise> { - const modules = await garden.getModules() - - // Remove circular references and superfluous keys. - for (const module of modules) { - delete module._ConfigType - - for (const service of module.services) { - delete service.module - } - for (const task of module.tasks) { - delete task.module - } - } - - const config: ConfigOutput = { - environmentName: garden.environment.name, - providers: garden.environment.providers, - variables: garden.environment.variables, - modules, - } + async action({ garden, log }: CommandParams): Promise> { + const config = await garden.dumpConfig() const yamlConfig = yaml.safeDump(config, { noRefs: true, skipInvalid: true }) diff --git a/garden-service/src/commands/logs.ts b/garden-service/src/commands/logs.ts index 4b19ec5819..9a45863d27 100644 --- a/garden-service/src/commands/logs.ts +++ b/garden-service/src/commands/logs.ts @@ -22,7 +22,7 @@ import { LoggerType } from "../logger/logger" import dedent = require("dedent") const logsArgs = { - service: new StringsParameter({ + services: new StringsParameter({ help: "The name(s) of the service(s) to log (skip to log all services). " + "Use comma as a separator to specify multiple services.", }), @@ -61,7 +61,7 @@ export class LogsCommand extends Command { async action({ garden, log, args, opts }: CommandParams): Promise> { const tail = opts.tail - const services = await garden.getServices(args.service) + const services = await garden.getServices(args.services) const result: ServiceLogEntry[] = [] const stream = new Stream() diff --git a/garden-service/src/commands/publish.ts b/garden-service/src/commands/publish.ts index 1bc15ea228..a0ff471461 100644 --- a/garden-service/src/commands/publish.ts +++ b/garden-service/src/commands/publish.ts @@ -24,7 +24,7 @@ import { logHeader } from "../logger/util" import dedent = require("dedent") const publishArgs = { - module: new StringsParameter({ + modules: new StringsParameter({ help: "The name(s) of the module(s) to publish (skip to publish all modules). " + "Use comma as a separator to specify multiple modules.", }), @@ -64,7 +64,7 @@ export class PublishCommand extends Command { async action({ garden, log, args, opts }: CommandParams): Promise> { logHeader({ log, emoji: "rocket", command: "Publish modules" }) - const modules = await garden.getModules(args.module) + const modules = await garden.getModules(args.modules) const results = await publishModules(garden, log, modules, !!opts["force-build"], !!opts["allow-dirty"]) diff --git a/garden-service/src/commands/serve.ts b/garden-service/src/commands/serve.ts index 8e03147ddf..aa6b1d05c3 100644 --- a/garden-service/src/commands/serve.ts +++ b/garden-service/src/commands/serve.ts @@ -11,7 +11,7 @@ import { LoggerType } from "../logger/logger" import { IntegerParameter } from "./base" import { Command, CommandResult, CommandParams } from "./base" import { sleep } from "../util/util" -import { startServer } from "../server" +import { startServer } from "../server/server" const serveArgs = {} diff --git a/garden-service/src/commands/test.ts b/garden-service/src/commands/test.ts index b9c548f80a..5b35d11bd1 100644 --- a/garden-service/src/commands/test.ts +++ b/garden-service/src/commands/test.ts @@ -26,7 +26,7 @@ import { getTestTasks } from "../tasks/test" import { logHeader } from "../logger/util" const testArgs = { - module: new StringsParameter({ + modules: new StringsParameter({ help: "The name(s) of the module(s) to test (skip to test all modules). " + "Use comma as a separator to specify multiple modules.", }), @@ -83,8 +83,8 @@ export class TestCommand extends Command { async action({ garden, log, args, opts }: CommandParams): Promise> { const dependencyGraph = await garden.getDependencyGraph() let modules: Module[] - if (args.module) { - modules = await dependencyGraph.withDependantModules(await garden.getModules(args.module)) + if (args.modules) { + modules = await dependencyGraph.withDependantModules(await garden.getModules(args.modules)) } else { // All modules are included in this case, so there's no need to compute dependants. modules = await garden.getModules() diff --git a/garden-service/src/commands/unlink/module.ts b/garden-service/src/commands/unlink/module.ts index 897179710b..984da69c5a 100644 --- a/garden-service/src/commands/unlink/module.ts +++ b/garden-service/src/commands/unlink/module.ts @@ -23,7 +23,7 @@ import { } from "../../config-store" const unlinkModuleArguments = { - module: new StringsParameter({ + modules: new StringsParameter({ help: "The name(s) of the module(s) to unlink. Use comma as a separator to specify multiple modules.", }), } @@ -59,7 +59,7 @@ export class UnlinkModuleCommand extends Command { const sourceType = "module" - const { module = [] } = args + const { modules = [] } = args if (opts.all) { await garden.localConfigStore.set([localConfigKeys.linkedModuleSources], []) @@ -67,7 +67,7 @@ export class UnlinkModuleCommand extends Command { return { result: [] } } - const linkedModuleSources = await removeLinkedSources({ garden, sourceType, names: module }) + const linkedModuleSources = await removeLinkedSources({ garden, sourceType, names: modules }) log.info(`Unlinked module(s) ${module}`) diff --git a/garden-service/src/commands/unlink/source.ts b/garden-service/src/commands/unlink/source.ts index e829977728..dc7406d66b 100644 --- a/garden-service/src/commands/unlink/source.ts +++ b/garden-service/src/commands/unlink/source.ts @@ -23,7 +23,7 @@ import { } from "../../config-store" const unlinkSourceArguments = { - source: new StringsParameter({ + sources: new StringsParameter({ help: "The name(s) of the source(s) to unlink. Use comma as a separator to specify multiple sources.", }), } @@ -59,7 +59,7 @@ export class UnlinkSourceCommand extends Command { const sourceType = "project" - const { source = [] } = args + const { sources = [] } = args if (opts.all) { await garden.localConfigStore.set([localConfigKeys.linkedProjectSources], []) @@ -67,9 +67,9 @@ export class UnlinkSourceCommand extends Command { return { result: [] } } - const linkedProjectSources = await removeLinkedSources({ garden, sourceType, names: source }) + const linkedProjectSources = await removeLinkedSources({ garden, sourceType, names: sources }) - log.info(`Unlinked source(s) ${source}`) + log.info(`Unlinked source(s) ${sources}`) return { result: linkedProjectSources } } diff --git a/garden-service/src/commands/update-remote/all.ts b/garden-service/src/commands/update-remote/all.ts index acded19997..02d43ec82d 100644 --- a/garden-service/src/commands/update-remote/all.ts +++ b/garden-service/src/commands/update-remote/all.ts @@ -42,13 +42,13 @@ export class UpdateRemoteAllCommand extends Command { const { result: projectSources } = await sourcesCmd.action({ garden, log, - args: { source: undefined }, + args: { sources: undefined }, opts: {}, }) const { result: moduleSources } = await modulesCmd.action({ garden, log, - args: { module: undefined }, + args: { modules: undefined }, opts: {}, }) diff --git a/garden-service/src/commands/update-remote/modules.ts b/garden-service/src/commands/update-remote/modules.ts index 5a4cf4ff68..e5d0c8e1b9 100644 --- a/garden-service/src/commands/update-remote/modules.ts +++ b/garden-service/src/commands/update-remote/modules.ts @@ -23,7 +23,7 @@ import { hasRemoteSource } from "../../util/ext-source-util" import { logHeader } from "../../logger/util" const updateRemoteModulesArguments = { - module: new StringsParameter({ + modules: new StringsParameter({ help: "The name(s) of the remote module(s) to update. Use comma as a separator to specify multiple modules.", }), } @@ -48,16 +48,16 @@ export class UpdateRemoteModulesCommand extends Command { async action({ garden, log, args }: CommandParams): Promise> { logHeader({ log, emoji: "hammer_and_wrench", command: "update-remote modules" }) - const { module } = args - const modules = await garden.getModules(module) + const { modules: moduleNames } = args + const modules = await garden.getModules(moduleNames) const moduleSources = modules .filter(hasRemoteSource) - .filter(src => module ? module.includes(src.name) : true) + .filter(src => moduleNames ? moduleNames.includes(src.name) : true) const names = moduleSources.map(src => src.name) - const diff = difference(module, names) + const diff = difference(moduleNames, names) if (diff.length > 0) { const modulesWithRemoteSource = (await garden.getModules()).filter(hasRemoteSource).sort() @@ -65,7 +65,7 @@ export class UpdateRemoteModulesCommand extends Command { `Expected module(s) ${chalk.underline(diff.join(","))} to have a remote source.`, { modulesWithRemoteSource, - input: module ? module.sort() : undefined, + input: moduleNames ? moduleNames.sort() : undefined, }, ) } diff --git a/garden-service/src/commands/update-remote/sources.ts b/garden-service/src/commands/update-remote/sources.ts index ef51b4c467..bf971bae3d 100644 --- a/garden-service/src/commands/update-remote/sources.ts +++ b/garden-service/src/commands/update-remote/sources.ts @@ -22,7 +22,7 @@ import { SourceConfig } from "../../config/project" import { logHeader } from "../../logger/util" const updateRemoteSourcesArguments = { - source: new StringsParameter({ + sources: new StringsParameter({ help: "The name(s) of the remote source(s) to update. Use comma as a separator to specify multiple sources.", }), } @@ -48,21 +48,21 @@ export class UpdateRemoteSourcesCommand extends Command { ): Promise> { logHeader({ log, emoji: "hammer_and_wrench", command: "update-remote sources" }) - const { source } = args + const { sources } = args const projectSources = garden.projectSources - .filter(src => source ? source.includes(src.name) : true) + .filter(src => sources ? sources.includes(src.name) : true) const names = projectSources.map(src => src.name) // TODO: Make external modules a cli type to avoid validation repetition - const diff = difference(source, names) + const diff = difference(sources, names) if (diff.length > 0) { throw new ParameterError( `Expected source(s) ${chalk.underline(diff.join(","))} to be specified in the project garden.yml config.`, { remoteSources: garden.projectSources.map(s => s.name).sort(), - input: source ? source.sort() : undefined, + input: sources ? sources.sort() : undefined, }, ) } diff --git a/garden-service/src/events.ts b/garden-service/src/events.ts new file mode 100644 index 0000000000..51027d7a18 --- /dev/null +++ b/garden-service/src/events.ts @@ -0,0 +1,61 @@ +/* + * 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 { EventEmitter2 } from "eventemitter2" +import { TaskResult } from "./task-graph" +import { ModuleVersion } from "./vcs/base" + +/** + * This simple class serves as the central event bus for a Garden instance. Its function + * is mainly to consolidate all events for the instance, to ensure type-safety. + * + * See below for the event interfaces. + */ +export class EventBus extends EventEmitter2 { + constructor() { + super({ + wildcard: false, + newListener: false, + maxListeners: 100, // we may need to adjust this + }) + } + + emit(name: T, payload: Events[T]) { + return super.emit(name, payload) + } + + on(name: T, listener: (payload: Events[T]) => void) { + return super.on(name, listener) + } + + onAny(listener: (name: T, payload: Events[T]) => void) { + return super.onAny(listener) + } + + once(name: T, listener: (payload: Events[T]) => void) { + return super.once(name, listener) + } + + // TODO: wrap more methods to make them type-safe +} + +/** + * The supported events and their interfaces. + */ +export interface Events { + _test: string, + taskPending: { + addedAt: Date, + key: string, + version: ModuleVersion, + }, + taskComplete: TaskResult, + taskError: TaskResult, +} + +export type EventName = keyof Events diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index 4ed440a24c..3a2a65b266 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -107,6 +107,7 @@ import { ModuleAndRuntimeActions, Plugins, RegisterPluginParam } from "./types/p import { SUPPORTED_PLATFORMS, SupportedPlatform } from "./constants" import { platform, arch } from "os" import { LogEntry } from "./logger/log-entry" +import { EventBus } from "./events" export interface ActionHandlerMap { [actionName: string]: PluginActions[T] @@ -130,9 +131,9 @@ export type ModuleActionMap = { } } -export interface ContextOpts { +export interface GardenOpts { config?: GardenConfig, - env?: string, + environmentName?: string, log?: LogEntry, plugins?: Plugins, } @@ -159,6 +160,7 @@ export class Garden { public readonly vcs: VcsHandler public readonly cache: TreeCache public readonly actions: ActionHelper + public readonly events: EventBus constructor( public readonly projectRoot: string, @@ -167,7 +169,7 @@ export class Garden { variables: PrimitiveMap, public readonly projectSources: SourceConfig[] = [], public readonly buildDir: BuildDir, - log?: LogEntry, + public readonly opts: GardenOpts, ) { // make sure we're on a supported platform const currentPlatform = platform() @@ -182,7 +184,7 @@ export class Garden { } this.modulesScanned = false - this.log = log || getLogger().placeholder() + this.log = opts.log || getLogger().placeholder() // TODO: Support other VCS options. this.vcs = new GitHandler(this.projectRoot) this.localConfigStore = new LocalConfigStore(this.projectRoot) @@ -203,13 +205,17 @@ export class Garden { this.actionHandlers = fromPairs(pluginActionNames.map(n => [n, {}])) this.moduleActionHandlers = fromPairs(moduleActionNames.map(n => [n, {}])) - this.taskGraph = new TaskGraph(this.log) + this.taskGraph = new TaskGraph(this, this.log) this.actions = new ActionHelper(this) this.hotReloadScheduler = new HotReloadScheduler() + this.events = new EventBus() } - static async factory(currentDirectory: string, { env, config, log, plugins = {} }: ContextOpts = {}) { + static async factory( + this: T, currentDirectory: string, opts: GardenOpts = {}, + ): Promise> { let parsedConfig: GardenConfig + let { environmentName, config, plugins = {} } = opts if (config) { parsedConfig = validate(config, configSchema, { context: "root configuration" }) @@ -243,12 +249,12 @@ export class Garden { sources: projectSources, } = parsedConfig.project! - if (!env) { - env = defaultEnvironment + if (!environmentName) { + environmentName = defaultEnvironment } - const parts = env.split(".") - const environmentName = parts[0] + const parts = environmentName.split(".") + environmentName = parts[0] const namespace = parts.slice(1).join(".") || DEFAULT_NAMESPACE const environmentConfig = findByName(environments, environmentName) @@ -256,7 +262,7 @@ export class Garden { if (!environmentConfig) { throw new ParameterError(`Project ${projectName} does not specify environment ${environmentName}`, { projectName, - env, + environmentName, definedEnvironments: getNames(environments), }) } @@ -264,7 +270,7 @@ export class Garden { if (!environmentConfig.providers || environmentConfig.providers.length === 0) { throw new ConfigurationError(`Environment '${environmentName}' does not specify any providers`, { projectName, - env, + environmentName, environmentConfig, }) } @@ -288,25 +294,26 @@ export class Garden { const buildDir = await BuildDir.factory(projectRoot) - const garden = new Garden( + const garden = new this( projectRoot, projectName, environmentName, variables, projectSources, buildDir, - log, - ) + opts, + ) as InstanceType // Register plugins for (const [name, pluginFactory] of Object.entries({ ...builtinPlugins, ...plugins })) { - garden.registerPlugin(name, pluginFactory) + // This cast is required for the linter to accept the instance type hackery. + (garden).registerPlugin(name, pluginFactory) } // Load configured plugins // Validate configuration for (const provider of Object.values(mergedProviderConfigs)) { - await garden.loadPlugin(provider.name, provider) + await (garden).loadPlugin(provider.name, provider) } return garden @@ -1082,5 +1089,35 @@ export class Garden { } } + public async dumpConfig(): Promise { + const modules = await this.getModules() + + // Remove circular references and superfluous keys. + for (const module of modules) { + delete module._ConfigType + + for (const service of module.services) { + delete service.module + } + for (const task of module.tasks) { + delete task.module + } + } + + return { + environmentName: this.environment.name, + providers: this.environment.providers, + variables: this.environment.variables, + modules: sortBy(modules, "name"), + } + } + //endregion } + +export interface ConfigDump { + environmentName: string + providers: Provider[] + variables: PrimitiveMap + modules: Module[] +} diff --git a/garden-service/src/plugins/kubernetes/system.ts b/garden-service/src/plugins/kubernetes/system.ts index 4c9f191927..53ad297fa7 100644 --- a/garden-service/src/plugins/kubernetes/system.ts +++ b/garden-service/src/plugins/kubernetes/system.ts @@ -22,7 +22,7 @@ export function isSystemGarden(provider: KubernetesProvider): boolean { export async function getSystemGarden(provider: KubernetesProvider): Promise { return Garden.factory(systemProjectPath, { - env: "default", + environmentName: "default", config: { version: "0", dirname: "system", diff --git a/garden-service/src/plugins/openfaas/openfaas.ts b/garden-service/src/plugins/openfaas/openfaas.ts index 2d5bda90f4..c023f342b6 100644 --- a/garden-service/src/plugins/openfaas/openfaas.ts +++ b/garden-service/src/plugins/openfaas/openfaas.ts @@ -434,7 +434,7 @@ export async function getOpenFaasGarden(ctx: PluginContext): Promise { // TODO: allow passing variables/parameters here to be parsed as part of the garden.yml project config // (this would allow us to use a garden.yml for the project config, instead of speccing it here) return Garden.factory(systemProjectPath, { - env: "default", + environmentName: "default", config: { version: "0", dirname: "system", diff --git a/garden-service/src/process.ts b/garden-service/src/process.ts index e446d8bb9d..04c1809aa8 100644 --- a/garden-service/src/process.ts +++ b/garden-service/src/process.ts @@ -19,7 +19,7 @@ import { registerCleanupFunction } from "./util/util" import { isModuleLinked } from "./util/ext-source-util" import { Garden } from "./garden" import { LogEntry } from "./logger/log-entry" -import { startServer } from "./server" +import { startServer } from "./server/server" export type ProcessHandler = (module: Module) => Promise diff --git a/garden-service/src/server.ts b/garden-service/src/server/commands.ts similarity index 52% rename from garden-service/src/server.ts rename to garden-service/src/server/commands.ts index e8e621ed8f..191f851713 100644 --- a/garden-service/src/server.ts +++ b/garden-service/src/server/commands.ts @@ -6,99 +6,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import chalk from "chalk" -import Koa = require("koa") -import Router = require("koa-router") -import bodyParser = require("koa-bodyparser") -import dedent = require("dedent") import Joi = require("joi") -import getPort = require("get-port") -import { Command, Parameters } from "./commands/base" -import { validate } from "./config/common" -import { coreCommands } from "./commands/commands" +import Koa = require("koa") +import { Command, Parameters } from "../commands/base" +import { validate } from "../config/common" +import { coreCommands } from "../commands/commands" import { mapValues, omitBy } from "lodash" -import { Garden } from "./garden" -import { LogLevel } from "./logger/log-node" - -/** - * Start an HTTP server that exposes commands and events for the given Garden instance. - * - * NOTE: - * If `port` is not specified, a random free port is chosen. This is done so that a process can always create its - * own server, but we won't need that functionality once we run a shared service across commands. - */ -export async function startServer(garden: Garden, port?: number) { - // prepare request-command map - const commands = await prepareCommands() - - const app = new Koa() - const http = new Router() - - /** - * HTTP API endpoint (POST /api) - * - * We don't expose a different route per command, but rather accept a JSON object via POST on /api - * with a `command` key. The API wouldn't be RESTful in any meaningful sense anyway, and this - * means we can keep a consistent format across mechanisms. - */ - http.post("/api", async (ctx) => { - // TODO: set response code when errors are in result object? - const result = await resolveRequest(ctx, garden, commands, ctx.request.body) - - ctx.response.body = result - }) - - /** - * Dashboard endpoint (GET /) - * - * TODO: flesh this out, just a placeholder - */ - http.get("/", async (ctx) => { - const status = await resolveRequest(ctx, garden, commands, { - command: "get.status", - }) - - ctx.response.body = dedent` - - -

Project status

-
-      ${JSON.stringify(status.result, null, 4)}
-        
- - - ` - }) - - app.use(bodyParser()) - app.use(http.routes()) - app.use(http.allowedMethods()) +import { Garden } from "../garden" +import { LogLevel } from "../logger/log-node" - // TODO: implement WebSocket endpoint - // const ws = new Router() - // ws.get("/ws", async (ctx) => { - - // }) - // app.use(ws.routes()) - // app.use(ws.allowedMethods()) - - // TODO: remove this once we stop running a server per CLI command - if (!port) { - port = await getPort() - } - - // TODO: secure the server - app.listen(port) - - const url = `http://localhost:${port}` - - garden.log.info({ - emoji: "sunflower", - msg: chalk.cyan("Garden dashboard and API server running on ") + url, - }) -} - -interface CommandMap { +export interface CommandMap { [key: string]: { command: Command, requestSchema: Joi.ObjectSchema, @@ -114,6 +31,7 @@ const baseRequestSchema = Joi.object() .example("get.status"), parameters: Joi.object() .keys({}) + .unknown(true) .default(() => ({}), "{}") .description("The parameters for the command."), }) @@ -121,7 +39,9 @@ const baseRequestSchema = Joi.object() /** * Validate and map a request body to a Command, execute its action, and return its result. */ -async function resolveRequest(ctx: Koa.Context, garden: Garden, commands: CommandMap, request: any) { +export async function resolveRequest( + ctx: Koa.Context, garden: Garden, commands: CommandMap, request: any, +) { // Perform basic validation and find command. try { request = validate(request, baseRequestSchema, { context: "API request" }) @@ -147,19 +67,19 @@ async function resolveRequest(ctx: Koa.Context, garden: Garden, commands: Comman // TODO: Creating a new Garden instance is not ideal, // need to revisit once we've refactored the TaskGraph and config resolution. - const cmdGarden = await Garden.factory(garden.projectRoot, { log: garden.log }) + const cmdGarden = await Garden.factory(garden.projectRoot, garden.opts) // We generally don't want actions to log anything in the server. - const log = garden.log.placeholder(LogLevel.silly) + const cmdLog = garden.log.placeholder(LogLevel.silly) const cmdArgs = mapParams(ctx, request.parameters, command.arguments) const cmdOpts = mapParams(ctx, request.parameters, command.options) - return command.action({ garden: cmdGarden, log, args: cmdArgs, opts: cmdOpts }) + return command.action({ garden: cmdGarden, log: cmdLog, args: cmdArgs, opts: cmdOpts }) // TODO: validate result schema } -async function prepareCommands(): Promise { +export async function prepareCommands(): Promise { const commands: CommandMap = {} function addCommand(command: Command) { @@ -169,7 +89,8 @@ async function prepareCommands(): Promise { .keys({ ...paramsToJoi(command.arguments), ...paramsToJoi(command.options), - }), + }) + .unknown(false), }) commands[command.getKey()] = { @@ -212,7 +133,7 @@ function mapParams(ctx: Koa.Context, values: object, params?: Parameters) { return {} } - return mapValues(params, (p, key) => { + const output = mapValues(params, (p, key) => { if (p.cliOnly) { return p.defaultValue } @@ -225,4 +146,6 @@ function mapParams(ctx: Koa.Context, values: object, params?: Parameters) { } return result.value }) + + return output } diff --git a/garden-service/src/server/server.ts b/garden-service/src/server/server.ts new file mode 100644 index 0000000000..9970eab27d --- /dev/null +++ b/garden-service/src/server/server.ts @@ -0,0 +1,103 @@ +/* + * 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 Koa = require("koa") +import Router = require("koa-router") +import websockify = require("koa-websocket") +import bodyParser = require("koa-bodyparser") +import dedent = require("dedent") +import getPort = require("get-port") +import { Garden } from "../garden" +import { addWebsocketEndpoint } from "./websocket" +import { prepareCommands, resolveRequest } from "./commands" + +/** + * Start an HTTP server that exposes commands and events for the given Garden instance. + * + * Please look at the tests for usage examples. + * + * NOTE: + * If `port` is not specified, a random free port is chosen. This is done so that a process can always create its + * own server, but we won't need that functionality once we run a shared service across commands. + */ +export async function startServer(garden: Garden, port?: number) { + const app = await createApp(garden) + + // TODO: remove this once we stop running a server per CLI command + if (!port) { + port = await getPort() + } + + // TODO: secure the server + const server = app.listen(port) + + const url = `http://localhost:${port}` + + garden.log.info({ + emoji: "sunflower", + msg: chalk.cyan("Garden dashboard and API server running on ") + url, + }) + + return server +} + +export async function createApp(garden: Garden) { + const log = garden.log.placeholder() + + // prepare request-command map + const commands = await prepareCommands() + + const app = websockify(new Koa()) + const http = new Router() + + /** + * HTTP API endpoint (POST /api) + * + * We don't expose a different route per command, but rather accept a JSON object via POST on /api + * with a `command` key. The API wouldn't be RESTful in any meaningful sense anyway, and this + * means we can keep a consistent format across mechanisms. + */ + http.post("/api", async (ctx) => { + // TODO: set response code when errors are in result object? + const result = await resolveRequest(ctx, garden, commands, ctx.request.body) + + ctx.status = 200 + ctx.response.body = result + }) + + /** + * Dashboard endpoint (GET /) + * + * TODO: flesh this out, just a placeholder + */ + http.get("/", async (ctx) => { + const status = await resolveRequest(ctx, garden, commands, { + command: "get.status", + }) + + ctx.response.body = dedent` + + +

Project status

+
+      ${JSON.stringify(status.result, null, 4)}
+        
+ + + ` + }) + + app.use(bodyParser()) + app.use(http.routes()) + app.use(http.allowedMethods()) + + addWebsocketEndpoint(app, garden, log, commands) + + return app +} diff --git a/garden-service/src/server/websocket.ts b/garden-service/src/server/websocket.ts new file mode 100644 index 0000000000..6fc66d1d87 --- /dev/null +++ b/garden-service/src/server/websocket.ts @@ -0,0 +1,108 @@ +/* + * 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 Joi = require("joi") +import Router = require("koa-router") +import websockify = require("koa-websocket") +import { Garden } from "../garden" +import { CommandResult } from "../commands/base" +import { EventName, Events } from "../events" +import { ValueOf } from "../util/util" +import { resolveRequest, CommandMap } from "./commands" +import { omit } from "lodash" +import { LogEntry } from "../logger/log-entry" +import { toGardenError, GardenError } from "../exceptions" + +/** + * Add the /ws endpoint to the Koa app. Every event emitted to the event bus is forwarded to open + * Websocket connections, and clients can send commands over the socket and receive results on the + * same connection. + */ +export function addWebsocketEndpoint(app: websockify.App, garden: Garden, log: LogEntry, commands: CommandMap) { + const ws = new Router() + + ws.get("/ws", async (ctx) => { + // Helper to make JSON messages, make them type-safe, and to log errors. + const send = (type: T, payload: ServerWebsocketMessages[T]) => { + ctx.websocket.send(JSON.stringify({ type, ...payload }), (err) => { + if (err) { + const error = toGardenError(err) + log.error({ error }) + } + }) + } + + // Pipe everything from the event bus to the socket. + const eventListener = (name, payload) => send("event", { name, payload }) + garden.events.onAny(eventListener) + + // Make sure we clean up listeners when connections end. + // TODO: detect broken connections - https://github.com/websockets/ws#how-to-detect-and-close-broken-connections + ctx.websocket.on("close", () => { + garden.events.offAny(eventListener) + }) + + // Respond to commands. + ctx.websocket.on("message", (msg) => { + let request + + try { + request = JSON.parse(msg.toString()) + } catch { + return send("error", { message: "Could not parse message as JSON" }) + } + + const requestId = request.id + + try { + Joi.attempt(requestId, Joi.string().uuid().required()) + } catch { + return send("error", { message: "Message should contain an `id` field with a UUID value" }) + } + + try { + Joi.attempt(request.type, Joi.string().required()) + } catch { + return send("error", { message: "Message should contain a type field" }) + } + + if (request.type === "command") { + resolveRequest(ctx, garden, commands, omit(request, ["id", "type"])) + .then(result => { + send("commandResult", { requestId, result: result.result, errors: result.errors }) + }) + .catch(err => { + send("error", { requestId, message: err.message }) + }) + } else { + return send("error", { requestId, message: `Unsupported request type: ${request.type}` }) + } + }) + }) + + app.ws.use(ws.routes()) + app.ws.use(ws.allowedMethods()) +} + +interface ServerWebsocketMessages { + commandResult: { + requestId: string, + result: CommandResult, + errors?: GardenError[], + } + error: { + requestId?: string, + message: string, + } + event: { + name: EventName, + payload: ValueOf, + } +} + +type ServerWebsocketMessageType = keyof ServerWebsocketMessages diff --git a/garden-service/src/task-graph.ts b/garden-service/src/task-graph.ts index 271c9b1e36..cf3804eada 100644 --- a/garden-service/src/task-graph.ts +++ b/garden-service/src/task-graph.ts @@ -14,6 +14,7 @@ import { BaseTask, TaskDefinitionError } from "./tasks/base" import { LogEntry } from "./logger/log-entry" import { toGardenError } from "./exceptions" +import { Garden } from "./garden" class TaskGraphError extends Error { } @@ -45,7 +46,7 @@ export class TaskGraph { private resultCache: ResultCache private opQueue: PQueue - constructor(private log: LogEntry, private concurrency: number = DEFAULT_CONCURRENCY) { + constructor(private garden: Garden, private log: LogEntry, private concurrency: number = DEFAULT_CONCURRENCY) { this.roots = new TaskNodeMap() this.index = new TaskNodeMap() this.inProgress = new TaskNodeMap() @@ -87,6 +88,13 @@ export class TaskGraph { } this.index.addNode(node) + + this.garden.events.emit("taskPending", { + addedAt: new Date(), + key: node.getKey(), + version: task.version, + }) + await this.addDependencies(node) if (node.getDependencies().length === 0) { @@ -142,8 +150,10 @@ export class TaskGraph { try { result = await node.process(dependencyResults) + this.garden.events.emit("taskComplete", result) } catch (error) { result = { type, description, error } + this.garden.events.emit("taskError", result) this.logTaskError(node, error) this.cancelDependants(node) } finally { diff --git a/garden-service/src/util/util.ts b/garden-service/src/util/util.ts index 7944fb2529..fa0ffeb4ad 100644 --- a/garden-service/src/util/util.ts +++ b/garden-service/src/util/util.ts @@ -35,6 +35,7 @@ export type HookCallback = (callback?: () => void) => void const exitHookNames: string[] = [] // For debugging/testing/inspection purposes +export type ValueOf = T[keyof T] export type Omit = Pick> export type Diff = T extends U ? never : T export type Nullable = { [P in keyof T]: T[P] | null } @@ -249,6 +250,38 @@ export function splitFirst(s: string, delimiter: string) { return [parts[0], parts.slice(1).join(delimiter)] } +/** + * Recursively process all values in the given input, + * walking through all object keys _and array items_. + */ +export function deepMap( + value: T | Iterable, fn: (value: any, key: string | number) => any, +): U | Iterable { + if (isArray(value)) { + return value.map(fn) + } else if (isPlainObject(value)) { + return mapValues(value, v => deepMap(v, fn)) + } else { + return value + } +} + +/** + * Recursively filter all keys and values in the given input, + * walking through all object keys _and array items_. + */ +export function deepFilter( + value: T | Iterable, fn: (value: any, key: string | number) => boolean, +): U | Iterable { + if (isArray(value)) { + return >value.filter(fn).map(v => deepFilter(v, fn)) + } else if (isPlainObject(value)) { + return mapValues(pickBy(value, fn), v => deepFilter(v, fn)) + } else { + return value + } +} + /** * Recursively resolves all promises in the given input, * walking through all object keys and array items. @@ -291,6 +324,14 @@ export function omitUndefined(o: object) { return pickBy(o, (v: any) => v !== undefined) } +/** + * Recursively go through an object or array and strip all keys with undefined values, as well as undefined + * values from arrays. Note: Also iterates through arrays recursively. + */ +export function deepOmitUndefined(obj: object) { + return deepFilter(obj, v => v !== undefined) +} + export function serializeObject(o: any): string { return Buffer.from(Cryo.stringify(o)).toString("base64") } diff --git a/garden-service/test/helpers.ts b/garden-service/test/helpers.ts index b28748bc42..cd2a3b77c5 100644 --- a/garden-service/test/helpers.ts +++ b/garden-service/test/helpers.ts @@ -12,9 +12,7 @@ import { remove, readdirSync, existsSync } from "fs-extra" import { containerModuleSpecSchema } from "../src/plugins/container" import { testGenericModule, buildGenericModule } from "../src/plugins/generic" import { TaskResults } from "../src/task-graph" -import { - validate, -} from "../src/config/common" +import { validate, PrimitiveMap } from "../src/config/common" import { GardenPlugin, PluginActions, @@ -34,13 +32,14 @@ import { RunTaskParams, SetSecretParams, } from "../src/types/plugin/params" -import { - helpers, -} from "../src/vcs/git" -import { - ModuleVersion, -} from "../src/vcs/base" +import { helpers } from "../src/vcs/git" +import { ModuleVersion } from "../src/vcs/base" import { GARDEN_DIR_NAME } from "../src/constants" +import { EventBus, Events } from "../src/events" +import { ValueOf } from "../src/util/util" +import { SourceConfig } from "../src/config/project" +import { BuildDir } from "../src/build-dir" +import timekeeper = require("timekeeper") export const dataDir = resolve(__dirname, "data") export const examplesDir = resolve(__dirname, "..", "..", "examples") @@ -230,7 +229,46 @@ export const makeTestModule = (params: Partial = {}) => { return { ...defaultModuleConfig, ...params } } -export const makeTestGarden = async (projectRoot: string, extraPlugins: Plugins = {}) => { +interface EventLogEntry { + name: string + payload: ValueOf +} + +/** + * Used for test Garden instances, to log emitted events. + */ +class TestEventBus extends EventBus { + log: EventLogEntry[] + + constructor() { + super() + this.log = [] + } + + emit(name: T, payload: Events[T]) { + this.log.push({ name, payload }) + return super.emit(name, payload) + } +} + +export class TestGarden extends Garden { + events: TestEventBus + + constructor( + public readonly projectRoot: string, + public readonly projectName: string, + environmentName: string, + variables: PrimitiveMap, + public readonly projectSources: SourceConfig[] = [], + public readonly buildDir: BuildDir, + log?, + ) { + super(projectRoot, projectName, environmentName, variables, projectSources, buildDir, log) + this.events = new TestEventBus() + } +} + +export const makeTestGarden = async (projectRoot: string, extraPlugins: Plugins = {}): Promise => { const testPlugins = { "test-plugin": testPlugin, "test-plugin-b": testPluginB, @@ -238,7 +276,7 @@ export const makeTestGarden = async (projectRoot: string, extraPlugins: Plugins } const plugins = { ...testPlugins, ...extraPlugins } - return Garden.factory(projectRoot, { plugins }) + return TestGarden.factory(projectRoot, { plugins }) } export const makeTestGardenA = async (extraPlugins: Plugins = {}) => { @@ -319,3 +357,11 @@ export function getExampleProjects() { const names = readdirSync(examplesDir).filter(n => existsSync(join(examplesDir, n, "garden.yml"))) return fromPairs(names.map(n => [n, join(examplesDir, n)])) } + +export function freezeTime(date?: Date) { + if (!date) { + date = new Date() + } + timekeeper.freeze(date) + return date +} diff --git a/garden-service/test/integ/run b/garden-service/test/integ/run index d36367fbba..65dffda2d0 100755 --- a/garden-service/test/integ/run +++ b/garden-service/test/integ/run @@ -24,4 +24,4 @@ ${garden_bin} scan ${garden_bin} build ${garden_bin} deploy ${garden_bin} test -${garden_bin} init environment +${garden_bin} init diff --git a/garden-service/test/setup.ts b/garden-service/test/setup.ts index ec0b224641..fd7113904d 100644 --- a/garden-service/test/setup.ts +++ b/garden-service/test/setup.ts @@ -1,4 +1,5 @@ import * as td from "testdouble" +import * as timekeeper from "timekeeper" import { Logger } from "../src/logger/logger" import { LogLevel } from "../src/logger/log-node" import { makeTestGardenA } from "./helpers" @@ -18,4 +19,7 @@ before(async function(this: any) { }) beforeEach(() => { }) -afterEach(() => td.reset()) +afterEach(() => { + td.reset() + timekeeper.reset() +}) diff --git a/garden-service/test/src/commands/build.ts b/garden-service/test/src/commands/build.ts index c900849d4e..87db286233 100644 --- a/garden-service/test/src/commands/build.ts +++ b/garden-service/test/src/commands/build.ts @@ -12,7 +12,7 @@ describe("commands.build", () => { const { result } = await command.action({ garden, log, - args: { module: undefined }, + args: { modules: undefined }, opts: { watch: false, force: true }, }) @@ -31,7 +31,7 @@ describe("commands.build", () => { const { result } = await command.action({ garden, log, - args: { module: ["module-b"] }, + args: { modules: ["module-b"] }, opts: { watch: false, force: true }, }) diff --git a/garden-service/test/src/commands/delete.ts b/garden-service/test/src/commands/delete.ts index 5908c6d022..677759e48d 100644 --- a/garden-service/test/src/commands/delete.ts +++ b/garden-service/test/src/commands/delete.ts @@ -114,7 +114,7 @@ describe("DeleteServiceCommand", () => { const garden = await Garden.factory(projectRootB, { plugins }) const log = garden.log - const { result } = await command.action({ garden, log, args: { service: ["service-a"] }, opts: {} }) + const { result } = await command.action({ garden, log, args: { services: ["service-a"] }, opts: {} }) expect(result).to.eql({ "service-a": { state: "unknown", ingresses: [] }, }) @@ -127,7 +127,7 @@ describe("DeleteServiceCommand", () => { const { result } = await command.action({ garden, log, - args: { service: ["service-a", "service-b"] }, + args: { services: ["service-a", "service-b"] }, opts: {}, }) expect(result).to.eql({ diff --git a/garden-service/test/src/commands/deploy.ts b/garden-service/test/src/commands/deploy.ts index 6b160a3a41..919f2fed29 100644 --- a/garden-service/test/src/commands/deploy.ts +++ b/garden-service/test/src/commands/deploy.ts @@ -100,7 +100,7 @@ describe("DeployCommand", () => { garden, log, args: { - service: undefined, + services: undefined, }, opts: { "hot-reload": undefined, @@ -135,7 +135,7 @@ describe("DeployCommand", () => { garden, log, args: { - service: ["service-b"], + services: ["service-b"], }, opts: { "hot-reload": undefined, diff --git a/garden-service/test/src/commands/get/get-config.ts b/garden-service/test/src/commands/get/get-config.ts index cf31b2d7fe..f85f03f2f4 100644 --- a/garden-service/test/src/commands/get/get-config.ts +++ b/garden-service/test/src/commands/get/get-config.ts @@ -2,6 +2,7 @@ import { expect } from "chai" import { makeTestGardenA } from "../../../helpers" import { GetConfigCommand } from "../../../../src/commands/get/get-config" import { isSubset } from "../../../../src/util/is-subset" +import { sortBy } from "lodash" describe("GetConfigCommand", () => { const pluginName = "test-plugin" @@ -23,7 +24,7 @@ describe("GetConfigCommand", () => { environmentName: garden.environment.name, providers: garden.environment.providers, variables: garden.environment.variables, - modules: await garden.getModules(), + modules: sortBy(await garden.getModules(), "name"), } expect(isSubset(config, res.result)).to.be.true diff --git a/garden-service/test/src/commands/publish.ts b/garden-service/test/src/commands/publish.ts index d50617e86e..c5a2181a85 100644 --- a/garden-service/test/src/commands/publish.ts +++ b/garden-service/test/src/commands/publish.ts @@ -58,7 +58,7 @@ describe("PublishCommand", () => { garden, log, args: { - module: undefined, + modules: undefined, }, opts: { "allow-dirty": false, @@ -84,7 +84,7 @@ describe("PublishCommand", () => { garden, log, args: { - module: undefined, + modules: undefined, }, opts: { "allow-dirty": false, @@ -110,7 +110,7 @@ describe("PublishCommand", () => { garden, log, args: { - module: ["module-a"], + modules: ["module-a"], }, opts: { "allow-dirty": false, @@ -133,7 +133,7 @@ describe("PublishCommand", () => { garden, log, args: { - module: ["module-c"], + modules: ["module-c"], }, opts: { "allow-dirty": false, @@ -157,7 +157,7 @@ describe("PublishCommand", () => { garden, log, args: { - module: ["module-a"], + modules: ["module-a"], }, opts: { "allow-dirty": false, @@ -200,7 +200,7 @@ describe("PublishCommand", () => { garden, log, args: { - module: ["module-a"], + modules: ["module-a"], }, opts: { "allow-dirty": false, @@ -216,7 +216,7 @@ describe("PublishCommand", () => { garden, log, args: { - module: ["module-a"], + modules: ["module-a"], }, opts: { "allow-dirty": true, diff --git a/garden-service/test/src/commands/test.ts b/garden-service/test/src/commands/test.ts index 3eb5b19806..6d59bec2da 100644 --- a/garden-service/test/src/commands/test.ts +++ b/garden-service/test/src/commands/test.ts @@ -12,7 +12,7 @@ describe("commands.test", () => { const { result } = await command.action({ garden, log, - args: { module: undefined }, + args: { modules: undefined }, opts: { "name": undefined, "force": true, "force-build": true, "watch": false }, }) @@ -49,7 +49,7 @@ describe("commands.test", () => { const { result } = await command.action({ garden, log, - args: { module: ["module-a"] }, + args: { modules: ["module-a"] }, opts: { "name": undefined, "force": true, "force-build": true, "watch": false }, }) diff --git a/garden-service/test/src/commands/unlink.ts b/garden-service/test/src/commands/unlink.ts index 7c44bc2169..e6212d3898 100644 --- a/garden-service/test/src/commands/unlink.ts +++ b/garden-service/test/src/commands/unlink.ts @@ -65,7 +65,7 @@ describe("UnlinkCommand", () => { await unlinkCmd.action({ garden, log, - args: { module: ["module-a", "module-b"] }, + args: { modules: ["module-a", "module-b"] }, opts: { all: false }, }) const { linkedModuleSources } = await garden.localConfigStore.get() @@ -78,7 +78,7 @@ describe("UnlinkCommand", () => { await unlinkCmd.action({ garden, log, - args: { module: undefined }, + args: { modules: undefined }, opts: { all: true }, }) const { linkedModuleSources } = await garden.localConfigStore.get() @@ -134,7 +134,7 @@ describe("UnlinkCommand", () => { await unlinkCmd.action({ garden, log, - args: { source: ["source-a", "source-b"] }, + args: { sources: ["source-a", "source-b"] }, opts: { all: false }, }) const { linkedProjectSources } = await garden.localConfigStore.get() @@ -147,7 +147,7 @@ describe("UnlinkCommand", () => { await unlinkCmd.action({ garden, log, - args: { source: undefined }, + args: { sources: undefined }, opts: { all: true }, }) const { linkedProjectSources } = await garden.localConfigStore.get() diff --git a/garden-service/test/src/commands/update-remote.ts b/garden-service/test/src/commands/update-remote.ts index 0d841f50dd..19638fc860 100644 --- a/garden-service/test/src/commands/update-remote.ts +++ b/garden-service/test/src/commands/update-remote.ts @@ -34,7 +34,7 @@ describe("UpdateRemoteCommand", () => { const { result } = await cmd.action({ garden, log, - args: { source: undefined }, + args: { sources: undefined }, opts: {}, }) expect(result!.map(s => s.name).sort()).to.eql(["source-a", "source-b", "source-c"]) @@ -44,7 +44,7 @@ describe("UpdateRemoteCommand", () => { const { result } = await cmd.action({ garden, log, - args: { source: ["source-a"] }, + args: { sources: ["source-a"] }, opts: {}, }) expect(result!.map(s => s.name).sort()).to.eql(["source-a"]) @@ -56,7 +56,7 @@ describe("UpdateRemoteCommand", () => { await cmd.action({ garden, log, - args: { source: undefined }, + args: { sources: undefined }, opts: {}, }) expect(await pathExists(stalePath)).to.be.false @@ -68,7 +68,7 @@ describe("UpdateRemoteCommand", () => { await cmd.action({ garden, log, - args: { source: ["banana"] }, + args: { sources: ["banana"] }, opts: {}, }) ), @@ -94,7 +94,7 @@ describe("UpdateRemoteCommand", () => { const { result } = await cmd.action({ garden, log, - args: { module: undefined }, + args: { modules: undefined }, opts: {}, }) expect(result!.map(s => s.name).sort()).to.eql(["module-a", "module-b", "module-c"]) @@ -104,7 +104,7 @@ describe("UpdateRemoteCommand", () => { const { result } = await cmd.action({ garden, log, - args: { module: ["module-a"] }, + args: { modules: ["module-a"] }, opts: {}, }) expect(result!.map(s => s.name).sort()).to.eql(["module-a"]) @@ -116,7 +116,7 @@ describe("UpdateRemoteCommand", () => { await cmd.action({ garden, log, - args: { module: undefined }, + args: { modules: undefined }, opts: {}, }) expect(await pathExists(stalePath)).to.be.false @@ -128,7 +128,7 @@ describe("UpdateRemoteCommand", () => { await cmd.action({ garden, log, - args: { module: ["banana"] }, + args: { modules: ["banana"] }, opts: {}, }) ), diff --git a/garden-service/test/src/events.ts b/garden-service/test/src/events.ts new file mode 100644 index 0000000000..3782af7688 --- /dev/null +++ b/garden-service/test/src/events.ts @@ -0,0 +1,48 @@ +/* + * 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 { EventBus } from "../../src/events" +import { expect } from "chai" + +describe("EventBus", () => { + let events: EventBus + + beforeEach(() => { + events = new EventBus() + }) + + it("should send+receive events", (done) => { + events.on("_test", (payload) => { + expect(payload).to.equal("foo") + done() + }) + events.emit("_test", "foo") + }) + + describe("onAny", () => { + it("should add listener for any supported event", (done) => { + events.onAny((name, payload) => { + expect(name).to.equal("_test") + expect(payload).to.equal("foo") + done() + }) + events.emit("_test", "foo") + }) + }) + + describe("once", () => { + it("should add a listener that only gets called once", (done) => { + events.once("_test", (payload) => { + expect(payload).to.equal("foo") + done() + }) + events.emit("_test", "foo") + events.emit("_test", "bar") + }) + }) +}) diff --git a/garden-service/test/src/garden.ts b/garden-service/test/src/garden.ts index 9409efa6fc..b6762f8787 100644 --- a/garden-service/test/src/garden.ts +++ b/garden-service/test/src/garden.ts @@ -89,15 +89,15 @@ describe("Garden", () => { }) it("should throw if the specified environment isn't configured", async () => { - await expectError(async () => Garden.factory(projectRootA, { env: "bla" }), "parameter") + await expectError(async () => Garden.factory(projectRootA, { environmentName: "bla" }), "parameter") }) it("should throw if namespace starts with 'garden-'", async () => { - await expectError(async () => Garden.factory(projectRootA, { env: "garden-bla" }), "parameter") + await expectError(async () => Garden.factory(projectRootA, { environmentName: "garden-bla" }), "parameter") }) it("should throw if no provider is configured for the environment", async () => { - await expectError(async () => Garden.factory(projectRootA, { env: "other" }), "configuration") + await expectError(async () => Garden.factory(projectRootA, { environmentName: "other" }), "configuration") }) it("should throw if plugin module exports invalid name", async () => { diff --git a/garden-service/test/src/server/server.ts b/garden-service/test/src/server/server.ts new file mode 100644 index 0000000000..770983340e --- /dev/null +++ b/garden-service/test/src/server/server.ts @@ -0,0 +1,198 @@ +/* + * 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 { makeTestGardenA } from "../../helpers" +import { startServer } from "../../../src/server/server" +import { Server } from "http" +import { Garden } from "../../../src/garden" +import { expect } from "chai" +import { deepOmitUndefined } from "../../../src/util/util" +import * as uuid from "uuid" +import request = require("supertest") +import getPort = require("get-port") +import WebSocket = require("ws") + +describe("startServer", () => { + let garden: Garden + let server: Server + let port: number + + before(async () => { + port = await getPort() + garden = await makeTestGardenA() + server = await startServer(garden, port) + }) + + after(async () => { + server.close() + }) + + describe("GET /", () => { + // TODO: test dashboard endpoint + }) + + describe("POST /api", () => { + it("should 400 on non-JSON body", async () => { + await request(server).post("/api").send("foo").expect(400) + }) + + it("should 400 on invalid payload", async () => { + await request(server).post("/api").send({ foo: "bar" }).expect(400) + }) + + it("should 404 on invalid command", async () => { + await request(server).post("/api").send({ command: "foo" }).expect(404) + }) + + it("should execute a command and return its results", async () => { + const res = await request(server).post("/api").send({ command: "get.config" }).expect(200) + const config = await garden.dumpConfig() + expect(res.body.result).to.eql(deepOmitUndefined(config)) + }) + + it("should correctly map arguments and options to commands", async () => { + const res = await request(server).post("/api").send({ + command: "build", + parameters: { + modules: ["module-a"], + force: true, + }, + }).expect(200) + + expect(res.body.result).to.eql({ + "build.module-a": { + dependencyResults: {}, + description: "building module-a", + output: { + buildLog: "A", + fresh: true, + }, + type: "build", + }, + }) + }) + }) + + describe("/ws", () => { + let ws: WebSocket + + beforeEach((done) => { + ws = new WebSocket(`ws://localhost:${port}/ws`) + ws.on("open", () => { + done() + }) + ws.on("error", done) + }) + + afterEach(() => { + ws.close() + }) + + const onMessage = (cb: (req: object) => void) => { + ws.on("message", msg => cb(JSON.parse(msg.toString()))) + } + + it("should emit events from the event bus", (done) => { + onMessage((req) => { + expect(req).to.eql({ type: "event", name: "_test", payload: "foo" }) + done() + }) + garden.events.emit("_test", "foo") + }) + + it("should send error when a request is not valid JSON", (done) => { + onMessage((req) => { + expect(req).to.eql({ type: "error", message: "Could not parse message as JSON" }) + done() + }) + ws.send("ijdgkasdghlasdkghals") + }) + + it("should error when a request is missing an ID", (done) => { + onMessage((req) => { + expect(req).to.eql({ type: "error", message: "Message should contain an `id` field with a UUID value" }) + done() + }) + ws.send(JSON.stringify({ type: "command" })) + }) + + it("should error when a request has an invalid ID", (done) => { + onMessage((req) => { + expect(req).to.eql({ type: "error", message: "Message should contain an `id` field with a UUID value" }) + done() + }) + ws.send(JSON.stringify({ type: "command", id: "ksdhgalsdkjghalsjkg" })) + }) + + it("should error when a request has an invalid type", (done) => { + const id = uuid.v4() + onMessage((req) => { + expect(req).to.eql({ + type: "error", + requestId: id, + message: "Unsupported request type: foo", + }) + done() + }) + ws.send(JSON.stringify({ type: "foo", id })) + }) + + it("should execute a command and return its results", (done) => { + const id = uuid.v4() + + garden.dumpConfig() + .then(config => { + onMessage((req) => { + expect(req).to.eql({ + type: "commandResult", + requestId: id, + result: deepOmitUndefined(config), + }) + done() + }) + ws.send(JSON.stringify({ + type: "command", + id, + command: "get.config", + })) + }) + .catch(done) + }) + + it("should correctly map arguments and options to commands", (done) => { + const id = uuid.v4() + onMessage((req) => { + expect(req).to.eql({ + type: "commandResult", + requestId: id, + result: { + "build.module-a": { + dependencyResults: {}, + description: "building module-a", + output: { + buildLog: "A", + fresh: true, + }, + type: "build", + }, + }, + }) + done() + }) + ws.send(JSON.stringify({ + type: "command", + id, + command: "build", + parameters: { + modules: ["module-a"], + force: true, + }, + })) + }) + }) +}) diff --git a/garden-service/test/src/task-graph.ts b/garden-service/test/src/task-graph.ts index 78bece8c56..f5ecb93201 100644 --- a/garden-service/test/src/task-graph.ts +++ b/garden-service/test/src/task-graph.ts @@ -6,7 +6,7 @@ import { TaskResult, TaskResults, } from "../../src/task-graph" -import { makeTestGarden } from "../helpers" +import { makeTestGarden, freezeTime } from "../helpers" import { Garden } from "../../src/garden" import { DependencyGraphNodeType } from "../../src/dependency-graph" @@ -16,6 +16,7 @@ type TestTaskCallback = (name: string, result: any) => Promise interface TestTaskOptions { callback?: TestTaskCallback + dependencies?: BaseTask[], id?: string throwError?: boolean } @@ -31,7 +32,6 @@ class TestTask extends BaseTask { constructor( garden: Garden, name: string, - dependencies?: BaseTask[], options?: TestTaskOptions, ) { super({ @@ -43,14 +43,16 @@ class TestTask extends BaseTask { dependencyVersions: {}, }, }) - this.name = name - this.callback = (options && options.callback) || null - this.id = (options && options.id) || "" - this.throwError = !!(options && options.throwError) - if (dependencies) { - this.dependencies = dependencies + if (!options) { + options = {} } + + this.name = name + this.callback = options.callback || null + this.id = options.id || "" + this.throwError = !!options.throwError + this.dependencies = options.dependencies || [] } getName() { @@ -93,7 +95,7 @@ describe("task-graph", () => { it("should successfully process a single task without dependencies", async () => { const garden = await getGarden() - const graph = new TaskGraph(garden.log) + const graph = new TaskGraph(garden, garden.log) const task = new TestTask(garden, "a") await graph.addTask(task) @@ -114,9 +116,55 @@ describe("task-graph", () => { expect(results).to.eql(expected) }) + it("should emit a taskPending event when adding a task", async () => { + const now = freezeTime() + + const garden = await getGarden() + const graph = new TaskGraph(garden, garden.log) + const task = new TestTask(garden, "a") + + await graph.addTask(task) + + expect(garden.events.log).to.eql([ + { name: "taskPending", payload: { addedAt: now, key: task.getKey(), version: task.version } }, + ]) + }) + + it("should emit a taskComplete event when completing a task", async () => { + const now = freezeTime() + + const garden = await getGarden() + const graph = new TaskGraph(garden, garden.log) + const task = new TestTask(garden, "a") + + await graph.addTask(task) + const result = await graph.processTasks() + + expect(garden.events.log).to.eql([ + { name: "taskPending", payload: { addedAt: now, key: task.getKey(), version: task.version } }, + { name: "taskComplete", payload: result["a"] }, + ]) + }) + + it("should emit a taskError event when failing a task", async () => { + const now = freezeTime() + + const garden = await getGarden() + const graph = new TaskGraph(garden, garden.log) + const task = new TestTask(garden, "a", { throwError: true }) + + await graph.addTask(task) + const result = await graph.processTasks() + + expect(garden.events.log).to.eql([ + { name: "taskPending", payload: { addedAt: now, key: task.getKey(), version: task.version } }, + { name: "taskError", payload: result["a"] }, + ]) + }) + it("should process multiple tasks in dependency order", async () => { const garden = await getGarden() - const graph = new TaskGraph(garden.log) + const graph = new TaskGraph(garden, garden.log) const callbackResults = {} const resultOrder: string[] = [] @@ -128,10 +176,10 @@ describe("task-graph", () => { const opts = { callback } - const taskA = new TestTask(garden, "a", [], opts) - const taskB = new TestTask(garden, "b", [taskA], opts) - const taskC = new TestTask(garden, "c", [taskB], opts) - const taskD = new TestTask(garden, "d", [taskB, taskC], opts) + const taskA = new TestTask(garden, "a", { ...opts }) + const taskB = new TestTask(garden, "b", { ...opts, dependencies: [taskA] }) + const taskC = new TestTask(garden, "c", { ...opts, dependencies: [taskB] }) + const taskD = new TestTask(garden, "d", { ...opts, dependencies: [taskB, taskC] }) // we should be able to add tasks multiple times and in any order await graph.addTask(taskC) @@ -208,7 +256,7 @@ describe("task-graph", () => { it("should recursively cancel a task's dependants when it throws an error", async () => { const garden = await getGarden() - const graph = new TaskGraph(garden.log) + const graph = new TaskGraph(garden, garden.log) const resultOrder: string[] = [] @@ -218,10 +266,10 @@ describe("task-graph", () => { const opts = { callback } - const taskA = new TestTask(garden, "a", [], opts) - const taskB = new TestTask(garden, "b", [taskA], { callback, throwError: true }) - const taskC = new TestTask(garden, "c", [taskB], opts) - const taskD = new TestTask(garden, "d", [taskB, taskC], opts) + const taskA = new TestTask(garden, "a", { ...opts }) + const taskB = new TestTask(garden, "b", { callback, throwError: true, dependencies: [taskA] }) + const taskC = new TestTask(garden, "c", { ...opts, dependencies: [taskB] }) + const taskD = new TestTask(garden, "d", { ...opts, dependencies: [taskB, taskC] }) await graph.addTask(taskA) await graph.addTask(taskB) @@ -249,7 +297,7 @@ describe("task-graph", () => { "should process a task as an inheritor of an existing, in-progress task when they have the same base key", async () => { const garden = await getGarden() - const graph = new TaskGraph(garden.log) + const graph = new TaskGraph(garden, garden.log) let callbackResults = {} let resultOrder: string[] = [] @@ -287,19 +335,20 @@ describe("task-graph", () => { callbackResults[key] = result } - const dependencyA = new TestTask(garden, "dependencyA", [], { callback: defaultCallback }) - const dependencyB = new TestTask(garden, "dependencyB", [], { callback: defaultCallback }) + const dependencyA = new TestTask(garden, "dependencyA", { callback: defaultCallback }) + const dependencyB = new TestTask(garden, "dependencyB", { callback: defaultCallback }) const parentTask = new TestTask( garden, "sharedName", - [dependencyA, dependencyB], - { callback: parentCallback, id: "1" }, + { callback: parentCallback, id: "1", dependencies: [dependencyA, dependencyB] }, ) - const dependantA = new TestTask(garden, "dependantA", [parentTask], { callback: defaultCallback }) - const dependantB = new TestTask(garden, "dependantB", [parentTask], { callback: defaultCallback }) + const dependantA = new TestTask(garden, "dependantA", { callback: defaultCallback, dependencies: [parentTask] }) + const dependantB = new TestTask(garden, "dependantB", { callback: defaultCallback, dependencies: [parentTask] }) - const inheritorTask = new TestTask(garden, - "sharedName", [dependencyA, dependencyB], { callback: defaultCallback, id: "2" }, + const inheritorTask = new TestTask( + garden, + "sharedName", + { callback: defaultCallback, id: "2", dependencies: [dependencyA, dependencyB] }, ) await graph.addTask(dependencyA) diff --git a/garden-service/test/src/util/util.ts b/garden-service/test/src/util/util.ts index 7235d8c6fa..fda0a810c9 100644 --- a/garden-service/test/src/util/util.ts +++ b/garden-service/test/src/util/util.ts @@ -1,7 +1,15 @@ import { expect } from "chai" import { join } from "path" import { getDataDir } from "../../helpers" -import { scanDirectory, getChildDirNames, toCygwinPath, pickKeys, getEnvVarName } from "../../../src/util/util" +import { + scanDirectory, + getChildDirNames, + toCygwinPath, + pickKeys, + getEnvVarName, + deepOmitUndefined, + deepFilter, +} from "../../../src/util/util" import { expectError } from "../../helpers" describe("util", () => { @@ -83,4 +91,86 @@ describe("util", () => { }) }) }) + + describe("deepFilter", () => { + const fn = v => v !== 99 + + it("should filter keys in a simple object", () => { + const obj = { + a: 1, + b: 2, + c: 99, + } + expect(deepFilter(obj, fn)).to.eql({ a: 1, b: 2 }) + }) + + it("should filter keys in a nested object", () => { + const obj = { + a: 1, + b: 2, + c: { d: 3, e: 99 }, + } + expect(deepFilter(obj, fn)).to.eql({ a: 1, b: 2, c: { d: 3 } }) + }) + + it("should filter values in lists", () => { + const obj = { + a: 1, + b: 2, + c: [3, 99], + } + expect(deepFilter(obj, fn)).to.eql({ a: 1, b: 2, c: [3] }) + }) + + it("should filter keys in objects in lists", () => { + const obj = { + a: 1, + b: 2, + c: [ + { d: 3, e: 99 }, + ], + } + expect(deepFilter(obj, fn)).to.eql({ a: 1, b: 2, c: [{ d: 3 }] }) + }) + }) + + describe("deepOmitUndefined", () => { + it("should omit keys with undefined values in a simple object", () => { + const obj = { + a: 1, + b: 2, + c: undefined, + } + expect(deepOmitUndefined(obj)).to.eql({ a: 1, b: 2 }) + }) + + it("should omit keys with undefined values in a nested object", () => { + const obj = { + a: 1, + b: 2, + c: { d: 3, e: undefined }, + } + expect(deepOmitUndefined(obj)).to.eql({ a: 1, b: 2, c: { d: 3 } }) + }) + + it("should omit undefined values in lists", () => { + const obj = { + a: 1, + b: 2, + c: [3, undefined], + } + expect(deepOmitUndefined(obj)).to.eql({ a: 1, b: 2, c: [3] }) + }) + + it("should omit undefined values in objects in lists", () => { + const obj = { + a: 1, + b: 2, + c: [ + { d: 3, e: undefined }, + ], + } + expect(deepOmitUndefined(obj)).to.eql({ a: 1, b: 2, c: [{ d: 3 }] }) + }) + }) })