@toa.io/extensions.origins 0.7.2-dev.0 → 0.8.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 +9 -8
- package/readme.md +113 -0
- package/source/.deployment/index.js +5 -0
- package/source/.deployment/uris.js +35 -0
- package/source/.test/constants.js +3 -0
- package/source/.test/deployment.fixtures.js +20 -0
- package/source/.test/factory.fixtures.js +13 -0
- package/source/constants.js +3 -0
- package/source/deployment.js +28 -0
- package/source/deployment.test.js +158 -0
- package/source/env.js +50 -0
- package/source/factory.js +46 -0
- package/source/factory.test.js +140 -0
- package/{src → source}/index.js +2 -0
- package/source/manifest.js +25 -0
- package/source/manifest.test.js +57 -0
- package/source/protocols/amqp/.test/aspect.fixtures.js +15 -0
- package/source/protocols/amqp/.test/mock.comq.js +13 -0
- package/source/protocols/amqp/aspect.js +77 -0
- package/source/protocols/amqp/aspect.test.js +119 -0
- package/source/protocols/amqp/deployment.js +63 -0
- package/source/protocols/amqp/id.js +3 -0
- package/source/protocols/amqp/index.js +11 -0
- package/source/protocols/amqp/protocols.js +3 -0
- package/source/protocols/http/.aspect/permissions.js +65 -0
- package/{test → source/protocols/http/.test}/aspect.fixtures.js +6 -6
- package/source/protocols/http/aspect.js +129 -0
- package/source/protocols/http/aspect.test.js +239 -0
- package/source/protocols/http/id.js +3 -0
- package/source/protocols/http/index.js +9 -0
- package/source/protocols/http/protocols.js +3 -0
- package/source/protocols/index.js +6 -0
- package/source/schemas/annotations.cos.yaml +1 -0
- package/source/schemas/index.js +8 -0
- package/source/schemas/manifest.cos.yaml +1 -0
- package/types/amqp.d.ts +9 -0
- package/types/deployment.d.ts +7 -0
- package/types/http.d.ts +28 -0
- package/src/.manifest/index.js +0 -7
- package/src/.manifest/normalize.js +0 -17
- package/src/.manifest/schema.yaml +0 -13
- package/src/.manifest/validate.js +0 -13
- package/src/aspect.js +0 -95
- package/src/factory.js +0 -14
- package/src/manifest.js +0 -13
- package/test/aspect.test.js +0 -144
- package/test/factory.fixtures.js +0 -5
- package/test/factory.test.js +0 -22
- package/test/manifest.fixtures.js +0 -11
- package/test/manifest.test.js +0 -58
- package/types/aspect.ts +0 -19
- package/types/declaration.d.ts +0 -11
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.origins",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Toa
|
|
3
|
+
"version": "0.8.0-dev.1",
|
|
4
|
+
"description": "Toa Origins",
|
|
5
5
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
7
|
-
"main": "
|
|
7
|
+
"main": "source/index.js",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/toa-io/toa.git"
|
|
@@ -19,14 +19,15 @@
|
|
|
19
19
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@toa.io/core": "0.
|
|
23
|
-
"@toa.io/generic": "0.
|
|
24
|
-
"@toa.io/
|
|
25
|
-
"@toa.io/yaml": "0.7.2-dev.
|
|
22
|
+
"@toa.io/core": "0.8.0-dev.1",
|
|
23
|
+
"@toa.io/generic": "0.8.0-dev.1",
|
|
24
|
+
"@toa.io/schemas": "0.8.0-dev.1",
|
|
25
|
+
"@toa.io/yaml": "0.7.2-dev.2",
|
|
26
|
+
"comq": "0.6.0",
|
|
26
27
|
"node-fetch": "2.6.7"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"@types/node-fetch": "2.6.2"
|
|
30
31
|
},
|
|
31
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "9bb2f372c556440ee3e2a42e10eb415220ba1cb0"
|
|
32
33
|
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Toa Origins
|
|
2
|
+
|
|
3
|
+
`origins` extension enables external communications over supported protocols (HTTP and AMQP).
|
|
4
|
+
|
|
5
|
+
## TL;DR
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
# manifest.toa.yaml
|
|
9
|
+
name: dummy
|
|
10
|
+
namespace: dummies
|
|
11
|
+
|
|
12
|
+
origins:
|
|
13
|
+
docs: http://www.domain.com/docs/
|
|
14
|
+
amazon: amqps://amqp.amazon.com
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
// Node.js bridge
|
|
19
|
+
async function transition (input, object, context) {
|
|
20
|
+
// direct Aspect invocation
|
|
21
|
+
await context.aspects.http('docs', './example', { method: 'GET' })
|
|
22
|
+
|
|
23
|
+
// shortcuts
|
|
24
|
+
await context.http.docs.example.get() // GET http://www.domain.com/docs/example
|
|
25
|
+
await context.amqp.amazon.emit('something_happened', { really: true })
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
# context.toa.yaml
|
|
31
|
+
origins:
|
|
32
|
+
dummies.dummy:
|
|
33
|
+
amazon: amqps://amqp.azure.com
|
|
34
|
+
amazon@staging: amqp://amqp.stage
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Manifest
|
|
38
|
+
|
|
39
|
+
`origins` manifest is an object conforming declaring origin names as keys an origin URLs as values.
|
|
40
|
+
|
|
41
|
+
## HTTP Aspect
|
|
42
|
+
|
|
43
|
+
Uses [node-fetch](https://github.com/node-fetch/node-fetch) and returns its result.
|
|
44
|
+
|
|
45
|
+
Aspect invocation function
|
|
46
|
+
signature: `async (origin: string, rel: string, reuest: fetch.Request): fetch.Response`
|
|
47
|
+
|
|
48
|
+
- `origin`: name of the origin in the manifest
|
|
49
|
+
- `rel`: relative reference to a resource
|
|
50
|
+
- `request`: `Request` form `node-fetch`
|
|
51
|
+
|
|
52
|
+
### Absolute URLs
|
|
53
|
+
|
|
54
|
+
Requests to arbitrary URLs can be implemented with overloaded direct Aspect invocation.
|
|
55
|
+
|
|
56
|
+
`async (url: string, request: fetch.Request): fetch.Response`
|
|
57
|
+
|
|
58
|
+
By default, requests to arbitrary URLs are not allowed and must be explicitly permitted by setting
|
|
59
|
+
permissions in the Origins Annotation.
|
|
60
|
+
|
|
61
|
+
The Rules object is stored in the `.http` property of the corresponding component. Each key in the
|
|
62
|
+
Rules object is a regular expression that URLs will be tested against, and each value is a
|
|
63
|
+
permission — either `true` to allow the URL or `false` to deny it. In cases where a URL matches
|
|
64
|
+
multiple rules, denial takes priority.
|
|
65
|
+
|
|
66
|
+
> The `null` key is a special case that represents "any URL".
|
|
67
|
+
|
|
68
|
+
#### Example
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
# context.toa.yaml
|
|
72
|
+
origins:
|
|
73
|
+
dummies.dummy:
|
|
74
|
+
.http:
|
|
75
|
+
/^https?:\/\/api.domain.com/: true
|
|
76
|
+
/^http:\/\/sandbox.domain.com/@staging: true # staging environment
|
|
77
|
+
/.*hackers.*/: false # deny rule
|
|
78
|
+
~: true # allow any URL
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
// Node.js bridge
|
|
83
|
+
async function transition (input, object, context) {
|
|
84
|
+
await context.aspects.http('https://api.domain.com/example', { method: 'POST' })
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### `null` origins
|
|
89
|
+
|
|
90
|
+
To enable the extension for a component that uses arbitrary URLs without any specific origins to
|
|
91
|
+
declare, the Origins manifest should be set to `null`.
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
# manifest.toa.yaml
|
|
95
|
+
origins: ~
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## AMQP Aspect
|
|
99
|
+
|
|
100
|
+
Uses [ComQ](https://github.com/toa-io/comq), thus, provides interface of `comq.IO` restricted
|
|
101
|
+
to `emit` and `request` methods.
|
|
102
|
+
|
|
103
|
+
AMQP origins can have credential secrets deployed. Secret's name must
|
|
104
|
+
follow `toa-origins-{namespace}-{component}-{origin}` and it must have keys `username`
|
|
105
|
+
and `password`.
|
|
106
|
+
|
|
107
|
+
### Example
|
|
108
|
+
|
|
109
|
+
```shell
|
|
110
|
+
# deploy credentials to the current kubectl context
|
|
111
|
+
$ toa conceal toa-origins-dummies-dummiy-messages username developer
|
|
112
|
+
$ toa conceal toa-origins-dummies-dummiy-messages password secret
|
|
113
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { PREFIX } = require('../constants')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {toa.norm.context.dependencies.Instance[]} instances
|
|
7
|
+
* @param {toa.origins.Annotations} annotations
|
|
8
|
+
* @returns {toa.deployment.dependency.Variables}
|
|
9
|
+
*/
|
|
10
|
+
function uris (instances, annotations) {
|
|
11
|
+
const variables = {}
|
|
12
|
+
|
|
13
|
+
for (const [id, annotation] of Object.entries(annotations)) {
|
|
14
|
+
const component = instances.find((instance) => instance.locator.id === id)
|
|
15
|
+
|
|
16
|
+
if (component === undefined) throw new Error(`Origins annotations error: component '${id}' is not found`)
|
|
17
|
+
|
|
18
|
+
for (const origin of Object.keys(annotation)) {
|
|
19
|
+
if (!(origin in component.manifest)) {
|
|
20
|
+
throw new Error(`Origins annotations error: component '${id}' doesn't have '${origin}' origin`)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const name = PREFIX + component.locator.uppercase
|
|
25
|
+
const json = JSON.stringify(annotation)
|
|
26
|
+
const value = btoa(json)
|
|
27
|
+
const variable = { name, value }
|
|
28
|
+
|
|
29
|
+
variables[component.locator.label] = [variable]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return variables
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
exports.uris = uris
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
const { Locator } = require('@toa.io/core')
|
|
5
|
+
const { random, sample } = require('@toa.io/generic')
|
|
6
|
+
|
|
7
|
+
const { PROTOCOLS } = require('./constants')
|
|
8
|
+
|
|
9
|
+
const component = () => ({
|
|
10
|
+
locator: new Locator(generate(), generate()),
|
|
11
|
+
manifest: { [generate()]: sample(PROTOCOLS) + '//' + generate() }
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const components = () => {
|
|
15
|
+
const length = random(20) + 10
|
|
16
|
+
|
|
17
|
+
return Array.from({ length }, component)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
exports.components = components
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
|
|
5
|
+
/** @type {Record<string, string>} */
|
|
6
|
+
const manifest = {
|
|
7
|
+
[generate()]: 'https://toa.io',
|
|
8
|
+
[generate()]: 'https://api.domain.com',
|
|
9
|
+
[generate()]: 'amqp://localhost',
|
|
10
|
+
[generate()]: 'amqps://localhost'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
exports.manifest = manifest
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { merge } = require('@toa.io/generic')
|
|
4
|
+
const schemas = require('./schemas')
|
|
5
|
+
const protocols = require('./protocols')
|
|
6
|
+
const create = require('./.deployment')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {toa.norm.context.dependencies.Instance[]} instances
|
|
10
|
+
* @param {toa.origins.Annotations} annotations
|
|
11
|
+
* @returns {toa.deployment.dependency.Declaration}
|
|
12
|
+
*/
|
|
13
|
+
function deployment (instances, annotations = {}) {
|
|
14
|
+
schemas.annotations.validate(annotations)
|
|
15
|
+
|
|
16
|
+
const uris = create.uris(instances, annotations)
|
|
17
|
+
const variables = { ...uris }
|
|
18
|
+
|
|
19
|
+
protocols.reduce((variables, provider) => {
|
|
20
|
+
const specifics = provider.deployment?.(instances)
|
|
21
|
+
|
|
22
|
+
return merge(variables, specifics)
|
|
23
|
+
}, variables)
|
|
24
|
+
|
|
25
|
+
return { variables }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
exports.deployment = deployment
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
const { sample, letters: { up } } = require('@toa.io/generic')
|
|
5
|
+
|
|
6
|
+
const fixtures = require('./.test/deployment.fixtures')
|
|
7
|
+
const { deployment } = require('../')
|
|
8
|
+
|
|
9
|
+
it('should be', async () => {
|
|
10
|
+
expect(deployment).toBeInstanceOf(Function)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
/** @type {toa.norm.context.dependencies.Instance[]} */
|
|
14
|
+
let components
|
|
15
|
+
|
|
16
|
+
/** @type {string} */
|
|
17
|
+
let origin
|
|
18
|
+
|
|
19
|
+
/** @type {toa.norm.context.dependencies.Instance} */
|
|
20
|
+
let component
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
components = fixtures.components()
|
|
24
|
+
|
|
25
|
+
component = sample(components)
|
|
26
|
+
|
|
27
|
+
const origins = Object.keys(component.manifest)
|
|
28
|
+
|
|
29
|
+
origin = sample(origins)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('validation', () => {
|
|
33
|
+
it('should throw on annotation component mismatch', async () => {
|
|
34
|
+
const id = generate()
|
|
35
|
+
|
|
36
|
+
const annotations = {
|
|
37
|
+
[id]: {
|
|
38
|
+
[origin]: 'dev://' + generate()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
expect(() => deployment(components, annotations))
|
|
43
|
+
.toThrow(`Origins annotations error: component '${id}' is not found`)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should throw on annotation origin mismatch', async () => {
|
|
47
|
+
const id = component.locator.id
|
|
48
|
+
const origin = generate()
|
|
49
|
+
|
|
50
|
+
const annotations = {
|
|
51
|
+
[id]: {
|
|
52
|
+
[origin]: 'dev://' + generate()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
expect(() => deployment(components, annotations))
|
|
57
|
+
.toThrow(`Origins annotations error: component '${id}' doesn't have '${origin}' origin`)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should throw if annotation is not valid', async () => {
|
|
61
|
+
const annotations = /** @type {toa.origins.Annotations} */ { [component.locator.id]: generate() }
|
|
62
|
+
|
|
63
|
+
expect(() => deployment(components, annotations)).toThrow('must be object')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should throw if annotation is URI is not valid', async () => {
|
|
67
|
+
const annotations = {
|
|
68
|
+
[component.locator.id]: {
|
|
69
|
+
[origin]: 'hello!'
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
expect(() => deployment(components, annotations)).toThrow('must match format')
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should create variables', () => {
|
|
78
|
+
const value = 'dev://' + generate()
|
|
79
|
+
|
|
80
|
+
/** @type {toa.origins.Annotations} */
|
|
81
|
+
const annotations = {
|
|
82
|
+
[component.locator.id]: {
|
|
83
|
+
[origin]: value
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const output = deployment(components, annotations)
|
|
88
|
+
|
|
89
|
+
expect(output.variables).not.toBeUndefined()
|
|
90
|
+
|
|
91
|
+
const variables = output.variables[component.locator.label]
|
|
92
|
+
const varName = 'TOA_ORIGINS_' + component.locator.uppercase
|
|
93
|
+
const variable = findVariable(variables, varName)
|
|
94
|
+
|
|
95
|
+
expect(variable).toBeDefined()
|
|
96
|
+
|
|
97
|
+
const json = JSON.stringify(annotations[component.locator.id])
|
|
98
|
+
const base64 = btoa(json)
|
|
99
|
+
|
|
100
|
+
expect(variable.value).toStrictEqual(base64)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('amqp', () => {
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
const amqpComponents = components.filter(
|
|
106
|
+
(component) => {
|
|
107
|
+
const origin = Object.keys(component.manifest)
|
|
108
|
+
const url = new URL(component.manifest[origin])
|
|
109
|
+
|
|
110
|
+
return url.protocol === 'amqp:' || url.protocol === 'amqps:'
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
component = sample(amqpComponents)
|
|
115
|
+
|
|
116
|
+
const origins = Object.keys(component.manifest)
|
|
117
|
+
|
|
118
|
+
origin = sample(origins)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should create credential secrets', () => {
|
|
122
|
+
/** @type {toa.origins.Annotations} */
|
|
123
|
+
const annotations = {
|
|
124
|
+
[component.locator.id]: {
|
|
125
|
+
[origin]: 'amqps://whatever'
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const output = deployment(components, annotations)
|
|
130
|
+
const variables = output.variables[component.locator.label]
|
|
131
|
+
|
|
132
|
+
expect(variables).toBeDefined()
|
|
133
|
+
|
|
134
|
+
const envPrefix = `TOA_ORIGINS_${component.locator.uppercase}_${up(origin)}_`
|
|
135
|
+
const secretName = `toa-origins-${component.locator.label}-${origin}`
|
|
136
|
+
|
|
137
|
+
for (const property of ['username', 'password']) {
|
|
138
|
+
const variableName = envPrefix + up(property)
|
|
139
|
+
const variable = findVariable(variables, variableName)
|
|
140
|
+
|
|
141
|
+
expect(variable).toBeDefined()
|
|
142
|
+
|
|
143
|
+
expect(variable.secret).toStrictEqual({
|
|
144
|
+
name: secretName,
|
|
145
|
+
key: property
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {toa.deployment.dependency.Variable[]} variables
|
|
153
|
+
* @param {string} name
|
|
154
|
+
* @returns {toa.deployment.dependency.Variable}
|
|
155
|
+
*/
|
|
156
|
+
function findVariable (variables, name) {
|
|
157
|
+
return variables.find((variable) => variable.name === name)
|
|
158
|
+
}
|
package/source/env.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { PREFIX } = require('./constants')
|
|
4
|
+
const { overwrite, remap, letters: { up } } = require('@toa.io/generic')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {toa.core.Locator} locator
|
|
8
|
+
* @param {toa.origins.Manifest} manifest
|
|
9
|
+
*/
|
|
10
|
+
function apply (locator, manifest) {
|
|
11
|
+
const variable = PREFIX + locator.uppercase
|
|
12
|
+
const envValue = readEnv(variable)
|
|
13
|
+
|
|
14
|
+
overwrite(manifest, envValue)
|
|
15
|
+
addCredentials(manifest, variable)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readEnv (variable) {
|
|
19
|
+
if (!(variable in process.env)) return
|
|
20
|
+
|
|
21
|
+
const base64 = process.env[variable]
|
|
22
|
+
const json = atob(base64)
|
|
23
|
+
|
|
24
|
+
return JSON.parse(json)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {toa.origins.Manifest} manifest
|
|
29
|
+
* @param {string} variable
|
|
30
|
+
*/
|
|
31
|
+
function addCredentials (manifest, variable) {
|
|
32
|
+
const prefix = variable + '_'
|
|
33
|
+
|
|
34
|
+
remap(manifest, (reference, origin) => {
|
|
35
|
+
const originPrefix = prefix + up(origin)
|
|
36
|
+
const username = process.env[originPrefix + '_USERNAME']
|
|
37
|
+
const password = process.env[originPrefix + '_PASSWORD']
|
|
38
|
+
|
|
39
|
+
if (username === undefined && password === undefined) return
|
|
40
|
+
|
|
41
|
+
const url = new URL(reference)
|
|
42
|
+
|
|
43
|
+
url.username = username
|
|
44
|
+
url.password = password
|
|
45
|
+
|
|
46
|
+
manifest[origin] = url.href
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
exports.apply = apply
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const protocols = require('./protocols')
|
|
4
|
+
const env = require('./env')
|
|
5
|
+
|
|
6
|
+
class Factory {
|
|
7
|
+
/**
|
|
8
|
+
* @param {toa.core.Locator} locator
|
|
9
|
+
* @param {toa.origins.Manifest} manifest
|
|
10
|
+
* @return {toa.core.extensions.Aspect[]}
|
|
11
|
+
*/
|
|
12
|
+
aspect (locator, manifest) {
|
|
13
|
+
env.apply(locator, manifest)
|
|
14
|
+
|
|
15
|
+
return protocols.map((protocol) => this.#createAspect(protocol, manifest))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} protocol
|
|
20
|
+
* @param {toa.origins.Manifest} manifest
|
|
21
|
+
* @return {toa.core.extensions.Aspect}
|
|
22
|
+
*/
|
|
23
|
+
#createAspect (protocol, manifest) {
|
|
24
|
+
const protocolManifest = {}
|
|
25
|
+
|
|
26
|
+
let properties
|
|
27
|
+
|
|
28
|
+
// let properties
|
|
29
|
+
|
|
30
|
+
for (const [origin, reference] of Object.entries(manifest)) {
|
|
31
|
+
if (origin[0] === '.') {
|
|
32
|
+
if (origin.substring(1) === protocol.id) properties = reference
|
|
33
|
+
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const url = new URL(reference)
|
|
38
|
+
|
|
39
|
+
if (protocol.protocols.includes(url.protocol)) protocolManifest[origin] = reference
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return protocol.create(protocolManifest, properties)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
exports.Factory = Factory
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
const { Locator } = require('@toa.io/core')
|
|
5
|
+
const { sample, overwrite, letters: { up } } = require('@toa.io/generic')
|
|
6
|
+
|
|
7
|
+
jest.mock('./protocols/http/aspect')
|
|
8
|
+
jest.mock('./protocols/amqp/aspect')
|
|
9
|
+
|
|
10
|
+
const http = require('./protocols/http/aspect')
|
|
11
|
+
const amqp = require('./protocols/amqp/aspect')
|
|
12
|
+
|
|
13
|
+
const fixtures = require('./.test/factory.fixtures')
|
|
14
|
+
const { Factory } = require('../')
|
|
15
|
+
|
|
16
|
+
let factory
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
jest.clearAllMocks()
|
|
20
|
+
|
|
21
|
+
factory = new Factory()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should create aspects', () => {
|
|
25
|
+
factory.aspect(new Locator(generate(), generate()), fixtures.manifest)
|
|
26
|
+
|
|
27
|
+
const httpManifest = filterManifest(fixtures.manifest, 'http')
|
|
28
|
+
const amqpManifest = filterManifest(fixtures.manifest, 'amqp')
|
|
29
|
+
|
|
30
|
+
expect(http.create).toHaveBeenCalledWith(httpManifest, undefined)
|
|
31
|
+
expect(amqp.create).toHaveBeenCalledWith(amqpManifest, undefined)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('env', () => {
|
|
35
|
+
it('should overwrites URLs from environment', async () => {
|
|
36
|
+
const httpManifest = filterManifest(fixtures.manifest, 'http')
|
|
37
|
+
const key = sample(Object.keys(httpManifest))
|
|
38
|
+
const override = { [key]: 'http://' + generate() }
|
|
39
|
+
const json = JSON.stringify(override)
|
|
40
|
+
const base64 = btoa(json)
|
|
41
|
+
const locator = new Locator(generate(), generate())
|
|
42
|
+
|
|
43
|
+
process.env['TOA_ORIGINS_' + locator.uppercase] = base64
|
|
44
|
+
|
|
45
|
+
factory.aspect(locator, fixtures.manifest)
|
|
46
|
+
|
|
47
|
+
const expected = overwrite(httpManifest, override)
|
|
48
|
+
|
|
49
|
+
expect(http.create.mock.calls[0][0]).toStrictEqual(expected)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('amqp', () => {
|
|
53
|
+
/** @type {toa.origins.Manifest} */
|
|
54
|
+
let amqpManifest
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
amqpManifest = filterManifest(fixtures.manifest, 'amqp')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should add credentials from environment', async () => {
|
|
61
|
+
const key = sample(Object.keys(amqpManifest))
|
|
62
|
+
const locator = new Locator(generate(), generate())
|
|
63
|
+
const envPrefix = 'TOA_ORIGINS_' + locator.uppercase + '_' + up(key) + '_'
|
|
64
|
+
const username = generate()
|
|
65
|
+
const password = generate()
|
|
66
|
+
|
|
67
|
+
process.env[envPrefix + 'USERNAME'] = username
|
|
68
|
+
process.env[envPrefix + 'PASSWORD'] = password
|
|
69
|
+
|
|
70
|
+
factory.aspect(locator, amqpManifest)
|
|
71
|
+
|
|
72
|
+
const manifest = amqp.create.mock.calls[0][0]
|
|
73
|
+
const origin = manifest[key]
|
|
74
|
+
const url = new URL(origin)
|
|
75
|
+
|
|
76
|
+
expect(url.username).toStrictEqual(username)
|
|
77
|
+
expect(url.password).toStrictEqual(password)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should add credentials to URLs from environment', async () => {
|
|
81
|
+
const key = sample(Object.keys(amqpManifest))
|
|
82
|
+
const hostname = generate().toLowerCase()
|
|
83
|
+
const override = { [key]: 'amqp://' + hostname }
|
|
84
|
+
const json = JSON.stringify(override)
|
|
85
|
+
const base64 = btoa(json)
|
|
86
|
+
const locator = new Locator(generate(), generate())
|
|
87
|
+
const envPrefix = 'TOA_ORIGINS_' + locator.uppercase + '_' + up(key) + '_'
|
|
88
|
+
const username = generate()
|
|
89
|
+
const password = generate()
|
|
90
|
+
|
|
91
|
+
process.env['TOA_ORIGINS_' + locator.uppercase] = base64
|
|
92
|
+
process.env[envPrefix + 'USERNAME'] = username
|
|
93
|
+
process.env[envPrefix + 'PASSWORD'] = password
|
|
94
|
+
|
|
95
|
+
factory.aspect(locator, fixtures.manifest)
|
|
96
|
+
|
|
97
|
+
const manifest = amqp.create.mock.calls[0][0]
|
|
98
|
+
const origin = manifest[key]
|
|
99
|
+
const url = new URL(origin)
|
|
100
|
+
|
|
101
|
+
expect(url.hostname).toStrictEqual(hostname)
|
|
102
|
+
expect(url.username).toStrictEqual(username)
|
|
103
|
+
expect(url.password).toStrictEqual(password)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('http', () => {
|
|
108
|
+
it('should read properties', async () => {
|
|
109
|
+
const properties = {
|
|
110
|
+
'.http': {
|
|
111
|
+
[generate()]: generate()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const locator = new Locator(generate(), generate())
|
|
116
|
+
const json = JSON.stringify(properties)
|
|
117
|
+
|
|
118
|
+
process.env['TOA_ORIGINS_' + locator.uppercase] = btoa(json)
|
|
119
|
+
|
|
120
|
+
factory.aspect(locator, fixtures.manifest)
|
|
121
|
+
|
|
122
|
+
expect(http.create).toHaveBeenCalledWith(expect.anything(), properties['.http'])
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {toa.origins.Manifest} manifest
|
|
129
|
+
* @param {string} protocol
|
|
130
|
+
* @return {toa.origins.Manifest}
|
|
131
|
+
*/
|
|
132
|
+
function filterManifest (manifest, protocol) {
|
|
133
|
+
const result = {}
|
|
134
|
+
|
|
135
|
+
for (const [origin, reference] of Object.entries(manifest)) {
|
|
136
|
+
if (reference.slice(0, protocol.length) === protocol) result[origin] = reference
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result
|
|
140
|
+
}
|
package/{src → source}/index.js
RENAMED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const schemas = require('./schemas')
|
|
4
|
+
const protocols = require('./protocols')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {toa.origins.Manifest} manifest
|
|
8
|
+
* @returns {toa.origins.Manifest}
|
|
9
|
+
*/
|
|
10
|
+
function manifest (manifest) {
|
|
11
|
+
if (manifest === null) return {}
|
|
12
|
+
|
|
13
|
+
schemas.manifest.validate(manifest)
|
|
14
|
+
|
|
15
|
+
for (const uri of Object.values(manifest)) {
|
|
16
|
+
const protocol = new URL(uri).protocol
|
|
17
|
+
const supported = protocols.find((provider) => provider.protocols.includes(protocol))
|
|
18
|
+
|
|
19
|
+
if (supported === undefined) throw new Error(`'${protocol}' protocol is not supported`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return manifest
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
exports.manifest = manifest
|