@toa.io/extensions.configuration 0.20.0-dev.9 → 0.20.1-alpha.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 +15 -9
- package/readme.md +71 -156
- 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 +53 -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 -121
- package/source/provider.test.js +0 -130
- package/source/secrets.js +0 -28
- package/source/secrets.test.js +0 -47
- package/types/aspect.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.
|
|
3
|
+
"version": "0.20.1-alpha.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": "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,17 @@
|
|
|
15
16
|
"publishConfig": {
|
|
16
17
|
"access": "public"
|
|
17
18
|
},
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
19
|
+
"scripts": {
|
|
20
|
+
"transpile": "tsc"
|
|
21
|
+
},
|
|
22
|
+
"jest": {
|
|
23
|
+
"preset": "ts-jest",
|
|
24
|
+
"testEnvironment": "node"
|
|
24
25
|
},
|
|
25
|
-
"gitHead": "
|
|
26
|
+
"gitHead": "ed28dc0d2823022fbb1188bb28b994bc827a4432",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@toa.io/core": "0.21.0-alpha.0",
|
|
29
|
+
"@toa.io/generic": "0.20.1-alpha.0",
|
|
30
|
+
"@toa.io/schemas": "0.20.1-alpha.0"
|
|
31
|
+
}
|
|
26
32
|
}
|
package/readme.md
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
|
-
# Toa Configuration
|
|
1
|
+
# Toa Configuration
|
|
2
2
|
|
|
3
3
|
## TL;DR
|
|
4
4
|
|
|
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,144 +34,111 @@ 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
|
|
39
43
|
|
|
40
44
|
```shell
|
|
41
|
-
$ toa conceal configuration BAZ_VALUE
|
|
45
|
+
$ toa conceal configuration BAZ_VALUE=$ecr3t
|
|
42
46
|
```
|
|
43
47
|
|
|
44
48
|
---
|
|
45
49
|
|
|
46
50
|
## Problem Definition
|
|
47
51
|
|
|
48
|
-
- Components
|
|
49
|
-
- Some algorithm parameters
|
|
50
|
-
|
|
51
|
-
## Definitions
|
|
52
|
-
|
|
53
|
-
### Configuration (Distributed System Configuration)
|
|
54
|
-
|
|
55
|
-
Set of static[^1] parameters for all algorithms within a given system.
|
|
56
|
-
|
|
57
|
-
### Configuration Schema
|
|
58
|
-
|
|
59
|
-
Schema is defining component's algorithm parameters (optionally with default values).
|
|
60
|
-
|
|
61
|
-
### Configuration Object
|
|
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.
|
|
62
55
|
|
|
63
|
-
|
|
56
|
+
## Manifest
|
|
64
57
|
|
|
65
|
-
|
|
58
|
+
Component's configuration is declared using `configuration` manifest,
|
|
59
|
+
containing `schema` and optionnaly `defaults` properties.
|
|
66
60
|
|
|
67
|
-
|
|
61
|
+
### Schema
|
|
68
62
|
|
|
69
|
-
|
|
63
|
+
Configuration schema is declared with [COS](/libraries/concise).
|
|
70
64
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
```yaml
|
|
66
|
+
# manifest.toa.yaml
|
|
67
|
+
name: dummy
|
|
68
|
+
namespace: dummies
|
|
74
69
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
configuration:
|
|
71
|
+
schema:
|
|
72
|
+
foo: string
|
|
73
|
+
bar: number
|
|
74
|
+
```
|
|
78
75
|
|
|
79
|
-
|
|
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.
|
|
80
79
|
|
|
81
|
-
|
|
80
|
+
If `configuration` object doesn't contain property `schema`, then it is considered to be schema.
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
```yaml
|
|
83
|
+
# manifest.toa.yaml
|
|
84
|
+
name: dummy
|
|
85
|
+
namespace: dummies
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
configuration:
|
|
88
|
+
foo: string
|
|
89
|
+
bar: number
|
|
90
|
+
```
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
> Having default values for all required parameters will allow components to be runnable
|
|
93
|
-
> without configuration (i.e. on local environment).
|
|
92
|
+
### Defaults
|
|
94
93
|
|
|
95
|
-
|
|
94
|
+
The default configuration value can be provided using the `defaults` property, which should conform
|
|
95
|
+
to the configuration schema.
|
|
96
96
|
|
|
97
97
|
```yaml
|
|
98
|
-
#
|
|
98
|
+
# manifest.toa.yaml
|
|
99
99
|
name: dummy
|
|
100
100
|
namespace: dummies
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
type: number
|
|
110
|
-
required: [foo]
|
|
102
|
+
configuration:
|
|
103
|
+
schema:
|
|
104
|
+
foo: string
|
|
105
|
+
bar: number
|
|
106
|
+
defaults:
|
|
107
|
+
foo: hello
|
|
108
|
+
bar: 0
|
|
111
109
|
```
|
|
112
110
|
|
|
113
|
-
|
|
111
|
+
#### Schema defaults hint
|
|
114
112
|
|
|
115
|
-
|
|
116
|
-
configuration declaration without defined `properties` considered as concise. Properties of concise
|
|
117
|
-
declaration are treated as required Configuration Schema properties with the same type as its value
|
|
118
|
-
type and no additional properties allowed.
|
|
119
|
-
|
|
120
|
-
Also note that a well-known shortcut `configuration` is available.
|
|
121
|
-
|
|
122
|
-
The next two declarations are equivalent.
|
|
113
|
+
The configuration schema itself can contain default primitive values using the COS syntax.
|
|
123
114
|
|
|
124
115
|
```yaml
|
|
125
|
-
#
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
bar: 1
|
|
129
|
-
```
|
|
116
|
+
# manifest.toa.yaml
|
|
117
|
+
name: dummy
|
|
118
|
+
namespace: dummies
|
|
130
119
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
properties:
|
|
136
|
-
foo:
|
|
137
|
-
type: string
|
|
138
|
-
default: baz
|
|
139
|
-
bar:
|
|
140
|
-
type: number
|
|
141
|
-
default: 1
|
|
142
|
-
additionalProperties: false
|
|
143
|
-
required: [foo, bar]
|
|
120
|
+
configuration:
|
|
121
|
+
schema:
|
|
122
|
+
foo: hello
|
|
123
|
+
bar: 0
|
|
144
124
|
```
|
|
145
125
|
|
|
146
|
-
##
|
|
147
|
-
|
|
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
|
|
150
|
-
components.
|
|
151
|
-
|
|
152
|
-
Context Configuration keys and Configuration Object keys may be defined
|
|
153
|
-
with [deployment environment discriminators](#).
|
|
126
|
+
## Annotation
|
|
154
127
|
|
|
155
|
-
|
|
128
|
+
A component's configuration can be overridden using the configuration context annotation.
|
|
156
129
|
|
|
157
130
|
```yaml
|
|
158
131
|
# context.toa.yaml
|
|
159
132
|
configuration:
|
|
160
133
|
dummies.dummy:
|
|
161
|
-
foo:
|
|
134
|
+
foo: bye
|
|
162
135
|
bar: 1
|
|
163
136
|
bar@staging: 2
|
|
164
137
|
```
|
|
165
138
|
|
|
166
|
-
##
|
|
139
|
+
## Secrets
|
|
167
140
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
### Example
|
|
141
|
+
Configuration annotation top-level values which are uppercase strings prefixed with `$` considered as secrets.
|
|
171
142
|
|
|
172
143
|
```yaml
|
|
173
144
|
# context.toa.yaml
|
|
@@ -176,81 +147,25 @@ configuration:
|
|
|
176
147
|
api-key: $STRIPE_API_KEY
|
|
177
148
|
```
|
|
178
149
|
|
|
179
|
-
Configuration values that are assigned with a reference to the Secret must be of type `string`.
|
|
180
|
-
|
|
181
|
-
### Secrets Deployment
|
|
182
|
-
|
|
183
150
|
Secrets are not being deployed with context
|
|
184
|
-
deployment ([`toa deploy`](
|
|
185
|
-
|
|
186
|
-
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)).
|
|
187
154
|
|
|
188
155
|
Deployed kubernetes secret's name is predefined as `configuration`.
|
|
189
156
|
|
|
190
157
|
```shell
|
|
191
|
-
$ toa conceal configuration STRIPE_API_KEY
|
|
158
|
+
$ toa conceal configuration STRIPE_API_KEY=xxxxxxxx
|
|
192
159
|
```
|
|
193
160
|
|
|
194
161
|
## Aspect
|
|
195
162
|
|
|
196
|
-
|
|
163
|
+
Component's configuration values are available as a well-known Aspect `configuration`.
|
|
197
164
|
|
|
198
165
|
```javascript
|
|
199
|
-
// Node.js bridge
|
|
200
|
-
|
|
201
166
|
function transition (input, entity, context) {
|
|
202
167
|
const foo = context.configiuration.foo
|
|
203
168
|
|
|
204
169
|
// ...
|
|
205
170
|
}
|
|
206
171
|
```
|
|
207
|
-
|
|
208
|
-
> <br/>
|
|
209
|
-
> It is strongly **not** recommended to store a copy of value type configuration
|
|
210
|
-
> values outside operation scope, thus it prevents operation to benefit
|
|
211
|
-
> from [hot updates](#).
|
|
212
|
-
>
|
|
213
|
-
> ```javascript
|
|
214
|
-
> // NOT RECOMMENDED
|
|
215
|
-
> let foo
|
|
216
|
-
>
|
|
217
|
-
> function transition (input, entity, context) {
|
|
218
|
-
> // NOT RECOMMENDED
|
|
219
|
-
> if (foo === undefined) foo = context.configuration.foo
|
|
220
|
-
>
|
|
221
|
-
> // ...
|
|
222
|
-
> }
|
|
223
|
-
> ```
|
|
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
|
-
```
|
|
249
|
-
|
|
250
|
-
## Appendix
|
|
251
|
-
|
|
252
|
-
- [Discussion](./docs/discussion.md)
|
|
253
|
-
- [Configuration consistency](./docs/consistency.md)
|
|
254
|
-
|
|
255
|
-
[^1]: Cannot be changed without a deployment as new values are considered to be a subject of
|
|
256
|
-
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,53 @@
|
|
|
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)
|
|
13
|
+
add(values, manifest.defaults)
|
|
14
|
+
|
|
15
|
+
const schema = schemas.schema(manifest.schema)
|
|
16
|
+
|
|
17
|
+
schema.validate(values)
|
|
18
|
+
|
|
19
|
+
return values
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getConfiguration (suffix: string): Configuration {
|
|
23
|
+
const variable = PREFIX + suffix
|
|
24
|
+
const string = process.env[variable]
|
|
25
|
+
|
|
26
|
+
if (string === undefined) return {}
|
|
27
|
+
else return decode(string)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function substituteSecrets (configuration: Configuration): void {
|
|
31
|
+
for (const [key, value] of Object.entries(configuration)) {
|
|
32
|
+
if (typeof value !== 'string') continue
|
|
33
|
+
|
|
34
|
+
const match = value.match(SECRET_RX)
|
|
35
|
+
|
|
36
|
+
if (match === null) continue
|
|
37
|
+
|
|
38
|
+
const name = match.groups?.variable as string
|
|
39
|
+
|
|
40
|
+
configuration[key] = getSecret(name)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getSecret (name: string): string {
|
|
45
|
+
const variable = PREFIX + '_' + name
|
|
46
|
+
const value = process.env[variable]
|
|
47
|
+
|
|
48
|
+
if (value === undefined) throw new Error(`${variable} is not set.`)
|
|
49
|
+
|
|
50
|
+
return value
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
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
|
+
validate(instance, values)
|
|
17
|
+
|
|
18
|
+
if (values === undefined) continue
|
|
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('not expected')
|
|
9
|
+
|
|
10
|
+
const wrongType = { schema: 'not ok' } as unknown as Manifest
|
|
11
|
+
|
|
12
|
+
expect(() => {
|
|
13
|
+
manifest(wrongType)
|
|
14
|
+
}).toThrow('object')
|
|
15
|
+
})
|