@toa.io/extensions.configuration 0.7.3 → 0.20.0-dev.10

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 +52 -27
  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": "0.7.3",
3
+ "version": "0.20.0-dev.10",
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": "0.8.1",
20
- "@toa.io/generic": "0.9.0",
21
- "@toa.io/schema": "0.7.3",
22
- "@toa.io/yaml": "0.7.3",
19
+ "@toa.io/core": "0.20.0-dev.10",
20
+ "@toa.io/generic": "0.20.0-dev.10",
21
+ "@toa.io/schema": "0.20.0-dev.10",
22
+ "@toa.io/yaml": "0.20.0-dev.10",
23
23
  "clone-deep": "4.0.1"
24
24
  },
25
- "gitHead": "a5019af0ad7b08182d0f245af3382f96a7e22d71"
25
+ "gitHead": "cd3ffb1ae1af014f13a62ef9fc8ada58d8688e4a"
26
26
  }
package/readme.md CHANGED
@@ -30,23 +30,22 @@ 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
  ---
45
45
 
46
46
  ## Problem Definition
47
47
 
48
- - Components must be reusable in different contexts and deployment environments,
49
- that is in different configurations.
48
+ - Components must be reusable in different contexts and deployment environments that are in different configurations.
50
49
  - Some algorithm parameters must be deployed secretly.
51
50
 
52
51
  ## Definitions
@@ -57,8 +56,7 @@ Set of static[^1] parameters for all algorithms within a given system.
57
56
 
58
57
  ### Configuration Schema
59
58
 
60
- Schema defining component's algorithms parameters (optionally with default
61
- values).
59
+ Schema is defining component's algorithm parameters (optionally with default values).
62
60
 
63
61
  ### Configuration Object
64
62
 
@@ -66,7 +64,7 @@ Value valid against Configuration Schema.
66
64
 
67
65
  ### Configuration Value
68
66
 
69
- Merge result of Configuration Schema's defaults and Configuration Object.
67
+ The merge result of Configuration Schema's defaults and Configuration Object.
70
68
 
71
69
  ### Context Configuration
72
70
 
@@ -86,7 +84,7 @@ Configuration Schema is declared as a component extension
86
84
  using [JSON Schema](https://json-schema.org) `object` type.
87
85
 
88
86
  > ![Warning](https://img.shields.io/badge/Warning-yellow)<br/>
89
- > By introducing non-backward compatible changes to a Configuration Schema the compatibility
87
+ > By introducing non-backward compatible changes to a Configuration Schema, the compatibility
90
88
  > with existent contexts and deployment environments will be broken. That is, Configuration
91
89
  > Schema changes are subjects of component versioning.
92
90
 
@@ -121,7 +119,7 @@ type and no additional properties allowed.
121
119
 
122
120
  Also note that a well-known shortcut `configuration` is available.
123
121
 
124
- Next two declarations are equivalent.
122
+ The next two declarations are equivalent.
125
123
 
126
124
  ```yaml
127
125
  # component.toa.yaml
@@ -147,8 +145,8 @@ extensions:
147
145
 
148
146
  ## Context Configuration
149
147
 
150
- Context Configuration is declared as a context annotaion. Its keys must be
151
- component identifiers and its values must be Configuration Objects for those
148
+ Context Configuration is declared as a context annotation. Its keys must be
149
+ component identifiers, and its values must be Configuration Objects for those
152
150
  components.
153
151
 
154
152
  Context Configuration keys and Configuration Object keys may be defined
@@ -165,15 +163,9 @@ configuration:
165
163
  bar@staging: 2
166
164
  ```
167
165
 
168
- ### Local environment
169
-
170
- Configuration Objects for local environment may be created
171
- by [`toa configure`](../../runtime/cli/readme.md#configure) command.
172
-
173
166
  ## Configuration Secrets
174
167
 
175
- Context Configuration values which are uppercase strings prefixed with `$`
176
- considered as Secrets.
168
+ Context Configuration values which are uppercase strings prefixed with `$` considered as Secrets.
177
169
 
178
170
  ### Example
179
171
 
@@ -184,6 +176,8 @@ configuration:
184
176
  api-key: $STRIPE_API_KEY
185
177
  ```
186
178
 
179
+ Configuration values that are assigned with a reference to the Secret must be of type `string`.
180
+
187
181
  ### Secrets Deployment
188
182
 
189
183
  Secrets are not being deployed with context
@@ -191,13 +185,19 @@ deployment ([`toa deploy`](../../runtime/cli/readme.md#deploy)),
191
185
  thus must be deployed separately at least once for each deployment environment
192
186
  manually ([`toa conceal`](../../runtime/cli/readme.md#conceal)).
193
187
 
194
- ## Operation Context
188
+ Deployed kubernetes secret's name is predefined as `configuration`.
189
+
190
+ ```shell
191
+ $ toa conceal configuration STRIPE_API_KEY xxxxxxxx
192
+ ```
195
193
 
196
- Configuration Value is available as a well-known operation context extension `configuration`.
194
+ ## Aspect
197
195
 
198
- ### Usage: node
196
+ Configuration Value is available as a well-known operation Aspect `configuration`.
199
197
 
200
198
  ```javascript
199
+ // Node.js bridge
200
+
201
201
  function transition (input, entity, context) {
202
202
  const foo = context.configiuration.foo
203
203
 
@@ -207,25 +207,50 @@ function transition (input, entity, context) {
207
207
 
208
208
  > ![Warning](https://img.shields.io/badge/Warning-yellow)<br/>
209
209
  > It is strongly **not** recommended to store a copy of value type configuration
210
- > values outside of operation scope, thus it prevents operation to benefit
210
+ > values outside operation scope, thus it prevents operation to benefit
211
211
  > from [hot updates](#).
212
212
  >
213
213
  > ```javascript
214
- > // THIS IS WEIRD, BAD AND NOT RECOMMENDED
214
+ > // NOT RECOMMENDED
215
215
  > let foo
216
216
  >
217
217
  > function transition (input, entity, context) {
218
+ > // NOT RECOMMENDED
218
219
  > if (foo === undefined) foo = context.configuration.foo
219
220
  >
220
221
  > // ...
221
222
  > }
222
223
  > ```
223
- > 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
+ ```
224
249
 
225
250
  ## Appendix
226
251
 
227
252
  - [Discussion](./docs/discussion.md)
228
253
  - [Configuration consistency](./docs/consistency.md)
229
254
 
230
- [^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
231
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