@toa.io/extensions.configuration 0.7.3 → 0.20.0-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 +77 -137
- 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/transpiled/Aspect.d.ts +7 -0
- package/transpiled/Aspect.js +21 -0
- package/transpiled/Aspect.js.map +1 -0
- package/transpiled/Aspect.test.d.ts +1 -0
- package/transpiled/Aspect.test.js +15 -0
- package/transpiled/Aspect.test.js.map +1 -0
- package/transpiled/Factory.d.ts +5 -0
- package/transpiled/Factory.js +13 -0
- package/transpiled/Factory.js.map +1 -0
- package/transpiled/configuration.d.ts +4 -0
- package/transpiled/configuration.js +66 -0
- package/transpiled/configuration.js.map +1 -0
- package/transpiled/configuration.test.d.ts +1 -0
- package/transpiled/configuration.test.js +64 -0
- package/transpiled/configuration.test.js.map +1 -0
- package/transpiled/deployment.d.ts +8 -0
- package/transpiled/deployment.js +75 -0
- package/transpiled/deployment.js.map +1 -0
- package/transpiled/deployment.test.d.ts +1 -0
- package/transpiled/deployment.test.js +18 -0
- package/transpiled/deployment.test.js.map +1 -0
- package/transpiled/index.d.ts +3 -0
- package/transpiled/index.js +10 -0
- package/transpiled/index.js.map +1 -0
- package/transpiled/manifest.d.ts +6 -0
- package/transpiled/manifest.js +35 -0
- package/transpiled/manifest.js.map +1 -0
- package/transpiled/manifest.test.d.ts +1 -0
- package/transpiled/manifest.test.js +14 -0
- package/transpiled/manifest.test.js.map +1 -0
- package/transpiled/schemas.d.ts +2 -0
- package/transpiled/schemas.js +33 -0
- package/transpiled/schemas.js.map +1 -0
- package/transpiled/tsconfig.tsbuildinfo +1 -0
- package/tsconfig.json +9 -0
- package/docs/discussion.md +0 -109
- package/src/.manifest/.normalize/verbose.js +0 -48
- package/src/.manifest/index.js +0 -7
- package/src/.manifest/normalize.js +0 -16
- package/src/.manifest/schema.yaml +0 -8
- package/src/.manifest/validate.js +0 -13
- package/src/.provider/form.js +0 -20
- package/src/annotation.js +0 -36
- package/src/aspect.js +0 -36
- package/src/configuration.js +0 -19
- package/src/deployment.js +0 -23
- package/src/factory.js +0 -39
- package/src/index.js +0 -13
- package/src/manifest.js +0 -13
- package/src/provider.js +0 -114
- package/test/annotations.fixtures.js +0 -38
- package/test/annotations.test.js +0 -43
- package/test/aspect.fixtures.js +0 -33
- package/test/aspect.test.js +0 -77
- package/test/deployment.fixtures.js +0 -38
- package/test/deployment.test.js +0 -45
- package/test/factory.test.js +0 -22
- package/test/manifest.test.js +0 -132
- 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.
|
|
3
|
+
"version": "0.20.0-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": "d047190899218b5249901a01a2a4caec5b34cf09",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@toa.io/core": "0.20.0-alpha.0",
|
|
29
|
+
"@toa.io/generic": "0.20.0-alpha.0",
|
|
30
|
+
"@toa.io/schemas": "0.20.0-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,152 +34,111 @@ function transition (input, entity, context) {
|
|
|
30
34
|
# context.toa.yaml
|
|
31
35
|
configuration:
|
|
32
36
|
dummies.dummy:
|
|
33
|
-
foo: qux
|
|
34
|
-
foo@staging: quux #
|
|
35
|
-
|
|
37
|
+
foo: qux # override default value
|
|
38
|
+
foo@staging: quux # deployment environment discriminator
|
|
39
|
+
bar: $BAZ_VALUE # secret
|
|
36
40
|
```
|
|
37
41
|
|
|
38
42
|
### Deploy secrets
|
|
39
43
|
|
|
40
44
|
```shell
|
|
41
|
-
$ toa conceal
|
|
45
|
+
$ toa conceal configuration BAZ_VALUE=$ecr3t
|
|
42
46
|
```
|
|
43
47
|
|
|
44
48
|
---
|
|
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
|
|
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.
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
values).
|
|
56
|
+
## Manifest
|
|
62
57
|
|
|
63
|
-
|
|
58
|
+
Component's configuration is declared using `configuration` manifest,
|
|
59
|
+
containing `schema` and optionnaly `defaults` properties.
|
|
64
60
|
|
|
65
|
-
|
|
61
|
+
### Schema
|
|
66
62
|
|
|
67
|
-
|
|
63
|
+
Configuration schema is declared with [COS](/libraries/concise).
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
Map of Configuration Objects for components added to a given context.
|
|
74
|
-
|
|
75
|
-
## Responsibility Segregation
|
|
65
|
+
```yaml
|
|
66
|
+
# manifest.toa.yaml
|
|
67
|
+
name: dummy
|
|
68
|
+
namespace: dummies
|
|
76
69
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
70
|
+
configuration:
|
|
71
|
+
schema:
|
|
72
|
+
foo: string
|
|
73
|
+
bar: number
|
|
74
|
+
```
|
|
80
75
|
|
|
81
|
-
|
|
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.
|
|
82
79
|
|
|
83
|
-
|
|
80
|
+
If `configuration` object doesn't contain property `schema`, then it is considered to be schema.
|
|
84
81
|
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
```yaml
|
|
83
|
+
# manifest.toa.yaml
|
|
84
|
+
name: dummy
|
|
85
|
+
namespace: dummies
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
configuration:
|
|
88
|
+
foo: string
|
|
89
|
+
bar: number
|
|
90
|
+
```
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
> Having default values for all required parameters will allow components to be runnable
|
|
95
|
-
> without configuration (i.e. on local environment).
|
|
92
|
+
### Defaults
|
|
96
93
|
|
|
97
|
-
|
|
94
|
+
The default configuration value can be provided using the `defaults` property, which should conform
|
|
95
|
+
to the configuration schema.
|
|
98
96
|
|
|
99
97
|
```yaml
|
|
100
|
-
#
|
|
98
|
+
# manifest.toa.yaml
|
|
101
99
|
name: dummy
|
|
102
100
|
namespace: dummies
|
|
103
101
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
type: number
|
|
112
|
-
required: [foo]
|
|
102
|
+
configuration:
|
|
103
|
+
schema:
|
|
104
|
+
foo: string
|
|
105
|
+
bar: number
|
|
106
|
+
defaults:
|
|
107
|
+
foo: hello
|
|
108
|
+
bar: 0
|
|
113
109
|
```
|
|
114
110
|
|
|
115
|
-
|
|
111
|
+
#### Schema defaults hint
|
|
116
112
|
|
|
117
|
-
|
|
118
|
-
configuration declaration without defined `properties` considered as concise. Properties of concise
|
|
119
|
-
declaration are treated as required Configuration Schema properties with the same type as its value
|
|
120
|
-
type and no additional properties allowed.
|
|
121
|
-
|
|
122
|
-
Also note that a well-known shortcut `configuration` is available.
|
|
123
|
-
|
|
124
|
-
Next two declarations are equivalent.
|
|
113
|
+
The configuration schema itself can contain default primitive values using the COS syntax.
|
|
125
114
|
|
|
126
115
|
```yaml
|
|
127
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
bar: 1
|
|
131
|
-
```
|
|
116
|
+
# manifest.toa.yaml
|
|
117
|
+
name: dummy
|
|
118
|
+
namespace: dummies
|
|
132
119
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
properties:
|
|
138
|
-
foo:
|
|
139
|
-
type: string
|
|
140
|
-
default: baz
|
|
141
|
-
bar:
|
|
142
|
-
type: number
|
|
143
|
-
default: 1
|
|
144
|
-
additionalProperties: false
|
|
145
|
-
required: [foo, bar]
|
|
120
|
+
configuration:
|
|
121
|
+
schema:
|
|
122
|
+
foo: hello
|
|
123
|
+
bar: 0
|
|
146
124
|
```
|
|
147
125
|
|
|
148
|
-
##
|
|
149
|
-
|
|
150
|
-
Context Configuration is declared as a context annotaion. Its keys must be
|
|
151
|
-
component identifiers and its values must be Configuration Objects for those
|
|
152
|
-
components.
|
|
126
|
+
## Annotation
|
|
153
127
|
|
|
154
|
-
|
|
155
|
-
with [deployment environment discriminators](#).
|
|
156
|
-
|
|
157
|
-
### Example
|
|
128
|
+
A component's configuration can be overridden using the configuration context annotation.
|
|
158
129
|
|
|
159
130
|
```yaml
|
|
160
131
|
# context.toa.yaml
|
|
161
132
|
configuration:
|
|
162
133
|
dummies.dummy:
|
|
163
|
-
foo:
|
|
134
|
+
foo: bye
|
|
164
135
|
bar: 1
|
|
165
136
|
bar@staging: 2
|
|
166
137
|
```
|
|
167
138
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Configuration Objects for local environment may be created
|
|
171
|
-
by [`toa configure`](../../runtime/cli/readme.md#configure) command.
|
|
172
|
-
|
|
173
|
-
## Configuration Secrets
|
|
139
|
+
## Secrets
|
|
174
140
|
|
|
175
|
-
|
|
176
|
-
considered as Secrets.
|
|
177
|
-
|
|
178
|
-
### Example
|
|
141
|
+
Configuration annotation top-level values which are uppercase strings prefixed with `$` considered as secrets.
|
|
179
142
|
|
|
180
143
|
```yaml
|
|
181
144
|
# context.toa.yaml
|
|
@@ -184,18 +147,20 @@ configuration:
|
|
|
184
147
|
api-key: $STRIPE_API_KEY
|
|
185
148
|
```
|
|
186
149
|
|
|
187
|
-
### Secrets Deployment
|
|
188
|
-
|
|
189
150
|
Secrets are not being deployed with context
|
|
190
|
-
deployment ([`toa deploy`](
|
|
191
|
-
|
|
192
|
-
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)).
|
|
193
154
|
|
|
194
|
-
|
|
155
|
+
Deployed kubernetes secret's name is predefined as `configuration`.
|
|
195
156
|
|
|
196
|
-
|
|
157
|
+
```shell
|
|
158
|
+
$ toa conceal configuration STRIPE_API_KEY=xxxxxxxx
|
|
159
|
+
```
|
|
197
160
|
|
|
198
|
-
|
|
161
|
+
## Aspect
|
|
162
|
+
|
|
163
|
+
Component's configuration values are available as a well-known Aspect `configuration`.
|
|
199
164
|
|
|
200
165
|
```javascript
|
|
201
166
|
function transition (input, entity, context) {
|
|
@@ -204,28 +169,3 @@ function transition (input, entity, context) {
|
|
|
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 of operation scope, thus it prevents operation to benefit
|
|
211
|
-
> from [hot updates](#).
|
|
212
|
-
>
|
|
213
|
-
> ```javascript
|
|
214
|
-
> // THIS IS WEIRD, BAD AND NOT RECOMMENDED
|
|
215
|
-
> let foo
|
|
216
|
-
>
|
|
217
|
-
> function transition (input, entity, context) {
|
|
218
|
-
> if (foo === undefined) foo = context.configuration.foo
|
|
219
|
-
>
|
|
220
|
-
> // ...
|
|
221
|
-
> }
|
|
222
|
-
> ```
|
|
223
|
-
> See [Genuine operations](#).
|
|
224
|
-
|
|
225
|
-
## Appendix
|
|
226
|
-
|
|
227
|
-
- [Discussion](./docs/discussion.md)
|
|
228
|
-
- [Configuration consistency](./docs/consistency.md)
|
|
229
|
-
|
|
230
|
-
[^1]: Cannot be changed without a deployment. New values are considered to be a subject of
|
|
231
|
-
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
|
+
})
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import * as schemas from '@toa.io/schemas'
|
|
3
|
+
|
|
4
|
+
const path = resolve(__dirname, '../schemas')
|
|
5
|
+
const namespace = schemas.namespace(path)
|
|
6
|
+
|
|
7
|
+
export const manifest = namespace.schema('manifest')
|
|
8
|
+
export const annotation = namespace.schema('annotation')
|