@toa.io/cli 0.20.0-dev.9 → 0.20.1-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.20.0-dev.9",
3
+ "version": "0.20.1-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.20.0-dev.10",
26
- "@toa.io/generic": "0.20.0-dev.10",
27
- "@toa.io/kubernetes": "0.20.0-dev.10",
28
- "@toa.io/norm": "0.20.0-dev.10",
29
- "@toa.io/yaml": "0.20.0-dev.10",
25
+ "@toa.io/console": "0.20.1-alpha.0",
26
+ "@toa.io/generic": "0.20.1-alpha.0",
27
+ "@toa.io/kubernetes": "0.20.1-alpha.0",
28
+ "@toa.io/norm": "0.21.0-alpha.0",
29
+ "@toa.io/yaml": "0.20.1-alpha.0",
30
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": "cd3ffb1ae1af014f13a62ef9fc8ada58d8688e4a"
35
+ "gitHead": "ed28dc0d2823022fbb1188bb28b994bc827a4432"
35
36
  }
package/readme.md CHANGED
@@ -26,6 +26,22 @@ Run composition.
26
26
 
27
27
  > Note that your `localhost` it is accessible from a container as `host.docker.internal`.
28
28
 
29
+ ### call
30
+
31
+ Call endpoint.
32
+
33
+ <dl>
34
+ <dt><code>toa call &lt;endpont&gt; [request]</code></dt>
35
+ <dd>
36
+ <code>endpoint</code> endpoint to call.<br/>
37
+ <code>request</code> Request object.<br/>
38
+ </dd>
39
+ </dl>
40
+
41
+ ```shell
42
+ $ toa call dummies.dummy.create "{ input: { name: 'foo' } }"
43
+ ```
44
+
29
45
  ### env
30
46
 
31
47
  Export environment to a `.env` file.
@@ -35,7 +51,8 @@ Export environment to a `.env` file.
35
51
  <dd>
36
52
  <code>environment</code> deployment environment name (default <code>local</code>).<br/>
37
53
  <code>--path</code> path to a Context (default <code>.</code>)<br/>
38
- <code>--as</code> output file path (default <code>.env</code>)
54
+ <code>--as</code> output file path (default <code>.env</code>)<br/>
55
+ <code>--interactive</code> prompt for secret values
39
56
  </dd>
40
57
  </dl>
41
58
 
@@ -72,7 +89,8 @@ $ toa replay ./path/to/context
72
89
  $ toa replay --title "should add numbers"
73
90
  ```
74
91
 
75
- If the path is a Context root (containing `context.toa.yaml` file), samples for components within the Context will be
92
+ If the path is a Context root (containing `context.toa.yaml` file), samples for components within
93
+ the Context will be
76
94
  found and replayed sequentially.
77
95
 
78
96
  ### export manifest
@@ -88,7 +106,7 @@ found and replayed sequentially.
88
106
 
89
107
  ## Operations
90
108
 
91
- > Some commands use current Kubernetes context.
109
+ > Some commands use current `kubectl` and `docker` context.
92
110
 
93
111
  ### build
94
112
 
@@ -124,12 +142,28 @@ Pods [are ready](https://helm.sh/docs/intro/using_helm/#helpful-options-for-inst
124
142
 
125
143
  ### conceal
126
144
 
127
- Deploy a `key` with a `value` to a secret named `toa-{secret}`.
145
+ Deploy a generic Kubernetes secret with the prefix `toa-`.
128
146
 
129
147
  <dl>
130
- <dt><code>toa conceal &lt;secret&gt; &lt;key&gt; &lt;value&gt;</code></dt>
148
+ <dt><code>toa conceal &lt;secret&gt; &lt;key-values...&gt;</code></dt>
149
+ <dd>
150
+ <code>secret</code> Secret name.<br/>
151
+ <code>key-values</code> List of keys and values of the secret as <code>key=value</code>.<br/>
152
+ <code>--namespace</code> Kubernetes namespace where the secret should be deployed.<br/>
153
+ <code>--interactive</code> prompt for secret values<br/>
154
+ <code>--environment</code> environment name for interactive mode<br/>
155
+ <code>--path</code> path to a context for interactive mode
156
+ </dd>
131
157
  </dl>
132
158
 
159
+ > If a secret already exists, then given `key-values` will be added to it.
160
+
161
+ #### Example
162
+
163
+ ```shell
164
+ $ toa conceal bindings-amqp-default username=developer password=secret
165
+ ```
166
+
133
167
  ### reveal
134
168
 
135
169
  Outputs keys and values of a secret.
@@ -149,12 +183,26 @@ Run interactive shell inside a disposable pod inside a Kubernetes cluster.
149
183
  <code>toa shell [image]</code>
150
184
  </dt>
151
185
  <dd>
152
- <code>image</code> Docker image<br/>
186
+ <code>image</code> Docker image to Run (default <code>alpine</code>).<br/>
153
187
  </dd>
154
188
  </dl>
155
189
 
156
- Extra arguments can be passed:
190
+ #### Examples
157
191
 
158
192
  ```shell
159
- $ toa shell -- ping 1.1
193
+ $ toa shell mongo
194
+ $ toa shell -- ping 1.1 # extra arguments can be passed
160
195
  ```
196
+
197
+ ### key
198
+
199
+ Generate a secret PASETO key.
200
+
201
+ <dl>
202
+ <dt>
203
+ <code>toa key</code>
204
+ </dt>
205
+ <dd>
206
+ <code>--public</code> <code>boolean</code> generate a public/private key pair.<br/>
207
+ </dd>
208
+ </dl>
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+
3
+ const { call } = require('../handlers/call')
4
+
5
+ const builder = (yargs) => {
6
+ yargs
7
+ .positional('endpoint', {
8
+ type: 'string',
9
+ desc: 'Operation endpoint'
10
+ })
11
+ .positional('request', {
12
+ type: 'string',
13
+ desc: 'Request object'
14
+ })
15
+ }
16
+
17
+ exports.command = 'call <endpoint> [request]'
18
+ exports.desc = 'Call operation'
19
+ exports.builder = builder
20
+ exports.handler = call
@@ -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
@@ -22,6 +22,13 @@ const builder = (yargs) => {
22
22
  type: 'string',
23
23
  default: '.env'
24
24
  })
25
+ .option('interactive', {
26
+ alias: 'i',
27
+ group: 'Command options:',
28
+ describe: 'Prompt for secrets',
29
+ type: 'boolean',
30
+ default: false
31
+ })
25
32
  }
26
33
 
27
34
  exports.command = 'env [environment]'
@@ -10,7 +10,7 @@ const builder = (yargs) => {
10
10
  })
11
11
  .positional('request', {
12
12
  type: 'string',
13
- desc: 'Request object (yaml)'
13
+ desc: 'Request object'
14
14
  })
15
15
  .option('path', {
16
16
  alias: 'p',
@@ -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
@@ -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,36 @@
1
+ 'use strict'
2
+
3
+ const { Readable } = require('node:stream')
4
+ const boot = require('@toa.io/boot')
5
+ const yaml = require('@toa.io/yaml')
6
+ const { Locator } = require('@toa.io/core')
7
+
8
+ async function call (argv) {
9
+ const [operation, component, namespace = 'default'] = argv.endpoint.split('.').reverse()
10
+ const locator = new Locator(component, namespace)
11
+ const request = argv.request ? yaml.parse(argv.request) : {}
12
+
13
+ const remote = await boot.remote(locator)
14
+ await remote.connect()
15
+
16
+ let reply
17
+ let exception
18
+
19
+ try {
20
+ reply = await remote.invoke(operation, request)
21
+ } catch (e) {
22
+ exception = e
23
+ } finally {
24
+ if (exception === undefined) {
25
+ if (reply instanceof Readable) {
26
+ for await (const chunk of reply) console.log(chunk)
27
+ } else console.log(reply)
28
+ } else console.error(exception)
29
+
30
+ await remote.disconnect()
31
+
32
+ if (exception !== undefined) process.exit(1)
33
+ }
34
+ }
35
+
36
+ exports.call = call
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { pick } = require('@toa.io/generic')
4
4
  const boot = require('@toa.io/boot')
5
+ const { version } = require('@toa.io/runtime')
5
6
 
6
7
  const docker = require('./docker')
7
8
  const { components: find } = require('../util/find')
@@ -11,6 +12,8 @@ const { components: find } = require('../util/find')
11
12
  * @return {Promise<void>}
12
13
  */
13
14
  async function compose (argv) {
15
+ console.log('Runtime', version)
16
+
14
17
  if (argv.dock === true) return dock(argv)
15
18
 
16
19
  const paths = find(argv.paths)
@@ -1,12 +1,58 @@
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
7
- const prefixed = PREFIX + secret
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
+ }
28
+
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
+ }
8
54
 
9
- await secrets.store(prefixed, { [key]: value })
55
+ return secrets
10
56
  }
11
57
 
12
58
  const PREFIX = 'toa-'
@@ -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')
@@ -12,13 +15,16 @@ async function env (argv) {
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
@@ -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
@@ -1,16 +1,16 @@
1
1
  'use strict'
2
2
 
3
3
  const { secrets } = require('@toa.io/kubernetes')
4
- const { remap, decode } = require('@toa.io/generic')
5
4
 
6
5
  const { PREFIX } = require('./conceal')
7
6
 
8
7
  const reveal = async (argv) => {
9
8
  const prefixed = PREFIX + argv.secret
10
- const secret = await secrets.get(prefixed)
11
- const values = remap(secret.data, decode)
9
+ const data = await secrets.get(prefixed)
12
10
 
13
- for (const [key, value] of Object.entries(values)) {
11
+ if (data === null) return
12
+
13
+ for (const [key, value] of Object.entries(data)) {
14
14
  const line = `${key}: ${value}`
15
15
 
16
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)
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