@toa.io/pointer 0.20.0-dev.9 → 0.20.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 +14 -7
- package/readme.md +113 -78
- package/schemas/urimap.cos.yaml +2 -0
- package/source/Deployment.ts +90 -0
- package/source/annotation.test.ts +66 -0
- package/source/annotation.ts +46 -0
- package/source/createVariables.test.ts +186 -0
- package/source/createVariables.ts +12 -0
- package/source/index.ts +7 -0
- package/source/naming.ts +11 -0
- package/source/resolve.ts +63 -0
- package/tsconfig.json +12 -0
- package/source/.deployment/.variables/credentials.js +0 -35
- package/source/.deployment/.variables/index.js +0 -7
- package/source/.deployment/.variables/pointer.js +0 -18
- package/source/.deployment/index.js +0 -8
- package/source/.deployment/validate.js +0 -19
- package/source/.deployment/variables.js +0 -22
- package/source/.pointer/env.js +0 -44
- package/source/.pointer/index.js +0 -9
- package/source/.pointer/label.js +0 -15
- package/source/.pointer/local.js +0 -17
- package/source/deployment.js +0 -17
- package/source/index.js +0 -11
- package/source/pointer.js +0 -34
- package/source/uris/construct.js +0 -18
- package/source/uris/index.js +0 -9
- package/source/uris/normalize.js +0 -12
- package/source/uris/resolve.js +0 -18
- package/source/uris/schema.yaml +0 -17
- package/source/uris/validate.js +0 -17
- package/source/variable.js +0 -12
- package/test/deployment.test.js +0 -92
- package/test/pointer.test.js +0 -213
- package/test/uris.fixtures.js +0 -10
- package/test/uris.test.js +0 -146
- package/test/variable.test.js +0 -37
- package/types/deployment.d.ts +0 -21
- package/types/pointer.d.ts +0 -19
- package/types/uris.d.ts +0 -27
- 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.20.0
|
|
3
|
+
"version": "0.20.0",
|
|
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": "
|
|
24
|
+
"main": "transpiled/index.js",
|
|
25
|
+
"types": "transpiled/index.d.ts",
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@toa.io/
|
|
27
|
-
"@toa.io/generic": "0.20.0
|
|
28
|
-
"@toa.io/schemas": "0.20.0
|
|
29
|
-
"@toa.io/yaml": "0.20.0-dev.9"
|
|
27
|
+
"@toa.io/dns": "0.8.0",
|
|
28
|
+
"@toa.io/generic": "0.20.0",
|
|
29
|
+
"@toa.io/schemas": "0.20.0"
|
|
30
30
|
},
|
|
31
|
-
"
|
|
31
|
+
"scripts": {
|
|
32
|
+
"transpile": "tsc"
|
|
33
|
+
},
|
|
34
|
+
"jest": {
|
|
35
|
+
"preset": "ts-jest",
|
|
36
|
+
"testEnvironment": "node"
|
|
37
|
+
},
|
|
38
|
+
"gitHead": "28fc4b45c224c3683acaaf0e4abd1eb04e07b408"
|
|
32
39
|
}
|
package/readme.md
CHANGED
|
@@ -1,126 +1,161 @@
|
|
|
1
|
-
# Pointer
|
|
1
|
+
# Toa Pointer
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
6
|
+
## Annotation syntax
|
|
6
7
|
|
|
7
|
-
Pointer
|
|
8
|
-
|
|
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
|
-
|
|
11
|
+
The values of the annotation object must be a URL or a list of URLs without credentials.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
+
If the Pointer annotation is a `string`, then its value is considered as default (`.`).
|
|
32
22
|
|
|
33
|
-
|
|
23
|
+
```yaml
|
|
24
|
+
mongodb: mongo://default.db.example.com
|
|
25
|
+
```
|
|
34
26
|
|
|
35
|
-
|
|
27
|
+
### Shards
|
|
36
28
|
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
### Deduplication
|
|
49
37
|
|
|
50
|
-
|
|
38
|
+
Pointer values are [DNS-deduplicated](/libraries/dns/readme.md#deduplication) except `http:` schema.
|
|
51
39
|
|
|
52
|
-
|
|
40
|
+
### `default` namespace
|
|
53
41
|
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
60
|
-
default
|
|
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
|
-
|
|
54
|
+
## Resolution
|
|
64
55
|
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
65
|
+
### Example
|
|
73
66
|
|
|
74
|
-
|
|
67
|
+
```yaml
|
|
68
|
+
# context.toa.yaml
|
|
75
69
|
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
76
|
+
URL resolution for the example above is as follows:
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
84
|
+
## Credentials
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
For security reasons, URL credentials cannot be specified in the annotation;
|
|
87
|
+
they must be deployed secretly.
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
94
|
-
> See [Locator](#).
|
|
92
|
+
### Example
|
|
95
93
|
|
|
96
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
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
|
-
|
|
126
|
+
### TLS
|
|
127
|
+
|
|
128
|
+
Not implemented. [#367](https://github.com/toa-io/toa/issues/367)
|
|
113
129
|
|
|
114
|
-
|
|
130
|
+
## API
|
|
115
131
|
|
|
116
|
-
|
|
132
|
+
### Deployment
|
|
117
133
|
|
|
118
|
-
|
|
134
|
+
```typescript
|
|
135
|
+
import { createVariables } from '@toa.io/pointer'
|
|
119
136
|
|
|
120
|
-
|
|
121
|
-
and compositions require variables to establish connection through that proxy.
|
|
137
|
+
// ...
|
|
122
138
|
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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,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
|
+
}
|