@toa.io/extensions.configuration 1.0.1-dev.0 → 1.1.0-canary.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 +7 -7
- package/readme.md +46 -22
- 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": "1.0
|
|
3
|
+
"version": "1.1.0-canary.0",
|
|
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": "1.0
|
|
20
|
-
"@toa.io/generic": "0.
|
|
21
|
-
"@toa.io/schema": "0.7.
|
|
22
|
-
"@toa.io/yaml": "0.7.
|
|
19
|
+
"@toa.io/core": "1.1.0-canary.0",
|
|
20
|
+
"@toa.io/generic": "0.11.0-canary.0",
|
|
21
|
+
"@toa.io/schema": "0.7.6-canary.0",
|
|
22
|
+
"@toa.io/yaml": "0.7.6-canary.0",
|
|
23
23
|
"clone-deep": "4.0.1"
|
|
24
24
|
},
|
|
25
|
-
"gitHead": "
|
|
25
|
+
"gitHead": "0adb45e6d6d309b21b976d257ac0a4fc48d866b3"
|
|
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 #
|
|
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
|
---
|
|
@@ -107,7 +107,7 @@ extensions:
|
|
|
107
107
|
default: 'baz'
|
|
108
108
|
bar:
|
|
109
109
|
type: number
|
|
110
|
-
required: [
|
|
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: [
|
|
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
|
-
|
|
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
|
-
|
|
188
|
+
Deployed kubernetes secret's name is predefined as `configuration`.
|
|
196
189
|
|
|
197
|
-
|
|
190
|
+
```shell
|
|
191
|
+
$ toa conceal configuration STRIPE_API_KEY xxxxxxxx
|
|
192
|
+
```
|
|
198
193
|
|
|
199
|
-
|
|
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
|
-
> //
|
|
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
|
|
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,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
|