@toa.io/extensions.configuration 1.0.1 → 1.1.0-dev.1

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.configuration",
3
- "version": "1.0.1",
3
+ "version": "1.1.0-dev.1",
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": "src/index.js",
7
+ "main": "source/index.js",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/toa-io/toa.git"
@@ -16,11 +16,11 @@
16
16
  "access": "public"
17
17
  },
18
18
  "dependencies": {
19
- "@toa.io/core": "1.0.1",
20
- "@toa.io/generic": "0.10.0",
21
- "@toa.io/schema": "0.7.5",
22
- "@toa.io/yaml": "0.7.5",
19
+ "@toa.io/core": "1.1.0-dev.1",
20
+ "@toa.io/generic": "0.11.0-dev.1",
21
+ "@toa.io/schema": "0.7.6-dev.1",
22
+ "@toa.io/yaml": "0.7.6-dev.1",
23
23
  "clone-deep": "4.0.1"
24
24
  },
25
- "gitHead": "25fdf15b413338d49b16ddefe17807e4e746f37e"
25
+ "gitHead": "ef65d2f42f13aac397c14f8c47eb026a191cda66"
26
26
  }
package/readme.md CHANGED
@@ -30,15 +30,15 @@ function transition (input, entity, context) {
30
30
  # context.toa.yaml
31
31
  configuration:
32
32
  dummies.dummy:
33
- foo: qux
34
- foo@staging: quux # use deployment environment discriminator
35
- baz: $BAZ_VALUE # use secrets
33
+ foo: qux # override default value defined by dummies.dummy
34
+ foo@staging: quux # deployment environment discriminator
35
+ baz: $BAZ_VALUE # secret
36
36
  ```
37
37
 
38
38
  ### Deploy secrets
39
39
 
40
40
  ```shell
41
- $ toa conceal
41
+ $ toa conceal configuration BAZ_VALUE '$ecr3t'
42
42
  ```
43
43
 
44
44
  ---
@@ -107,7 +107,7 @@ extensions:
107
107
  default: 'baz'
108
108
  bar:
109
109
  type: number
110
- required: [ foo ]
110
+ required: [foo]
111
111
  ```
112
112
 
113
113
  ### Concise Declaration
@@ -140,7 +140,7 @@ extensions:
140
140
  type: number
141
141
  default: 1
142
142
  additionalProperties: false
143
- required: [ foo, bar ]
143
+ required: [foo, bar]
144
144
  ```
145
145
 
146
146
  ## Context Configuration
@@ -170,11 +170,7 @@ by [`toa configure`](../../runtime/cli/readme.md#configure) command.
170
170
 
171
171
  ## Configuration Secrets
172
172
 
173
- > ![Important](https://img.shields.io/badge/Important-red)<br/>
174
- > Not implemented. [#132](https://github.com/toa-io/toa/issues/132)
175
-
176
- Context Configuration values which are uppercase strings prefixed with `$`
177
- considered as Secrets.
173
+ Context Configuration values which are uppercase strings prefixed with `$` considered as Secrets.
178
174
 
179
175
  ### Example
180
176
 
@@ -185,6 +181,8 @@ configuration:
185
181
  api-key: $STRIPE_API_KEY
186
182
  ```
187
183
 
184
+ Configuration values that are assigned with a reference to the Secret must be of type `string`.
185
+
188
186
  ### Secrets Deployment
189
187
 
190
188
  Secrets are not being deployed with context
@@ -192,6 +190,12 @@ deployment ([`toa deploy`](../../runtime/cli/readme.md#deploy)),
192
190
  thus must be deployed separately at least once for each deployment environment
193
191
  manually ([`toa conceal`](../../runtime/cli/readme.md#conceal)).
194
192
 
193
+ Deployed kubernetes secret's name is predefined as `configuration`.
194
+
195
+ ```shell
196
+ $ toa conceal configuration STRIPE_API_KEY xxxxxxxx
197
+ ```
198
+
195
199
  ## Operation Context
196
200
 
197
201
  Configuration Value is available as a well-known operation context extension `configuration`.
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ const { variables } = require('./variables')
4
+ const { secrets } = require('./secrets')
5
+
6
+ exports.variables = variables
7
+ exports.secrets = secrets
@@ -0,0 +1,33 @@
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
@@ -3,9 +3,12 @@
3
3
  const { encode } = require('@toa.io/generic')
4
4
 
5
5
  /**
6
- * @type {toa.deployment.dependency.Constructor}
6
+ * @param {toa.norm.context.dependencies.Instance[]} components
7
+ * @param {object} annotations
8
+ * @return {toa.deployment.dependency.Variables}
7
9
  */
8
- const deployment = (components, annotations) => {
10
+ function variables (components, annotations) {
11
+ /** @type {toa.deployment.dependency.Variables} */
9
12
  const variables = {}
10
13
 
11
14
  for (const [id, annotation] of Object.entries(annotations)) {
@@ -17,7 +20,7 @@ const deployment = (components, annotations) => {
17
20
  }]
18
21
  }
19
22
 
20
- return { variables }
23
+ return variables
21
24
  }
22
25
 
23
- exports.deployment = deployment
26
+ exports.variables = variables
@@ -21,7 +21,8 @@ const convert = (node) => {
21
21
  }
22
22
 
23
23
  function property (node) {
24
- if (node === null) throw new Error('Configuration: cannot resolve type of null, use JSONSchema declaration.')
24
+ // if (node === null) throw new Error('Configuration: cannot resolve type of null, use JSONSchema declaration.')
25
+ if (node === null) return { type: 'null', default: null }
25
26
 
26
27
  const type = Array.isArray(node) ? 'array' : typeof node
27
28
 
@@ -36,11 +37,14 @@ const array = (array) => {
36
37
 
37
38
  const type = typeof array[0]
38
39
 
39
- return {
40
+ const schema = {
40
41
  type: 'array',
41
- items: { type },
42
42
  default: array
43
43
  }
44
+
45
+ if (array.length === 1) schema.items = array[0]
46
+
47
+ return schema
44
48
  }
45
49
 
46
50
  const SYM = Symbol()
@@ -4,7 +4,7 @@ const clone = require('clone-deep')
4
4
  const { generate } = require('randomstring')
5
5
  const { sample } = require('@toa.io/generic')
6
6
 
7
- const fixtures = require('./annotations.fixtures')
7
+ const fixtures = require('./annotation.fixtures')
8
8
  const { annotation } = require('../')
9
9
 
10
10
  let input
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+
3
+ const { merge } = require('@toa.io/generic')
4
+ const get = require('./.deployment')
5
+
6
+ /**
7
+ * @param {toa.norm.context.dependencies.Instance[]} components
8
+ * @param {object} annotations
9
+ * @return {toa.deployment.dependency.Declaration}
10
+ */
11
+ const deployment = (components, annotations) => {
12
+ const variables = get.variables(components, annotations)
13
+ const secrets = get.secrets(components, annotations)
14
+
15
+ merge(variables, secrets)
16
+
17
+ return { variables }
18
+ }
19
+
20
+ exports.deployment = deployment
@@ -1,9 +1,11 @@
1
1
  'use strict'
2
2
 
3
- const { encode } = require('@toa.io/generic')
3
+ const clone = require('clone-deep')
4
+ const { encode, sample } = require('@toa.io/generic')
4
5
 
5
6
  const fixtures = require('./deployment.fixtures')
6
7
  const { deployment } = require('../')
8
+ const { generate } = require('randomstring')
7
9
 
8
10
  /** @type {toa.deployment.dependency.Declaration} */
9
11
  let declaration
@@ -43,3 +45,26 @@ it('should map configurations', () => {
43
45
  expect(env.value).toStrictEqual(encoded)
44
46
  }
45
47
  })
48
+
49
+ it('should declare secrets', async () => {
50
+ const annotations = clone(fixtures.annotations)
51
+ const component = sample(fixtures.components)
52
+ const id = component.locator.id
53
+ const key = generate()
54
+ const name = generate().substring(0, 16).toUpperCase()
55
+ const value = '$' + name
56
+
57
+ if (annotations[id] === undefined) annotations[id] = {}
58
+
59
+ annotations[id][key] = value
60
+
61
+ declaration = deployment(fixtures.components, annotations)
62
+
63
+ const variables = declaration.variables[component.locator.label]
64
+
65
+ expect(variables).toBeDefined()
66
+
67
+ const secret = variables.find((variable) => variable.name === 'TOA_CONFIGURATION__' + name)
68
+
69
+ expect(secret).toBeDefined()
70
+ })
@@ -109,9 +109,6 @@ describe('normalization', () => {
109
109
  properties: {
110
110
  foo: {
111
111
  type: 'array',
112
- items: {
113
- type: 'number'
114
- },
115
112
  default: [1, 2, 3]
116
113
  }
117
114
  }
@@ -4,6 +4,8 @@ const clone = require('clone-deep')
4
4
  const { decode, encode, empty, overwrite } = require('@toa.io/generic')
5
5
 
6
6
  const { Connector } = require('@toa.io/core')
7
+
8
+ const { secrets } = require('./secrets')
7
9
  const { form } = require('./.provider/form')
8
10
 
9
11
  /**
@@ -39,11 +41,7 @@ class Provider extends Connector {
39
41
  }
40
42
 
41
43
  async open () {
42
- await this.#retrieve()
43
- }
44
-
45
- async #source () {
46
- return this.#value
44
+ this.#retrieve()
47
45
  }
48
46
 
49
47
  set (key, value) {
@@ -77,7 +75,11 @@ class Provider extends Connector {
77
75
  return this.object === undefined ? undefined : encode(this.object)
78
76
  }
79
77
 
80
- async #retrieve () {
78
+ #source () {
79
+ return this.#value
80
+ }
81
+
82
+ #retrieve () {
81
83
  const string = process.env[this.key]
82
84
  const object = string === undefined ? {} : decode(string)
83
85
 
@@ -85,18 +87,12 @@ class Provider extends Connector {
85
87
  }
86
88
 
87
89
  #set (object) {
88
- this.#validate(object)
90
+ object = this.#reveal(object)
89
91
  this.#merge(object)
90
92
 
91
93
  this.object = empty(object) ? undefined : object
92
94
  }
93
95
 
94
- #validate (object) {
95
- const error = this.#schema.match(object)
96
-
97
- if (error !== null) throw new TypeError(error.message)
98
- }
99
-
100
96
  #merge (object) {
101
97
  object = clone(object)
102
98
 
@@ -104,9 +100,18 @@ class Provider extends Connector {
104
100
  const value = overwrite(form, object)
105
101
 
106
102
  this.#schema.validate(value)
107
-
108
103
  this.#value = value
109
104
  }
105
+
106
+ #reveal (object) {
107
+ return secrets(object, (variable) => {
108
+ if (!(variable in process.env)) throw new Error(`Configuration secret value ${variable} is not set`)
109
+
110
+ const base64 = process.env[variable]
111
+
112
+ return decode(base64)
113
+ })
114
+ }
110
115
  }
111
116
 
112
117
  const PREFIX = 'TOA_CONFIGURATION_'
@@ -0,0 +1,89 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+ const { encode } = require('@toa.io/generic')
5
+
6
+ const { Provider } = require('./provider')
7
+
8
+ it('should be', async () => {
9
+ expect(Provider).toBeInstanceOf(Function)
10
+ })
11
+
12
+ const locator = /** @type {toa.core.Locator} */ { uppercase: generate().toUpperCase() }
13
+ const schema = /** @type {toa.schema.Schema} */ { validate: () => undefined }
14
+
15
+ /** @type {Provider} */
16
+ let provider
17
+
18
+ beforeEach(() => {
19
+ cleanEnv()
20
+ provider = new Provider(locator, schema)
21
+ })
22
+
23
+ it('should replace secret values', async () => {
24
+ const configuration = { foo: '$FOO_SECRET' }
25
+ const secrets = { FOO_SECRET: generate() }
26
+
27
+ setEnv(configuration, secrets)
28
+
29
+ await provider.open()
30
+ const value = provider.source()
31
+
32
+ expect(value).toStrictEqual({ foo: secrets.FOO_SECRET })
33
+ })
34
+
35
+ it('should throw if secret value is not set', async () => {
36
+ const configuration = { foo: '$FOO_SECRET' }
37
+
38
+ setEnv(configuration)
39
+
40
+ await expect(provider.open()).rejects.toThrow('FOO_SECRET is not set')
41
+ })
42
+
43
+ it('should replace nested secrets', async () => {
44
+ const configuration = { foo: { bar: '$BAR' } }
45
+ const secrets = { BAR: generate() }
46
+
47
+ setEnv(configuration, secrets)
48
+
49
+ await provider.open()
50
+ const value = provider.source()
51
+
52
+ expect(value).toStrictEqual({ foo: { bar: secrets.BAR } })
53
+ })
54
+
55
+ const usedVariables = []
56
+
57
+ /**
58
+ * @param {object} configuration
59
+ * @param {Record<string, string>} [secrets]
60
+ */
61
+ function setEnv (configuration, secrets) {
62
+ const variable = PREFIX + locator.uppercase
63
+
64
+ setVal(variable, configuration)
65
+
66
+ if (secrets !== undefined) {
67
+ for (const [key, value] of Object.entries(secrets)) {
68
+ const variable = PREFIX + '_' + key
69
+
70
+ process.env[variable] = encode(value)
71
+ usedVariables.push(variable)
72
+ }
73
+ }
74
+ }
75
+
76
+ function setVal (variable, value) {
77
+ process.env[variable] = encode(value)
78
+ usedVariables.push(variable)
79
+ }
80
+
81
+ function cleanEnv () {
82
+ for (const variable of usedVariables) {
83
+ delete process.env[variable]
84
+ }
85
+
86
+ usedVariables.length = 0
87
+ }
88
+
89
+ const PREFIX = 'TOA_CONFIGURATION_'
@@ -0,0 +1,28 @@
1
+ 'use strict'
2
+
3
+ const { map } = require('@toa.io/generic')
4
+
5
+ /**
6
+ * @param {object} configuration
7
+ * @param {(variable: string, name?: string) => void} callback
8
+ * @returns {object}
9
+ */
10
+ function secrets (configuration, callback) {
11
+ return map(configuration, (value) => {
12
+ if (typeof value !== 'string') return
13
+
14
+ const match = value.match(SECRET_RX)
15
+
16
+ if (match === null) return
17
+
18
+ const name = match.groups.variable
19
+ const variable = PREFIX + name
20
+
21
+ return callback(variable, name)
22
+ })
23
+ }
24
+
25
+ const PREFIX = 'TOA_CONFIGURATION__'
26
+ const SECRET_RX = /^\$(?<variable>[A-Z0-9_]{1,32})$/
27
+
28
+ exports.secrets = secrets
@@ -0,0 +1,47 @@
1
+ 'use strict'
2
+
3
+ const { secrets } = require('./secrets')
4
+
5
+ it('should be', async () => {
6
+ expect(secrets).toBeInstanceOf(Function)
7
+ })
8
+
9
+ it('should find secrets', async () => {
10
+ const configuration = {
11
+ foo: {
12
+ bar: '$BAR_VALUE'
13
+ },
14
+ baz: '$BAZ_VALUE'
15
+ }
16
+
17
+ const variables = new Set()
18
+ const names = new Set()
19
+
20
+ secrets(configuration, (variable, name) => {
21
+ variables.add(variable)
22
+ names.add(name)
23
+ })
24
+
25
+ expect(variables.has('TOA_CONFIGURATION__BAR_VALUE')).toStrictEqual(true)
26
+ expect(variables.has('TOA_CONFIGURATION__BAZ_VALUE')).toStrictEqual(true)
27
+
28
+ expect(names.has('BAR_VALUE')).toStrictEqual(true)
29
+ expect(names.has('BAZ_VALUE')).toStrictEqual(true)
30
+ })
31
+
32
+ it('should replace values', async () => {
33
+ const configuration = { foo: '$FOO' }
34
+
35
+ const output = secrets(configuration, (variable) => 'hello')
36
+
37
+ expect(output).toStrictEqual({ foo: 'hello' })
38
+ })
39
+
40
+ it('should allow numbers in secret names', async () => {
41
+ const configuration = { foo: '$HOST_0' }
42
+ const found = new Set()
43
+
44
+ secrets(configuration, (variable) => found.add(variable))
45
+
46
+ expect(found.has('TOA_CONFIGURATION__HOST_0')).toStrictEqual(true)
47
+ })
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes