@toa.io/extensions.configuration 1.0.1-dev.0 → 1.0.2-alpha.56

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.
Files changed (32) hide show
  1. package/package.json +7 -7
  2. package/readme.md +46 -22
  3. package/source/.deployment/index.js +7 -0
  4. package/source/.deployment/secrets.js +33 -0
  5. package/{src/deployment.js → source/.deployment/variables.js} +7 -4
  6. package/{src → source}/.manifest/.normalize/verbose.js +6 -3
  7. package/{src → source}/.manifest/schema.yaml +1 -0
  8. package/source/.provider/env.js +19 -0
  9. package/{src → source}/.provider/form.js +1 -1
  10. package/source/.provider/index.js +7 -0
  11. package/{test/annotations.fixtures.js → source/annotation.fixtures.js} +1 -0
  12. package/{test/annotations.test.js → source/annotation.test.js} +1 -1
  13. package/{test → source}/aspect.fixtures.js +2 -3
  14. package/{test → source}/aspect.test.js +1 -2
  15. package/source/deployment.js +20 -0
  16. package/{test → source}/deployment.test.js +26 -1
  17. package/{src → source}/factory.js +3 -3
  18. package/{test → source}/manifest.test.js +0 -9
  19. package/{src → source}/provider.js +22 -15
  20. package/source/provider.test.js +130 -0
  21. package/source/secrets.js +28 -0
  22. package/source/secrets.test.js +47 -0
  23. /package/{src → source}/.manifest/index.js +0 -0
  24. /package/{src → source}/.manifest/normalize.js +0 -0
  25. /package/{src → source}/.manifest/validate.js +0 -0
  26. /package/{src → source}/annotation.js +0 -0
  27. /package/{src → source}/aspect.js +0 -0
  28. /package/{src → source}/configuration.js +0 -0
  29. /package/{test → source}/deployment.fixtures.js +0 -0
  30. /package/{test → source}/factory.test.js +0 -0
  31. /package/{src → source}/index.js +0 -0
  32. /package/{src → source}/manifest.js +0 -0
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.configuration",
3
- "version": "1.0.1-dev.0",
3
+ "version": "1.0.2-alpha.56+a80d81d4",
4
4
  "description": "Toa Configuration",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
7
- "main": "src/index.js",
7
+ "main": "source/index.js",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/toa-io/toa.git"
@@ -16,11 +16,11 @@
16
16
  "access": "public"
17
17
  },
18
18
  "dependencies": {
19
- "@toa.io/core": "1.0.1-dev.0",
20
- "@toa.io/generic": "0.10.0-dev.0",
21
- "@toa.io/schema": "0.7.5-dev.0",
22
- "@toa.io/yaml": "0.7.5-dev.0",
19
+ "@toa.io/core": "1.0.2-alpha.56+a80d81d4",
20
+ "@toa.io/generic": "0.10.1-alpha.56+a80d81d4",
21
+ "@toa.io/schema": "0.7.6-alpha.56+a80d81d4",
22
+ "@toa.io/yaml": "0.7.6-alpha.56+a80d81d4",
23
23
  "clone-deep": "4.0.1"
24
24
  },
25
- "gitHead": "5e7998b39ea6111a2cf7d38fad8aeae71d742621"
25
+ "gitHead": "a80d81d4d9da9fec07606154c959dd6512de4c76"
26
26
  }
package/readme.md CHANGED
@@ -30,15 +30,15 @@ function transition (input, entity, context) {
30
30
  # context.toa.yaml
31
31
  configuration:
32
32
  dummies.dummy:
33
- foo: qux
34
- foo@staging: quux # use deployment environment discriminator
35
- baz: $BAZ_VALUE # use secrets
33
+ foo: qux # override default value defined by dummies.dummy
34
+ foo@staging: quux # deployment environment discriminator
35
+ baz: $BAZ_VALUE # secret
36
36
  ```
37
37
 
38
38
  ### Deploy secrets
39
39
 
40
40
  ```shell
41
- $ toa conceal
41
+ $ toa conceal configuration BAZ_VALUE '$ecr3t'
42
42
  ```
43
43
 
44
44
  ---
@@ -107,7 +107,7 @@ extensions:
107
107
  default: 'baz'
108
108
  bar:
109
109
  type: number
110
- required: [ foo ]
110
+ required: [foo]
111
111
  ```
112
112
 
113
113
  ### Concise Declaration
@@ -140,7 +140,7 @@ extensions:
140
140
  type: number
141
141
  default: 1
142
142
  additionalProperties: false
143
- required: [ foo, bar ]
143
+ required: [foo, bar]
144
144
  ```
145
145
 
146
146
  ## Context Configuration
@@ -163,18 +163,9 @@ configuration:
163
163
  bar@staging: 2
164
164
  ```
165
165
 
166
- ### Local environment
167
-
168
- Configuration Objects for local environment may be created
169
- by [`toa configure`](../../runtime/cli/readme.md#configure) command.
170
-
171
166
  ## Configuration Secrets
172
167
 
173
- > ![Important](https://img.shields.io/badge/Important-red)<br/>
174
- > Not implemented. [#132](https://github.com/toa-io/toa/issues/132)
175
-
176
- Context Configuration values which are uppercase strings prefixed with `$`
177
- considered as Secrets.
168
+ Context Configuration values which are uppercase strings prefixed with `$` considered as Secrets.
178
169
 
179
170
  ### Example
180
171
 
@@ -185,6 +176,8 @@ configuration:
185
176
  api-key: $STRIPE_API_KEY
186
177
  ```
187
178
 
179
+ Configuration values that are assigned with a reference to the Secret must be of type `string`.
180
+
188
181
  ### Secrets Deployment
189
182
 
190
183
  Secrets are not being deployed with context
@@ -192,13 +185,19 @@ deployment ([`toa deploy`](../../runtime/cli/readme.md#deploy)),
192
185
  thus must be deployed separately at least once for each deployment environment
193
186
  manually ([`toa conceal`](../../runtime/cli/readme.md#conceal)).
194
187
 
195
- ## Operation Context
188
+ Deployed kubernetes secret's name is predefined as `configuration`.
196
189
 
197
- Configuration Value is available as a well-known operation context extension `configuration`.
190
+ ```shell
191
+ $ toa conceal configuration STRIPE_API_KEY xxxxxxxx
192
+ ```
198
193
 
199
- ### Usage: node
194
+ ## Aspect
195
+
196
+ Configuration Value is available as a well-known operation Aspect `configuration`.
200
197
 
201
198
  ```javascript
199
+ // Node.js bridge
200
+
202
201
  function transition (input, entity, context) {
203
202
  const foo = context.configiuration.foo
204
203
 
@@ -212,21 +211,46 @@ function transition (input, entity, context) {
212
211
  > from [hot updates](#).
213
212
  >
214
213
  > ```javascript
215
- > // THIS IS WEIRD, BAD AND NOT RECOMMENDED
214
+ > // NOT RECOMMENDED
216
215
  > let foo
217
216
  >
218
217
  > function transition (input, entity, context) {
218
+ > // NOT RECOMMENDED
219
219
  > if (foo === undefined) foo = context.configuration.foo
220
220
  >
221
221
  > // ...
222
222
  > }
223
223
  > ```
224
- > See [Genuine operations](#).
224
+ > See [Genuine operations](/documentation/design.md#genuine-operations).
225
+
226
+ ## Development Configuration
227
+
228
+ Configuration can be exported by [`toa env`](/runtime/cli/readme.md#env).
229
+
230
+ ### Local Environment Placeholders
231
+
232
+ Context Configuration values may contain placeholders that reference environment variables.
233
+ Placeholders are replaced with values if the corresponding environment variables are set.
234
+
235
+ > Placeholders can only be used with local environment (exported by `toa env`), as these values are not
236
+ > deployed.
237
+
238
+ ```yaml
239
+ # context.toa.yaml
240
+ configuration:
241
+ dummies.dummy:
242
+ url@local: https://stage${STAGE}.intranet/
243
+ ```
244
+
245
+ ```dotenv
246
+ # .env
247
+ STAGE=82
248
+ ```
225
249
 
226
250
  ## Appendix
227
251
 
228
252
  - [Discussion](./docs/discussion.md)
229
253
  - [Configuration consistency](./docs/consistency.md)
230
254
 
231
- [^1]: Cannot be changed without a deployment. New values are considered to be a subject of
255
+ [^1]: Cannot be changed without a deployment as new values are considered to be a subject of
232
256
  testing. [#146](https://github.com/toa-io/toa/issues/146)
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ const { variables } = require('./variables')
4
+ const { secrets } = require('./secrets')
5
+
6
+ exports.variables = variables
7
+ exports.secrets = secrets
@@ -0,0 +1,33 @@
1
+ 'use strict'
2
+
3
+ const find = require('../secrets')
4
+
5
+ /**
6
+ * @param {toa.norm.context.dependencies.Instance[]} components
7
+ * @param {object} annotations
8
+ * @return {toa.deployment.dependency.Variables}
9
+ */
10
+ function secrets (components, annotations) {
11
+ /** @type {toa.deployment.dependency.Variables} */
12
+ const variables = {}
13
+
14
+ for (const [id, annotation] of Object.entries(annotations)) {
15
+ const component = components.find((component) => component.locator.id === id)
16
+ const label = component.locator.label
17
+
18
+ find.secrets(annotation, (variable, key) => {
19
+ if (variables[label] === undefined) variables[label] = []
20
+
21
+ variables[label].push({
22
+ name: variable,
23
+ secret: { name: SECRET_NAME, key }
24
+ })
25
+ })
26
+ }
27
+
28
+ return variables
29
+ }
30
+
31
+ const SECRET_NAME = 'toa-configuration'
32
+
33
+ exports.secrets = secrets
@@ -3,9 +3,12 @@
3
3
  const { encode } = require('@toa.io/generic')
4
4
 
5
5
  /**
6
- * @type {toa.deployment.dependency.Constructor}
6
+ * @param {toa.norm.context.dependencies.Instance[]} components
7
+ * @param {object} annotations
8
+ * @return {toa.deployment.dependency.Variables}
7
9
  */
8
- const deployment = (components, annotations) => {
10
+ function variables (components, annotations) {
11
+ /** @type {toa.deployment.dependency.Variables} */
9
12
  const variables = {}
10
13
 
11
14
  for (const [id, annotation] of Object.entries(annotations)) {
@@ -17,7 +20,7 @@ const deployment = (components, annotations) => {
17
20
  }]
18
21
  }
19
22
 
20
- return { variables }
23
+ return variables
21
24
  }
22
25
 
23
- exports.deployment = deployment
26
+ exports.variables = variables
@@ -21,7 +21,7 @@ const convert = (node) => {
21
21
  }
22
22
 
23
23
  function property (node) {
24
- if (node === null) throw new Error('Configuration: cannot resolve type of null, use JSONSchema declaration.')
24
+ if (node === null) return { type: 'null', default: null }
25
25
 
26
26
  const type = Array.isArray(node) ? 'array' : typeof node
27
27
 
@@ -36,11 +36,14 @@ const array = (array) => {
36
36
 
37
37
  const type = typeof array[0]
38
38
 
39
- return {
39
+ const schema = {
40
40
  type: 'array',
41
- items: { type },
42
41
  default: array
43
42
  }
43
+
44
+ if (array.length === 1) schema.items = array[0]
45
+
46
+ return schema
44
47
  }
45
48
 
46
49
  const SYM = Symbol()
@@ -2,6 +2,7 @@ $schema: https://json-schema.org/draft/2019-09/schema
2
2
  $id: https://schemas.toa.io/0.0.0/extensions/configuration/manifest
3
3
 
4
4
  $ref: 'https://schemas.toa.io/0.0.0/definitions#/definitions/schema'
5
+ type: object
5
6
  properties:
6
7
  type:
7
8
  const: object
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ const { map } = require('@toa.io/generic')
4
+
5
+ function env (object) {
6
+ return map(object,
7
+ /**
8
+ * @type {toa.generic.map.transform<string>}
9
+ */
10
+ (value) => {
11
+ if (typeof value !== 'string') return
12
+
13
+ return value.replaceAll(RX, (match, variable) => process.env[variable] ?? match)
14
+ })
15
+ }
16
+
17
+ const RX = /\${(?<variable>[A-Z0-9_]{1,32})}/g
18
+
19
+ exports.env = env
@@ -8,7 +8,7 @@ const { traverse } = require('@toa.io/generic')
8
8
  */
9
9
  const form = (schema) => {
10
10
  const defaults = (node) => {
11
- if (node.properties !== undefined) return { ...node.properties }
11
+ if (node.type === 'object' && node.properties !== undefined) return { ...node.properties }
12
12
  if (node.default !== undefined) return node.default
13
13
 
14
14
  return null
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ const { form } = require('./form')
4
+ const { env } = require('./env')
5
+
6
+ exports.form = form
7
+ exports.env = env
@@ -10,6 +10,7 @@ const instance = () => {
10
10
  const locator = new Locator(name, namespace)
11
11
 
12
12
  const manifest = {
13
+ type: 'object',
13
14
  properties: {
14
15
  foo: {
15
16
  type: 'number',
@@ -4,7 +4,7 @@ const clone = require('clone-deep')
4
4
  const { generate } = require('randomstring')
5
5
  const { sample } = require('@toa.io/generic')
6
6
 
7
- const fixtures = require('./annotations.fixtures')
7
+ const fixtures = require('./annotation.fixtures')
8
8
  const { annotation } = require('../')
9
9
 
10
10
  let input
@@ -3,21 +3,20 @@
3
3
  const { generate } = require('randomstring')
4
4
 
5
5
  const schema = {
6
+ type: 'object',
6
7
  properties: {
7
8
  foo: {
8
9
  type: 'string',
9
10
  default: generate()
10
11
  },
11
12
  bar: {
13
+ type: 'object',
12
14
  properties: {
13
15
  baz: {
14
16
  type: 'number',
15
17
  default: 1
16
18
  }
17
19
  }
18
- },
19
- quu: {
20
- type: 'number'
21
20
  }
22
21
  }
23
22
  }
@@ -46,8 +46,7 @@ describe('defaults', () => {
46
46
  foo: fixtures.schema.properties.foo.default,
47
47
  bar: {
48
48
  baz: fixtures.schema.properties.bar.properties.baz.default
49
- },
50
- quu: 0
49
+ }
51
50
  })
52
51
  })
53
52
  })
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+
3
+ const { merge } = require('@toa.io/generic')
4
+ const get = require('./.deployment')
5
+
6
+ /**
7
+ * @param {toa.norm.context.dependencies.Instance[]} components
8
+ * @param {object} annotations
9
+ * @return {toa.deployment.dependency.Declaration}
10
+ */
11
+ const deployment = (components, annotations) => {
12
+ const variables = get.variables(components, annotations)
13
+ const secrets = get.secrets(components, annotations)
14
+
15
+ merge(variables, secrets)
16
+
17
+ return { variables }
18
+ }
19
+
20
+ exports.deployment = deployment
@@ -1,9 +1,11 @@
1
1
  'use strict'
2
2
 
3
- const { encode } = require('@toa.io/generic')
3
+ const clone = require('clone-deep')
4
+ const { encode, sample } = require('@toa.io/generic')
4
5
 
5
6
  const fixtures = require('./deployment.fixtures')
6
7
  const { deployment } = require('../')
8
+ const { generate } = require('randomstring')
7
9
 
8
10
  /** @type {toa.deployment.dependency.Declaration} */
9
11
  let declaration
@@ -43,3 +45,26 @@ it('should map configurations', () => {
43
45
  expect(env.value).toStrictEqual(encoded)
44
46
  }
45
47
  })
48
+
49
+ it('should declare secrets', async () => {
50
+ const annotations = clone(fixtures.annotations)
51
+ const component = sample(fixtures.components)
52
+ const id = component.locator.id
53
+ const key = generate()
54
+ const name = generate().substring(0, 16).toUpperCase()
55
+ const value = '$' + name
56
+
57
+ if (annotations[id] === undefined) annotations[id] = {}
58
+
59
+ annotations[id][key] = value
60
+
61
+ declaration = deployment(fixtures.components, annotations)
62
+
63
+ const variables = declaration.variables[component.locator.label]
64
+
65
+ expect(variables).toBeDefined()
66
+
67
+ const secret = variables.find((variable) => variable.name === 'TOA_CONFIGURATION__' + name)
68
+
69
+ expect(secret).toBeDefined()
70
+ })
@@ -11,11 +11,11 @@ const { Provider } = require('./provider')
11
11
  class Factory {
12
12
  /**
13
13
  * @param {toa.core.Locator} locator
14
- * @param {toa.schema.JSON | Object} declaration
14
+ * @param {toa.schema.JSON | Object} annotation
15
15
  * @return {toa.extensions.configuration.Aspect}
16
16
  */
17
- aspect (locator, declaration) {
18
- const schema = new Schema(declaration)
17
+ aspect (locator, annotation) {
18
+ const schema = new Schema(annotation)
19
19
  const provider = new Provider(locator, schema)
20
20
  const configuration = new Configuration(provider)
21
21
 
@@ -109,9 +109,6 @@ describe('normalization', () => {
109
109
  properties: {
110
110
  foo: {
111
111
  type: 'array',
112
- items: {
113
- type: 'number'
114
- },
115
112
  default: [1, 2, 3]
116
113
  }
117
114
  }
@@ -123,10 +120,4 @@ describe('normalization', () => {
123
120
 
124
121
  expect(() => manifest(concise)).toThrow(/array items type because it's empty/)
125
122
  })
126
-
127
- it('should throw on null', () => {
128
- const concise = { foo: null }
129
-
130
- expect(() => manifest(concise)).toThrow(/type of null/)
131
- })
132
123
  })
@@ -4,7 +4,9 @@ const clone = require('clone-deep')
4
4
  const { decode, encode, empty, overwrite } = require('@toa.io/generic')
5
5
 
6
6
  const { Connector } = require('@toa.io/core')
7
- const { form } = require('./.provider/form')
7
+
8
+ const { secrets } = require('./secrets')
9
+ const { env, form } = require('./.provider')
8
10
 
9
11
  /**
10
12
  * @implements {toa.extensions.configuration.Provider}
@@ -39,11 +41,7 @@ class Provider extends Connector {
39
41
  }
40
42
 
41
43
  async open () {
42
- await this.#retrieve()
43
- }
44
-
45
- async #source () {
46
- return this.#value
44
+ this.#retrieve()
47
45
  }
48
46
 
49
47
  set (key, value) {
@@ -77,7 +75,11 @@ class Provider extends Connector {
77
75
  return this.object === undefined ? undefined : encode(this.object)
78
76
  }
79
77
 
80
- async #retrieve () {
78
+ #source () {
79
+ return this.#value
80
+ }
81
+
82
+ #retrieve () {
81
83
  const string = process.env[this.key]
82
84
  const object = string === undefined ? {} : decode(string)
83
85
 
@@ -85,18 +87,14 @@ class Provider extends Connector {
85
87
  }
86
88
 
87
89
  #set (object) {
88
- this.#validate(object)
90
+ object = this.#reveal(object)
91
+ object = env(object)
92
+
89
93
  this.#merge(object)
90
94
 
91
95
  this.object = empty(object) ? undefined : object
92
96
  }
93
97
 
94
- #validate (object) {
95
- const error = this.#schema.match(object)
96
-
97
- if (error !== null) throw new TypeError(error.message)
98
- }
99
-
100
98
  #merge (object) {
101
99
  object = clone(object)
102
100
 
@@ -104,9 +102,18 @@ class Provider extends Connector {
104
102
  const value = overwrite(form, object)
105
103
 
106
104
  this.#schema.validate(value)
107
-
108
105
  this.#value = value
109
106
  }
107
+
108
+ #reveal (object) {
109
+ return secrets(object, (variable) => {
110
+ if (!(variable in process.env)) throw new Error(`Configuration secret value ${variable} is not set`)
111
+
112
+ const base64 = process.env[variable]
113
+
114
+ return decode(base64)
115
+ })
116
+ }
110
117
  }
111
118
 
112
119
  const PREFIX = 'TOA_CONFIGURATION_'
@@ -0,0 +1,130 @@
1
+ 'use strict'
2
+
3
+ /* eslint-disable no-template-curly-in-string */
4
+
5
+ const { generate } = require('randomstring')
6
+ const { encode } = require('@toa.io/generic')
7
+
8
+ const { Provider } = require('./provider')
9
+
10
+ it('should be', async () => {
11
+ expect(Provider).toBeInstanceOf(Function)
12
+ })
13
+
14
+ const locator = /** @type {toa.core.Locator} */ { uppercase: generate().toUpperCase() }
15
+ const schema = /** @type {toa.schema.Schema} */ { validate: () => undefined }
16
+
17
+ /** @type {Provider} */
18
+ let provider
19
+
20
+ beforeEach(() => {
21
+ cleanEnv()
22
+ provider = new Provider(locator, schema)
23
+ })
24
+
25
+ it('should replace secret values', async () => {
26
+ const configuration = { foo: '$FOO_SECRET' }
27
+ const secrets = { FOO_SECRET: generate() }
28
+
29
+ setEnv(configuration, secrets)
30
+
31
+ await provider.open()
32
+ const value = provider.source()
33
+
34
+ expect(value).toStrictEqual({ foo: secrets.FOO_SECRET })
35
+ })
36
+
37
+ it('should throw if secret value is not set', async () => {
38
+ const configuration = { foo: '$FOO_SECRET' }
39
+
40
+ setEnv(configuration)
41
+
42
+ await expect(provider.open()).rejects.toThrow('FOO_SECRET is not set')
43
+ })
44
+
45
+ it('should replace nested secrets', async () => {
46
+ const configuration = { foo: { bar: '$BAR' } }
47
+ const secrets = { BAR: generate() }
48
+
49
+ setEnv(configuration, secrets)
50
+
51
+ await provider.open()
52
+ const value = provider.source()
53
+
54
+ expect(value).toStrictEqual({ foo: { bar: secrets.BAR } })
55
+ })
56
+
57
+ it('should replace placeholders', async () => {
58
+ const name = 'FOO_VALUE'
59
+ const configuration = { foo: { bar: 'foo_${' + name + '}' } }
60
+ const value = generate()
61
+
62
+ setEnv(configuration)
63
+ setVal(name, value)
64
+
65
+ await provider.open()
66
+ const source = provider.source()
67
+
68
+ expect(source).toStrictEqual({ foo: { bar: 'foo_' + value } })
69
+ })
70
+
71
+ it('should replace multiple placeholders', async () => {
72
+ const configuration = { foo: '${FOO} ${BAR}' }
73
+
74
+ setEnv(configuration)
75
+ setVal('FOO', 'hello')
76
+ setVal('BAR', 'world')
77
+
78
+ await provider.open()
79
+ const source = provider.source()
80
+
81
+ expect(source).toStrictEqual({ foo: 'hello world' })
82
+ })
83
+
84
+ it('should not replace if variable not set', async () => {
85
+ const configuration = { foo: '${FOO}' }
86
+
87
+ setEnv(configuration)
88
+
89
+ await provider.open()
90
+ const value = provider.source()
91
+
92
+ expect(value).toStrictEqual(configuration)
93
+ })
94
+
95
+ const usedVariables = []
96
+
97
+ /**
98
+ * @param {object} configuration
99
+ * @param {Record<string, string>} [secrets]
100
+ */
101
+ function setEnv (configuration, secrets) {
102
+ const variable = PREFIX + locator.uppercase
103
+ const encoded = encode(configuration)
104
+
105
+ setVal(variable, encoded)
106
+
107
+ if (secrets !== undefined) {
108
+ for (const [key, value] of Object.entries(secrets)) {
109
+ const variable = PREFIX + '_' + key
110
+
111
+ process.env[variable] = encode(value)
112
+ usedVariables.push(variable)
113
+ }
114
+ }
115
+ }
116
+
117
+ function setVal (variable, value) {
118
+ process.env[variable] = value
119
+ usedVariables.push(variable)
120
+ }
121
+
122
+ function cleanEnv () {
123
+ for (const variable of usedVariables) {
124
+ delete process.env[variable]
125
+ }
126
+
127
+ usedVariables.length = 0
128
+ }
129
+
130
+ const PREFIX = 'TOA_CONFIGURATION_'
@@ -0,0 +1,28 @@
1
+ 'use strict'
2
+
3
+ const { map } = require('@toa.io/generic')
4
+
5
+ /**
6
+ * @param {object} configuration
7
+ * @param {(variable: string, name?: string) => void} callback
8
+ * @returns {object}
9
+ */
10
+ function secrets (configuration, callback) {
11
+ return map(configuration, (value) => {
12
+ if (typeof value !== 'string') return
13
+
14
+ const match = value.match(SECRET_RX)
15
+
16
+ if (match === null) return
17
+
18
+ const name = match.groups.variable
19
+ const variable = PREFIX + name
20
+
21
+ return callback(variable, name)
22
+ })
23
+ }
24
+
25
+ const PREFIX = 'TOA_CONFIGURATION__'
26
+ const SECRET_RX = /^\$(?<variable>[A-Z0-9_]{1,32})$/
27
+
28
+ exports.secrets = secrets
@@ -0,0 +1,47 @@
1
+ 'use strict'
2
+
3
+ const { secrets } = require('./secrets')
4
+
5
+ it('should be', async () => {
6
+ expect(secrets).toBeInstanceOf(Function)
7
+ })
8
+
9
+ it('should find secrets', async () => {
10
+ const configuration = {
11
+ foo: {
12
+ bar: '$BAR_VALUE'
13
+ },
14
+ baz: '$BAZ_VALUE'
15
+ }
16
+
17
+ const variables = new Set()
18
+ const names = new Set()
19
+
20
+ secrets(configuration, (variable, name) => {
21
+ variables.add(variable)
22
+ names.add(name)
23
+ })
24
+
25
+ expect(variables.has('TOA_CONFIGURATION__BAR_VALUE')).toStrictEqual(true)
26
+ expect(variables.has('TOA_CONFIGURATION__BAZ_VALUE')).toStrictEqual(true)
27
+
28
+ expect(names.has('BAR_VALUE')).toStrictEqual(true)
29
+ expect(names.has('BAZ_VALUE')).toStrictEqual(true)
30
+ })
31
+
32
+ it('should replace values', async () => {
33
+ const configuration = { foo: '$FOO' }
34
+
35
+ const output = secrets(configuration, (variable) => 'hello')
36
+
37
+ expect(output).toStrictEqual({ foo: 'hello' })
38
+ })
39
+
40
+ it('should allow numbers in secret names', async () => {
41
+ const configuration = { foo: '$HOST_0' }
42
+ const found = new Set()
43
+
44
+ secrets(configuration, (variable) => found.add(variable))
45
+
46
+ expect(found.has('TOA_CONFIGURATION__HOST_0')).toStrictEqual(true)
47
+ })
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes