@toa.io/extensions.configuration 0.20.0-dev.34 → 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.
Files changed (51) hide show
  1. package/package.json +16 -9
  2. package/readme.md +19 -27
  3. package/schemas/annotation.cos.yaml +1 -0
  4. package/schemas/manifest.cos.yaml +2 -0
  5. package/source/Aspect.test.ts +15 -0
  6. package/source/Aspect.ts +23 -0
  7. package/source/Factory.ts +12 -0
  8. package/source/configuration.test.ts +89 -0
  9. package/source/configuration.ts +52 -0
  10. package/source/deployment.test.ts +21 -0
  11. package/source/deployment.ts +69 -0
  12. package/source/index.ts +3 -0
  13. package/source/manifest.test.ts +15 -0
  14. package/source/manifest.ts +15 -0
  15. package/source/schemas.ts +8 -0
  16. package/tsconfig.json +9 -0
  17. package/docs/discussion.md +0 -109
  18. package/source/.deployment/index.js +0 -7
  19. package/source/.deployment/secrets.js +0 -35
  20. package/source/.deployment/variables.js +0 -23
  21. package/source/.manifest/.normalize/verbose.js +0 -51
  22. package/source/.manifest/index.js +0 -7
  23. package/source/.manifest/normalize.js +0 -16
  24. package/source/.manifest/schema.yaml +0 -9
  25. package/source/.manifest/validate.js +0 -13
  26. package/source/.provider/env.js +0 -19
  27. package/source/.provider/form.js +0 -20
  28. package/source/.provider/index.js +0 -7
  29. package/source/annotation.fixtures.js +0 -39
  30. package/source/annotation.js +0 -36
  31. package/source/annotation.test.js +0 -43
  32. package/source/aspect.fixtures.js +0 -32
  33. package/source/aspect.js +0 -36
  34. package/source/aspect.test.js +0 -76
  35. package/source/configuration.js +0 -19
  36. package/source/deployment.fixtures.js +0 -38
  37. package/source/deployment.js +0 -20
  38. package/source/deployment.test.js +0 -70
  39. package/source/factory.js +0 -39
  40. package/source/factory.test.js +0 -22
  41. package/source/index.js +0 -13
  42. package/source/manifest.js +0 -13
  43. package/source/manifest.test.js +0 -123
  44. package/source/provider.js +0 -119
  45. package/source/provider.test.js +0 -130
  46. package/source/secrets.js +0 -28
  47. package/source/secrets.test.js +0 -47
  48. package/types/aspect.d.ts +0 -11
  49. package/types/factory.d.ts +0 -12
  50. package/types/provider.d.ts +0 -22
  51. /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.34",
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": "source/index.js",
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
- "dependencies": {
19
- "@toa.io/core": "0.20.0-dev.34",
20
- "@toa.io/generic": "0.20.0-dev.34",
21
- "@toa.io/schema": "0.20.0-dev.34",
22
- "@toa.io/yaml": "0.20.0-dev.34",
23
- "clone-deep": "4.0.1"
19
+ "scripts": {
20
+ "prepublishOnly": "npm run transpile",
21
+ "transpile": "tsc"
22
+ },
23
+ "jest": {
24
+ "preset": "ts-jest",
25
+ "testEnvironment": "node"
24
26
  },
25
- "gitHead": "7035b1985fe9bb844069308a272d061bfbd38bf0"
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
@@ -49,9 +49,9 @@ $ toa conceal configuration BAZ_VALUE=$ecr3t
49
49
 
50
50
  ## Problem Definition
51
51
 
52
- - Components must be reusable in different contexts and deployment environments that are in
53
- different configurations.
54
- - Some algorithm parameters must be deployed secretly.
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.
55
55
 
56
56
  ## Manifest
57
57
 
@@ -77,6 +77,18 @@ configuration:
77
77
  > compatibility with existing contexts and deployment environments.
78
78
  > Therefore, configuration schema changes are subject to component versioning.
79
79
 
80
+ If `configuration` object doesn't contain property `schema`, then it is considered to be schema.
81
+
82
+ ```yaml
83
+ # manifest.toa.yaml
84
+ name: dummy
85
+ namespace: dummies
86
+
87
+ configuration:
88
+ foo: string
89
+ bar: number
90
+ ```
91
+
80
92
  ### Defaults
81
93
 
82
94
  The default configuration value can be provided using the `defaults` property, which should conform
@@ -126,7 +138,7 @@ configuration:
126
138
 
127
139
  ## Secrets
128
140
 
129
- Configuration annotation values which are uppercase strings prefixed with `$` considered as secrets.
141
+ Configuration annotation top-level values which are uppercase strings prefixed with `$` considered as secrets.
130
142
 
131
143
  ```yaml
132
144
  # context.toa.yaml
@@ -136,8 +148,8 @@ configuration:
136
148
  ```
137
149
 
138
150
  Secrets are not being deployed with context
139
- deployment ([`toa deploy`](/runtime/cli/readme.md#deploy)),
140
- thus must be deployed separately at least once for each deployment environment
151
+ deployment ([`toa deploy`](/runtime/cli/readme.md#deploy)), thus must be deployed separately at
152
+ least once for each deployment environment
141
153
  manually ([`toa conceal`](/runtime/cli/readme.md#conceal)).
142
154
 
143
155
  Deployed kubernetes secret's name is predefined as `configuration`.
@@ -148,7 +160,7 @@ $ toa conceal configuration STRIPE_API_KEY=xxxxxxxx
148
160
 
149
161
  ## Aspect
150
162
 
151
- Component's configuration value is available as a well-known Aspect `configuration`.
163
+ Component's configuration values are available as a well-known Aspect `configuration`.
152
164
 
153
165
  ```javascript
154
166
  function transition (input, entity, context) {
@@ -157,23 +169,3 @@ function transition (input, entity, context) {
157
169
  // ...
158
170
  }
159
171
  ```
160
-
161
- ### Local environment placeholders
162
-
163
- Configuration annotation values may contain placeholders that reference environment variables.
164
- Placeholders are replaced with values if the corresponding environment variables are set.
165
-
166
- > Placeholders can only be used with local environment (exported
167
- > by [`toa env`](/runtime/cli/readme.md#env)), as these values are not deployed.
168
-
169
- ```yaml
170
- # context.toa.yaml
171
- configuration:
172
- dummies.dummy:
173
- url@local: https://stage${STAGE}.intranet/
174
- ```
175
-
176
- ```dotenv
177
- # .env
178
- STAGE=82
179
- ```
@@ -0,0 +1 @@
1
+ <object>
@@ -0,0 +1,2 @@
1
+ schema: object
2
+ defaults?: 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
+ })
@@ -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>
@@ -0,0 +1,3 @@
1
+ export { manifest } from './manifest'
2
+ export { deployment } from './deployment'
3
+ export { Factory } from './Factory'
@@ -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
+ }
@@ -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')
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./transpiled"
5
+ },
6
+ "include": [
7
+ "source"
8
+ ]
9
+ }
@@ -1,109 +0,0 @@
1
- # Discussion
2
-
3
- ## Change Requests
4
-
5
- - [x] feat(configuration): add configuration extension
6
- - manifest (schema) validation
7
- - context extension
8
- - [x] feat(formation): add well-known extension 'configuration'
9
- - component
10
- - context
11
- - [x] feat(node): add well-known context extension 'configuration'
12
- - [x] feat(configuration): add concise declarations
13
- - [x] feat(configuration): add runtime configuration resolution
14
- - [x] feat(cli): add `toa configure <key> <value> --reset`
15
- - validate type
16
- - [x] feat(operations): add configuration deployment
17
- - annotations (values) validation
18
- - [ ] feat(configuration): add secrets resolution
19
- - [ ] feat(operations): add secrets deployment
20
- - [ ] feat(cli): add `toa conceal`
21
- - validate type
22
- - [ ] feat(cli): add `toa configure`
23
- - prompt required values
24
- - use JSON Schema title
25
-
26
- ## Statements
27
-
28
- ### Common
29
-
30
- - Secrets are being deployed separately by `toa conceal` command
31
-
32
- ### 1: Environment variables
33
-
34
- - Configuration values and secrets are mapped as environment variables to composition deployments
35
- - Extensions may expose *deployment mutators*, which are able to modify deployment declaration
36
- - Configuration context extension reads environment variables to resolve configuration and secrets
37
-
38
- ### 2: Dedicated Components
39
-
40
- - Hot updates
41
- - [Configuration consistency](consistency.md)
42
-
43
- ## Questions
44
-
45
- ### Where are values comes from?
46
-
47
- Environment variables.
48
-
49
- ### Is there a configuration service or configuration component?
50
-
51
- No. It will be implemented later as a part of [consistent configuration](consistency.md).
52
-
53
- ### How are configuration values being stored?
54
-
55
- As a kubernetes secrets mapped as environment variables.
56
-
57
- ### Where are secrets being stored and how do they resolve to value?
58
-
59
- As a kubernetes secrets mapped as environment variables.
60
-
61
- ### Is configuration a single environment variable or a set (one per component)?
62
-
63
- #### Context Configuration
64
-
65
- In later versions, context extension will resolve configuration values by component locator. Given
66
- that it is yet
67
- unknown when this will happen, a certain context might have appeared which configuration is big
68
- enough to not fit the
69
- environment variable limitations.
70
-
71
- That is, Context Configuration must be mapped as a set of environment variables (one per component).
72
- Values are
73
- serialized Configuration Objects.
74
-
75
- > This will also allow to configure local environment per component.
76
-
77
- #### Secrets
78
-
79
- Secrets are mapped per secret as they are not bound to components.
80
-
81
- ### Is configuration a single kubernetes secret or a set (one per component)?
82
-
83
- #### Configuration
84
-
85
- Single secret with a set of values per component.
86
-
87
- #### Secrets
88
-
89
- Once kubernetes secret per configuration secret.
90
-
91
- ### Is there an option to configure local environment?
92
-
93
- <dl>
94
- <dt><code>toa configure &lt;component&gt;</code></dt>
95
- <dd>Create local environment configuration values</dd>
96
- </dl>
97
-
98
- ### Whose responsibility is to call annotations?
99
-
100
- - norm
101
- - deployment
102
-
103
- `toa export context` should throw errors if context has invalid annotations, and it's not a part of
104
- the deployment.
105
-
106
- ## References
107
-
108
- - [#125](https://github.com/toa-io/toa/issues/125)
109
- - [#132](https://github.com/toa-io/toa/issues/132)
@@ -1,7 +0,0 @@
1
- 'use strict'
2
-
3
- const { variables } = require('./variables')
4
- const { secrets } = require('./secrets')
5
-
6
- exports.variables = variables
7
- exports.secrets = secrets
@@ -1,35 +0,0 @@
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
- if (annotations === undefined) return {}
12
-
13
- /** @type {toa.deployment.dependency.Variables} */
14
- const variables = {}
15
-
16
- for (const [id, annotation] of Object.entries(annotations)) {
17
- const component = components.find((component) => component.locator.id === id)
18
- const label = component.locator.label
19
-
20
- find.secrets(annotation, (variable, key) => {
21
- if (variables[label] === undefined) variables[label] = []
22
-
23
- variables[label].push({
24
- name: variable,
25
- secret: { name: SECRET_NAME, key }
26
- })
27
- })
28
- }
29
-
30
- return variables
31
- }
32
-
33
- const SECRET_NAME = 'toa-configuration'
34
-
35
- exports.secrets = secrets
@@ -1,23 +0,0 @@
1
- 'use strict'
2
-
3
- const { encode } = require('@toa.io/generic')
4
-
5
- function variables (components, annotations) {
6
- if (annotations === undefined) return {}
7
-
8
- /** @type {toa.deployment.dependency.Variables} */
9
- const variables = {}
10
-
11
- for (const [id, annotation] of Object.entries(annotations)) {
12
- const component = components.find((component) => component.locator.id === id)
13
-
14
- variables[component.locator.label] = [{
15
- name: 'TOA_CONFIGURATION_' + component.locator.uppercase,
16
- value: encode(annotation)
17
- }]
18
- }
19
-
20
- return variables
21
- }
22
-
23
- exports.variables = variables