@toa.io/cli 0.20.0-dev.30 → 0.20.0-dev.33

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.30",
3
+ "version": "0.20.0-dev.33",
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,14 @@
22
22
  "@toa.io/runtime": "*"
23
23
  },
24
24
  "dependencies": {
25
- "@toa.io/console": "0.20.0-dev.31",
26
- "@toa.io/generic": "0.20.0-dev.31",
27
- "@toa.io/kubernetes": "0.20.0-dev.31",
28
- "@toa.io/norm": "0.20.0-dev.31",
29
- "@toa.io/yaml": "0.20.0-dev.31",
25
+ "@toa.io/console": "0.20.0-dev.34",
26
+ "@toa.io/generic": "0.20.0-dev.34",
27
+ "@toa.io/kubernetes": "0.20.0-dev.34",
28
+ "@toa.io/norm": "0.20.0-dev.34",
29
+ "@toa.io/yaml": "0.20.0-dev.34",
30
30
  "dotenv": "16.1.1",
31
31
  "find-up": "5.0.0",
32
32
  "yargs": "17.6.2"
33
33
  },
34
- "gitHead": "f9ad6bf2bab1298b96ca52557b7e36b6041cfc88"
34
+ "gitHead": "7035b1985fe9bb844069308a272d061bfbd38bf0"
35
35
  }
package/readme.md CHANGED
@@ -35,7 +35,8 @@ Export environment to a `.env` file.
35
35
  <dd>
36
36
  <code>environment</code> deployment environment name (default <code>local</code>).<br/>
37
37
  <code>--path</code> path to a Context (default <code>.</code>)<br/>
38
- <code>--as</code> output file path (default <code>.env</code>)
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
 
@@ -72,7 +73,8 @@ $ toa replay ./path/to/context
72
73
  $ toa replay --title "should add numbers"
73
74
  ```
74
75
 
75
- If the path is a Context root (containing `context.toa.yaml` file), samples for components within the Context will be
76
+ If the path is a Context root (containing `context.toa.yaml` file), samples for components within
77
+ the Context will be
76
78
  found and replayed sequentially.
77
79
 
78
80
  ### export manifest
@@ -131,11 +133,14 @@ Deploy a generic Kubernetes secret with the prefix `toa-`.
131
133
  <dd>
132
134
  <code>secret</code> Secret name.<br/>
133
135
  <code>key-values</code> List of keys and values of the secret as <code>key=value</code>.<br/>
134
- <code>--namespace</code> Kubernetes namespace where the secret should be deployed.
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
135
140
  </dd>
136
141
  </dl>
137
142
 
138
- > Existing secret will be replaced.
143
+ > If a secret already exists, then given `key-values` will be added to it.
139
144
 
140
145
  #### Example
141
146
 
@@ -18,14 +18,35 @@ const builder = (yargs) => {
18
18
  type: 'string',
19
19
  desc: 'Target Kubernetes namespace'
20
20
  })
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',
32
+ type: 'string'
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
+ })
21
41
  .example([
22
- ['$0 conceal amqp-credentials username=developer'],
23
- ['$0 conceal amqp-credentials username=developer password=secret'],
24
- ['$0 conceal amqp-credentials username=developer --namespace app']
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']
25
46
  ])
26
47
  }
27
48
 
28
- exports.command = 'conceal <secret> <key-values...>'
49
+ exports.command = 'conceal [secret] [key-values...]'
29
50
  exports.desc = 'Deploy a secret'
30
51
  exports.builder = builder
31
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]'
@@ -1,8 +1,18 @@
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) => {
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
+
6
16
  const values = argv['key-values'].reduce((values, pair) => {
7
17
  const [key, value] = pair.split('=')
8
18
 
@@ -13,7 +23,36 @@ const conceal = async (argv) => {
13
23
 
14
24
  const secret = PREFIX + argv.secret
15
25
 
16
- await secrets.store(secret, values, argv.namespace)
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
+ }
54
+
55
+ return secrets
17
56
  }
18
57
 
19
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
@@ -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)