diff --git a/docs/basics/quick-start.md b/docs/basics/quick-start.md index 585b3c3308..fb147ae6e2 100644 --- a/docs/basics/quick-start.md +++ b/docs/basics/quick-start.md @@ -45,7 +45,7 @@ To run tests for all modules: $ garden test ``` -And if you prefer an interactive terminal that watches your project for changes and re-builds, re-deploys, and re-tests automatically, try: +And if you prefer an all-in-one command that watches your project for changes and re-builds, re-deploys, and re-tests automatically, try: ```sh $ garden dev @@ -53,6 +53,6 @@ $ garden dev Go ahead, leave it running and change one of the files in the project, then watch it re-build. -That's it for now. Check out our [Using Garden](../using-garden/README.md) section for other features like hot reload, remote clusters, integration tests, and lots more. +That's it for now. Check out our [Using Garden](../using-garden/README.md) section for other features like hot reload, remote clusters, integration tests, and lots more. To see how a Garden project is configured from scratch check, out the [Simple Project](../examples/simple-project.md) guide for a more in-depth presentation. diff --git a/docs/reference/commands.md b/docs/reference/commands.md index bdff7f9e8b..0a38e45587 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -17,7 +17,7 @@ The following option flags can be used with any of the CLI commands: | `--silent` | `-s` | boolean | Suppress log output. | `--env` | `-e` | string | The environment (and optionally namespace) to work against. | `--loglevel` | `-l` | `error` `warn` `info` `verbose` `debug` `silly` `0` `1` `2` `3` `4` `5` | Set logger level. Values can be either string or numeric and are prioritized from 0 to 5 (highest to lowest) as follows: error: 0, warn: 1, info: 2, verbose: 3, debug: 4, silly: 5. - | `--output` | `-o` | `json` `yaml` | Output command result in specified format (note: disables progress logging). + | `--output` | `-o` | `json` `yaml` | Output command result in specified format (note: disables progress logging and interactive functionality). | `--emoji` | | boolean | Enable emoji in output (defaults to true if the environment supports it). ### garden build @@ -271,7 +271,7 @@ Examples: ##### Usage - garden exec + garden exec [options] ##### Arguments @@ -280,6 +280,12 @@ Examples: | `service` | Yes | The service to exec the command in. | `command` | Yes | The command to run. +##### Options + +| Argument | Alias | Type | Description | +| -------- | ----- | ---- | ----------- | + | `--interactive` | | boolean | Set to false to skip interactive mode and just output the command result + ### garden get secret Get a secret from the environment. @@ -558,6 +564,24 @@ Scans your project and outputs an overview of all modules. garden scan +### garden serve + +Starts the Garden HTTP API service - **Experimental** + +**Experimental** + +Starts an HTTP server that exposes Garden commands and events. + +##### Usage + + garden serve [options] + +##### Options + +| Argument | Alias | Type | Description | +| -------- | ----- | ---- | ----------- | + | `--port` | | number | The port number for the Garden service to listen on. + ### garden set secret Set a secret value for a provider in an environment. diff --git a/garden-service/package-lock.json b/garden-service/package-lock.json index 351d50ac19..52ced2b18f 100644 --- a/garden-service/package-lock.json +++ b/garden-service/package-lock.json @@ -392,6 +392,14 @@ "rimraf": "^2.5.2" } }, + "@types/accepts": { + "version": "1.3.5", + "resolved": "http://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "requires": { + "@types/node": "*" + } + }, "@types/async-lock": { "version": "1.1.0", "resolved": "http://registry.npmjs.org/@types/async-lock/-/async-lock-1.1.0.tgz", @@ -404,6 +412,15 @@ "integrity": "sha512-YeQoDpq4Lm8ppSBqAnAeF/xy1cYp/dMTif2JFcvmAbETMRlvKHT2iLcWu+WyYiJO3b3Ivokwo7EQca/xfLVJmg==", "dev": true }, + "@types/body-parser": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, "@types/chai": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", @@ -429,6 +446,25 @@ "commander": "*" } }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "requires": { + "@types/node": "*" + } + }, + "@types/cookies": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.1.tgz", + "integrity": "sha512-ku6IvbucEyuC6i4zAVK/KnuzWNXdbFd1HkXlNLg/zhWDGTtQT5VhumiPruB/BHW34PWVFwyfwGftDQHfWNxu3Q==", + "requires": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, "@types/cross-spawn": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.0.tgz", @@ -463,8 +499,7 @@ "@types/events": { "version": "1.2.0", "resolved": "http://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", - "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", - "dev": true + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==" }, "@types/execa": { "version": "0.9.0", @@ -475,6 +510,26 @@ "@types/node": "*" } }, + "@types/express": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.0.tgz", + "integrity": "sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "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==", + "requires": { + "@types/events": "*", + "@types/node": "*", + "@types/range-parser": "*" + } + }, "@types/fancy-log": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@types/fancy-log/-/fancy-log-1.3.0.tgz", @@ -490,6 +545,12 @@ "@types/node": "*" } }, + "@types/get-port": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/get-port/-/get-port-4.0.0.tgz", + "integrity": "sha512-Zp1GxOt3GNbIQqz2hSHSH7LALpTPvPHMA/aYut3VeitDgGwqcXEVvDQWWP3kPABsh3CGSHPSt67DN6jnc7oJMA==", + "dev": true + }, "@types/glob": { "version": "5.0.35", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-5.0.35.tgz", @@ -534,6 +595,11 @@ "integrity": "sha512-H3vFOwfLlFEC0MOOrcSkus8PCnMCzz4N0EqUbdJZCdDhBTfkAu86aRYA+MTxjKW6jCpUvxcn4715US8g+28BMA==", "dev": true }, + "@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==" + }, "@types/inquirer": { "version": "0.0.43", "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-0.0.43.tgz", @@ -562,6 +628,11 @@ "integrity": "sha512-UUA1sH0RSRROdInuDOA1yoRzbi5xVFD1RHCoOvNRPTNwR8zBkJ/84PZ6NhKVDtKp0FTeIccJCdQz1X2aJPr4uw==", "dev": true }, + "@types/keygrip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.1.tgz", + "integrity": "sha1-/1QEYtL7TQqIRBzq8n0oewHD2Hg=" + }, "@types/klaw": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@types/klaw/-/klaw-2.1.1.tgz", @@ -571,6 +642,41 @@ "@types/node": "*" } }, + "@types/koa": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.0.47.tgz", + "integrity": "sha512-llhCaHNWKFDMx1GCrqwgsWgUO+C4Da0SccbgevHIYOKVxwegEjFzl0WaMWHk3wWx0P0AdqHR+gQYZ2ZAb0ez0Q==", + "requires": { + "@types/accepts": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "@types/koa-bodyparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@types/koa-bodyparser/-/koa-bodyparser-4.2.1.tgz", + "integrity": "sha512-dd6mVT30OmGYIOmNRF3269Bv+IJ68AVrvYcPViB7bYnzxk7nZyfeAsUx96lvXmaTpOGF4XZ7WDCuSOd7Npi6pw==", + "requires": { + "@types/koa": "*" + } + }, + "@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=" + }, + "@types/koa-router": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/@types/koa-router/-/koa-router-7.0.35.tgz", + "integrity": "sha512-WSdZ0FkUSCDiGQBtsEAmTjsM3l5o4eq2WDSCR9UXm/buLY73b5MSkfSt4f8+LAhoZYa9uNNcEyiE43J0xISF5A==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, "@types/lodash": { "version": "4.14.117", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.117.tgz", @@ -592,6 +698,11 @@ "@types/node": "*" } }, + "@types/mime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz", + "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==" + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -648,6 +759,11 @@ "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==" + }, "@types/rx": { "version": "4.1.1", "resolved": "http://registry.npmjs.org/@types/rx/-/rx-4.1.1.tgz", @@ -780,6 +896,15 @@ "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", "dev": true }, + "@types/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, "@types/string-width": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/string-width/-/string-width-2.0.0.tgz", @@ -907,6 +1032,15 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, "acorn": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", @@ -1596,8 +1730,7 @@ "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, "cache-base": { "version": "1.0.1", @@ -1615,6 +1748,15 @@ "unset-value": "^1.0.0" } }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, "caller-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", @@ -1935,6 +2077,17 @@ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" }, + "co-body": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.0.0.tgz", + "integrity": "sha512-9ZIcixguuuKIptnY8yemEOuhb71L/lLf+Rl5JfJEUiDNJk0e02MBt7BPxR2GEh5mw8dPthQYR4jPI/BnS1MQgw==", + "requires": { + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + } + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -2092,6 +2245,16 @@ } } }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, "conventional-changelog-angular": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-1.6.6.tgz", @@ -2148,6 +2311,15 @@ "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", "dev": true }, + "cookies": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.3.tgz", + "integrity": "sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A==", + "requires": { + "depd": "~1.1.2", + "keygrip": "~1.0.3" + } + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -2163,6 +2335,11 @@ "is-plain-object": "^2.0.1" } }, + "copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" + }, "core-js": { "version": "2.5.7", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", @@ -2358,8 +2535,7 @@ "deep-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" }, "deep-is": { "version": "0.1.3", @@ -2469,6 +2645,11 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, "deline": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/deline/-/deline-1.0.4.tgz", @@ -2477,8 +2658,12 @@ "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, "detect-file": { "version": "1.0.0", @@ -2659,6 +2844,11 @@ "sigmund": "^1.0.1" } }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, "elegant-spinner": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", @@ -2708,6 +2898,11 @@ } } }, + "error-inject": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", + "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" + }, "es5-ext": { "version": "0.10.46", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz", @@ -2796,6 +2991,11 @@ "es6-symbol": "^3.1.1" } }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -3209,6 +3409,11 @@ "map-cache": "^0.2.2" } }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -3839,6 +4044,11 @@ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, + "get-port": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.0.0.tgz", + "integrity": "sha512-Yy3yNI2oShgbaWg4cmPhWjkZfktEvpKI09aDX4PZzNtlU9obuYrX7x2mumQsrNxlF+Ls7OtMQW/u+X4s896bOQ==" + }, "get-stdin": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", @@ -4683,11 +4893,33 @@ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", "dev": true }, + "http-assert": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.0.tgz", + "integrity": "sha512-tPVv62a6l3BbQoM/N5qo969l0OFxqpnQzNUPeYfTP6Spo4zkgWeDBD1D5thI7sDLg7jCCihXTLB0X8UtdyAy8A==", + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.7.1" + }, + "dependencies": { + "http-errors": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.1.tgz", + "integrity": "sha512-jWEUgtZWGSMba9I1N3gc1HmvpBUaNC9vDdA46yScAdp+C5rdEuKWUBLWTQpW9FwSWSbYYs++b6SDCxf9UEJzfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + } + } + }, "http-errors": { "version": "1.6.3", "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, "requires": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -4784,6 +5016,11 @@ "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", "dev": true }, + "inflation": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", + "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -4961,6 +5198,11 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" + }, "is-glob": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", @@ -5362,6 +5604,11 @@ "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=", "dev": true }, + "keygrip": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.3.tgz", + "integrity": "sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g==" + }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", @@ -5375,6 +5622,121 @@ "graceful-fs": "^4.1.9" } }, + "koa": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.6.2.tgz", + "integrity": "sha512-KdnBFhTgh9ysMMoYe4J4fLvaKjT7mF3nRYV8MjxLzx6qywFNeptqi4xevyUltg1fZl2CFJ+HeLXuCGx07Yvl/A==", + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.7.1", + "debug": "~3.1.0", + "delegates": "^1.0.0", + "depd": "^1.1.2", + "destroy": "^1.0.4", + "error-inject": "^1.0.0", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^1.2.0", + "koa-is-json": "^1.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "koa-bodyparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-4.2.1.tgz", + "integrity": "sha512-UIjPAlMZfNYDDe+4zBaOAUKYqkwAGcIU6r2ARf1UOXPAlfennQys5IiShaVeNf7KkVBlf88f2LeLvBFvKylttw==", + "requires": { + "co-body": "^6.0.0", + "copy-to": "^2.0.1" + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==" + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "requires": { + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, + "koa-is-json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", + "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" + }, + "koa-router": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-7.4.0.tgz", + "integrity": "sha512-IWhaDXeAnfDBEpWS6hkGdZ1ablgr6Q6pGdXCyK38RbzuH4LkUOpPqPw+3f8l8aTDrQmBQ7xJc0bs2yV4dzcO+g==", + "requires": { + "debug": "^3.1.0", + "http-errors": "^1.3.1", + "koa-compose": "^3.0.0", + "methods": "^1.0.1", + "path-to-regexp": "^1.1.1", + "urijs": "^1.19.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" + } + }, + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + }, + "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==" + } + } + }, "kuler": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", @@ -5852,6 +6214,11 @@ "stack-trace": "0.0.10" } }, + "media-typer": { + "version": "0.3.0", + "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, "mem": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", @@ -5904,6 +6271,11 @@ } } }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -6254,6 +6626,11 @@ "sax": "^1.2.4" } }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, "netmask": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", @@ -7813,6 +8190,14 @@ "make-iterator": "^1.0.0" } }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7834,6 +8219,11 @@ "mimic-fn": "^1.0.0" } }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, "opn": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.4.0.tgz", @@ -8048,6 +8438,11 @@ "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==" }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -8108,6 +8503,21 @@ "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", "dev": true }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + } + } + }, "path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", @@ -8336,7 +8746,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", - "dev": true, "requires": { "bytes": "3.0.0", "http-errors": "1.6.3", @@ -8348,7 +8757,6 @@ "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -8740,8 +9148,7 @@ "setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" }, "shallow-clone": { "version": "0.1.2", @@ -9586,8 +9993,7 @@ "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, "stream-exhaust": { "version": "1.0.2", @@ -9980,6 +10386,11 @@ "through2": "^2.0.3" } }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "toml": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/toml/-/toml-2.3.3.tgz", @@ -10157,6 +10568,15 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -10327,8 +10747,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "unset-value": { "version": "1.0.0", @@ -10402,6 +10821,11 @@ "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==" }, + "urijs": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", + "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==" + }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", @@ -10456,6 +10880,11 @@ "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", "dev": true }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -10693,6 +11122,11 @@ "camelcase": "^4.1.0" } }, + "ylru": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", + "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==" + }, "yn": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", diff --git a/garden-service/package.json b/garden-service/package.json index 62e6413a71..4722115848 100644 --- a/garden-service/package.json +++ b/garden-service/package.json @@ -23,6 +23,7 @@ ], "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", @@ -46,6 +47,7 @@ "escape-string-regexp": "^1.0.5", "execa": "^1.0.0", "fs-extra": "^7.0.0", + "get-port": "^4.0.0", "has-ansi": "^3.0.0", "ignore": "^5.0.2", "inquirer": "^6.2.0", @@ -53,6 +55,9 @@ "js-yaml": "^3.12.0", "json-stringify-safe": "^5.0.1", "klaw": "^3.0.0", + "koa": "^2.6.2", + "koa-bodyparser": "^4.2.1", + "koa-router": "^7.4.0", "lodash": "^4.17.11", "log-symbols": "^2.2.0", "moment": "^2.22.2", @@ -86,6 +91,7 @@ "@types/dockerode": "^2.5.5", "@types/execa": "^0.9.0", "@types/fs-extra": "^5.0.4", + "@types/get-port": "^4.0.0", "@types/gulp": "^4.0.5", "@types/handlebars": "^4.0.39", "@types/has-ansi": "^3.0.0", @@ -94,6 +100,8 @@ "@types/js-yaml": "^3.11.2", "@types/json-stringify-safe": "^5.0.0", "@types/klaw": "^2.1.1", + "@types/koa": "^2.0.47", + "@types/koa-router": "^7.0.35", "@types/lodash": "^4.14.117", "@types/log-symbols": "^2.0.0", "@types/log-update": "^2.0.0", diff --git a/garden-service/src/cli/cli.ts b/garden-service/src/cli/cli.ts index 70c17afbef..4209e17fea 100644 --- a/garden-service/src/cli/cli.ts +++ b/garden-service/src/cli/cli.ts @@ -127,7 +127,7 @@ export const GLOBAL_OPTIONS = { output: new ChoicesParameter({ alias: "o", choices: Object.keys(OUTPUT_RENDERERS), - help: "Output command result in specified format (note: disables progress logging).", + help: "Output command result in specified format (note: disables progress logging and interactive functionality).", }), emoji: new BooleanParameter({ help: "Enable emoji in output (defaults to true if the environment supports it).", @@ -247,7 +247,6 @@ export class GardenCli { arguments: args = {}, loggerType = DEFAULT_CLI_LOGGER_TYPE, options = {}, - subCommands, } = command const argKeys = getKeys(args) @@ -306,7 +305,9 @@ export class GardenCli { // Command specific positional args and options are set inside the builder function const setup = parser => { - subCommands.forEach(subCommandCls => this.addCommand(new subCommandCls(command), parser)) + const subCommands = command.getSubCommands() + subCommands.forEach(subCommand => this.addCommand(subCommand, parser)) + argKeys.forEach(key => parser.positional(getArgSynopsis(key, args[key]), prepareArgConfig(args[key]))) optKeys.forEach(key => parser.option(getOptionSynopsis(key, options[key]), prepareOptionConfig(options[key]))) diff --git a/garden-service/src/cli/helpers.ts b/garden-service/src/cli/helpers.ts index 6576d28f7b..3141b25f6e 100644 --- a/garden-service/src/cli/helpers.ts +++ b/garden-service/src/cli/helpers.ts @@ -130,12 +130,14 @@ export interface SywacOptionConfig { export function prepareOptionConfig(param: Parameter): SywacOptionConfig { const { coerce, - defaultValue, help: desc, hints, required, type, } = param + + const defaultValue = param.cliDefault === undefined ? param.defaultValue : param.cliDefault + if (!VALID_PARAMETER_TYPES.includes(type)) { throw new InternalError(`Invalid parameter type for cli: ${type}`, { type, diff --git a/garden-service/src/commands/base.ts b/garden-service/src/commands/base.ts index 2a771c065b..b86220b13e 100644 --- a/garden-service/src/commands/base.ts +++ b/garden-service/src/commands/base.ts @@ -6,10 +6,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { - GardenError, - RuntimeError, -} from "../exceptions" +import Joi = require("joi") +import { GardenError, RuntimeError, InternalError } from "../exceptions" import { TaskResults } from "../task-graph" import { LoggerType } from "../logger/logger" import { ProcessResults } from "../process" @@ -27,11 +25,16 @@ export interface ParameterConstructor { valueName?: string, hints?: string, overrides?: string[], + cliDefault?: T, + cliOnly?: boolean, } export abstract class Parameter { abstract type: string + // TODO: use this for validation in the CLI (currently just used in the service API) + abstract schema: Joi.Schema + _valueType: T defaultValue: T | undefined @@ -42,7 +45,12 @@ export abstract class Parameter { valueName: string overrides: string[] - constructor({ help, required, alias, defaultValue, valueName, overrides, hints }: ParameterConstructor) { + readonly cliDefault: T | undefined // Optionally specify a separate default for CLI invocation + readonly cliOnly: boolean // If true, only expose in the CLI, and not in the HTTP/WS server. + + constructor( + { help, required, alias, defaultValue, valueName, overrides, hints, cliDefault, cliOnly }: ParameterConstructor, + ) { this.help = help this.required = required || false this.alias = alias @@ -50,13 +58,15 @@ export abstract class Parameter { this.defaultValue = defaultValue this.valueName = valueName || "_valueType" this.overrides = overrides || [] + this.cliDefault = cliDefault + this.cliOnly = cliOnly || false } coerce(input: T): T | undefined { return input } - abstract validate(input: string): T + abstract parseString(input: string): T async autoComplete(): Promise { return [] @@ -65,8 +75,9 @@ export abstract class Parameter { export class StringParameter extends Parameter { type = "string" + schema = Joi.string() - validate(input: string) { + parseString(input: string) { return input } } @@ -75,14 +86,16 @@ export class StringParameter extends Parameter { // FIXME: Maybe use a Required type to enforce presence, rather that an option flag? export class StringOption extends Parameter { type = "string" + schema = Joi.string() - validate(input?: string) { + parseString(input?: string) { return input } } export class StringsParameter extends Parameter { type = "array:string" + schema = Joi.array().items(Joi.string()) // Sywac returns [undefined] if input is empty so we coerce that into undefined. // This only applies to optional parameters since Sywac would throw if input is empty for a required parameter. @@ -94,35 +107,38 @@ export class StringsParameter extends Parameter { return filtered } - validate(input: string) { + parseString(input: string) { return input.split(",") } } export class PathParameter extends Parameter { type = "path" + schema = Joi.string().uri({ relativeOnly: true }) - validate(input: string) { + parseString(input: string) { return input } } export class PathsParameter extends Parameter { type = "array:path" + schema = Joi.array().items(Joi.string().uri({ relativeOnly: true })) - validate(input: string) { + parseString(input: string) { return input.split(",") } } -export class NumberParameter extends Parameter { +export class IntegerParameter extends Parameter { type = "number" + schema = Joi.number().integer() - validate(input: string) { + parseString(input: string) { try { return parseInt(input, 10) } catch { - throw new ValidationError(`Could not parse "${input}" as number`) + throw new ValidationError(`Could not parse "${input}" as integer`) } } } @@ -134,14 +150,16 @@ export interface ChoicesConstructor extends ParameterConstructor { export class ChoicesParameter extends Parameter { type = "choice" choices: string[] + schema = Joi.string() constructor(args: ChoicesConstructor) { super(args) this.choices = args.choices + this.schema = Joi.string().only(args.choices) } - validate(input: string) { + parseString(input: string) { if (this.choices.includes(input)) { return input } else { @@ -156,8 +174,9 @@ export class ChoicesParameter extends Parameter { export class BooleanParameter extends Parameter { type = "boolean" + schema = Joi.boolean() - validate(input: any) { + parseString(input: any) { return !!input } } @@ -198,30 +217,56 @@ export abstract class Command new cls(this)) + } + describe() { - const { name, help, description } = this + const { name, help, description, cliOnly } = this + const subCommands = this.subCommands.map(S => new S(this).describe()) return { name, fullName: this.getFullName(), help, description, + cliOnly, + subCommands, arguments: describeParameters(this.arguments), options: describeParameters(this.options), } diff --git a/garden-service/src/commands/build.ts b/garden-service/src/commands/build.ts index 469d2de43e..1ce947a6d2 100644 --- a/garden-service/src/commands/build.ts +++ b/garden-service/src/commands/build.ts @@ -29,7 +29,11 @@ const buildArguments = { const buildOptions = { force: new BooleanParameter({ help: "Force rebuild of module(s)." }), - watch: new BooleanParameter({ help: "Watch for changes in module(s) and auto-build.", alias: "w" }), + watch: new BooleanParameter({ + help: "Watch for changes in module(s) and auto-build.", + alias: "w", + cliOnly: true, + }), } type BuildArguments = typeof buildArguments diff --git a/garden-service/src/commands/commands.ts b/garden-service/src/commands/commands.ts index f7003e39ee..537c84b059 100644 --- a/garden-service/src/commands/commands.ts +++ b/garden-service/src/commands/commands.ts @@ -26,6 +26,7 @@ import { UnlinkCommand } from "./unlink/unlink" import { UpdateRemoteCommand } from "./update-remote/update-remote" import { ValidateCommand } from "./validate" import { ExecCommand } from "./exec" +import { ServeCommand } from "./serve" export const coreCommands: Command[] = [ new BuildCommand(), @@ -42,6 +43,7 @@ export const coreCommands: Command[] = [ new PublishCommand(), new RunCommand(), new ScanCommand(), + new ServeCommand(), new SetCommand(), new TestCommand(), new UnlinkCommand(), diff --git a/garden-service/src/commands/create/create.ts b/garden-service/src/commands/create/create.ts index e64195af59..37b37ca0fa 100644 --- a/garden-service/src/commands/create/create.ts +++ b/garden-service/src/commands/create/create.ts @@ -14,6 +14,8 @@ export class CreateCommand extends Command { name = "create" help = "Create a new project or add a new module" + cliOnly = true + subCommands = [ CreateProjectCommand, CreateModuleCommand, diff --git a/garden-service/src/commands/deploy.ts b/garden-service/src/commands/deploy.ts index b9c1c448cd..509dbc44cc 100644 --- a/garden-service/src/commands/deploy.ts +++ b/garden-service/src/commands/deploy.ts @@ -33,7 +33,11 @@ const deployArgs = { const deployOpts = { "force": new BooleanParameter({ help: "Force redeploy of service(s)." }), "force-build": new BooleanParameter({ help: "Force rebuild of module(s)." }), - "watch": new BooleanParameter({ help: "Watch for changes in module(s) and auto-deploy.", alias: "w" }), + "watch": new BooleanParameter({ + help: "Watch for changes in module(s) and auto-deploy.", + alias: "w", + cliOnly: true, + }), "hot-reload": new StringsParameter({ help: deline`The name(s) of the service(s) to deploy with hot reloading enabled. Use comma as a separator to specify multiple services. When this option is used, diff --git a/garden-service/src/commands/dev.ts b/garden-service/src/commands/dev.ts index a87141d24b..9f0386786a 100644 --- a/garden-service/src/commands/dev.ts +++ b/garden-service/src/commands/dev.ts @@ -49,6 +49,9 @@ export class DevCommand extends Command { name = "dev" help = "Starts the garden development console." + // Currently it doesn't make sense to do file watching except in the CLI + cliOnly = true + description = dedent` The Garden dev console is a combination of the \`build\`, \`deploy\` and \`test\` commands. It builds, deploys and tests all your modules and services, and re-builds, re-deploys and re-tests diff --git a/garden-service/src/commands/exec.ts b/garden-service/src/commands/exec.ts index 5e82907626..1756ae878e 100644 --- a/garden-service/src/commands/exec.ts +++ b/garden-service/src/commands/exec.ts @@ -16,6 +16,7 @@ import { CommandParams, StringParameter, StringsParameter, + BooleanParameter, } from "./base" import dedent = require("dedent") @@ -31,13 +32,16 @@ const runArgs = { } const runOpts = { - // interactive: new BooleanParameter({ - // help: "Set to false to skip interactive mode and just output the command result", - // defaultValue: true, - // }), + interactive: new BooleanParameter({ + help: "Set to false to skip interactive mode and just output the command result", + defaultValue: false, + cliDefault: true, + cliOnly: true, + }), } type Args = typeof runArgs +type Opts = typeof runOpts export class ExecCommand extends Command { name = "exec" @@ -58,7 +62,7 @@ export class ExecCommand extends Command { options = runOpts loggerType = LoggerType.basic - async action({ garden, log, args }: CommandParams): Promise> { + async action({ garden, log, args, opts }: CommandParams): Promise> { const serviceName = args.service const command = args.command || [] @@ -69,7 +73,7 @@ export class ExecCommand extends Command { }) const service = await garden.getService(serviceName) - const result = await garden.actions.execInService({ log, service, command, interactive: true }) + const result = await garden.actions.execInService({ log, service, command, interactive: opts.interactive }) return { result } } diff --git a/garden-service/src/commands/init.ts b/garden-service/src/commands/init.ts index 46e2c5deda..e5542b5a94 100644 --- a/garden-service/src/commands/init.ts +++ b/garden-service/src/commands/init.ts @@ -25,6 +25,9 @@ export class InitCommand extends Command { name = "init" help = "Initialize system, environment or other runtime components." + // This command is generally only used when user input is needed, which will need to happen via the CLI + cliOnly = true + description = dedent` This command needs to be run before first deploying a Garden project, and occasionally after updating Garden, plugins or project configuration. diff --git a/garden-service/src/commands/logs.ts b/garden-service/src/commands/logs.ts index 3c604fcfb9..4b19ec5819 100644 --- a/garden-service/src/commands/logs.ts +++ b/garden-service/src/commands/logs.ts @@ -29,7 +29,11 @@ const logsArgs = { } const logsOpts = { - tail: new BooleanParameter({ help: "Continuously stream new logs from the service(s).", alias: "t" }), + tail: new BooleanParameter({ + help: "Continuously stream new logs from the service(s).", + alias: "t", + cliOnly: true, + }), // TODO // since: new MomentParameter({ help: "Retrieve logs from the specified point onwards" }), } @@ -62,7 +66,6 @@ export class LogsCommand extends Command { const result: ServiceLogEntry[] = [] const stream = new Stream() - // TODO: use basic logger (no need for fancy stuff here, just causes flickering) void stream.forEach((entry) => { // TODO: color each service differently for easier visual parsing let timestamp = " " diff --git a/garden-service/src/commands/run/module.ts b/garden-service/src/commands/run/module.ts index aa98b7bd9f..18306f79fd 100644 --- a/garden-service/src/commands/run/module.ts +++ b/garden-service/src/commands/run/module.ts @@ -42,7 +42,9 @@ const runOpts = { //entrypoint: new StringParameter({ help: "Override default entrypoint in module" }), "interactive": new BooleanParameter({ help: "Set to false to skip interactive mode and just output the command result.", - defaultValue: true, + defaultValue: false, + cliDefault: true, + cliOnly: true, }), "force-build": new BooleanParameter({ help: "Force rebuild of module before running." }), } diff --git a/garden-service/src/commands/run/service.ts b/garden-service/src/commands/run/service.ts index f10fcf2fbb..e1a7cf7ad9 100644 --- a/garden-service/src/commands/run/service.ts +++ b/garden-service/src/commands/run/service.ts @@ -39,6 +39,9 @@ export class RunServiceCommand extends Command { name = "service" help = "Run an ad-hoc instance of the specified service." + // Makes no sense to run a service (which is expected to stay running) except when attaching in the CLI + cliOnly = true + description = dedent` This can be useful for debugging or ad-hoc experimentation with services. diff --git a/garden-service/src/commands/run/test.ts b/garden-service/src/commands/run/test.ts index ecfef24749..c3e7ed51a4 100644 --- a/garden-service/src/commands/run/test.ts +++ b/garden-service/src/commands/run/test.ts @@ -40,7 +40,9 @@ const runArgs = { const runOpts = { "interactive": new BooleanParameter({ help: "Set to false to skip interactive mode and just output the command result.", - defaultValue: true, + defaultValue: false, + cliDefault: true, + cliOnly: true, }), "force-build": new BooleanParameter({ help: "Force rebuild of module before running." }), } diff --git a/garden-service/src/commands/serve.ts b/garden-service/src/commands/serve.ts new file mode 100644 index 0000000000..8e03147ddf --- /dev/null +++ b/garden-service/src/commands/serve.ts @@ -0,0 +1,52 @@ +/* + * 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 dedent = require("dedent") +import { LoggerType } from "../logger/logger" +import { IntegerParameter } from "./base" +import { Command, CommandResult, CommandParams } from "./base" +import { sleep } from "../util/util" +import { startServer } from "../server" + +const serveArgs = {} + +const serveOpts = { + port: new IntegerParameter({ + help: `The port number for the Garden service to listen on.`, + defaultValue: 9777, + }), +} + +type Args = typeof serveArgs +type Opts = typeof serveOpts + +export class ServeCommand extends Command { + name = "serve" + help = "Starts the Garden HTTP API service - **Experimental**" + + cliOnly = true + loggerType = LoggerType.basic + + description = dedent` + **Experimental** + + Starts an HTTP server that exposes Garden commands and events. + ` + + arguments = serveArgs + options = serveOpts + + async action({ garden, opts }: CommandParams): Promise> { + await startServer(garden, opts.port) + + // The server doesn't block, so we need to loop indefinitely here. + while (true) { + await sleep(10000) + } + } +} diff --git a/garden-service/src/commands/test.ts b/garden-service/src/commands/test.ts index 2411f286e8..103942e108 100644 --- a/garden-service/src/commands/test.ts +++ b/garden-service/src/commands/test.ts @@ -39,7 +39,11 @@ const testOpts = { }), "force": new BooleanParameter({ help: "Force re-test of module(s).", alias: "f" }), "force-build": new BooleanParameter({ help: "Force rebuild of module(s)." }), - "watch": new BooleanParameter({ help: "Watch for changes in module(s) and auto-test.", alias: "w" }), + "watch": new BooleanParameter({ + help: "Watch for changes in module(s) and auto-test.", + alias: "w", + cliOnly: true, + }), } type Args = typeof testArgs diff --git a/garden-service/src/process.ts b/garden-service/src/process.ts index 209f6525fe..e446d8bb9d 100644 --- a/garden-service/src/process.ts +++ b/garden-service/src/process.ts @@ -19,6 +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" export type ProcessHandler = (module: Module) => Promise @@ -119,6 +120,11 @@ export async function processModules( }) }) + // Experimental HTTP API and dashboard server. + if (process.env.GARDEN_ENABLE_SERVER === "1") { + await startServer(garden) + } + await restartPromise watcher.close() diff --git a/garden-service/src/server.ts b/garden-service/src/server.ts new file mode 100644 index 0000000000..e8e621ed8f --- /dev/null +++ b/garden-service/src/server.ts @@ -0,0 +1,228 @@ +/* + * 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 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 { 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()) + + // 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 { + [key: string]: { + command: Command, + requestSchema: Joi.ObjectSchema, + // TODO: implement resultSchema on Commands, so we can include it here as well (for docs mainly) + } +} + +const baseRequestSchema = Joi.object() + .keys({ + command: Joi.string() + .required() + .description("The command name to run.") + .example("get.status"), + parameters: Joi.object() + .keys({}) + .default(() => ({}), "{}") + .description("The parameters for the command."), + }) + +/** + * 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) { + // Perform basic validation and find command. + try { + request = validate(request, baseRequestSchema, { context: "API request" }) + } catch { + ctx.throw(400, "Invalid request format") + } + + const commandSpec = commands[request.command] + + if (!commandSpec) { + ctx.throw(404, `Could not find command ${request.command}`) + } + + // Validate command parameters. + try { + request = validate(request, commandSpec.requestSchema) + } catch { + ctx.throw(400, `Invalid request format for command ${request.command}`) + } + + // Prepare arguments for command action. + const command = commandSpec.command + + // 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 }) + + // We generally don't want actions to log anything in the server. + const log = 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 }) + // TODO: validate result schema +} + +async function prepareCommands(): Promise { + const commands: CommandMap = {} + + function addCommand(command: Command) { + const requestSchema = baseRequestSchema + .keys({ + parameters: Joi.object() + .keys({ + ...paramsToJoi(command.arguments), + ...paramsToJoi(command.options), + }), + }) + + commands[command.getKey()] = { + command, + requestSchema, + } + + command.getSubCommands().forEach(addCommand) + } + + coreCommands.forEach(addCommand) + + return commands +} + +function paramsToJoi(params?: Parameters) { + if (!params) { + return {} + } + + params = omitBy(params, p => p.cliOnly) + + return mapValues(params, p => { + let schema = p.schema.description(p.help) + if (p.required) { + schema = schema.required() + } + if (p.defaultValue) { + schema = schema.default(p.defaultValue) + } + return schema + }) +} + +/** + * Prepare the args or opts for a Command action, by mapping input values to the parameter specs. + */ +function mapParams(ctx: Koa.Context, values: object, params?: Parameters) { + if (!params) { + return {} + } + + return mapValues(params, (p, key) => { + if (p.cliOnly) { + return p.defaultValue + } + + const value = values[key] + + const result = p.schema.validate(value) + if (result.error) { + ctx.throw(400, result.error.message) + } + return result.value + }) +} diff --git a/garden-service/src/tasks/task.ts b/garden-service/src/tasks/task.ts index d91386d417..bc4c6658fa 100644 --- a/garden-service/src/tasks/task.ts +++ b/garden-service/src/tasks/task.ts @@ -23,7 +23,6 @@ export interface TaskTaskParams { task: Task force: boolean forceBuild: boolean - logEntry?: LogEntry } export class TaskTask extends BaseTask { // ... to be renamed soon.