@toa.io/pointer 0.9.0-canary.2 → 0.20.0-alpha.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.
Files changed (60) hide show
  1. package/package.json +14 -7
  2. package/readme.md +113 -78
  3. package/schemas/urimap.cos.yaml +2 -0
  4. package/source/Deployment.ts +90 -0
  5. package/source/annotation.test.ts +66 -0
  6. package/source/annotation.ts +46 -0
  7. package/source/createVariables.test.ts +186 -0
  8. package/source/createVariables.ts +12 -0
  9. package/source/index.ts +7 -0
  10. package/source/naming.ts +11 -0
  11. package/source/resolve.ts +63 -0
  12. package/transpiled/Deployment.d.ts +21 -0
  13. package/transpiled/Deployment.js +63 -0
  14. package/transpiled/Deployment.js.map +1 -0
  15. package/transpiled/annotation.d.ts +3 -0
  16. package/transpiled/annotation.js +62 -0
  17. package/transpiled/annotation.js.map +1 -0
  18. package/transpiled/createVariables.d.ts +4 -0
  19. package/transpiled/createVariables.js +12 -0
  20. package/transpiled/createVariables.js.map +1 -0
  21. package/transpiled/index.d.ts +6 -0
  22. package/transpiled/index.js +35 -0
  23. package/transpiled/index.js.map +1 -0
  24. package/transpiled/naming.d.ts +2 -0
  25. package/transpiled/naming.js +16 -0
  26. package/transpiled/naming.js.map +1 -0
  27. package/transpiled/resolve.d.ts +3 -0
  28. package/transpiled/resolve.js +54 -0
  29. package/transpiled/resolve.js.map +1 -0
  30. package/transpiled/tsconfig.tsbuildinfo +1 -0
  31. package/tsconfig.json +12 -0
  32. package/source/.deployment/.variables/credentials.js +0 -35
  33. package/source/.deployment/.variables/index.js +0 -7
  34. package/source/.deployment/.variables/pointer.js +0 -18
  35. package/source/.deployment/index.js +0 -8
  36. package/source/.deployment/validate.js +0 -19
  37. package/source/.deployment/variables.js +0 -22
  38. package/source/.pointer/env.js +0 -44
  39. package/source/.pointer/index.js +0 -9
  40. package/source/.pointer/label.js +0 -15
  41. package/source/.pointer/local.js +0 -17
  42. package/source/deployment.js +0 -17
  43. package/source/index.js +0 -11
  44. package/source/pointer.js +0 -34
  45. package/source/uris/construct.js +0 -18
  46. package/source/uris/index.js +0 -9
  47. package/source/uris/normalize.js +0 -12
  48. package/source/uris/resolve.js +0 -18
  49. package/source/uris/schema.yaml +0 -18
  50. package/source/uris/validate.js +0 -17
  51. package/source/variable.js +0 -12
  52. package/test/deployment.test.js +0 -92
  53. package/test/pointer.test.js +0 -213
  54. package/test/uris.fixtures.js +0 -10
  55. package/test/uris.test.js +0 -144
  56. package/test/variable.test.js +0 -37
  57. package/types/deployment.d.ts +0 -21
  58. package/types/pointer.d.ts +0 -19
  59. package/types/uris.d.ts +0 -27
  60. package/types/variable.d.ts +0 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/pointer",
3
- "version": "0.9.0-canary.2",
3
+ "version": "0.20.0-alpha.1",
4
4
  "description": "Toa Pointer",
5
5
  "homepage": "https://toa.io",
6
6
  "author": {
@@ -21,12 +21,19 @@
21
21
  "publishConfig": {
22
22
  "access": "public"
23
23
  },
24
- "main": "source/index.js",
24
+ "main": "transpiled/index.js",
25
+ "types": "transpiled/index.d.ts",
25
26
  "dependencies": {
26
- "@toa.io/console": "0.6.0",
27
- "@toa.io/generic": "0.11.0-canary.2",
28
- "@toa.io/schemas": "0.9.0-canary.2",
29
- "@toa.io/yaml": "0.8.0-canary.2"
27
+ "@toa.io/dns": "0.8.0-alpha.1",
28
+ "@toa.io/generic": "0.20.0-alpha.1",
29
+ "@toa.io/schemas": "0.20.0-alpha.1"
30
30
  },
31
- "gitHead": "a80d81d4d9da9fec07606154c959dd6512de4c76"
31
+ "scripts": {
32
+ "transpile": "tsc"
33
+ },
34
+ "jest": {
35
+ "preset": "ts-jest",
36
+ "testEnvironment": "node"
37
+ },
38
+ "gitHead": "d1a5369c0ee4ee423f792b83e9753fd2fd943cce"
32
39
  }
package/readme.md CHANGED
@@ -1,126 +1,161 @@
1
- # Pointer
1
+ # Toa Pointer
2
2
 
3
- Library to declare, deploy and resolve URL to connect to at the runtime. Used by connectors.
3
+ The Pointer is a mechanism to deploy and resolve at runtime a set of URLs.
4
+ Used by [connectors](/connectors) and [extensions](/extensions).
4
5
 
5
- ## Pointer class
6
+ ## Annotation syntax
6
7
 
7
- Pointer builds URL for a given locator, protocol and a package prefix. Prefix must be unique value
8
- for a given package conforming [label format](#) (ex.: `bindings-ampq`).
8
+ The Pointer annotation is an object whose keys are arbitrary dot-separated strings.
9
+ The meaning of these keys are defined in the corresponding module that unilizes the Poiner.
9
10
 
10
- ### Constructor signature
11
+ The values of the annotation object must be a URL or a list of URLs without credentials.
11
12
 
12
- <dl>
13
- <dt><code>prefix</code></dt>
14
- <dd><code>string</code> unique for each package</dd>
15
- <dt><code>locator</code></dt>
16
- <dd><code>toa.core.Locator</code> component for which Pointer is intended</dd>
17
- <dt><code>options</code></dt>
18
- <dd><code><a href="./types/pointer.d.ts">toa.pointer.Options</a></code> Pointer options</dd>
19
- </dl>
20
-
21
- ### Local Environment
22
-
23
- If `TOA_ENV` is `local` then these values are used:
24
-
25
- | Property | Value |
26
- |----------|-------------|
27
- | hostname | `localhost` |
28
- | username | `developer` |
29
- | password | `secret` |
13
+ ```yaml
14
+ something:
15
+ one: amqp://all-dummies.rmq.example.com
16
+ two:
17
+ - amqp://rmq0.example.com
18
+ - amqp://rmq1.example.com
19
+ ```
30
20
 
31
- > In the local environment there is no way to provide values for `protocol` or `port`.
21
+ If the Pointer annotation is a `string`, then its value is considered as default (`.`).
32
22
 
33
- See [types](types/pointer.d.ts) and [tests](test/pointer.test.js) for details.
23
+ ```yaml
24
+ mongodb: mongo://default.db.example.com
25
+ ```
34
26
 
35
- ## Annotation
27
+ ### Shards
36
28
 
37
- Declaration of a set of hosts, matching exact components, namespaces with a default value for
38
- non-matched ones.
29
+ Pointer values can contain [shards](/libraries/generic/readme.md#shards) syntax.
39
30
 
40
31
  ```yaml
41
32
  something:
42
- default: protocol://host1
43
- namespace1: protocol://host2
44
- namespace1.component1: protocol://host3
45
- namespace2.component2: protocol://host4
33
+ one: redis://redis{0-2}.example.com
46
34
  ```
47
35
 
48
- See the [schema](source/uris/.construct/schema.yaml).
36
+ ### Deduplication
49
37
 
50
- ### Concise Declaration
38
+ Pointer values are [DNS-deduplicated](/libraries/dns/readme.md#deduplication) except `http:` schema.
51
39
 
52
- Next two declarations are equivalent.
40
+ ### `default` namespace
53
41
 
54
- ```yaml
55
- something: host1
56
- ```
42
+ When the Pointer is used for component-specific URLs and the component is in the `default`
43
+ namespace (that is, no `namepsace` is specified in its manifest), the annotation value must
44
+ explicitly contain the `default` namespace.
57
45
 
58
46
  ```yaml
59
- something:
60
- default: host1
47
+ amqp:
48
+ # value for the `default` namespace
49
+ default: amqp://default-ns.rmq.example.com
50
+ # value for `teapots` component of the `default` namespace
51
+ default.teapots: amqp://default-dummy.rmq.example.com
61
52
  ```
62
53
 
63
- ### Usage
54
+ ## Resolution
64
55
 
65
- ```javascript
66
- const { uris } = require('@toa.io/pointer')
56
+ Although the most common use case of the Pointer is component-specific connection strings, where
57
+ the keys are namespaces or component IDs, it is not mandatory.
58
+ The Pointer considers keys as nested dot-separated alphanumeric strings.
67
59
 
68
- const annotation = uris.construct(declaration)
69
- const url = uris.resolve(annotation, locator)
70
- ```
60
+ A module that uses the Pointer, resolves a set of URLs by providing annotation and a set of
61
+ _selectors_ during deployment.
62
+ The resolution finds _the deepest match for a given dot-separated string_ within the annotation,
63
+ falling back to the default URL (`.` key) if no matches are found.
71
64
 
72
- See [types](types/uris.d.ts) and [tests](test/uris.test.js) for details.
65
+ ### Example
73
66
 
74
- ### Custom Extensions
67
+ ```yaml
68
+ # context.toa.yaml
75
69
 
76
- Packages using Pointer Annotation may use or require additional properties.
77
- See [Deployment Extensions](#extensions) below.
70
+ amqp:
71
+ .: amqp://default.rmq.example.com # default URL
72
+ dummies: amqp://all-dummies.rmq.example.com # default URL for the `dummies` namespace
73
+ dummies.dummy: amqp://dummies1.rmq.example.com # URL for the `dummies.dummy` component
74
+ ```
78
75
 
79
- ## Deployment
76
+ URL resolution for the example above is as follows:
80
77
 
81
- Deployment function builds global[^1] variables: containing Annotation and a set of variables for
82
- pointer credentials.
78
+ | Selector | Result | Description |
79
+ |-------------------|--------------------------------------|--------------------|
80
+ | `dummies.dummy` | `amqp://d1.rmq.example.com` | Exact match |
81
+ | `dummies.another` | `amqp://all-dummies.rmq.example.com` | Partial match |
82
+ | `what.ever` | `amqp://default.rmq.example.com` | No match (default) |
83
83
 
84
- See [types](types/deployment.d.ts) and [tests](test/deployment.test.js) for details.
84
+ ## Credentials
85
85
 
86
- ### Credentials
86
+ For security reasons, URL credentials cannot be specified in the annotation;
87
+ they must be deployed secretly.
87
88
 
88
- Each entry of the Annotation requires values for `username` and `password`. These values are being
89
- deployed as global[^1] secret variables. Secret names are following the
90
- convention: `toa-package-prefix-entry-label` and `key` names match corresponding URL
91
- properties (`username` and `password`).
89
+ Secret names must match the annotation keys with dots replaced by dashes and prefixed by an
90
+ identifier provided by the corresponding connector or extension using the Pointer.
92
91
 
93
- > Note that declaration is made of `Locator.id` while secret names contain `Locator.label`.
94
- > See [Locator](#).
92
+ ### Example
95
93
 
96
- #### Example
94
+ Given the [AMQP binding](/connectors/bindings.amqp)
95
+ providing `amqp-context` Pointer identifier and the follwing annotation:
97
96
 
98
97
  ```yaml
99
98
  # context.toa.yaml
100
99
  amqp:
101
- default: amqp://host0
102
- dummies.dummy: amqps://host1:5671
100
+ context:
101
+ .: amqp://default.rmq.example.com
102
+ dummies: amqp://dummies.rmq.example.com
103
+ dummies.dummy: amqp://dummy.rmq.example.com
103
104
  ```
104
105
 
106
+ Secret names for the specified keys are as follows:
107
+
108
+ - `amqp-context.default`
109
+ - `amqp-context-dummies`
110
+ - `amqp-context-dummies-dummy`
111
+
112
+ > If secrets are not deployed with the [`toa conceal`](/runtime/cli/readme.md#conceal), then their
113
+ > names must be prefixed with `toa-`.
114
+
115
+ Only one secret can be deployed per annotation key. Therefore, when using a URL set, all URLs will
116
+ utilize the same credentials.
117
+
118
+ ### Plain
119
+
120
+ Secret's value for plain authentication must contain `username` and `password` keys.
121
+
105
122
  ```shell
106
- $ toa conceal bindings-amqp-default username admin
107
- $ toa conceal bindings-amqp-default password ibreakthings
108
- $ toa conceal bindings-amqp-dummies-dummy username developer
109
- $ toa conceal bindings-amqp-dummies-dummy password iluvtests
123
+ $ toa conceal amqp-context-dummies-dummy username=developer password=secret
110
124
  ```
111
125
 
112
- See [`toa conceal`](../../runtime/cli/readme.md#conceal).
126
+ ### TLS
127
+
128
+ Not implemented. [#367](https://github.com/toa-io/toa/issues/367)
113
129
 
114
- ### Extensions
130
+ ## API
115
131
 
116
- For each custom extension additional proxy is being deployed.
132
+ ### Deployment
117
133
 
118
- #### Example
134
+ ```typescript
135
+ import { createVariables } from '@toa.io/pointer'
119
136
 
120
- AMQP binding is a systematic binding used by the runtime. That is a system proxy must be deployed
121
- and compositions require variables to establish connection through that proxy.
137
+ // ...
122
138
 
123
- Therefore, AMQP binding declares a `system` extension and deployment function declares additional
124
- proxy `bindings-amqp-system`.
139
+ const variables = createVariables(id, annotation, requests)
140
+ ```
141
+
142
+ `id` is a Pointer identifier, `annotation` the Pointer annotation value, and `requests` is an array
143
+ of objects conforming to the following:
144
+
145
+ ```yaml
146
+ # label of the component
147
+ label: string
148
+ # list of the annotation keys to be deployed for the component
149
+ selectors: string[]
150
+ ```
151
+
152
+ ### Resolution
125
153
 
126
- [^1]: [#174](https://github.com/toa-io/toa/issues/174)
154
+ ```typescript
155
+ // runtime
156
+ import { resolve } from '@toa.io/pointer'
157
+
158
+ // resolve a set of URLs with credentials
159
+ // for a given pointer identifier and a selector
160
+ const urls = resolve(id, locator.id)
161
+ ```
@@ -0,0 +1,2 @@
1
+ .: [uri]
2
+ /^[a-zA-Z0-9]{1,32}(\.[a-zA-Z0-9]{1,32}){0,3}$/: [uri]
@@ -0,0 +1,90 @@
1
+ import { nameVariable, nameSecret } from './naming'
2
+ import { resolveRecord } from './resolve'
3
+ import type { Variables, Variable } from '@toa.io/operations'
4
+
5
+ export class Deployment {
6
+ private readonly id: string
7
+ private readonly annotation: URIMap
8
+
9
+ public constructor (id: string, annotation: URIMap) {
10
+ this.id = id
11
+ this.annotation = annotation
12
+ }
13
+
14
+ public export (requests: Request[]): Variables {
15
+ const variables: Variables = {}
16
+
17
+ for (const request of requests)
18
+ variables[request.group] = this.createVariables(request.selectors)
19
+
20
+ return variables
21
+ }
22
+
23
+ private createVariables (selectors: string[]): Variable[] {
24
+ const variables: Variable[] = []
25
+
26
+ for (const selector of selectors) {
27
+ const variable = this.createVariable(selector)
28
+ const secrets = this.createSecrets(selector)
29
+
30
+ variables.push(variable, ...secrets)
31
+ }
32
+
33
+ return variables
34
+ }
35
+
36
+ private createVariable (selector: string): Variable {
37
+ const name = nameVariable(this.id, selector)
38
+ const { references } = this.resolveRecord(selector)
39
+ const value = references.join(' ')
40
+
41
+ return { name, value }
42
+ }
43
+
44
+ private createSecrets (selector: string): Variable[] {
45
+ const varialbes: Variable[] = []
46
+ const { key, references } = this.resolveRecord(selector)
47
+
48
+ if (this.insecure(references)) return []
49
+
50
+ for (const token of ['username', 'password']) {
51
+ const varName = nameVariable(this.id, selector, token)
52
+ const secretName = nameSecret(this.id, key)
53
+
54
+ varialbes.push({
55
+ name: varName,
56
+ secret: {
57
+ name: secretName,
58
+ key: token
59
+ }
60
+ })
61
+ }
62
+
63
+ return varialbes
64
+ }
65
+
66
+ private resolveRecord (selector: string): AnnotationRecord {
67
+ return resolveRecord(this.annotation, selector)
68
+ }
69
+
70
+ private insecure (references: string[]): boolean {
71
+ const reference = references[0]
72
+ const url = new URL(reference)
73
+
74
+ return insecureProtocols.includes(url.protocol)
75
+ }
76
+ }
77
+
78
+ export interface Request {
79
+ group: string
80
+ selectors: string[]
81
+ }
82
+
83
+ export interface AnnotationRecord {
84
+ key: string
85
+ references: string[]
86
+ }
87
+
88
+ const insecureProtocols = ['http:', 'https:', 'redis:']
89
+
90
+ export type URIMap = Record<string, string[]>
@@ -0,0 +1,66 @@
1
+ import { normalize } from './annotation'
2
+
3
+ describe('normalize', () => {
4
+ it('should expand default shards', async () => {
5
+ const declaration = 'amqp://rmq{0-2}.example.com'
6
+ const annotation = normalize(declaration)
7
+
8
+ expect(annotation)
9
+ .toStrictEqual({
10
+ '.': [
11
+ 'amqp://rmq0.example.com',
12
+ 'amqp://rmq1.example.com',
13
+ 'amqp://rmq2.example.com'
14
+ ]
15
+ })
16
+ })
17
+
18
+ it('should expand shards within array', async () => {
19
+ const declaration = {
20
+ foo: ['amqp://rmq.example.com', 'amqp://rmq{0-2}.example.com']
21
+ }
22
+
23
+ const annotation = normalize(declaration)
24
+
25
+ expect(annotation)
26
+ .toStrictEqual({
27
+ foo: [
28
+ 'amqp://rmq.example.com',
29
+ 'amqp://rmq0.example.com',
30
+ 'amqp://rmq1.example.com',
31
+ 'amqp://rmq2.example.com'
32
+ ]
33
+ })
34
+ })
35
+ })
36
+
37
+ describe('validation', () => {
38
+ it('should pass', async () => {
39
+ const declaration = 'http://localhost'
40
+
41
+ expect(() => normalize(declaration))
42
+ .not.toThrow()
43
+ })
44
+
45
+ it('should throw if non-uri', async () => {
46
+ const declaration = 'non uri'
47
+
48
+ expect(() => normalize(declaration))
49
+ .toThrow('must match format')
50
+ })
51
+
52
+ it.each(['user:pass', 'user', ':pass'])('should throw if uri has credentials',
53
+ async (credentials) => {
54
+ const declaration = `http://${credentials}@localhost`
55
+
56
+ expect(() => normalize(declaration))
57
+ .toThrow('must not contain credentials')
58
+ })
59
+
60
+ it('should throw if key is not deployable', async () => {
61
+ const declaration = { 'foo bar': 'http://localhost' }
62
+
63
+ expect(() => normalize(declaration))
64
+ .toThrow('not expected')
65
+ })
66
+ })
@@ -0,0 +1,46 @@
1
+ import { resolve } from 'node:path'
2
+ import * as schemas from '@toa.io/schemas'
3
+ import { shards } from '@toa.io/generic'
4
+ import type { URIMap } from './Deployment'
5
+
6
+ export function normalize (declaration: Declaration): URIMap {
7
+ const map: URIMap = {}
8
+
9
+ if (declaration === undefined) return map
10
+
11
+ if (typeof declaration === 'string' || Array.isArray(declaration))
12
+ declaration = { '.': declaration }
13
+
14
+ for (const [key, value] of Object.entries(declaration))
15
+ if (typeof value === 'string') map[key] = format([value])
16
+ else map[key] = format(value)
17
+
18
+ validate(map)
19
+
20
+ return map
21
+ }
22
+
23
+ function format (values: string[]): string[] {
24
+ return values.map(shards).flat()
25
+ }
26
+
27
+ function validate (map: URIMap): void {
28
+ schema.validate(map)
29
+
30
+ for (const uris of Object.values(map))
31
+ for (const uri of uris)
32
+ checkCredentials(uri)
33
+ }
34
+
35
+ function checkCredentials (uri: string): void {
36
+ const url = new URL(uri)
37
+
38
+ if (url.username !== '' || url.password !== '')
39
+ throw new Error(`Pointer URI '${uri}' must not contain credentials. ` +
40
+ 'Please refer to the "Credentials" section in the documentation for more information.')
41
+ }
42
+
43
+ const path = resolve(__dirname, '../schemas/urimap.cos.yaml')
44
+ const schema = schemas.schema(path)
45
+
46
+ export type Declaration = string | string[] | Record<string, string | string[]> | undefined
@@ -0,0 +1,186 @@
1
+ import { generate } from 'randomstring'
2
+ import { createVariables } from './createVariables'
3
+ import type { Variable, Variables } from '@toa.io/operations'
4
+ import type { Request } from './Deployment'
5
+
6
+ it('should create from selector', async () => {
7
+ const id = generate()
8
+ const selector = generate()
9
+ const annotation = { [selector]: uri() }
10
+ const request: Request = { group: generate(), selectors: [selector] }
11
+ const variables = createVariables(id, annotation, [request])
12
+
13
+ const expectation: Variables = {
14
+ [request.group]: [{
15
+ name: `TOA_${id.toUpperCase()}_${selector.toUpperCase()}`,
16
+ value: annotation[selector]
17
+ }]
18
+ }
19
+
20
+ expect(variables)
21
+ .toStrictEqual(expectation)
22
+ })
23
+
24
+ it('should create from partial match', async () => {
25
+ const id = generate()
26
+ const key = generate()
27
+ const selector = `${key}.${generate()}`
28
+ const annotation = { [key]: uri() }
29
+ const request: Request = { group: generate(), selectors: [selector] }
30
+ const variables = createVariables(id, annotation, [request])
31
+
32
+ const expectation: Variables = {
33
+ [request.group]: [{
34
+ name: `TOA_${id.toUpperCase()}_${
35
+ selector
36
+ .replace('.', '_')
37
+ .toUpperCase()
38
+ }`,
39
+ value: annotation[key]
40
+ }]
41
+ }
42
+
43
+ expect(variables)
44
+ .toStrictEqual(expectation)
45
+ })
46
+
47
+ it('should create from default', async () => {
48
+ const id = generate()
49
+ const annotation = uri()
50
+ const selector = generate()
51
+ const request: Request = { group: generate(), selectors: [selector] }
52
+ const variables = createVariables(id, annotation, [request])
53
+
54
+ const expectation: Variables = {
55
+ [request.group]: [{
56
+ name: `TOA_${id.toUpperCase()}_${selector.toUpperCase()}`,
57
+ value: annotation
58
+ }]
59
+ }
60
+
61
+ expect(variables)
62
+ .toStrictEqual(expectation)
63
+ })
64
+
65
+ it('should create from array', async () => {
66
+ const id = generate()
67
+ const selector = generate()
68
+ const values = [uri(), uri()]
69
+ const annotation = { [selector]: values }
70
+ const request: Request = { group: generate(), selectors: [selector] }
71
+ const variables = createVariables(id, annotation, [request])
72
+
73
+ const expectation: Variables = {
74
+ [request.group]: [{
75
+ name: `TOA_${id.toUpperCase()}_${selector.toUpperCase()}`,
76
+ value: values.join(' ')
77
+ }]
78
+ }
79
+
80
+ expect(variables)
81
+ .toStrictEqual(expectation)
82
+ })
83
+
84
+ it('should create from default array', async () => {
85
+ const id = generate()
86
+ const selector = generate()
87
+ const values = [uri(), uri()]
88
+ const annotation = values
89
+ const request: Request = { group: generate(), selectors: [selector] }
90
+ const variables = createVariables(id, annotation, [request])
91
+
92
+ const expectation: Variables = {
93
+ [request.group]: [{
94
+ name: `TOA_${id.toUpperCase()}_${selector.toUpperCase()}`,
95
+ value: values.join(' ')
96
+ }]
97
+ }
98
+
99
+ expect(variables)
100
+ .toStrictEqual(expectation)
101
+ })
102
+
103
+ it('should throw if selector cannot be resolved', async () => {
104
+ const id = generate()
105
+ const selector = generate()
106
+ const annotation = {}
107
+ const request: Request = { group: generate(), selectors: [selector] }
108
+
109
+ expect(() => createVariables(id, annotation, [request]))
110
+ .toThrow('cannot be resolved.')
111
+ })
112
+
113
+ it('should create credential secrets for annotation keys', async () => {
114
+ const id = generate()
115
+
116
+ const group = generate()
117
+ const namespace = generate()
118
+ const name1 = 'one'
119
+ const name2 = 'two'
120
+ const name3 = 'three'
121
+ const selector1 = `${namespace}.${name1}`
122
+ const selector2 = `${namespace}.${name2}`
123
+ const selector3 = `${generate()}.${name3}`
124
+
125
+ const annotation = {
126
+ '.': uri('amqp:'),
127
+ [namespace]: uri('amqp:'),
128
+ [selector1]: uri('amqp:')
129
+ }
130
+
131
+ const request: Request = { group, selectors: [selector1, selector2, selector3] }
132
+ const variables = createVariables(id, annotation, [request])
133
+
134
+ const secrets1 = createExpectedSecrets(id, selector1, selector1)
135
+ const secrets2 = createExpectedSecrets(id, namespace, selector2)
136
+ const secrets3 = createExpectedSecrets(id, '.', selector3)
137
+
138
+ const expectation: Variables = {
139
+ [group]: expect.arrayContaining([
140
+ ...secrets1,
141
+ ...secrets2,
142
+ ...secrets3
143
+ ])
144
+ }
145
+
146
+ expect(variables).toStrictEqual(expectation)
147
+ })
148
+
149
+ it.each([
150
+ 'http:', 'redis:'
151
+ ])('should not create credetial secrets for %s', async (protocol) => {
152
+ const id = generate()
153
+ const selector = generate()
154
+ const annotation = { [selector]: uri(protocol) }
155
+ const request: Request = { group: generate(), selectors: [selector] }
156
+ const variables = createVariables(id, annotation, [request])
157
+
158
+ for (const variable of variables[request.group])
159
+ expect(variable.secret).toBeUndefined()
160
+ })
161
+
162
+ function uri (protocol = 'http:'): string {
163
+ return protocol + '//host-' + generate()
164
+ }
165
+
166
+ function createExpectedSecrets (id: string, key: string, selector: string): Variable[] {
167
+ key = key === '.' ? '.default' : '-' + key.replaceAll('.', '-')
168
+ selector = selector.replaceAll('.', '_').toUpperCase()
169
+
170
+ return [
171
+ {
172
+ name: `TOA_${id.toUpperCase()}_${selector.toUpperCase()}_USERNAME`,
173
+ secret: {
174
+ name: `toa-${id}${key}`,
175
+ key: 'username'
176
+ }
177
+ },
178
+ {
179
+ name: `TOA_${id.toUpperCase()}_${selector.toUpperCase()}_PASSWORD`,
180
+ secret: {
181
+ name: `toa-${id}${key}`,
182
+ key: 'password'
183
+ }
184
+ }
185
+ ]
186
+ }
@@ -0,0 +1,12 @@
1
+ import { Deployment, type Request } from './Deployment'
2
+ import { normalize } from './annotation'
3
+ import type { Variables } from '@toa.io/operations'
4
+ import type { Declaration } from './annotation'
5
+
6
+ export function createVariables
7
+ (id: string, declaration: Declaration, requests: Request[]): Variables {
8
+ const annotation = normalize(declaration)
9
+ const deployment = new Deployment(id, annotation)
10
+
11
+ return deployment.export(requests)
12
+ }