@toa.io/extensions.configuration 0.20.0-dev.31 → 0.20.0-dev.35
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 +16 -9
- package/readme.md +68 -155
- package/schemas/annotation.cos.yaml +1 -0
- package/schemas/manifest.cos.yaml +2 -0
- package/source/Aspect.test.ts +15 -0
- package/source/Aspect.ts +23 -0
- package/source/Factory.ts +12 -0
- package/source/configuration.test.ts +89 -0
- package/source/configuration.ts +52 -0
- package/source/deployment.test.ts +21 -0
- package/source/deployment.ts +69 -0
- package/source/index.ts +3 -0
- package/source/manifest.test.ts +15 -0
- package/source/manifest.ts +15 -0
- package/source/schemas.ts +8 -0
- package/tsconfig.json +9 -0
- package/docs/discussion.md +0 -109
- package/source/.deployment/index.js +0 -7
- package/source/.deployment/secrets.js +0 -33
- package/source/.deployment/variables.js +0 -26
- package/source/.manifest/.normalize/verbose.js +0 -51
- package/source/.manifest/index.js +0 -7
- package/source/.manifest/normalize.js +0 -16
- package/source/.manifest/schema.yaml +0 -9
- package/source/.manifest/validate.js +0 -13
- package/source/.provider/env.js +0 -19
- package/source/.provider/form.js +0 -20
- package/source/.provider/index.js +0 -7
- package/source/annotation.fixtures.js +0 -39
- package/source/annotation.js +0 -36
- package/source/annotation.test.js +0 -43
- package/source/aspect.fixtures.js +0 -32
- package/source/aspect.js +0 -36
- package/source/aspect.test.js +0 -76
- package/source/configuration.js +0 -19
- package/source/deployment.fixtures.js +0 -38
- package/source/deployment.js +0 -20
- package/source/deployment.test.js +0 -70
- package/source/factory.js +0 -39
- package/source/factory.test.js +0 -22
- package/source/index.js +0 -13
- package/source/manifest.js +0 -13
- package/source/manifest.test.js +0 -123
- package/source/provider.js +0 -119
- package/source/provider.test.js +0 -130
- package/source/secrets.js +0 -28
- package/source/secrets.test.js +0 -47
- package/types/aspect.d.ts +0 -11
- package/types/factory.d.ts +0 -12
- package/types/provider.d.ts +0 -22
- /package/{docs → notes}/consistency.md +0 -0
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.configuration",
|
|
3
|
-
"version": "0.20.0-dev.
|
|
3
|
+
"version": "0.20.0-dev.35",
|
|
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": "transpiled/index.js",
|
|
8
|
+
"types": "transpiled/index.d.ts",
|
|
8
9
|
"repository": {
|
|
9
10
|
"type": "git",
|
|
10
11
|
"url": "git+https://github.com/toa-io/toa.git"
|
|
@@ -15,12 +16,18 @@
|
|
|
15
16
|
"publishConfig": {
|
|
16
17
|
"access": "public"
|
|
17
18
|
},
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
19
|
+
"scripts": {
|
|
20
|
+
"prepublishOnly": "npm run transpile",
|
|
21
|
+
"transpile": "tsc"
|
|
22
|
+
},
|
|
23
|
+
"jest": {
|
|
24
|
+
"preset": "ts-jest",
|
|
25
|
+
"testEnvironment": "node"
|
|
24
26
|
},
|
|
25
|
-
"gitHead": "
|
|
27
|
+
"gitHead": "04d3317272256b98003a414ee1eb2555b65163a5",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@toa.io/core": "0.20.0-dev.35",
|
|
30
|
+
"@toa.io/generic": "0.20.0-dev.35",
|
|
31
|
+
"@toa.io/schemas": "0.20.0-dev.35"
|
|
32
|
+
}
|
|
26
33
|
}
|
package/readme.md
CHANGED
|
@@ -5,20 +5,24 @@
|
|
|
5
5
|
### Define
|
|
6
6
|
|
|
7
7
|
```yaml
|
|
8
|
-
#
|
|
8
|
+
# manifest.toa.yaml
|
|
9
9
|
name: dummy
|
|
10
10
|
namespace: dummies
|
|
11
11
|
|
|
12
12
|
configuration:
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
schema:
|
|
14
|
+
foo: string
|
|
15
|
+
bar: number
|
|
16
|
+
defaults:
|
|
17
|
+
foo: bar
|
|
18
|
+
bar: 1
|
|
15
19
|
```
|
|
16
20
|
|
|
17
21
|
### Use
|
|
18
22
|
|
|
19
23
|
```javascript
|
|
20
24
|
function transition (input, entity, context) {
|
|
21
|
-
const { foo,
|
|
25
|
+
const { foo, bar } = context.configuration
|
|
22
26
|
|
|
23
27
|
// ...
|
|
24
28
|
}
|
|
@@ -30,9 +34,9 @@ function transition (input, entity, context) {
|
|
|
30
34
|
# context.toa.yaml
|
|
31
35
|
configuration:
|
|
32
36
|
dummies.dummy:
|
|
33
|
-
foo: qux # override default value
|
|
37
|
+
foo: qux # override default value
|
|
34
38
|
foo@staging: quux # deployment environment discriminator
|
|
35
|
-
|
|
39
|
+
bar: $BAZ_VALUE # secret
|
|
36
40
|
```
|
|
37
41
|
|
|
38
42
|
### Deploy secrets
|
|
@@ -45,130 +49,96 @@ $ toa conceal configuration BAZ_VALUE=$ecr3t
|
|
|
45
49
|
|
|
46
50
|
## Problem Definition
|
|
47
51
|
|
|
48
|
-
- Components
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
## Definitions
|
|
53
|
-
|
|
54
|
-
### Configuration (Distributed System Configuration)
|
|
55
|
-
|
|
56
|
-
Set of static[^1] parameters for all algorithms within a given system.
|
|
57
|
-
|
|
58
|
-
### Configuration Schema
|
|
59
|
-
|
|
60
|
-
Schema is defining component's algorithm parameters (optionally with default values).
|
|
52
|
+
- Components should be runnable in different deployment environments.
|
|
53
|
+
- Some algorithm's parameters should be deployed secretly.
|
|
54
|
+
- Components should be reusable in different contexts.
|
|
61
55
|
|
|
62
|
-
|
|
56
|
+
## Manifest
|
|
63
57
|
|
|
64
|
-
|
|
58
|
+
Component's configuration is declared using `configuration` manifest,
|
|
59
|
+
containing `schema` and optionnaly `defaults` properties.
|
|
65
60
|
|
|
66
|
-
###
|
|
61
|
+
### Schema
|
|
67
62
|
|
|
68
|
-
|
|
63
|
+
Configuration schema is declared with [COS](/libraries/concise).
|
|
69
64
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
## Responsibility Segregation
|
|
65
|
+
```yaml
|
|
66
|
+
# manifest.toa.yaml
|
|
67
|
+
name: dummy
|
|
68
|
+
namespace: dummies
|
|
75
69
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
configuration:
|
|
71
|
+
schema:
|
|
72
|
+
foo: string
|
|
73
|
+
bar: number
|
|
74
|
+
```
|
|
79
75
|
|
|
80
|
-
|
|
76
|
+
> Introducing non-backward compatible changes to a configuration schema will result in a loss of
|
|
77
|
+
> compatibility with existing contexts and deployment environments.
|
|
78
|
+
> Therefore, configuration schema changes are subject to component versioning.
|
|
81
79
|
|
|
82
|
-
|
|
80
|
+
If `configuration` object doesn't contain property `schema`, then it is considered to be schema.
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
```yaml
|
|
83
|
+
# manifest.toa.yaml
|
|
84
|
+
name: dummy
|
|
85
|
+
namespace: dummies
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
configuration:
|
|
88
|
+
foo: string
|
|
89
|
+
bar: number
|
|
90
|
+
```
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
> Having default values for all required parameters will allow components to be runnable
|
|
94
|
-
> without configuration (i.e. on local environment).
|
|
92
|
+
### Defaults
|
|
95
93
|
|
|
96
|
-
|
|
94
|
+
The default configuration value can be provided using the `defaults` property, which should conform
|
|
95
|
+
to the configuration schema.
|
|
97
96
|
|
|
98
97
|
```yaml
|
|
99
|
-
#
|
|
98
|
+
# manifest.toa.yaml
|
|
100
99
|
name: dummy
|
|
101
100
|
namespace: dummies
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
type: number
|
|
111
|
-
required: [foo]
|
|
102
|
+
configuration:
|
|
103
|
+
schema:
|
|
104
|
+
foo: string
|
|
105
|
+
bar: number
|
|
106
|
+
defaults:
|
|
107
|
+
foo: hello
|
|
108
|
+
bar: 0
|
|
112
109
|
```
|
|
113
110
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
As it is known that Configuration Schema is declared with a JSON Schema `object` type, any
|
|
117
|
-
configuration declaration without defined `properties` considered as concise. Properties of concise
|
|
118
|
-
declaration are treated as required Configuration Schema properties with the same type as its value
|
|
119
|
-
type and no additional properties allowed.
|
|
120
|
-
|
|
121
|
-
Also note that a well-known shortcut `configuration` is available.
|
|
111
|
+
#### Schema defaults hint
|
|
122
112
|
|
|
123
|
-
The
|
|
113
|
+
The configuration schema itself can contain default primitive values using the COS syntax.
|
|
124
114
|
|
|
125
115
|
```yaml
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
bar: 1
|
|
130
|
-
```
|
|
116
|
+
# manifest.toa.yaml
|
|
117
|
+
name: dummy
|
|
118
|
+
namespace: dummies
|
|
131
119
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
properties:
|
|
137
|
-
foo:
|
|
138
|
-
type: string
|
|
139
|
-
default: baz
|
|
140
|
-
bar:
|
|
141
|
-
type: number
|
|
142
|
-
default: 1
|
|
143
|
-
additionalProperties: false
|
|
144
|
-
required: [foo, bar]
|
|
120
|
+
configuration:
|
|
121
|
+
schema:
|
|
122
|
+
foo: hello
|
|
123
|
+
bar: 0
|
|
145
124
|
```
|
|
146
125
|
|
|
147
|
-
##
|
|
148
|
-
|
|
149
|
-
Context Configuration is declared as a context annotation. Its keys must be
|
|
150
|
-
component identifiers, and its values must be Configuration Objects for those
|
|
151
|
-
components.
|
|
152
|
-
|
|
153
|
-
Context Configuration keys and Configuration Object keys may be defined
|
|
154
|
-
with [deployment environment discriminators](#).
|
|
126
|
+
## Annotation
|
|
155
127
|
|
|
156
|
-
|
|
128
|
+
A component's configuration can be overridden using the configuration context annotation.
|
|
157
129
|
|
|
158
130
|
```yaml
|
|
159
131
|
# context.toa.yaml
|
|
160
132
|
configuration:
|
|
161
133
|
dummies.dummy:
|
|
162
|
-
foo:
|
|
134
|
+
foo: bye
|
|
163
135
|
bar: 1
|
|
164
136
|
bar@staging: 2
|
|
165
137
|
```
|
|
166
138
|
|
|
167
|
-
##
|
|
139
|
+
## Secrets
|
|
168
140
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
### Example
|
|
141
|
+
Configuration annotation top-level values which are uppercase strings prefixed with `$` considered as secrets.
|
|
172
142
|
|
|
173
143
|
```yaml
|
|
174
144
|
# context.toa.yaml
|
|
@@ -177,14 +147,10 @@ configuration:
|
|
|
177
147
|
api-key: $STRIPE_API_KEY
|
|
178
148
|
```
|
|
179
149
|
|
|
180
|
-
Configuration values that are assigned with a reference to the Secret must be of type `string`.
|
|
181
|
-
|
|
182
|
-
### Secrets Deployment
|
|
183
|
-
|
|
184
150
|
Secrets are not being deployed with context
|
|
185
|
-
deployment ([`toa deploy`](
|
|
186
|
-
|
|
187
|
-
manually ([`toa conceal`](
|
|
151
|
+
deployment ([`toa deploy`](/runtime/cli/readme.md#deploy)), thus must be deployed separately at
|
|
152
|
+
least once for each deployment environment
|
|
153
|
+
manually ([`toa conceal`](/runtime/cli/readme.md#conceal)).
|
|
188
154
|
|
|
189
155
|
Deployed kubernetes secret's name is predefined as `configuration`.
|
|
190
156
|
|
|
@@ -194,65 +160,12 @@ $ toa conceal configuration STRIPE_API_KEY=xxxxxxxx
|
|
|
194
160
|
|
|
195
161
|
## Aspect
|
|
196
162
|
|
|
197
|
-
|
|
163
|
+
Component's configuration values are available as a well-known Aspect `configuration`.
|
|
198
164
|
|
|
199
165
|
```javascript
|
|
200
|
-
// Node.js bridge
|
|
201
|
-
|
|
202
166
|
function transition (input, entity, context) {
|
|
203
167
|
const foo = context.configiuration.foo
|
|
204
168
|
|
|
205
169
|
// ...
|
|
206
170
|
}
|
|
207
171
|
```
|
|
208
|
-
|
|
209
|
-
> <br/>
|
|
210
|
-
> It is strongly **not** recommended to store a copy of value type configuration
|
|
211
|
-
> values outside operation scope, thus it prevents operation to benefit
|
|
212
|
-
> from [hot updates](#).
|
|
213
|
-
>
|
|
214
|
-
> ```javascript
|
|
215
|
-
> // NOT RECOMMENDED
|
|
216
|
-
> let foo
|
|
217
|
-
>
|
|
218
|
-
> function transition (input, entity, context) {
|
|
219
|
-
> // NOT RECOMMENDED
|
|
220
|
-
> if (foo === undefined) foo = context.configuration.foo
|
|
221
|
-
>
|
|
222
|
-
> // ...
|
|
223
|
-
> }
|
|
224
|
-
> ```
|
|
225
|
-
> See [Genuine operations](/documentation/design.md#genuine-operations).
|
|
226
|
-
|
|
227
|
-
## Development Configuration
|
|
228
|
-
|
|
229
|
-
Configuration can be exported by [`toa env`](/runtime/cli/readme.md#env).
|
|
230
|
-
|
|
231
|
-
### Local Environment Placeholders
|
|
232
|
-
|
|
233
|
-
Context Configuration values may contain placeholders that reference environment variables.
|
|
234
|
-
Placeholders are replaced with values if the corresponding environment variables are set.
|
|
235
|
-
|
|
236
|
-
> Placeholders can only be used with local environment (exported by `toa env`), as these values are
|
|
237
|
-
> not
|
|
238
|
-
> deployed.
|
|
239
|
-
|
|
240
|
-
```yaml
|
|
241
|
-
# context.toa.yaml
|
|
242
|
-
configuration:
|
|
243
|
-
dummies.dummy:
|
|
244
|
-
url@local: https://stage${STAGE}.intranet/
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
```dotenv
|
|
248
|
-
# .env
|
|
249
|
-
STAGE=82
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
## Appendix
|
|
253
|
-
|
|
254
|
-
- [Discussion](./docs/discussion.md)
|
|
255
|
-
- [Configuration consistency](./docs/consistency.md)
|
|
256
|
-
|
|
257
|
-
[^1]: Cannot be changed without a deployment as new values are considered to be a subject of
|
|
258
|
-
testing. [#146](https://github.com/toa-io/toa/issues/146)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<object>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Aspect } from './Aspect'
|
|
2
|
+
|
|
3
|
+
it('should return value', async () => {
|
|
4
|
+
const configuration = {
|
|
5
|
+
foo: 'bar',
|
|
6
|
+
bar: {
|
|
7
|
+
baz: 'quux'
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const aspect = new Aspect(configuration)
|
|
12
|
+
|
|
13
|
+
expect(aspect.invoke(['foo'])).toStrictEqual(configuration.foo)
|
|
14
|
+
expect(aspect.invoke(['bar', 'baz'])).toStrictEqual(configuration.bar.baz)
|
|
15
|
+
})
|
package/source/Aspect.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Connector, type extensions } from '@toa.io/core'
|
|
2
|
+
|
|
3
|
+
export class Aspect extends Connector implements extensions.Aspect {
|
|
4
|
+
public readonly name = 'configuration'
|
|
5
|
+
|
|
6
|
+
private readonly value: object
|
|
7
|
+
|
|
8
|
+
public constructor (value: object) {
|
|
9
|
+
super()
|
|
10
|
+
|
|
11
|
+
this.value = value
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public invoke (path: string[]): any {
|
|
15
|
+
let cursor: any = this.value
|
|
16
|
+
|
|
17
|
+
if (path !== undefined)
|
|
18
|
+
for (const segment of path)
|
|
19
|
+
cursor = cursor[segment]
|
|
20
|
+
|
|
21
|
+
return cursor
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Locator, type extensions } from '@toa.io/core'
|
|
2
|
+
import { Aspect } from './Aspect'
|
|
3
|
+
import { type Manifest } from './manifest'
|
|
4
|
+
import { get } from './configuration'
|
|
5
|
+
|
|
6
|
+
export class Factory implements extensions.Factory {
|
|
7
|
+
public aspect (locator: Locator, manifest: Manifest): extensions.Aspect {
|
|
8
|
+
const value = get(locator, manifest)
|
|
9
|
+
|
|
10
|
+
return new Aspect(value)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { encode } from '@toa.io/generic'
|
|
2
|
+
import { Locator } from '@toa.io/core'
|
|
3
|
+
import { generate } from 'randomstring'
|
|
4
|
+
import { get } from './configuration'
|
|
5
|
+
import { type Manifest } from './manifest'
|
|
6
|
+
|
|
7
|
+
let locator: Locator
|
|
8
|
+
let manifest: Manifest
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
locator = new Locator(generate(), generate())
|
|
12
|
+
manifest = { schema: { foo: 'string' } }
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
for (const name of used)
|
|
17
|
+
process.env[name] = undefined
|
|
18
|
+
|
|
19
|
+
used = []
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should read value', async () => {
|
|
23
|
+
manifest.schema = { foo: 'string' }
|
|
24
|
+
const value: object = { foo: generate() }
|
|
25
|
+
|
|
26
|
+
set(value)
|
|
27
|
+
|
|
28
|
+
const result = get(locator, manifest)
|
|
29
|
+
|
|
30
|
+
expect(result).toStrictEqual(value)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should return empty object if no value set', async () => {
|
|
34
|
+
expect(get(locator, manifest)).toStrictEqual({})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should substitute secrets', async () => {
|
|
38
|
+
const value: object = { foo: '$BAR' }
|
|
39
|
+
|
|
40
|
+
set(value)
|
|
41
|
+
set('bar', '_BAR')
|
|
42
|
+
|
|
43
|
+
const result = get(locator, manifest)
|
|
44
|
+
|
|
45
|
+
expect(result).toStrictEqual({ foo: 'bar' })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should use defaults', async () => {
|
|
49
|
+
manifest.schema = { foo: 'string', bar: ['number'], 'baz?': 'string' }
|
|
50
|
+
manifest.defaults = { foo: 'bar', bar: [1] }
|
|
51
|
+
|
|
52
|
+
const values = { bar: [2], baz: 'foo' }
|
|
53
|
+
|
|
54
|
+
set(values)
|
|
55
|
+
|
|
56
|
+
const result = get(locator, manifest)
|
|
57
|
+
|
|
58
|
+
expect(result).toStrictEqual({
|
|
59
|
+
foo: 'bar',
|
|
60
|
+
bar: [2],
|
|
61
|
+
baz: 'foo'
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should validate', async () => {
|
|
66
|
+
manifest.schema = { foo: 'hello', bar: 'number' }
|
|
67
|
+
|
|
68
|
+
const values = { bar: 5 }
|
|
69
|
+
|
|
70
|
+
set(values)
|
|
71
|
+
|
|
72
|
+
const result = get(locator, manifest)
|
|
73
|
+
|
|
74
|
+
expect(result).toStrictEqual({
|
|
75
|
+
foo: 'hello',
|
|
76
|
+
bar: 5
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
function set (value: object | string, key = locator.uppercase): void {
|
|
81
|
+
const string = typeof value === 'string' ? value : encode(value)
|
|
82
|
+
const name = 'TOA_CONFIGURATION_' + key
|
|
83
|
+
|
|
84
|
+
process.env[name] = string
|
|
85
|
+
|
|
86
|
+
used.push(name)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let used: string[] = []
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Locator } from '@toa.io/core'
|
|
2
|
+
import { decode, add } from '@toa.io/generic'
|
|
3
|
+
import * as schemas from '@toa.io/schemas'
|
|
4
|
+
import { PREFIX, SECRET_RX } from './deployment'
|
|
5
|
+
import { type Manifest } from './manifest'
|
|
6
|
+
|
|
7
|
+
export function get (locator: Locator, manifest: Manifest): Configuration {
|
|
8
|
+
const values = getConfiguration(locator.uppercase)
|
|
9
|
+
|
|
10
|
+
substituteSecrets(values)
|
|
11
|
+
|
|
12
|
+
if (manifest.defaults !== undefined) add(values, manifest.defaults)
|
|
13
|
+
|
|
14
|
+
const schema = schemas.schema(manifest.schema)
|
|
15
|
+
|
|
16
|
+
schema.validate(values)
|
|
17
|
+
|
|
18
|
+
return values
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getConfiguration (suffix: string): Configuration {
|
|
22
|
+
const variable = PREFIX + suffix
|
|
23
|
+
const string = process.env[variable]
|
|
24
|
+
|
|
25
|
+
if (string === undefined) return {}
|
|
26
|
+
else return decode(string)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function substituteSecrets (configuration: Configuration): void {
|
|
30
|
+
for (const [key, value] of Object.entries(configuration)) {
|
|
31
|
+
if (typeof value !== 'string') continue
|
|
32
|
+
|
|
33
|
+
const match = value.match(SECRET_RX)
|
|
34
|
+
|
|
35
|
+
if (match === null) continue
|
|
36
|
+
|
|
37
|
+
const name = match.groups?.variable as string
|
|
38
|
+
|
|
39
|
+
configuration[key] = getSecret(name)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getSecret (name: string): string {
|
|
44
|
+
const variable = PREFIX + '_' + name
|
|
45
|
+
const value = process.env[variable]
|
|
46
|
+
|
|
47
|
+
if (value === undefined) throw new Error(`${variable} is not set.`)
|
|
48
|
+
|
|
49
|
+
return value
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type Configuration = Record<string, any>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Annotation, deployment, type Instance } from './deployment'
|
|
2
|
+
import { type Manifest } from './manifest'
|
|
3
|
+
|
|
4
|
+
it('should validate annotation', async () => {
|
|
5
|
+
const wrongType = 'not ok' as unknown as Annotation
|
|
6
|
+
|
|
7
|
+
expect(() => deployment([], wrongType)).toThrow('object')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('should validate values', async () => {
|
|
11
|
+
const manifest: Manifest = {
|
|
12
|
+
schema: { foo: 'string', bar: 'number' },
|
|
13
|
+
defaults: { foo: 'ok', bar: 0 }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const locator = { id: 'component' }
|
|
17
|
+
const instances = [{ manifest, locator }] as unknown as Instance[]
|
|
18
|
+
const annotation: Annotation = { [locator.id]: { bar: 'not a number' } }
|
|
19
|
+
|
|
20
|
+
expect(() => deployment(instances, annotation)).toThrow('number')
|
|
21
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Dependency, type Variable, type Variables } from '@toa.io/operations'
|
|
2
|
+
import * as schemas from '@toa.io/schemas'
|
|
3
|
+
import { encode, overwrite } from '@toa.io/generic'
|
|
4
|
+
import { type Manifest } from './manifest'
|
|
5
|
+
import * as validators from './schemas'
|
|
6
|
+
import type { context } from '@toa.io/norm'
|
|
7
|
+
|
|
8
|
+
export function deployment (instances: Instance[], annotation: Annotation): Dependency {
|
|
9
|
+
validators.annotation.validate(annotation)
|
|
10
|
+
|
|
11
|
+
const variables: Variables = {}
|
|
12
|
+
|
|
13
|
+
for (const instance of instances) {
|
|
14
|
+
const values = annotation[instance.locator.id]
|
|
15
|
+
|
|
16
|
+
if (values === undefined) continue
|
|
17
|
+
|
|
18
|
+
validate(instance, values)
|
|
19
|
+
|
|
20
|
+
variables[instance.locator.label] = [{
|
|
21
|
+
name: PREFIX + instance.locator.uppercase,
|
|
22
|
+
value: encode(values)
|
|
23
|
+
}]
|
|
24
|
+
|
|
25
|
+
const secrets = createSecrets(values)
|
|
26
|
+
|
|
27
|
+
variables[instance.locator.label].push(...secrets)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { variables }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createSecrets (values: object): Variable[] {
|
|
34
|
+
const secrets: Variable[] = []
|
|
35
|
+
|
|
36
|
+
for (const value of Object.values(values)) {
|
|
37
|
+
if (typeof value !== 'string') continue
|
|
38
|
+
|
|
39
|
+
const match = value.match(SECRET_RX)
|
|
40
|
+
|
|
41
|
+
if (match === null) continue
|
|
42
|
+
|
|
43
|
+
const name = match.groups?.variable as string
|
|
44
|
+
|
|
45
|
+
secrets.push({
|
|
46
|
+
name: PREFIX + '_' + name,
|
|
47
|
+
secret: {
|
|
48
|
+
name: 'toa-configuration',
|
|
49
|
+
key: name
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return secrets
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validate (instace: Instance, values: object): void {
|
|
58
|
+
const defaults = instace.manifest.defaults ?? {}
|
|
59
|
+
const configuration = overwrite(defaults, values)
|
|
60
|
+
const schema = schemas.schema(instace.manifest.schema)
|
|
61
|
+
|
|
62
|
+
schema.validate(configuration)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const SECRET_RX = /^\$(?<variable>[A-Z0-9_]{1,32})$/
|
|
66
|
+
export const PREFIX = 'TOA_CONFIGURATION_'
|
|
67
|
+
|
|
68
|
+
export type Annotation = Record<string, object>
|
|
69
|
+
export type Instance = context.Dependency<Manifest>
|
package/source/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Manifest, manifest } from './manifest'
|
|
2
|
+
|
|
3
|
+
it('should validate', async () => {
|
|
4
|
+
const additional = { schema: {}, foo: 'bar' } as unknown as Manifest
|
|
5
|
+
|
|
6
|
+
expect(() => {
|
|
7
|
+
manifest(additional)
|
|
8
|
+
}).toThrow('additional')
|
|
9
|
+
|
|
10
|
+
const wrongType = { schema: 'not ok' } as unknown as Manifest
|
|
11
|
+
|
|
12
|
+
expect(() => {
|
|
13
|
+
manifest(wrongType)
|
|
14
|
+
}).toThrow('object')
|
|
15
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as schemas from './schemas'
|
|
2
|
+
import { type Configuration } from './configuration'
|
|
3
|
+
|
|
4
|
+
export function manifest (manifest: Manifest): Manifest {
|
|
5
|
+
if (manifest.schema === undefined) manifest = { schema: manifest }
|
|
6
|
+
|
|
7
|
+
schemas.manifest.validate(manifest)
|
|
8
|
+
|
|
9
|
+
return manifest
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Manifest {
|
|
13
|
+
schema: object
|
|
14
|
+
defaults?: Configuration
|
|
15
|
+
}
|