@toa.io/extensions.configuration 0.20.0-dev.4 → 0.20.0-dev.40

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 +71 -156
  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 -33
  20. package/source/.deployment/variables.js +0 -26
  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 -121
  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.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
@@ -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,33 +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
- /** @type {toa.deployment.dependency.Variables} */
12
- const variables = {}
13
-
14
- for (const [id, annotation] of Object.entries(annotations)) {
15
- const component = components.find((component) => component.locator.id === id)
16
- const label = component.locator.label
17
-
18
- find.secrets(annotation, (variable, key) => {
19
- if (variables[label] === undefined) variables[label] = []
20
-
21
- variables[label].push({
22
- name: variable,
23
- secret: { name: SECRET_NAME, key }
24
- })
25
- })
26
- }
27
-
28
- return variables
29
- }
30
-
31
- const SECRET_NAME = 'toa-configuration'
32
-
33
- exports.secrets = secrets
@@ -1,26 +0,0 @@
1
- 'use strict'
2
-
3
- const { encode } = require('@toa.io/generic')
4
-
5
- /**
6
- * @param {toa.norm.context.dependencies.Instance[]} components
7
- * @param {object} annotations
8
- * @return {toa.deployment.dependency.Variables}
9
- */
10
- function variables (components, annotations) {
11
- /** @type {toa.deployment.dependency.Variables} */
12
- const variables = {}
13
-
14
- for (const [id, annotation] of Object.entries(annotations)) {
15
- const component = components.find((component) => component.locator.id === id)
16
-
17
- variables[component.locator.label] = [{
18
- name: 'TOA_CONFIGURATION_' + component.locator.uppercase,
19
- value: encode(annotation)
20
- }]
21
- }
22
-
23
- return variables
24
- }
25
-
26
- exports.variables = variables
@@ -1,51 +0,0 @@
1
- 'use strict'
2
-
3
- const { remap } = require('@toa.io/generic')
4
-
5
- const verbose = (node) => {
6
- const isObject = node.properties !== undefined
7
- const isValue = node.type !== undefined && node.type !== 'object'
8
- const isProperties = node[SYM] === 1
9
-
10
- if (!isObject && !isValue && !isProperties) return convert(node)
11
-
12
- return node
13
- }
14
-
15
- const convert = (node) => {
16
- const properties = remap(node, property)
17
-
18
- properties[SYM] = 1
19
-
20
- return { type: 'object', properties, additionalProperties: false }
21
- }
22
-
23
- function property (node) {
24
- if (node === null) return { type: 'null', default: null }
25
-
26
- const type = Array.isArray(node) ? 'array' : typeof node
27
-
28
- if (type === 'object') return node
29
- if (type === 'array') return array(node)
30
-
31
- return { type, default: node }
32
- }
33
-
34
- const array = (array) => {
35
- if (array.length === 0) throw new Error('Configuration: cannot resolve concise array items type because it\'s empty')
36
-
37
- const type = typeof array[0]
38
-
39
- const schema = {
40
- type: 'array',
41
- default: array
42
- }
43
-
44
- if (array.length === 1) schema.items = array[0]
45
-
46
- return schema
47
- }
48
-
49
- const SYM = Symbol()
50
-
51
- exports.verbose = verbose
@@ -1,7 +0,0 @@
1
- 'use strict'
2
-
3
- const { normalize } = require('./normalize')
4
- const { validate } = require('./validate')
5
-
6
- exports.normalize = normalize
7
- exports.validate = validate
@@ -1,16 +0,0 @@
1
- 'use strict'
2
-
3
- const { traverse } = require('@toa.io/generic')
4
- const { verbose } = require('./.normalize/verbose')
5
-
6
- const normalize = (manifest) => {
7
- if (manifest.properties === undefined) manifest = expand(manifest)
8
-
9
- return manifest
10
- }
11
-
12
- const expand = (concise) => {
13
- return traverse(concise, verbose)
14
- }
15
-
16
- exports.normalize = normalize
@@ -1,9 +0,0 @@
1
- $schema: https://json-schema.org/draft/2019-09/schema
2
- $id: https://schemas.toa.io/0.0.0/extensions/configuration/manifest
3
-
4
- $ref: 'https://schemas.toa.io/0.0.0/definitions#/definitions/schema'
5
- type: object
6
- properties:
7
- type:
8
- const: object
9
- default: object
@@ -1,13 +0,0 @@
1
- 'use strict'
2
-
3
- const path = require('path')
4
-
5
- const { Schema } = require('@toa.io/schema')
6
- const { load } = require('@toa.io/yaml')
7
-
8
- const schema = load.sync(path.resolve(__dirname, 'schema.yaml'))
9
- const validator = new Schema(schema)
10
-
11
- const validate = (declaration) => validator.validate(declaration)
12
-
13
- exports.validate = validate
@@ -1,19 +0,0 @@
1
- 'use strict'
2
-
3
- const { map } = require('@toa.io/generic')
4
-
5
- function env (object) {
6
- return map(object,
7
- /**
8
- * @type {toa.generic.map.transform<string>}
9
- */
10
- (value) => {
11
- if (typeof value !== 'string') return
12
-
13
- return value.replaceAll(RX, (match, variable) => process.env[variable] ?? match)
14
- })
15
- }
16
-
17
- const RX = /\${(?<variable>[A-Z0-9_]{1,32})}/g
18
-
19
- exports.env = env
@@ -1,20 +0,0 @@
1
- 'use strict'
2
-
3
- const { traverse } = require('@toa.io/generic')
4
-
5
- /**
6
- * @param {toa.schema.JSON | Object} schema
7
- * @return {Object}
8
- */
9
- const form = (schema) => {
10
- const defaults = (node) => {
11
- if (node.type === 'object' && node.properties !== undefined) return { ...node.properties }
12
- if (node.default !== undefined) return node.default
13
-
14
- return null
15
- }
16
-
17
- return traverse(schema, defaults)
18
- }
19
-
20
- exports.form = form
@@ -1,7 +0,0 @@
1
- 'use strict'
2
-
3
- const { form } = require('./form')
4
- const { env } = require('./env')
5
-
6
- exports.form = form
7
- exports.env = env
@@ -1,39 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
- const { Locator } = require('@toa.io/core')
5
- const { random } = require('@toa.io/generic')
6
-
7
- const instance = () => {
8
- const name = generate()
9
- const namespace = generate()
10
- const locator = new Locator(name, namespace)
11
-
12
- const manifest = {
13
- type: 'object',
14
- properties: {
15
- foo: {
16
- type: 'number',
17
- default: 1
18
- }
19
- }
20
- }
21
-
22
- return { locator, manifest }
23
- }
24
-
25
- /** @type {toa.norm.context.dependencies.Instance[]} */
26
- const instances = []
27
-
28
- for (let i = 0; i < random(5) + 5; i++) instances.push(instance())
29
-
30
- /** @type {Object} */
31
- const annotation = {}
32
-
33
- for (let i = 0; i < random(instances.length - 1) + 1; i++) {
34
- const instance = instances[i]
35
- annotation[instance.locator.id] = { foo: random() }
36
- }
37
-
38
- exports.instances = instances
39
- exports.annotation = annotation
@@ -1,36 +0,0 @@
1
- 'use strict'
2
-
3
- const { Schema } = require('@toa.io/schema')
4
-
5
- /**
6
- * @param {Object} annotation
7
- * @param {toa.norm.context.dependencies.Instance[]} instances
8
- */
9
- const annotation = (annotation, instances) => {
10
- const keys = Object.keys(annotation)
11
-
12
- check(keys, instances)
13
-
14
- for (const instance of instances) {
15
- const object = annotation[instance.locator.id] ?? {}
16
- const schema = new Schema(instance.manifest)
17
-
18
- schema.validate(object)
19
- }
20
-
21
- return annotation
22
- }
23
-
24
- /**
25
- * @param {string[]} keys
26
- * @param {toa.norm.context.dependencies.Instance[]} instances
27
- */
28
- const check = (keys, instances) => {
29
- const ids = instances.map((instance) => instance.locator.id)
30
-
31
- for (const key of keys) {
32
- if (!ids.includes(key)) throw new Error(`Configuration Schema '${key}' is not defined`)
33
- }
34
- }
35
-
36
- exports.annotation = annotation
@@ -1,43 +0,0 @@
1
- 'use strict'
2
-
3
- const clone = require('clone-deep')
4
- const { generate } = require('randomstring')
5
- const { sample } = require('@toa.io/generic')
6
-
7
- const fixtures = require('./annotation.fixtures')
8
- const { annotation } = require('../')
9
-
10
- let input
11
- /** @type {toa.norm.context.dependencies.Instance[]} */ let instances
12
-
13
- const call = () => annotation(input, instances)
14
-
15
- beforeEach(() => {
16
- input = clone(fixtures.annotation)
17
- instances = clone(fixtures.instances)
18
- })
19
-
20
- it('sample must be valid', () => {
21
- expect(call).not.toThrow()
22
- })
23
-
24
- it('should must be a function', () => {
25
- expect(annotation).toBeDefined()
26
- expect(annotation).toBeInstanceOf(Function)
27
- })
28
-
29
- it('should throw on non-existent component', () => {
30
- input[generate()] = {}
31
-
32
- expect(call).toThrow(/Configuration Schema/)
33
- })
34
-
35
- it('should throw if object doesn\'t match schema', () => {
36
- const keys = Object.keys(input)
37
- const key = sample(keys)
38
- const object = input[key]
39
-
40
- object.foo = generate()
41
-
42
- expect(call).toThrow(/foo must be number/)
43
- })
@@ -1,32 +0,0 @@
1
- 'use strict'
2
-
3
- const { generate } = require('randomstring')
4
-
5
- const schema = {
6
- type: 'object',
7
- properties: {
8
- foo: {
9
- type: 'string',
10
- default: generate()
11
- },
12
- bar: {
13
- type: 'object',
14
- properties: {
15
- baz: {
16
- type: 'number',
17
- default: 1
18
- }
19
- }
20
- }
21
- }
22
- }
23
-
24
- const concise = {
25
- foo: schema.properties.foo.default,
26
- bar: {
27
- baz: schema.properties.bar.properties.baz.default
28
- }
29
- }
30
-
31
- exports.schema = schema
32
- exports.concise = concise
package/source/aspect.js DELETED
@@ -1,36 +0,0 @@
1
- 'use strict'
2
-
3
- const { Connector } = require('@toa.io/core')
4
-
5
- /**
6
- * @implements {toa.extensions.configuration.Aspect}
7
- */
8
- class Aspect extends Connector {
9
- /** @readonly */
10
- name = 'configuration'
11
-
12
- /** @type {toa.core.Reflection} */
13
- #refection
14
-
15
- /**
16
- * @param {toa.core.Reflection} reflection
17
- */
18
- constructor (reflection) {
19
- super()
20
-
21
- this.#refection = reflection
22
-
23
- this.depends(reflection)
24
- }
25
-
26
- invoke (path) {
27
- /** @type {any} */
28
- let cursor = this.#refection.value
29
-
30
- if (path !== undefined) for (const segment of path) cursor = cursor[segment]
31
-
32
- return cursor
33
- }
34
- }
35
-
36
- exports.Aspect = Aspect
@@ -1,76 +0,0 @@
1
- 'use strict'
2
-
3
- const { Locator } = require('@toa.io/core')
4
- const { encode } = require('@toa.io/generic')
5
-
6
- const fixtures = require('./aspect.fixtures')
7
- const { Factory } = require('../')
8
- const { generate } = require('randomstring')
9
-
10
- const factory = new Factory()
11
-
12
- /** @type {toa.extensions.configuration.Aspect} */
13
- let aspect
14
-
15
- /** @type {toa.core.Locator} */
16
- let locator
17
-
18
- describe('defaults', () => {
19
- beforeEach(async () => {
20
- const namespace = generate()
21
- const name = generate()
22
-
23
- locator = new Locator(name, namespace)
24
-
25
- aspect = factory.aspect(locator, fixtures.schema)
26
-
27
- await aspect.connect()
28
- })
29
-
30
- it('should return schema defaults', () => {
31
- const foo = aspect.invoke(['foo'])
32
-
33
- expect(foo).toStrictEqual(fixtures.schema.properties.foo.default)
34
- })
35
-
36
- it('should return nested values', () => {
37
- const baz = aspect.invoke(['bar', 'baz'])
38
-
39
- expect(baz).toStrictEqual(fixtures.schema.properties.bar.properties.baz.default)
40
- })
41
-
42
- it('should expose configuration tree', () => {
43
- const configuration = aspect.invoke()
44
-
45
- expect(configuration).toStrictEqual({
46
- foo: fixtures.schema.properties.foo.default,
47
- bar: {
48
- baz: fixtures.schema.properties.bar.properties.baz.default
49
- }
50
- })
51
- })
52
- })
53
-
54
- describe('resolution', () => {
55
- let object
56
- let varname
57
-
58
- beforeEach(() => {
59
- object = { foo: generate() }
60
-
61
- varname = 'TOA_CONFIGURATION_' + locator.uppercase
62
- })
63
-
64
- it('should resolve configuration object from environment variable', async () => {
65
- process.env[varname] = encode(object)
66
-
67
- aspect = factory.aspect(locator, fixtures.schema)
68
-
69
- await aspect.connect()
70
-
71
- const configuration = aspect.invoke()
72
-
73
- expect(configuration.foo).toStrictEqual(object.foo)
74
- expect(configuration.bar.baz).toStrictEqual(fixtures.schema.properties.bar.properties.baz.default)
75
- })
76
- })
@@ -1,19 +0,0 @@
1
- 'use strict'
2
-
3
- const { Reflection } = require('@toa.io/core')
4
-
5
- /**
6
- * @implements {toa.core.Reflection}
7
- */
8
- class Configuration extends Reflection {
9
- /**
10
- * @param {toa.extensions.configuration.Provider} provider
11
- */
12
- constructor (provider) {
13
- super(provider.source)
14
-
15
- this.depends(provider)
16
- }
17
- }
18
-
19
- exports.Configuration = Configuration