@toa.io/cli 0.9.2-dev.0 → 0.20.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/cli",
3
- "version": "0.9.2-dev.0",
3
+ "version": "0.20.0-alpha.0",
4
4
  "description": "Toa CLI",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -22,14 +22,15 @@
22
22
  "@toa.io/runtime": "*"
23
23
  },
24
24
  "dependencies": {
25
- "@toa.io/console": "0.6.0",
26
- "@toa.io/generic": "0.9.0",
27
- "@toa.io/kubernetes": "0.7.2",
28
- "@toa.io/norm": "0.10.1-dev.0",
29
- "@toa.io/yaml": "0.7.3",
30
- "dotenv": "16.0.3",
25
+ "@toa.io/console": "0.20.0-alpha.0",
26
+ "@toa.io/generic": "0.20.0-alpha.0",
27
+ "@toa.io/kubernetes": "0.20.0-alpha.0",
28
+ "@toa.io/norm": "0.20.0-alpha.0",
29
+ "@toa.io/yaml": "0.20.0-alpha.0",
30
+ "dotenv": "16.1.1",
31
31
  "find-up": "5.0.0",
32
+ "paseto": "3.1.4",
32
33
  "yargs": "17.6.2"
33
34
  },
34
- "gitHead": "7f1fdbfe3aa33eb4a06cb1a33d15be18854bcd88"
35
+ "gitHead": "d047190899218b5249901a01a2a4caec5b34cf09"
35
36
  }
package/readme.md CHANGED
@@ -1,44 +1,49 @@
1
1
  # Toa Command Line Interface
2
2
 
3
+ ## Common Options
4
+
5
+ <dl>
6
+ <dt><code>--env</code></dt>
7
+ <dd>Path to the environment variables file (`.env` format)</dd>
8
+ </dl>
9
+
3
10
  ## Development
4
11
 
5
- ### configure
12
+ ### compose
6
13
 
7
- Outputs shell commands to manipulate local environment variables, thus must be piped
8
- with `source /dev/stdin` to apply.
14
+ Run composition.
9
15
 
10
16
  <dl>
11
- <dt><code>toa configure &lt;key&gt; [value]</code></dt>
17
+ <dt><code>toa compose [paths]</code></dt>
12
18
  <dd>
13
- Set Configuration Object key. Nested keys are addressed with dot notation.
14
-
15
- <code>--path</code> path to component (default <code>.</code>)<br/>
16
- <code>--reset</code> clear <code>key</code><br/>
17
-
18
- #### Examples
19
-
20
- It is assumed you are in the component's directory, use `--path` otherwise.
19
+ <code>paths</code> Glob patterns to look for components.<br/>
20
+ <code>--kill</code> Shutdown composition after it's started<br/>
21
+ <code>--dock</code> Run in Docker using current <code>.env</code>.<br/>
22
+ <code>--context</code> Path to the Context root (default <code>.</code>).<br/>
23
+ <code>--bindnings</code> Override bindings (obsolete).
24
+ </dd>
25
+ </dl>
21
26
 
22
- ```shell
23
- # set new value
24
- $ toa configure foo 'new value' | source /dev/stdin
25
- ```
27
+ > Note that your `localhost` it is accessible from a container as `host.docker.internal`.
26
28
 
27
- ```shell
28
- # clear key
29
- $ toa configure bar.baz --reset | source /dev/stdin
30
- ```
29
+ ### env
31
30
 
32
- </dd>
33
- <dt><code>toa configure reset</code></dt>
34
- <dd>Remove Configuration Object.</dd>
35
- <dt><code>toa configure print</code></dt>
36
- <dd>Output Configuration Object as YAML.
31
+ Export environment to a `.env` file.
37
32
 
38
- <code>--json</code> as JSON
33
+ <dl>
34
+ <dt><code>toa env [environment]</code></dt>
35
+ <dd>
36
+ <code>environment</code> deployment environment name (default <code>local</code>).<br/>
37
+ <code>--path</code> path to a Context (default <code>.</code>)<br/>
38
+ <code>--as</code> output file path (default <code>.env</code>)<br/>
39
+ <code>--interactive</code> prompt for secret values
39
40
  </dd>
40
41
  </dl>
41
42
 
43
+ Credentials specified in the output file are preserved.
44
+
45
+ > It is recommended to add `.env*` to `.gitignore`.
46
+
42
47
  ### replay
43
48
 
44
49
  [Replay](/extensions/sampling/docs/replay.md) samples. Reports in [TAP](https://testanything.org)
@@ -47,11 +52,13 @@ format.
47
52
  <dl>
48
53
  <dt><code>toa replay [paths...]</code></dt>
49
54
  <dd>
50
- <code>paths</code> path(s) to component(s) or a context (default <code>.</code>)<br/>
51
- <code>--integration</code> replay integration tests only<br/>
52
- <code>--component &lt;id&gt;</code> replay samples for specified component id<br/>
53
- <code>--operation &lt;name&gt;</code> replay samples for specified operation<br/>
54
- <code>--title &lt;regexp&gt;</code> regexp to match sample titles<br/>
55
+ <code>paths</code> Path(s) to Component(s) or a Context (default <code>.</code>).<br/>
56
+ <code>--component &lt;id&gt;</code> Replay samples for a specified component <code>id</code>.<br/>
57
+ <code>--integration</code> Replay integration tests only.<br/>
58
+ <code>--autonomous</code> Replay autonomous tests only.<br/>
59
+ <code>--operation &lt;name&gt;</code> Replay samples for specified operation.<br/>
60
+ <code>--title &lt;regexp&gt;</code> Regexp to match sample titles.<br/>
61
+ <code>--dock</code> Run in Docker. Applicable only for component samples.
55
62
  </dd>
56
63
  </dl>
57
64
 
@@ -60,16 +67,15 @@ format.
60
67
  ```shell
61
68
  $ toa replay
62
69
  $ toa replay ./path/to/component
63
- $ toa replay ./components/a ./components/b
70
+ $ toa replay ./components/a ./components/b --dock
64
71
  $ toa replay ./components/*
65
72
  $ toa replay ./path/to/context
66
73
  $ toa replay --title "should add numbers"
67
74
  ```
68
75
 
69
- If path is a context directory (containing `context.toa.yaml` file), samples for components within
70
- the context will be found and replayed sequentially.
71
-
72
- ## Exporting
76
+ If the path is a Context root (containing `context.toa.yaml` file), samples for components within
77
+ the Context will be
78
+ found and replayed sequentially.
73
79
 
74
80
  ### export manifest
75
81
 
@@ -78,59 +84,109 @@ the context will be found and replayed sequentially.
78
84
  <dd>Print normalized manifest.
79
85
 
80
86
  <code>--path</code> path to component (default <code>.</code>)<br/>
81
- <code>--error</code> print errors only<br/>
87
+ <code>--error</code> print errors only
82
88
  </dd>
83
89
  </dl>
84
90
 
85
- ### env
91
+ ## Operations
86
92
 
87
- <dl>
88
- <dt><code>toa env [environment]</code></dt>
89
- <dd>Select environment. Set local environment variables to <code>.env</code> file.
93
+ > Some commands use current `kubectl` and `docker` context.
90
94
 
91
- <code>environment</code> deployment environment name (default <code>local</code>).<br/>
92
- <code>--path</code> path to context (default <code>.</code>)<br/>
93
- </dd>
94
- </dl>
95
+ ### build
95
96
 
96
- > It is recommended to add `.env` to `.gitignore`.
97
+ Build Docker images.
97
98
 
98
- > Credentials specified in a `.env` file are preserved during environment selection.
99
+ <dl>
100
+ <dt><code>toa build</code></dt>
101
+ <dd>
102
+ <code>--path</code> path to a Context (default <code>.</code>)
103
+ </dd>
104
+ </dl>
99
105
 
100
- ## Deployment
106
+ ### deploy
101
107
 
102
- > Deployment commands use current `kubectl` context.
108
+ Deploy a Context.
103
109
 
104
- ### deploy
110
+ - Build Docker images.
111
+ - Push Docker images to the registry.
112
+ - Build a Helm chart.
113
+ - Apply the Helm chart to the current Kubernetes context.
105
114
 
106
115
  <dl>
107
116
  <dt><code>toa deploy [environment]</code></dt>
108
- <dd>Deploy context.
109
-
117
+ <dd>
110
118
  <code>environment</code> deployment environment name (default <code>default</code>).<br/>
119
+ <code>--path</code> path to a Context (default <code>.</code>)<br/>
120
+ <code>--namespace</code> Kubernetes namespace to apply the Helm chat to<br/>
121
+ <code>--wait</code> wait until all
122
+ Pods [are ready](https://helm.sh/docs/intro/using_helm/#helpful-options-for-installupgraderollback)<br/>
123
+ <code>--dry</code> do not apply the Helm chart
111
124
  </dd>
112
125
  </dl>
113
126
 
114
127
  ### conceal
115
128
 
129
+ Deploy a generic Kubernetes secret with the prefix `toa-`.
130
+
131
+ <dl>
132
+ <dt><code>toa conceal &lt;secret&gt; &lt;key-values...&gt;</code></dt>
133
+ <dd>
134
+ <code>secret</code> Secret name.<br/>
135
+ <code>key-values</code> List of keys and values of the secret as <code>key=value</code>.<br/>
136
+ <code>--namespace</code> Kubernetes namespace where the secret should be deployed.<br/>
137
+ <code>--interactive</code> prompt for secret values<br/>
138
+ <code>--environment</code> environment name for interactive mode<br/>
139
+ <code>--path</code> path to a context for interactive mode
140
+ </dd>
141
+ </dl>
142
+
143
+ > If a secret already exists, then given `key-values` will be added to it.
144
+
145
+ #### Example
146
+
147
+ ```shell
148
+ $ toa conceal bindings-amqp-default username=developer password=secret
149
+ ```
150
+
151
+ ### reveal
152
+
153
+ Outputs keys and values of a secret.
154
+
116
155
  <dl>
117
156
  <dt>
118
- <code>toa conceal</code>
119
- <img src="https://img.shields.io/badge/Not_Implemented-red" alt="Not Implemented"/>
157
+ <code>toa reveal &lt;secret&gt;</code>
120
158
  </dt>
121
- <dd>Deploy new declared secrets.
159
+ </dl>
160
+
161
+ ### shell
122
162
 
123
- <code>--reset</code> don't skip already deployed</dd>
163
+ Run interactive shell inside a disposable pod inside a Kubernetes cluster.
124
164
 
125
- <dt><code>toa conceal &lt;secret&gt; &lt;key&gt; &lt;value&gt;</code></dt>
126
- <dd>Deploy a <code>key</code> with a <code>value</code> to a <code>secret</code>.</dd>
165
+ <dl>
166
+ <dt>
167
+ <code>toa shell [image]</code>
168
+ </dt>
169
+ <dd>
170
+ <code>image</code> Docker image to Run (default <code>alpine</code>).<br/>
171
+ </dd>
127
172
  </dl>
128
173
 
129
- ### reveal
174
+ #### Examples
175
+
176
+ ```shell
177
+ $ toa shell mongo
178
+ $ toa shell -- ping 1.1 # extra arguments can be passed
179
+ ```
180
+
181
+ ### key
182
+
183
+ Generate a secret PASETO key.
130
184
 
131
185
  <dl>
132
186
  <dt>
133
- <code>toa reveal &lt;secret&gt;</code>
187
+ <code>toa key</code>
134
188
  </dt>
135
- <dd>Print keys and values of a secret.</dd>
189
+ <dd>
190
+ <code>--public</code> <code>boolean</code> generate a public/private key pair.<br/>
191
+ </dd>
136
192
  </dl>
@@ -4,10 +4,6 @@ const { build } = require('../handlers/build')
4
4
 
5
5
  const builder = (yargs) => {
6
6
  yargs
7
- .positional('environment', {
8
- type: 'string',
9
- desc: 'Deployment environment'
10
- })
11
7
  .option('path', {
12
8
  alias: 'p',
13
9
  group: 'Command options:',
@@ -17,7 +13,7 @@ const builder = (yargs) => {
17
13
  })
18
14
  }
19
15
 
20
- exports.command = 'build [environment]'
21
- exports.desc = 'Build and push docker images'
16
+ exports.command = 'build'
17
+ exports.desc = 'Build Docker images'
22
18
  exports.builder = builder
23
19
  exports.handler = build
@@ -12,10 +12,26 @@ const builder = (yargs) => {
12
12
  default: '.'
13
13
  })
14
14
  .array('paths')
15
+ .option('kill', {
16
+ group: 'Command options:',
17
+ type: 'boolean',
18
+ desc: 'Immediate shutdown'
19
+ })
20
+ .option('dock', {
21
+ group: 'Command options:',
22
+ type: 'boolean',
23
+ desc: 'Run in Docker'
24
+ })
25
+ .option('context', {
26
+ group: 'Command options:',
27
+ type: 'string',
28
+ desc: 'Path to the Context (used with --dock)',
29
+ default: '.'
30
+ })
15
31
  .option('bindings', {
16
32
  group: 'Command options:',
17
33
  type: 'string',
18
- desc: 'Bindings'
34
+ desc: 'OBSOLETE'
19
35
  })
20
36
  .array('bindings')
21
37
  .example([
@@ -7,15 +7,46 @@ const builder = (yargs) => {
7
7
  .positional('secret', {
8
8
  type: 'string'
9
9
  })
10
- .positional('key', {
11
- type: 'string'
10
+ .positional('key-values', {
11
+ type: 'string',
12
+ array: true,
13
+ desc: 'Secret key-value pairs'
14
+ })
15
+ .option('namespace', {
16
+ alias: 'n',
17
+ group: 'Command options:',
18
+ type: 'string',
19
+ desc: 'Target Kubernetes namespace'
12
20
  })
13
- .positional('value', {
21
+ .option('interactive', {
22
+ alias: 'i',
23
+ group: 'Command options:',
24
+ describe: 'Prompt for secrets',
25
+ type: 'boolean',
26
+ default: false
27
+ })
28
+ .option('environment', {
29
+ alias: 'e',
30
+ group: 'Command options:',
31
+ describe: 'Environment name for interactive mode',
14
32
  type: 'string'
15
33
  })
34
+ .option('path', {
35
+ alias: 'p',
36
+ group: 'Command options:',
37
+ describe: 'Path to a Context for interactive mode',
38
+ type: 'string',
39
+ default: '.'
40
+ })
41
+ .example([
42
+ ['$0 conceal -i'],
43
+ ['$0 conceal credentials username=developer'],
44
+ ['$0 conceal credentials username=developer password=secret'],
45
+ ['$0 conceal credentials username=developer --namespace app']
46
+ ])
16
47
  }
17
48
 
18
- exports.command = 'conceal <secret> <key> <value>'
19
- exports.desc = 'Deploy a key with a value to a secret'
49
+ exports.command = 'conceal [secret] [key-values...]'
50
+ exports.desc = 'Deploy a secret'
20
51
  exports.builder = builder
21
52
  exports.handler = conceal
@@ -12,10 +12,23 @@ const builder = (yargs) => {
12
12
  .option('path', {
13
13
  alias: 'p',
14
14
  group: 'Command options:',
15
- describe: 'Path to component',
15
+ describe: 'Path to a Context',
16
16
  type: 'string',
17
17
  default: '.'
18
18
  })
19
+ .option('as', {
20
+ group: 'Command options:',
21
+ describe: 'Output file path',
22
+ type: 'string',
23
+ default: '.env'
24
+ })
25
+ .option('interactive', {
26
+ alias: 'i',
27
+ group: 'Command options:',
28
+ describe: 'Prompt for secrets',
29
+ type: 'boolean',
30
+ default: false
31
+ })
19
32
  }
20
33
 
21
34
  exports.command = 'env [environment]'
@@ -6,7 +6,7 @@ const builder = (yargs) => {
6
6
  yargs
7
7
  .positional('target', {
8
8
  type: 'string',
9
- desc: 'Export target path'
9
+ desc: 'Path to export to'
10
10
  })
11
11
  .positional('environment', {
12
12
  type: 'string',
@@ -21,7 +21,7 @@ const builder = (yargs) => {
21
21
  })
22
22
  }
23
23
 
24
- exports.command = ['deployment <target> [environment]', 'dep']
24
+ exports.command = ['deployment <environment> <target>', 'dep']
25
25
  exports.desc = 'Export context deployment'
26
26
  exports.builder = builder
27
27
  exports.handler = dump
@@ -6,7 +6,7 @@ const builder = (yargs) => {
6
6
  yargs
7
7
  .positional('target', {
8
8
  type: 'string',
9
- desc: 'Export target path'
9
+ desc: 'Path to export to'
10
10
  })
11
11
  .option('path', {
12
12
  alias: 'p',
@@ -17,7 +17,7 @@ const builder = (yargs) => {
17
17
  })
18
18
  }
19
19
 
20
- exports.command = ['images [target]', 'imgs']
20
+ exports.command = ['images <target>', 'img']
21
21
  exports.desc = 'Export docker image sources'
22
22
  exports.builder = builder
23
23
  exports.handler = prepare
@@ -0,0 +1,18 @@
1
+ 'use strict'
2
+
3
+ const { key } = require('../handlers/key')
4
+
5
+ const builder = (yargs) => {
6
+ yargs
7
+ .option('public', {
8
+ group: 'Command options:',
9
+ describe: 'Generate a public/private key pair',
10
+ type: 'boolean',
11
+ default: false
12
+ })
13
+ }
14
+
15
+ exports.command = 'key'
16
+ exports.desc = 'Generate a secret PASETO key'
17
+ exports.builder = builder
18
+ exports.handler = key
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ const { push } = require('../handlers/push')
4
+
5
+ const builder = (yargs) => {
6
+ yargs
7
+ .option('path', {
8
+ alias: 'p',
9
+ group: 'Command options:',
10
+ type: 'string',
11
+ desc: 'Path to context',
12
+ default: '.'
13
+ })
14
+ }
15
+
16
+ exports.command = 'push'
17
+ exports.desc = 'Build and push Docker images'
18
+ exports.builder = builder
19
+ exports.handler = push
@@ -4,6 +4,10 @@
4
4
 
5
5
  const { replay } = require('../handlers/replay')
6
6
 
7
+ /*
8
+ !!! OPTIONS MUST BE SYNCHRONIZED WITH ../handlers/.replay/args !!!
9
+ */
10
+
7
11
  const builder = (yargs) => {
8
12
  yargs
9
13
  .positional('paths', {
@@ -17,6 +21,12 @@ const builder = (yargs) => {
17
21
  group: 'Command options:',
18
22
  describe: 'Replay samples for specified component'
19
23
  })
24
+ .option('autonomous', {
25
+ alias: 'a',
26
+ type: 'boolean',
27
+ group: 'Command options:',
28
+ describe: 'Replay autonomous tests only'
29
+ })
20
30
  .option('integration', {
21
31
  alias: 'i',
22
32
  type: 'boolean',
@@ -35,6 +45,19 @@ const builder = (yargs) => {
35
45
  group: 'Command options:',
36
46
  describe: 'Replay samples with titles matching given regexp'
37
47
  })
48
+ .option('dock', {
49
+ alias: 'd',
50
+ type: 'boolean',
51
+ default: false,
52
+ group: 'Command options:',
53
+ describe: 'Replay inside Docker container'
54
+ })
55
+ .option('context', {
56
+ group: 'Command options:',
57
+ type: 'string',
58
+ desc: 'Path to the Context (used with --dock)',
59
+ default: '.'
60
+ })
38
61
  }
39
62
 
40
63
  exports.command = 'replay [paths...]'
@@ -5,15 +5,14 @@ const { serve } = require('../handlers/serve')
5
5
  const builder = (yargs) => {
6
6
  yargs
7
7
  .positional('path', {
8
- alias: 'p',
9
8
  group: 'Command options:',
10
9
  type: 'string',
11
- desc: 'Path to package',
10
+ desc: 'Path or a shortcut of an extension',
12
11
  default: '.'
13
12
  })
14
13
  }
15
14
 
16
15
  exports.command = 'serve [path]'
17
- exports.desc = 'Run service'
16
+ exports.desc = 'Run an extension service'
18
17
  exports.builder = builder
19
18
  exports.handler = serve
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ const { shell } = require('../handlers/shell')
4
+
5
+ const builder = (yargs) => {
6
+ yargs
7
+ .positional('image', {
8
+ group: 'Command options:',
9
+ type: 'string',
10
+ desc: 'Docker image',
11
+ default: 'alpine'
12
+ })
13
+ .example([
14
+ ['$0 shell'],
15
+ ['$0 shell -- ping localhost']
16
+ ])
17
+ }
18
+
19
+ exports.command = 'shell [image]'
20
+ exports.desc = 'Run interactive shell from the current Kubernetes context'
21
+ exports.builder = builder
22
+ exports.handler = shell
@@ -6,9 +6,9 @@ const { context: find } = require('../util/find')
6
6
 
7
7
  const build = async (argv) => {
8
8
  const path = find(argv.path)
9
- const operator = await boot.deployment(path, argv.environment)
9
+ const registry = await boot.registry(path)
10
10
 
11
- await operator.build()
11
+ await registry.build()
12
12
  }
13
13
 
14
14
  exports.build = build
@@ -1,14 +1,39 @@
1
1
  'use strict'
2
2
 
3
+ const { pick } = require('@toa.io/generic')
3
4
  const boot = require('@toa.io/boot')
5
+ const { version } = require('@toa.io/runtime')
4
6
 
7
+ const docker = require('./docker')
5
8
  const { components: find } = require('../util/find')
6
9
 
10
+ /**
11
+ * @param {Record<string, string | boolean>} argv
12
+ * @return {Promise<void>}
13
+ */
7
14
  async function compose (argv) {
15
+ console.log('Runtime version:', version)
16
+
17
+ if (argv.dock === true) return dock(argv)
18
+
8
19
  const paths = find(argv.paths)
9
20
  const composition = await boot.composition(paths, argv)
10
21
 
11
22
  await composition.connect()
23
+
24
+ if (argv.kill === true) await composition.disconnect()
25
+ }
26
+
27
+ /**
28
+ * @param {Record<string, string | string[] | boolean>} argv
29
+ * @return {Promise<void>}
30
+ */
31
+ async function dock (argv) {
32
+ const repository = await docker.build(argv.context, argv.paths)
33
+ const args = pick(argv, ['kill', 'bindings'])
34
+ const command = docker.command('toa compose *', args)
35
+
36
+ await docker.run(repository, command, argv.env)
12
37
  }
13
38
 
14
39
  exports.compose = compose
@@ -1,11 +1,61 @@
1
1
  'use strict'
2
2
 
3
3
  const { secrets } = require('@toa.io/kubernetes')
4
+ const boot = require('@toa.io/boot')
5
+ const { context: find } = require('../util/find')
6
+ const { promptSecrets } = require('./env')
4
7
 
5
8
  const conceal = async (argv) => {
6
- const { secret, key, value } = argv
9
+ if (argv.interactive) await concealValues(argv)
10
+ else await concealValue(argv)
11
+ }
12
+
13
+ async function concealValue (argv) {
14
+ if (argv['key-values'].length === 0) throw new Error('Key-values must be passed')
15
+
16
+ const values = argv['key-values'].reduce((values, pair) => {
17
+ const [key, value] = pair.split('=')
18
+
19
+ values[key] = value
20
+
21
+ return values
22
+ }, {})
23
+
24
+ const secret = PREFIX + argv.secret
25
+
26
+ await secrets.upsert(secret, values, argv.namespace)
27
+ }
7
28
 
8
- await secrets.store(secret, { [key]: value })
29
+ async function concealValues (argv) {
30
+ const path = find(argv.path)
31
+ const operator = await boot.deployment(path, argv.environment)
32
+ const variables = operator.variables()
33
+ const values = await promptSecrets(variables)
34
+ const groups = groupValues(values)
35
+
36
+ for (const [secret, values] of Object.entries(groups)) {
37
+ await secrets.upsert(secret, values, argv.namespace)
38
+ }
39
+ }
40
+
41
+ /**
42
+ * @return {Record<string, Record<string, string>>}
43
+ */
44
+ function groupValues (values) {
45
+ const secrets = {}
46
+
47
+ for (const [key, value] of Object.entries(values)) {
48
+ const [secret, variable] = key.split('/')
49
+
50
+ if (!(secret in secrets)) secrets[secret] = {}
51
+
52
+ secrets[secret][variable] = value
53
+ }
54
+
55
+ return secrets
9
56
  }
10
57
 
58
+ const PREFIX = 'toa-'
59
+
11
60
  exports.conceal = conceal
61
+ exports.PREFIX = PREFIX
@@ -0,0 +1,61 @@
1
+ 'use strict'
2
+
3
+ const { newid } = require('@toa.io/generic')
4
+ const norm = require('@toa.io/norm')
5
+ const { deployment: { Factory } } = require('@toa.io/operations')
6
+
7
+ const find = require('../../util/find')
8
+
9
+ /**
10
+ * @param {string} contextPath
11
+ * @param {string[]} componentPatterns
12
+ * @return {Promise<string>}
13
+ */
14
+ async function build (contextPath, componentPatterns) {
15
+ const context = await createContext(contextPath, componentPatterns)
16
+ const factory = new Factory(context)
17
+ const registry = factory.registry()
18
+
19
+ await registry.build()
20
+
21
+ const composition = context.compositions[0].name
22
+
23
+ return `${context.registry.base === undefined ? '' : context.registry.base + '/'}${context.name}/composition-${composition}`
24
+ }
25
+
26
+ /**
27
+ * @param {string} contextPath
28
+ * @param {string[]} componentPatterns
29
+ * @return {Promise<toa.norm.Context>}
30
+ */
31
+ async function createContext (contextPath, componentPatterns) {
32
+ const contextRoot = find.context(contextPath)
33
+ const context = await norm.context(contextRoot)
34
+ const paths = componentPatterns.map((pattern) => find.components(pattern))
35
+ const components = await loadComponents(paths)
36
+ const rnd = newid().substring(0, 6)
37
+ const name = 'replay-' + rnd
38
+
39
+ context.name += '-' + rnd
40
+ context.compositions = [{ name, components }]
41
+
42
+ return context
43
+ }
44
+
45
+ /**
46
+ * @param {string[]} paths
47
+ * @return {Promise<toa.norm.Component[]>}
48
+ */
49
+ async function loadComponents (paths) {
50
+ const components = []
51
+
52
+ for (const path of paths) {
53
+ const component = await norm.component(path)
54
+
55
+ components.push(component)
56
+ }
57
+
58
+ return components
59
+ }
60
+
61
+ exports.build = build
@@ -0,0 +1,23 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * @param {string} command
5
+ * @param {Record<string, any>} args
6
+ */
7
+ const command = (command, args) => {
8
+ const options = []
9
+
10
+ for (const [name, value] of Object.entries(args)) {
11
+ if (value === undefined) continue
12
+
13
+ options.push('--' + name)
14
+
15
+ if (typeof value !== 'boolean') options.push(`"${value}"`)
16
+ }
17
+
18
+ const argumentLine = options.join(' ')
19
+
20
+ return command + ' ' + argumentLine
21
+ }
22
+
23
+ exports.command = command
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ const { build } = require('./build')
4
+ const { command } = require('./command')
5
+ const { run } = require('./run')
6
+
7
+ exports.build = build
8
+ exports.command = command
9
+ exports.run = run
@@ -0,0 +1,39 @@
1
+ 'use strict'
2
+
3
+ const { spawn, exec } = require('node:child_process')
4
+ const { promisify } = require('node:util')
5
+
6
+ const { promex } = require('@toa.io/generic')
7
+ const { file: { dot } } = require('@toa.io/filesystem')
8
+
9
+ const execute = promisify(exec)
10
+
11
+ /**
12
+ * @param {string} repository
13
+ * @param {string} command
14
+ * @param {string} [envFile]
15
+ * @return {Promise<void>}
16
+ */
17
+ async function run (repository, command, envFile) {
18
+ if (envFile === undefined) envFile = await dot('env')
19
+
20
+ const envArgs = envFile === undefined ? [] : ['--env-file', envFile]
21
+
22
+ const found =
23
+ /** @type {{ stdout: string }} */
24
+ await execute(`docker images -q ${repository} | head -n 1`)
25
+
26
+ const id = found.stdout.trim()
27
+ const args = ['run', '--rm', ...envArgs, id, 'sh', '-c', command]
28
+ const done = promex()
29
+
30
+ const running = await spawn('docker', args, { stdio: 'inherit' })
31
+
32
+ running.on('exit', done.resolve)
33
+
34
+ await done
35
+
36
+ await execute(`docker rmi --force ${id}`)
37
+ }
38
+
39
+ exports.run = run
@@ -1,6 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const { join } = require('node:path')
4
+ const readline = require('node:readline/promises')
5
+ const { stdin: input, stdout: output } = require('node:process')
6
+
4
7
  const dotenv = require('dotenv')
5
8
  const { file } = require('@toa.io/filesystem')
6
9
  const boot = require('@toa.io/boot')
@@ -8,17 +11,20 @@ const { context: find } = require('../util/find')
8
11
 
9
12
  async function env (argv) {
10
13
  const path = find(argv.path)
11
- const filepath = join(path, '.env')
14
+ const filepath = join(path, argv.as)
12
15
  const operator = await boot.deployment(path, argv.environment)
13
16
  const variables = operator.variables()
14
17
  const currentValues = await read(filepath)
15
- const values = []
16
18
 
17
- for (const scoped of Object.values(variables)) values.push(...scoped)
19
+ const result = merge(variables, currentValues)
20
+
21
+ if (argv.interactive) {
22
+ const secrets = await promptSecrets(result)
18
23
 
19
- const nextValues = merge(values, currentValues)
24
+ mergeSecrets(result, secrets)
25
+ }
20
26
 
21
- await write(filepath, nextValues)
27
+ await write(filepath, result)
22
28
  }
23
29
 
24
30
  /**
@@ -41,25 +47,71 @@ async function read (path) {
41
47
  * @return {Promise<void>}
42
48
  */
43
49
  async function write (path, values) {
44
- const contents = values.reduce((lines, { name, value }) => lines + `${name}=${value}\n`, '')
50
+ const contents = values.reduce((lines, { name, value }) => lines + `${name}=${value ?? ''}\n`, '')
45
51
 
46
52
  await file.write(path, contents)
47
53
  }
48
54
 
49
55
  /**
50
- * @param {toa.deployment.dependency.Variable[] } variables
56
+ * @param {toa.deployment.dependency.Variable[]} variables
51
57
  * @param {Record<string, string>} current
52
58
  * @return {toa.deployment.dependency.Variable[]}
53
59
  */
54
60
  function merge (variables, current) {
55
61
  return variables.map((variable) => {
56
- if (variable.secret === undefined) return variable
62
+ if (variable.secret === undefined || !current[variable.name]) return variable
57
63
 
58
64
  return {
59
65
  name: variable.name,
60
- value: current[variable.name] ?? ''
66
+ value: current[variable.name]
61
67
  }
62
68
  })
63
69
  }
64
70
 
71
+ async function promptSecrets (variables) {
72
+ const rl = readline.createInterface({ input, output })
73
+ const secrets = {}
74
+
75
+ for (const variable of variables) {
76
+ if (variable.secret === undefined) continue
77
+
78
+ const key = getKey(variable.secret)
79
+
80
+ secrets[key] = await promptSecret(key, rl)
81
+ }
82
+
83
+ rl.close()
84
+
85
+ return secrets
86
+ }
87
+
88
+ async function promptSecret (key, rl) {
89
+ if (SECRETS[key] === undefined) SECRETS[key] = await rl.question(`${key}: `)
90
+
91
+ return SECRETS[key]
92
+ }
93
+
94
+ /**
95
+ * @param {toa.deployment.dependency.Variable[]} variables
96
+ * @param {Record<string, string>} secrets
97
+ */
98
+ function mergeSecrets (variables, secrets) {
99
+ for (const variable of variables) {
100
+ if (variable.secret === undefined) continue
101
+
102
+ const key = getKey(variable.secret)
103
+
104
+ variable.value = secrets[key]
105
+
106
+ delete variable.secret
107
+ }
108
+ }
109
+
110
+ function getKey (secret) {
111
+ return `${secret.name}/${secret.key}`
112
+ }
113
+
114
+ const SECRETS = {}
115
+
65
116
  exports.env = env
117
+ exports.promptSecrets = promptSecrets
@@ -13,4 +13,12 @@ const prepare = async (argv) => {
13
13
  console.log(path)
14
14
  }
15
15
 
16
+ // const prepare = async (argv) => {
17
+ // const context = find(argv.path)
18
+ // const registry = await boot.registry(context)
19
+ // const path = await registry.prepare(argv.target)
20
+ //
21
+ // console.log(path)
22
+ // }
23
+
16
24
  exports.prepare = prepare
@@ -0,0 +1,16 @@
1
+ 'use strict'
2
+
3
+ const { V3 } = require('paseto')
4
+
5
+ async function key (argv) {
6
+ const purpose = argv.public ? 'public' : 'local'
7
+ const key = await V3.generateKey(purpose, { format: 'paserk' })
8
+
9
+ if (argv.public) {
10
+ console.log(key.secretKey)
11
+ console.log(key.publicKey)
12
+ } else
13
+ console.log(key)
14
+ }
15
+
16
+ exports.key = key
@@ -0,0 +1,14 @@
1
+ 'use strict'
2
+
3
+ const boot = require('@toa.io/boot')
4
+
5
+ const { context: find } = require('../util/find')
6
+
7
+ const push = async (argv) => {
8
+ const path = find(argv.path)
9
+ const registry = await boot.registry(path)
10
+
11
+ await registry.push()
12
+ }
13
+
14
+ exports.push = push
@@ -1,10 +1,17 @@
1
1
  'use strict'
2
2
 
3
+ const { pick } = require('@toa.io/generic')
4
+ const { context, components } = require('@toa.io/userland/samples')
5
+
3
6
  const find = require('../util/find')
7
+ const docker = require('./docker')
4
8
 
9
+ /**
10
+ * @param {Record<string, string | string[] | boolean>} argv
11
+ * @return {Promise<void>}
12
+ */
5
13
  async function replay (argv) {
6
- // prevent loading userland which is intended for local use only
7
- const { context, components } = require('@toa.io/userland/samples')
14
+ if (argv.dock) return dock(argv)
8
15
 
9
16
  /** @type {boolean} */
10
17
  let ok
@@ -14,9 +21,11 @@ async function replay (argv) {
14
21
  /** @type {toa.samples.suite.Options} */
15
22
  const options = {
16
23
  component: argv.component,
24
+ autonomous: argv.autonomous,
17
25
  integration: argv.integration,
18
26
  operation: argv.operation,
19
- title: argv.title
27
+ title: argv.title,
28
+ runner: { bail: true }
20
29
  }
21
30
 
22
31
  if (paths !== null) {
@@ -36,6 +45,18 @@ async function replay (argv) {
36
45
  process.on('beforeExit', () => console.log(message))
37
46
  }
38
47
 
48
+ /**
49
+ * @param {Record<string, string | string[] | boolean>} argv
50
+ * @return {Promise<void>}
51
+ */
52
+ async function dock (argv) {
53
+ const repository = await docker.build(argv.context, argv.paths)
54
+ const args = pick(argv, ['component', 'operation', 'integration', 'title'])
55
+ const command = docker.command('toa replay *', args)
56
+
57
+ await docker.run(repository, command, argv.env)
58
+ }
59
+
39
60
  const GREEN = '\x1b[32m'
40
61
  const RED = '\x1b[31m'
41
62
  const RESET = '\x1b[0m'
@@ -1,13 +1,16 @@
1
1
  'use strict'
2
2
 
3
3
  const { secrets } = require('@toa.io/kubernetes')
4
- const { remap, decode } = require('@toa.io/generic')
4
+
5
+ const { PREFIX } = require('./conceal')
5
6
 
6
7
  const reveal = async (argv) => {
7
- const secret = await secrets.get(argv.secret)
8
- const values = remap(secret.data, decode)
8
+ const prefixed = PREFIX + argv.secret
9
+ const data = await secrets.get(prefixed)
10
+
11
+ if (data === null) return
9
12
 
10
- for (const [key, value] of Object.entries(values)) {
13
+ for (const [key, value] of Object.entries(data)) {
11
14
  const line = `${key}: ${value}`
12
15
 
13
16
  console.log(line)
@@ -3,21 +3,22 @@
3
3
  const boot = require('@toa.io/boot')
4
4
  const { shortcuts } = require('@toa.io/norm')
5
5
  const { directory: { find } } = require('@toa.io/filesystem')
6
+ const { version } = require('@toa.io/runtime')
6
7
 
7
8
  const serve = async (argv) => {
9
+ console.log('Runtime version:', version)
10
+
8
11
  argv.path = shortcuts.resolve(argv.path)
9
12
 
10
13
  const module = find(argv.path, process.cwd())
11
14
 
12
15
  const { Factory } = require(module)
13
16
 
14
- /** @type {toa.core.extensions.Factory} */
15
17
  const factory = new Factory(boot)
16
18
 
17
- /** @type {toa.core.Connector} */
18
- const service = factory.service()
19
+ if (factory.service === undefined) throw new Error(`Service is not implemented by ${argv.path}`)
19
20
 
20
- if (service === undefined) throw new Error(`Cannot find service '${argv.name}' in ${argv.path}`)
21
+ const service = factory.service()
21
22
 
22
23
  await service.connect()
23
24
 
@@ -0,0 +1,29 @@
1
+ 'use strict'
2
+
3
+ const { spawn } = require('node:child_process')
4
+ const { newid } = require('@toa.io/generic')
5
+
6
+ const shell = async (argv) => {
7
+ const rnd = newid().substring(0, 6)
8
+
9
+ const args = [
10
+ 'run',
11
+ 'shell-' + rnd,
12
+ '--rm',
13
+ '-i',
14
+ '--tty',
15
+ '--image',
16
+ argv.image,
17
+ '--restart=Never',
18
+ '--'
19
+ ]
20
+
21
+ const extra = argv._.splice(1)
22
+
23
+ if (extra.length > 0) args.push(...extra)
24
+ else args.push('sh')
25
+
26
+ await spawn('kubectl', args, { stdio: 'inherit' })
27
+ }
28
+
29
+ exports.shell = shell
package/src/program.js CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  'use strict'
4
4
 
5
- if (!('TOA_ENV' in process.env)) require('dotenv').config()
6
-
7
5
  const yargs = require('yargs/yargs')
8
6
 
9
7
  const { console } = require('@toa.io/console')
@@ -18,16 +16,25 @@ yargs(process.argv.slice(2))
18
16
 
19
17
  console.level(argv.log)
20
18
  })
19
+ .middleware(async (argv) => {
20
+ if (argv.env === undefined) return
21
+
22
+ require('dotenv').config({ path: /** @type {string} */ argv.env })
23
+ })
21
24
  .fail((msg, err) => {
22
25
  const actual = err || new Error(msg)
23
26
 
24
- console.error(process.env.TOA_DEBUG === '1' ? actual : actual.message)
27
+ console.error(actual)
25
28
 
26
29
  process.exit(actual.exitCode > 0 ? actual.exitCode : 1)
27
30
  })
28
31
  .option('log', {
29
32
  describe: 'Log level'
30
33
  })
34
+ .option('env', {
35
+ type: 'string',
36
+ describe: 'Path to environment variables file (.env format)'
37
+ })
31
38
  .commandDir('./commands')
32
39
  .demandCommand(1, 'A command is required. Pass --help to see all available commands and options.')
33
40
  .strict()
package/src/util/find.js CHANGED
@@ -17,7 +17,7 @@ const find = (from, filename, test) => {
17
17
 
18
18
  if (found.size === 0) {
19
19
  if (test === true) return null
20
- else throw new Error(`File '${filename}' is found in ${from.join(', ')}`)
20
+ else throw new Error(`File '${filename}' is not found in ${from.join(', ')}`)
21
21
  }
22
22
 
23
23
  return [...found]
@@ -1,43 +0,0 @@
1
- 'use strict'
2
-
3
- const { configure } = require('../handlers/configure')
4
-
5
- const builder = (yargs) => {
6
- yargs
7
- .positional('key', {
8
- type: 'string',
9
- desc: 'Configuration Object key'
10
- })
11
- .positional('value', {
12
- type: 'string',
13
- desc: 'Key value'
14
- })
15
- .option('path', {
16
- alias: 'p',
17
- group: 'Command options:',
18
- describe: 'Path to component',
19
- type: 'string',
20
- default: '.'
21
- })
22
- .option('reset', {
23
- group: 'Command options:',
24
- type: 'boolean',
25
- desc: 'Remove Configuration Object key'
26
- })
27
- .option('json', {
28
- group: 'Command options:',
29
- type: 'boolean',
30
- desc: 'Print as JSON'
31
- })
32
- .example([
33
- ['$0 configure myKey \'new value\' | source /dev/stdin'],
34
- ['$0 configure myObject.myKey --reset | source /dev/stdin'],
35
- ['$0 configure reset | source /dev/stdin'],
36
- ['$0 configure print --json']
37
- ])
38
- }
39
-
40
- exports.command = 'configure [key] [value]'
41
- exports.desc = 'Output shell command to update local environment Configuration Object'
42
- exports.builder = builder
43
- exports.handler = configure
@@ -1,15 +0,0 @@
1
- 'use strict'
2
-
3
- const { dump } = require('@toa.io/yaml')
4
-
5
- const reset = (provider) => console.log('unset ' + provider.key)
6
-
7
- const print = async (provider, argv) => {
8
- await provider.connect()
9
-
10
- const formatter = argv.json ? JSON.stringify : dump
11
-
12
- console.log(formatter(provider.object))
13
- }
14
-
15
- exports.subcommands = { print, reset }
@@ -1,46 +0,0 @@
1
- 'use strict'
2
-
3
- const boot = require('@toa.io/boot')
4
-
5
- const { Factory } = require('@toa.io/extensions.configuration')
6
-
7
- const { subcommands } = require('./.configure/subcommands')
8
- const { components: find } = require('../util/find')
9
-
10
- async function configure (argv) {
11
- const path = find(argv.path)
12
- const manifest = await boot.manifest(path)
13
- const factory = new Factory()
14
- const provider = factory.provider(manifest)
15
-
16
- if (argv.value === undefined && subcommands[argv.key] !== undefined) {
17
- await subcommands[argv.key](provider, argv)
18
-
19
- return
20
- }
21
-
22
- await provider.connect()
23
-
24
- let key = argv.key
25
- let value = argv.value
26
-
27
- // :(
28
- if (key !== undefined) {
29
- if (value === undefined && key.includes('=')) [key, value] = key.split('=')
30
-
31
- if (value === undefined) {
32
- if (argv.reset !== true) throw new Error('Key value expected')
33
- else provider.unset(key)
34
- } else provider.set(key, value)
35
- }
36
-
37
- let command
38
- const exported = provider.export()
39
-
40
- if (exported === undefined) command = `unset ${provider.key}`
41
- else command = `export ${provider.key}=` + exported
42
-
43
- console.log(command)
44
- }
45
-
46
- exports.configure = configure