@toa.io/extensions.configuration 0.7.3-dev.2 → 0.20.0-dev.1
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 +7 -7
- package/readme.md +52 -27
- package/source/.deployment/index.js +7 -0
- package/source/.deployment/secrets.js +33 -0
- package/{src/deployment.js → source/.deployment/variables.js} +7 -4
- package/{src → source}/.manifest/.normalize/verbose.js +6 -3
- package/{src → source}/.manifest/schema.yaml +1 -0
- package/source/.provider/env.js +19 -0
- package/{src → source}/.provider/form.js +1 -1
- package/source/.provider/index.js +7 -0
- package/{test/annotations.fixtures.js → source/annotation.fixtures.js} +1 -0
- package/{test/annotations.test.js → source/annotation.test.js} +1 -1
- package/{test → source}/aspect.fixtures.js +2 -3
- package/{test → source}/aspect.test.js +1 -2
- package/source/deployment.js +20 -0
- package/{test → source}/deployment.test.js +26 -1
- package/{src → source}/factory.js +3 -3
- package/{test → source}/manifest.test.js +0 -9
- package/{src → source}/provider.js +22 -15
- package/source/provider.test.js +130 -0
- package/source/secrets.js +28 -0
- package/source/secrets.test.js +47 -0
- /package/{src → source}/.manifest/index.js +0 -0
- /package/{src → source}/.manifest/normalize.js +0 -0
- /package/{src → source}/.manifest/validate.js +0 -0
- /package/{src → source}/annotation.js +0 -0
- /package/{src → source}/aspect.js +0 -0
- /package/{src → source}/configuration.js +0 -0
- /package/{test → source}/deployment.fixtures.js +0 -0
- /package/{test → source}/factory.test.js +0 -0
- /package/{src → source}/index.js +0 -0
- /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.
|
|
3
|
+
"version": "0.20.0-dev.1",
|
|
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": "
|
|
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.
|
|
20
|
-
"@toa.io/generic": "0.
|
|
21
|
-
"@toa.io/schema": "0.
|
|
22
|
-
"@toa.io/yaml": "0.
|
|
19
|
+
"@toa.io/core": "0.20.0-dev.1",
|
|
20
|
+
"@toa.io/generic": "0.11.0-dev.3",
|
|
21
|
+
"@toa.io/schema": "0.20.0-dev.1",
|
|
22
|
+
"@toa.io/yaml": "0.20.0-dev.1",
|
|
23
23
|
"clone-deep": "4.0.1"
|
|
24
24
|
},
|
|
25
|
-
"gitHead": "
|
|
25
|
+
"gitHead": "23a9b4399af908101aac4ae03f22f7b948dfe55f"
|
|
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 #
|
|
35
|
-
baz: $BAZ_VALUE #
|
|
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
|
|
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
|
-
|
|
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
|
> <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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
194
|
+
## Aspect
|
|
197
195
|
|
|
198
|
-
|
|
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
|
> <br/>
|
|
209
209
|
> It is strongly **not** recommended to store a copy of value type configuration
|
|
210
|
-
> values outside
|
|
210
|
+
> values outside operation scope, thus it prevents operation to benefit
|
|
211
211
|
> from [hot updates](#).
|
|
212
212
|
>
|
|
213
213
|
> ```javascript
|
|
214
|
-
> //
|
|
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
|
|
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,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
|
-
* @
|
|
6
|
+
* @param {toa.norm.context.dependencies.Instance[]} components
|
|
7
|
+
* @param {object} annotations
|
|
8
|
+
* @return {toa.deployment.dependency.Variables}
|
|
7
9
|
*/
|
|
8
|
-
|
|
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
|
|
23
|
+
return variables
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
exports.
|
|
26
|
+
exports.variables = variables
|
|
@@ -21,7 +21,7 @@ const convert = (node) => {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function property (node) {
|
|
24
|
-
if (node === null)
|
|
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
|
-
|
|
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()
|
|
@@ -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
|
|
@@ -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('./
|
|
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
|
}
|
|
@@ -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
|
|
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}
|
|
14
|
+
* @param {toa.schema.JSON | Object} annotation
|
|
15
15
|
* @return {toa.extensions.configuration.Aspect}
|
|
16
16
|
*/
|
|
17
|
-
aspect (locator,
|
|
18
|
-
const schema = new Schema(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.#
|
|
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
|
/package/{src → source}/index.js
RENAMED
|
File without changes
|
|
File without changes
|