@toa.io/operations 1.0.0-alpha.283 → 1.0.0-alpha.286
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 +4 -4
- package/readme.md +43 -0
- package/src/deployment/.deployment/.describe/compositions.js +7 -5
- package/src/deployment/.deployment/.describe/events.js +7 -7
- package/src/deployment/.deployment/.describe/fold.js +10 -3
- package/src/deployment/.deployment/.describe/fold.test.js +30 -14
- package/src/deployment/.deployment/.describe/mounts.js +4 -7
- package/src/deployment/.deployment/.describe/resources.js +7 -5
- package/src/deployment/.deployment/.describe/resources.test.js +13 -6
- package/src/deployment/.deployment/.describe/services.js +10 -9
- package/src/deployment/.deployment/.describe/services.test.js +22 -5
- package/src/deployment/.deployment/.describe/variables.js +2 -3
- package/src/deployment/.deployment/describe.js +11 -7
- package/src/deployment/.deployment/merge.js +11 -6
- package/src/deployment/.deployment/merge.test.js +41 -10
- package/src/deployment/composition.js +1 -1
- package/src/deployment/deployment.js +35 -16
- package/src/deployment/factory.js +39 -19
- package/src/deployment/images/composition.Dockerfile +1 -1
- package/src/deployment/images/composition.js +10 -9
- package/src/deployment/images/factory.js +17 -6
- package/src/deployment/images/format.js +9 -5
- package/src/deployment/images/image.fixtures.js +2 -2
- package/src/deployment/images/image.js +38 -21
- package/src/deployment/images/image.test.js +7 -2
- package/src/deployment/images/mono.Dockerfile +1 -1
- package/src/deployment/images/mono.js +10 -9
- package/src/deployment/images/publish.js +98 -0
- package/src/deployment/images/runtime-base.test.js +12 -6
- package/src/deployment/images/service.Dockerfile +1 -1
- package/src/deployment/images/service.js +4 -4
- package/src/deployment/operator.js +8 -8
- package/src/deployment/registry.js +89 -61
- package/src/deployment/registry.test.js +117 -43
- package/src/deployment/service.js +3 -1
- package/src/deployment/workspace.js +1 -1
- package/src/process.js +1 -1
- package/src/process.test.js +8 -4
- package/types/_deployment/composition.d.ts +0 -2
- package/types/_deployment/dependency.d.ts +10 -6
- package/types/_deployment/deployment.d.ts +0 -4
- package/types/_deployment/factory.d.ts +0 -2
- package/types/_deployment/images/factory.d.ts +4 -6
- package/types/_deployment/images/image.d.ts +2 -4
- package/types/_deployment/images/registry.d.ts +7 -10
- package/types/_deployment/operator.d.ts +5 -7
- package/types/_deployment/registry.d.ts +6 -8
- package/types/_deployment/service.d.ts +0 -2
- package/types/dependency.ts +3 -0
- package/CHANGELOG.md +0 -223
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/operations",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.286",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Toa Deployment",
|
|
6
6
|
"homepage": "https://toa.io",
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@toa.io/generic": "1.0.0-alpha.
|
|
32
|
-
"@toa.io/norm": "1.0.0-alpha.
|
|
31
|
+
"@toa.io/generic": "1.0.0-alpha.286",
|
|
32
|
+
"@toa.io/norm": "1.0.0-alpha.286",
|
|
33
33
|
"execa": "10.0.1",
|
|
34
34
|
"fs-extra": "11.4.0",
|
|
35
35
|
"js-yaml": "5.4.1"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "81f3d0688f17668b902be336d1749cc715edcdb6"
|
|
38
38
|
}
|
package/readme.md
CHANGED
|
@@ -38,6 +38,49 @@ registry:
|
|
|
38
38
|
run: npm i -g @toa.io/runtime --omit=dev
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
#### Extension Service Images
|
|
42
|
+
|
|
43
|
+
`registry.services` says where an extension service's image comes from.
|
|
44
|
+
|
|
45
|
+
`build`, the default, builds one per service into `registry.base`. Its tag carries the
|
|
46
|
+
runtime version, so every Toa release rebuilds them all.
|
|
47
|
+
|
|
48
|
+
`published` deploys the image the extension ships and builds nothing:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
# context.toa.yaml
|
|
52
|
+
|
|
53
|
+
registry:
|
|
54
|
+
services: published
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The images are tagged with the runtime version:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
ghcr.io/toa-io/extension-exposition-gateway:1.0.0-alpha.285
|
|
61
|
+
ghcr.io/toa-io/extension-realtime-streams:1.0.0-alpha.285
|
|
62
|
+
ghcr.io/toa-io/extension-introspection-explorer:1.0.0-alpha.285
|
|
63
|
+
ghcr.io/toa-io/extension-configuration-values:1.0.0-alpha.285
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The cluster pulls them from `ghcr.io` rather than from `registry.base`. They are public and
|
|
67
|
+
`registry.credentials` does not apply; a cluster that cannot reach `ghcr.io` needs `build`.
|
|
68
|
+
|
|
69
|
+
The field is environment-scoped like the rest:
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
# context.toa.yaml
|
|
73
|
+
|
|
74
|
+
registry:
|
|
75
|
+
base@local: localhost:5000
|
|
76
|
+
base@production: registry.digitalocean.com/acme
|
|
77
|
+
services: published
|
|
78
|
+
services@local: build
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
An extension that states no image is built whatever this says, as is a service a
|
|
82
|
+
composition runs in its own pod.
|
|
83
|
+
|
|
41
84
|
#### Build Options
|
|
42
85
|
|
|
43
86
|
```yaml
|
|
@@ -2,15 +2,17 @@ import { addVariables } from './variables.js'
|
|
|
2
2
|
import { addMounts } from './mounts.js'
|
|
3
3
|
import { fold } from './fold.js'
|
|
4
4
|
|
|
5
|
-
export function compositions
|
|
5
|
+
export function compositions(compositions, dependency) {
|
|
6
6
|
for (const composition of compositions) {
|
|
7
|
-
const claimed = (dependency.services ?? [])
|
|
8
|
-
|
|
7
|
+
const claimed = (dependency.services ?? []).filter(
|
|
8
|
+
(service) => service.workload?.includes(composition.name) === true
|
|
9
|
+
)
|
|
9
10
|
|
|
10
11
|
// a service brings components of its own — the identity components inside the gateway —
|
|
11
12
|
// and their variables are keyed by their own labels, not by the composition's
|
|
12
|
-
const keys = composition.components
|
|
13
|
-
|
|
13
|
+
const keys = composition.components.concat(
|
|
14
|
+
...claimed.map((service) => service.components ?? [])
|
|
15
|
+
)
|
|
14
16
|
|
|
15
17
|
addVariables(composition, dependency.variables, keys)
|
|
16
18
|
addMounts(composition, dependency.mounts, keys)
|
|
@@ -9,15 +9,16 @@
|
|
|
9
9
|
* @param {toa.norm.Context} context
|
|
10
10
|
* @param {toa.deployment.Dependency} dependency
|
|
11
11
|
*/
|
|
12
|
-
export function events
|
|
12
|
+
export function events(context, dependency) {
|
|
13
13
|
const components = deployed(context)
|
|
14
14
|
const consumed = collect(components, context.events, dependency.events)
|
|
15
15
|
|
|
16
16
|
for (const { locator, events } of components) {
|
|
17
17
|
if (events === undefined) continue
|
|
18
18
|
|
|
19
|
-
const labels = Object.keys(events)
|
|
20
|
-
|
|
19
|
+
const labels = Object.keys(events).filter((label) =>
|
|
20
|
+
consumed.has(locator.id + '.' + label)
|
|
21
|
+
)
|
|
21
22
|
|
|
22
23
|
dependency.variables[locator.label] ??= []
|
|
23
24
|
|
|
@@ -33,12 +34,11 @@ export function events (context, dependency) {
|
|
|
33
34
|
* holds only the former; the latter reach a deployment as instances of what they depend on,
|
|
34
35
|
* which is also how they are given the rest of their variables.
|
|
35
36
|
*/
|
|
36
|
-
function deployed
|
|
37
|
+
function deployed(context) {
|
|
37
38
|
const components = new Map()
|
|
38
39
|
|
|
39
40
|
for (const instances of Object.values(context.dependencies ?? {}))
|
|
40
|
-
for (const { component } of instances)
|
|
41
|
-
components.set(component.locator.id, component)
|
|
41
|
+
for (const { component } of instances) components.set(component.locator.id, component)
|
|
42
42
|
|
|
43
43
|
return [...components.values()]
|
|
44
44
|
}
|
|
@@ -47,7 +47,7 @@ function deployed (context) {
|
|
|
47
47
|
* Everything that consumes an event of this context: the receivers of its own components, what
|
|
48
48
|
* a dependency declares it consumes, and what the context says is consumed outside it.
|
|
49
49
|
*/
|
|
50
|
-
function collect
|
|
50
|
+
function collect(components, declared, contributed) {
|
|
51
51
|
const consumed = new Set(declared ?? [])
|
|
52
52
|
|
|
53
53
|
for (const label of contributed ?? []) consumed.add(label)
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @param {toa.deployment.dependency.Service[]} services
|
|
11
11
|
* @param {toa.deployment.Dependency} dependency
|
|
12
12
|
*/
|
|
13
|
-
export function fold
|
|
13
|
+
export function fold(workload, services, dependency) {
|
|
14
14
|
workload.variables ??= []
|
|
15
15
|
|
|
16
16
|
for (const service of services) {
|
|
@@ -21,13 +21,20 @@ export function fold (workload, services, dependency) {
|
|
|
21
21
|
|
|
22
22
|
// every declared port is bound by the single process, none is primary
|
|
23
23
|
if (service.port !== undefined)
|
|
24
|
-
(workload.backends ??= []).push({
|
|
24
|
+
(workload.backends ??= []).push({
|
|
25
|
+
port: service.port,
|
|
26
|
+
path: service.ingress?.path ?? '/'
|
|
27
|
+
})
|
|
25
28
|
|
|
26
29
|
if (service.probe !== undefined && service.probe !== false)
|
|
27
30
|
workload.probe = service.probe
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
if (
|
|
33
|
+
if (
|
|
34
|
+
workload.probe === undefined &&
|
|
35
|
+
dependency.probe !== undefined &&
|
|
36
|
+
dependency.probe !== false
|
|
37
|
+
)
|
|
31
38
|
workload.probe = dependency.probe
|
|
32
39
|
|
|
33
40
|
// the more specific prefix must come first, whatever the controller's tie-break
|
|
@@ -10,8 +10,10 @@ it('should collect the ports of every service', () => {
|
|
|
10
10
|
|
|
11
11
|
fold(workload, [service('one', { port: 8000 }), service('two', { port: 8002 })], {})
|
|
12
12
|
|
|
13
|
-
assert.deepStrictEqual(workload.backends,
|
|
14
|
-
|
|
13
|
+
assert.deepStrictEqual(workload.backends, [
|
|
14
|
+
{ port: 8000, path: '/' },
|
|
15
|
+
{ port: 8002, path: '/' }
|
|
16
|
+
])
|
|
15
17
|
})
|
|
16
18
|
|
|
17
19
|
it('should take the path a service declares', () => {
|
|
@@ -25,12 +27,19 @@ it('should take the path a service declares', () => {
|
|
|
25
27
|
it('should put the more specific prefix first', () => {
|
|
26
28
|
const workload = {}
|
|
27
29
|
|
|
28
|
-
fold(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
fold(
|
|
31
|
+
workload,
|
|
32
|
+
[
|
|
33
|
+
service('one', { port: 8000, ingress: { path: '/' } }),
|
|
34
|
+
service('two', { port: 8002, ingress: { path: '/explorer' } })
|
|
35
|
+
],
|
|
36
|
+
{}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
assert.deepStrictEqual(
|
|
40
|
+
workload.backends.map((backend) => backend.path),
|
|
41
|
+
['/explorer', '/']
|
|
42
|
+
)
|
|
34
43
|
})
|
|
35
44
|
|
|
36
45
|
it('should leave a service without a port out of the backends', () => {
|
|
@@ -45,12 +54,19 @@ describe('variables', () => {
|
|
|
45
54
|
it('should take those of every service', () => {
|
|
46
55
|
const workload = {}
|
|
47
56
|
|
|
48
|
-
fold(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
fold(
|
|
58
|
+
workload,
|
|
59
|
+
[
|
|
60
|
+
service('one', { variables: [{ name: 'A', value: '1' }] }),
|
|
61
|
+
service('two', { variables: [{ name: 'B', value: '2' }] })
|
|
62
|
+
],
|
|
63
|
+
{}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
assert.deepStrictEqual(workload.variables, [
|
|
67
|
+
{ name: 'A', value: '1' },
|
|
68
|
+
{ name: 'B', value: '2' }
|
|
69
|
+
])
|
|
54
70
|
})
|
|
55
71
|
|
|
56
72
|
it('should keep the first of a name', () => {
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
export function addMounts
|
|
2
|
-
if (mounts === undefined)
|
|
3
|
-
return
|
|
1
|
+
export function addMounts(composition, mounts, keys = composition.components) {
|
|
2
|
+
if (mounts === undefined) return
|
|
4
3
|
|
|
5
4
|
const used = new Set()
|
|
6
5
|
|
|
7
6
|
for (const [key, mount] of Object.entries(mounts)) {
|
|
8
|
-
if (key !== 'global' && !keys?.includes(key))
|
|
9
|
-
continue
|
|
7
|
+
if (key !== 'global' && !keys?.includes(key)) continue
|
|
10
8
|
|
|
11
9
|
for (const { name, path, claim } of mount) {
|
|
12
|
-
if (used.has(name))
|
|
13
|
-
continue
|
|
10
|
+
if (used.has(name)) continue
|
|
14
11
|
|
|
15
12
|
composition.mounts ??= []
|
|
16
13
|
composition.mounts.push({ name, path, claim })
|
|
@@ -8,20 +8,22 @@
|
|
|
8
8
|
* Deploying without any is a decision rather than an omission, and it is spelled
|
|
9
9
|
* `resources: null` — at either place.
|
|
10
10
|
*/
|
|
11
|
-
export function resources
|
|
11
|
+
export function resources(context, values) {
|
|
12
12
|
for (const unit of units(values)) {
|
|
13
13
|
// `null` is an answer and `undefined` is not one, so the fallback reads only the latter
|
|
14
14
|
if (unit.deployment.resources === undefined)
|
|
15
15
|
unit.deployment.resources = context.resources
|
|
16
16
|
|
|
17
17
|
if (unit.deployment.resources === undefined)
|
|
18
|
-
throw new Error(
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
throw new Error(
|
|
19
|
+
`${unit.subject} declares no resources. ` +
|
|
20
|
+
"Declare them on it or as the context's 'resources', " +
|
|
21
|
+
"or 'resources: null' to deploy it without any."
|
|
22
|
+
)
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
function
|
|
26
|
+
function* units(values) {
|
|
25
27
|
if (values.mono !== undefined)
|
|
26
28
|
yield { deployment: values.mono, subject: 'The mono deployment' }
|
|
27
29
|
|
|
@@ -32,22 +32,29 @@ describe('refusal', () => {
|
|
|
32
32
|
it('should refuse a composition that declares none', () => {
|
|
33
33
|
const values = { compositions: [{ name: 'edge' }] }
|
|
34
34
|
|
|
35
|
-
assert.throws(
|
|
36
|
-
(
|
|
35
|
+
assert.throws(
|
|
36
|
+
() => resources({}, values),
|
|
37
|
+
(error) => /^Composition 'edge' declares no resources\./.test(error.message)
|
|
38
|
+
)
|
|
37
39
|
})
|
|
38
40
|
|
|
39
41
|
it('should refuse a service that declares none', () => {
|
|
40
42
|
const values = { services: [{ name: 'exposition-gateway' }] }
|
|
41
43
|
|
|
42
|
-
assert.throws(
|
|
43
|
-
(
|
|
44
|
+
assert.throws(
|
|
45
|
+
() => resources({}, values),
|
|
46
|
+
(error) =>
|
|
47
|
+
/^Service 'exposition-gateway' declares no resources\./.test(error.message)
|
|
48
|
+
)
|
|
44
49
|
})
|
|
45
50
|
|
|
46
51
|
it('should refuse the mono deployment when it declares none', () => {
|
|
47
52
|
const values = { mono: {} }
|
|
48
53
|
|
|
49
|
-
assert.throws(
|
|
50
|
-
(
|
|
54
|
+
assert.throws(
|
|
55
|
+
() => resources({}, values),
|
|
56
|
+
(error) => /^The mono deployment declares no resources\./.test(error.message)
|
|
57
|
+
)
|
|
51
58
|
})
|
|
52
59
|
|
|
53
60
|
it('should not ask a service a composition runs', () => {
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import { addVariables } from './variables.js'
|
|
2
2
|
|
|
3
|
-
export function services
|
|
3
|
+
export function services(services, variables, probe, ingress) {
|
|
4
4
|
for (const service of services) {
|
|
5
5
|
// one a composition runs has no deployment of its own to carry them; the composition
|
|
6
6
|
// it runs in states them, and repeating them here is dead weight in the values
|
|
7
7
|
if (service.workload === undefined) addVariables(service, variables)
|
|
8
8
|
|
|
9
|
-
if (service.probe === false)
|
|
10
|
-
delete service.probe
|
|
9
|
+
if (service.probe === false) delete service.probe
|
|
11
10
|
else if (service.probe === undefined && probe !== undefined && probe !== false)
|
|
12
11
|
service.probe = probe
|
|
13
12
|
|
|
14
|
-
if (service.ingress !== undefined)
|
|
15
|
-
expose(service, ingress)
|
|
13
|
+
if (service.ingress !== undefined) expose(service, ingress)
|
|
16
14
|
}
|
|
17
15
|
}
|
|
18
16
|
|
|
@@ -21,15 +19,18 @@ export function services (services, variables, probe, ingress) {
|
|
|
21
19
|
* the context's business, so everything else comes from its `ingress` section.
|
|
22
20
|
* What the service did declare wins.
|
|
23
21
|
*/
|
|
24
|
-
function expose
|
|
22
|
+
function expose(service, ingress = {}) {
|
|
25
23
|
const declared = Object.fromEntries(
|
|
26
|
-
Object.entries(service.ingress).filter(([, value]) => value !== undefined)
|
|
24
|
+
Object.entries(service.ingress).filter(([, value]) => value !== undefined)
|
|
25
|
+
)
|
|
27
26
|
|
|
28
27
|
service.ingress = Object.assign({}, ingress, declared)
|
|
29
28
|
|
|
30
29
|
if (service.ingress.hosts === undefined)
|
|
31
|
-
throw new Error(
|
|
32
|
-
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Service '${service.name}' declares an ingress, but no hosts are defined. ` +
|
|
32
|
+
"Declare them in the context's 'ingress' section."
|
|
33
|
+
)
|
|
33
34
|
|
|
34
35
|
if (service.port === undefined)
|
|
35
36
|
throw new Error(`Service '${service.name}' declares an ingress, but no port.`)
|
|
@@ -3,7 +3,12 @@ import assert from 'node:assert/strict'
|
|
|
3
3
|
|
|
4
4
|
import { services } from './services.js'
|
|
5
5
|
|
|
6
|
-
const service = (extra = {}) => ({
|
|
6
|
+
const service = (extra = {}) => ({
|
|
7
|
+
group: 'group',
|
|
8
|
+
name: 'group-one',
|
|
9
|
+
version: '0',
|
|
10
|
+
...extra
|
|
11
|
+
})
|
|
7
12
|
|
|
8
13
|
it('should leave a service without an ingress alone', () => {
|
|
9
14
|
const list = [service()]
|
|
@@ -20,7 +25,11 @@ it('should leave a service without an ingress alone', () => {
|
|
|
20
25
|
it('should supply hosts, class and annotations from the context', () => {
|
|
21
26
|
const list = [service({ port: 8002, ingress: { path: '/.introspection' } })]
|
|
22
27
|
|
|
23
|
-
services(list, {}, undefined, {
|
|
28
|
+
services(list, {}, undefined, {
|
|
29
|
+
hosts: ['api.dev'],
|
|
30
|
+
class: 'alb',
|
|
31
|
+
annotations: { a: 'b' }
|
|
32
|
+
})
|
|
24
33
|
|
|
25
34
|
assert.deepStrictEqual(list[0].ingress, {
|
|
26
35
|
path: '/.introspection',
|
|
@@ -50,12 +59,20 @@ it('should ignore properties the service left undefined', () => {
|
|
|
50
59
|
it('should reject an ingress with nowhere to land', () => {
|
|
51
60
|
const list = [service({ port: 8002, ingress: { path: '/.introspection' } })]
|
|
52
61
|
|
|
53
|
-
assert.throws(
|
|
62
|
+
assert.throws(
|
|
63
|
+
() => services(list, {}, undefined, undefined),
|
|
64
|
+
(error) =>
|
|
65
|
+
/Service 'group-one' declares an ingress, but no hosts are defined/.test(
|
|
66
|
+
error.message
|
|
67
|
+
)
|
|
68
|
+
)
|
|
54
69
|
})
|
|
55
70
|
|
|
56
71
|
it('should reject an ingress without a port', () => {
|
|
57
72
|
const list = [service({ ingress: { path: '/.introspection' } })]
|
|
58
73
|
|
|
59
|
-
assert.throws(
|
|
74
|
+
assert.throws(
|
|
75
|
+
() => services(list, {}, undefined, { hosts: ['api.dev'] }),
|
|
76
|
+
(error) => /Service 'group-one' declares an ingress, but no port/.test(error.message)
|
|
77
|
+
)
|
|
60
78
|
})
|
|
61
|
-
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
export function addVariables
|
|
1
|
+
export function addVariables(composition, variables, keys = composition.components) {
|
|
2
2
|
composition.variables ??= []
|
|
3
3
|
|
|
4
4
|
const used = new Set(composition.variables.map((variable) => variable.name))
|
|
5
5
|
|
|
6
6
|
for (const [key, set] of Object.entries(variables)) {
|
|
7
|
-
if (key !== 'global' && !keys?.includes(key))
|
|
8
|
-
continue
|
|
7
|
+
if (key !== 'global' && !keys?.includes(key)) continue
|
|
9
8
|
|
|
10
9
|
for (const variable of set) {
|
|
11
10
|
if (used.has(variable.name)) continue
|
|
@@ -14,7 +14,8 @@ export const describe = (context, compositions, dependency, image) => {
|
|
|
14
14
|
{
|
|
15
15
|
name: 'TOA_CONTEXT',
|
|
16
16
|
value: context.name
|
|
17
|
-
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
18
19
|
name: 'TOA_ENV',
|
|
19
20
|
value: context.environment
|
|
20
21
|
}
|
|
@@ -28,8 +29,10 @@ export const describe = (context, compositions, dependency, image) => {
|
|
|
28
29
|
// a lock is taken on `floor(n / 2) + 1` of them, so an even number tolerates no more
|
|
29
30
|
// losses than the odd number below it, and two tolerate fewer than one does
|
|
30
31
|
if (addresses.length % 2 === 0)
|
|
31
|
-
throw new Error(
|
|
32
|
-
|
|
32
|
+
throw new Error(
|
|
33
|
+
`'atomicity.redis' takes an odd number of addresses, ` +
|
|
34
|
+
`${addresses.length} given`
|
|
35
|
+
)
|
|
33
36
|
|
|
34
37
|
dependency.variables.global.push({
|
|
35
38
|
name: 'TOA_ATOMICITY_REDIS',
|
|
@@ -102,8 +105,10 @@ export const describe = (context, compositions, dependency, image) => {
|
|
|
102
105
|
return values
|
|
103
106
|
}
|
|
104
107
|
|
|
105
|
-
function unit
|
|
106
|
-
const components = (context.components ?? []).map(
|
|
108
|
+
function unit(context, dependency) {
|
|
109
|
+
const components = (context.components ?? []).map(
|
|
110
|
+
(component) => component.locator.label
|
|
111
|
+
)
|
|
107
112
|
|
|
108
113
|
const variables = dependency.variables ?? {}
|
|
109
114
|
const mounts = dependency.mounts ?? {}
|
|
@@ -115,8 +120,7 @@ function unit (context, dependency) {
|
|
|
115
120
|
variables: []
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
if (context.ingress !== undefined)
|
|
119
|
-
mono.ingress = Object.assign({}, context.ingress)
|
|
123
|
+
if (context.ingress !== undefined) mono.ingress = Object.assign({}, context.ingress)
|
|
120
124
|
|
|
121
125
|
addVariables(mono, variables, Object.keys(variables))
|
|
122
126
|
addMounts(mono, dependency.mounts, Object.keys(mounts))
|
|
@@ -61,22 +61,27 @@ const reserve = (services, probe) => {
|
|
|
61
61
|
const conflicting = claimed.get(port)
|
|
62
62
|
|
|
63
63
|
if (conflicting !== undefined)
|
|
64
|
-
throw new Error(
|
|
65
|
-
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Port ${port} is claimed by both ${conflicting} and ${claimant}` +
|
|
66
|
+
(service.workload === undefined ? '' : ` in '${workload}'`)
|
|
67
|
+
)
|
|
66
68
|
|
|
67
69
|
claimed.set(port, claimant)
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
function
|
|
74
|
+
function* ports(service) {
|
|
73
75
|
// every service reaching here is named the way it is deployed, `<group>-<name>`
|
|
74
76
|
const name = `'${service.name}'`
|
|
75
77
|
|
|
76
|
-
if (service.port !== undefined)
|
|
77
|
-
yield [service.port, name]
|
|
78
|
+
if (service.port !== undefined) yield [service.port, name]
|
|
78
79
|
|
|
79
|
-
if (
|
|
80
|
+
if (
|
|
81
|
+
service.probe !== undefined &&
|
|
82
|
+
service.probe !== false &&
|
|
83
|
+
service.probe.port !== service.port
|
|
84
|
+
)
|
|
80
85
|
yield [service.probe.port, `the readiness probe of ${name}`]
|
|
81
86
|
}
|
|
82
87
|
|
|
@@ -4,7 +4,12 @@ import assert from 'node:assert/strict'
|
|
|
4
4
|
import { merge } from './merge.js'
|
|
5
5
|
|
|
6
6
|
// a service is named the way it is deployed by the time it reaches `merge`
|
|
7
|
-
const service = (name, extra = {}) => ({
|
|
7
|
+
const service = (name, extra = {}) => ({
|
|
8
|
+
group: 'group',
|
|
9
|
+
name: `group-${name}`,
|
|
10
|
+
version: '0',
|
|
11
|
+
...extra
|
|
12
|
+
})
|
|
8
13
|
|
|
9
14
|
it('should merge services of all dependencies', () => {
|
|
10
15
|
const merged = merge([
|
|
@@ -28,7 +33,13 @@ describe('port reservation', () => {
|
|
|
28
33
|
{ services: [hosted('two', { port: 8000 })] }
|
|
29
34
|
]
|
|
30
35
|
|
|
31
|
-
assert.throws(
|
|
36
|
+
assert.throws(
|
|
37
|
+
() => merge(dependencies),
|
|
38
|
+
(error) =>
|
|
39
|
+
/Port 8000 is claimed by both 'group-one' and 'group-two' in 'mono'/.test(
|
|
40
|
+
error.message
|
|
41
|
+
)
|
|
42
|
+
)
|
|
32
43
|
})
|
|
33
44
|
|
|
34
45
|
it('should allow two workloads to claim one port', () => {
|
|
@@ -46,7 +57,13 @@ describe('port reservation', () => {
|
|
|
46
57
|
{ services: [service('two', { port: 8000, workload: ['inner'] })] }
|
|
47
58
|
]
|
|
48
59
|
|
|
49
|
-
assert.throws(
|
|
60
|
+
assert.throws(
|
|
61
|
+
() => merge(dependencies),
|
|
62
|
+
(error) =>
|
|
63
|
+
/Port 8000 is claimed by both 'group-one' and 'group-two' in 'inner'/.test(
|
|
64
|
+
error.message
|
|
65
|
+
)
|
|
66
|
+
)
|
|
50
67
|
})
|
|
51
68
|
|
|
52
69
|
it('should allow two services deployed on their own to claim one port', () => {
|
|
@@ -64,30 +81,44 @@ describe('port reservation', () => {
|
|
|
64
81
|
{ services: [service('one', { port: 8001 })] }
|
|
65
82
|
]
|
|
66
83
|
|
|
67
|
-
assert.throws(
|
|
84
|
+
assert.throws(
|
|
85
|
+
() => merge(dependencies),
|
|
86
|
+
(error) =>
|
|
87
|
+
/Port 8001 is claimed by both the readiness probe and 'group-one'/.test(
|
|
88
|
+
error.message
|
|
89
|
+
)
|
|
90
|
+
)
|
|
68
91
|
})
|
|
69
92
|
|
|
70
93
|
it('should reject a probe claiming the port of another service of the workload', () => {
|
|
71
94
|
const dependencies = [
|
|
72
95
|
{ services: [hosted('one', { port: 8000 })] },
|
|
73
|
-
{
|
|
96
|
+
{
|
|
97
|
+
services: [hosted('two', { port: 8002, probe: { path: '/.ready', port: 8000 } })]
|
|
98
|
+
}
|
|
74
99
|
]
|
|
75
100
|
|
|
76
|
-
assert.throws(
|
|
101
|
+
assert.throws(
|
|
102
|
+
() => merge(dependencies),
|
|
103
|
+
(error) =>
|
|
104
|
+
/Port 8000 is claimed by both 'group-one' and the readiness probe of 'group-two' in 'mono'/.test(
|
|
105
|
+
error.message
|
|
106
|
+
)
|
|
107
|
+
)
|
|
77
108
|
})
|
|
78
109
|
|
|
79
110
|
it('should allow a service to probe its own port', () => {
|
|
80
111
|
const dependencies = [
|
|
81
|
-
{
|
|
112
|
+
{
|
|
113
|
+
services: [service('one', { port: 8000, probe: { path: '/.ready', port: 8000 } })]
|
|
114
|
+
}
|
|
82
115
|
]
|
|
83
116
|
|
|
84
117
|
assert.doesNotThrow(() => merge(dependencies))
|
|
85
118
|
})
|
|
86
119
|
|
|
87
120
|
it('should ignore services without a port', () => {
|
|
88
|
-
const dependencies = [
|
|
89
|
-
{ services: [service('one'), service('two')] }
|
|
90
|
-
]
|
|
121
|
+
const dependencies = [{ services: [service('one'), service('two')] }]
|
|
91
122
|
|
|
92
123
|
assert.doesNotThrow(() => merge(dependencies))
|
|
93
124
|
})
|