@toa.io/operations 1.0.0-alpha.29 → 1.0.0-alpha.291
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/CHANGELOG.md +50 -0
- package/image/runtime.Dockerfile +4 -0
- package/package.json +8 -10
- package/readme.md +93 -0
- package/src/deployment/.deployment/.describe/components.js +1 -5
- package/src/deployment/.deployment/.describe/compositions.js +26 -7
- package/src/deployment/.deployment/.describe/dependencies.js +1 -6
- package/src/deployment/.deployment/.describe/events.js +63 -0
- package/src/deployment/.deployment/.describe/fold.js +42 -0
- package/src/deployment/.deployment/.describe/fold.test.js +108 -0
- package/src/deployment/.deployment/.describe/index.js +4 -13
- package/src/deployment/.deployment/.describe/mounts.js +17 -0
- package/src/deployment/.deployment/.describe/resources.js +103 -0
- package/src/deployment/.deployment/.describe/resources.test.js +129 -0
- package/src/deployment/.deployment/.describe/services.js +32 -6
- package/src/deployment/.deployment/.describe/services.test.js +78 -0
- package/src/deployment/.deployment/.describe/variables.js +5 -9
- package/src/deployment/.deployment/declare.js +1 -5
- package/src/deployment/.deployment/describe.js +125 -10
- package/src/deployment/.deployment/index.js +3 -9
- package/src/deployment/.deployment/merge.js +66 -6
- package/src/deployment/.deployment/merge.test.js +133 -0
- package/src/deployment/chart/templates/components.yaml +1 -1
- package/src/deployment/chart/templates/compositions.yaml +96 -4
- package/src/deployment/chart/templates/mono.yaml +182 -0
- package/src/deployment/chart/templates/services.yaml +82 -7
- package/src/deployment/chart/values.yaml +23 -1
- package/src/deployment/composition.js +6 -6
- package/src/deployment/deployment.js +55 -25
- package/src/deployment/drain.js +70 -0
- package/src/deployment/drain.test.js +64 -0
- package/src/deployment/factory.js +121 -32
- package/src/deployment/images/bundle.js +82 -0
- package/src/deployment/images/composition.Dockerfile +8 -16
- package/src/deployment/images/composition.js +9 -56
- package/src/deployment/images/dependencies.Dockerfile +16 -0
- package/src/deployment/images/dependencies.js +141 -0
- package/src/deployment/images/dependencies.test.js +214 -0
- package/src/deployment/images/factory.js +31 -15
- package/src/deployment/images/format.js +52 -0
- package/src/deployment/images/format.test.js +74 -0
- package/src/deployment/images/image.fixtures.js +13 -13
- package/src/deployment/images/image.js +77 -38
- package/src/deployment/images/image.test.js +11 -5
- package/src/deployment/images/index.js +1 -5
- package/src/deployment/images/mono.Dockerfile +11 -0
- package/src/deployment/images/mono.js +15 -0
- package/src/deployment/images/publish.js +93 -0
- package/src/deployment/images/runtime-base.test.js +97 -0
- package/src/deployment/images/service.Dockerfile +5 -4
- package/src/deployment/images/service.js +13 -13
- package/src/deployment/index.js +1 -5
- package/src/deployment/operator.js +13 -13
- package/src/deployment/operator.test.js +8 -7
- package/src/deployment/registry.js +165 -47
- package/src/deployment/registry.test.js +407 -0
- package/src/deployment/service.js +4 -6
- package/src/deployment/workspace.js +13 -8
- package/src/index.js +1 -3
- package/src/process.js +51 -13
- package/src/process.test.js +33 -0
- package/types/_deployment/composition.d.ts +1 -3
- package/types/_deployment/dependency.d.ts +13 -7
- package/types/_deployment/deployment.d.ts +3 -7
- package/types/_deployment/factory.d.ts +2 -4
- package/types/_deployment/images/factory.d.ts +6 -8
- package/types/_deployment/images/image.d.ts +16 -1
- package/types/_deployment/images/registry.d.ts +8 -11
- package/types/_deployment/operator.d.ts +7 -9
- package/types/_deployment/registry.d.ts +4 -4
- package/types/_deployment/service.d.ts +4 -3
- package/types/dependency.ts +79 -0
- package/types/index.ts +1 -0
- package/types/dependency.d.ts +0 -39
- package/types/index.d.ts +0 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { resources, quantity } from './resources.js'
|
|
5
|
+
|
|
6
|
+
const declared = { cpu: ['100m', '1'], memory: ['100Mi', '1Gi'] }
|
|
7
|
+
|
|
8
|
+
it('should take the declaration of a deployment', () => {
|
|
9
|
+
const values = { compositions: [{ name: 'edge', resources: declared }] }
|
|
10
|
+
|
|
11
|
+
resources({}, values)
|
|
12
|
+
|
|
13
|
+
assert.deepStrictEqual(values.compositions[0].resources, declared)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should fall back to the context', () => {
|
|
17
|
+
const values = { compositions: [{ name: 'edge' }] }
|
|
18
|
+
|
|
19
|
+
resources({ resources: declared }, values)
|
|
20
|
+
|
|
21
|
+
assert.deepStrictEqual(values.compositions[0].resources, declared)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should take `null` as an answer', () => {
|
|
25
|
+
const values = { compositions: [{ name: 'edge', resources: null }] }
|
|
26
|
+
|
|
27
|
+
assert.doesNotThrow(() => resources({ resources: declared }, values))
|
|
28
|
+
assert.strictEqual(values.compositions[0].resources, null)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('refusal', () => {
|
|
32
|
+
it('should refuse a composition that declares none', () => {
|
|
33
|
+
const values = { compositions: [{ name: 'edge' }] }
|
|
34
|
+
|
|
35
|
+
assert.throws(
|
|
36
|
+
() => resources({}, values),
|
|
37
|
+
(error) => /^Composition 'edge' declares no resources\./.test(error.message)
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should refuse a service that declares none', () => {
|
|
42
|
+
const values = { services: [{ name: 'exposition-gateway' }] }
|
|
43
|
+
|
|
44
|
+
assert.throws(
|
|
45
|
+
() => resources({}, values),
|
|
46
|
+
(error) =>
|
|
47
|
+
/^Service 'exposition-gateway' declares no resources\./.test(error.message)
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should refuse the mono deployment when it declares none', () => {
|
|
52
|
+
const values = { mono: {} }
|
|
53
|
+
|
|
54
|
+
assert.throws(
|
|
55
|
+
() => resources({}, values),
|
|
56
|
+
(error) => /^The mono deployment declares no resources\./.test(error.message)
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should not ask a service a composition runs', () => {
|
|
61
|
+
// it has no deployment of its own to size; the composition running it states the pod's
|
|
62
|
+
const values = {
|
|
63
|
+
compositions: [{ name: 'edge', resources: declared }],
|
|
64
|
+
services: [{ name: 'exposition-gateway', workload: ['edge'] }]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
assert.doesNotThrow(() => resources({}, values))
|
|
68
|
+
assert.strictEqual(values.services[0].resources, undefined)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('heap', () => {
|
|
73
|
+
it('should size the heap by the memory limit', () => {
|
|
74
|
+
const values = { compositions: [{ name: 'edge', resources: declared }] }
|
|
75
|
+
|
|
76
|
+
resources({}, values)
|
|
77
|
+
|
|
78
|
+
assert.deepStrictEqual(values.compositions[0].variables, [
|
|
79
|
+
{ name: 'NODE_OPTIONS', value: '--max-old-space-size=768' }
|
|
80
|
+
])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should size the heap of a service and of the mono deployment', () => {
|
|
84
|
+
const values = {
|
|
85
|
+
services: [
|
|
86
|
+
{ name: 'exposition-gateway', resources: { memory: ['200Mi', '500Mi'] } }
|
|
87
|
+
],
|
|
88
|
+
mono: { variables: [{ name: 'TOA_ENV', value: 'test' }] }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
resources({ resources: declared }, values)
|
|
92
|
+
|
|
93
|
+
assert.deepStrictEqual(values.services[0].variables, [
|
|
94
|
+
{ name: 'NODE_OPTIONS', value: '--max-old-space-size=375' }
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
assert.deepStrictEqual(values.mono.variables, [
|
|
98
|
+
{ name: 'TOA_ENV', value: 'test' },
|
|
99
|
+
{ name: 'NODE_OPTIONS', value: '--max-old-space-size=768' }
|
|
100
|
+
])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should keep the options a deployment states', () => {
|
|
104
|
+
const variables = [{ name: 'NODE_OPTIONS', value: '--max-old-space-size=100' }]
|
|
105
|
+
const values = { compositions: [{ name: 'edge', resources: declared, variables }] }
|
|
106
|
+
|
|
107
|
+
resources({}, values)
|
|
108
|
+
|
|
109
|
+
assert.deepStrictEqual(values.compositions[0].variables, variables)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should not size the heap without a memory limit', () => {
|
|
113
|
+
const values = { compositions: [{ name: 'edge', resources: { cpu: ['100m', '1'] } }] }
|
|
114
|
+
|
|
115
|
+
resources({}, values)
|
|
116
|
+
|
|
117
|
+
assert.strictEqual(values.compositions[0].variables, undefined)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should read a quantity', () => {
|
|
121
|
+
assert.strictEqual(quantity('512Mi'), 512 * 2 ** 20)
|
|
122
|
+
assert.strictEqual(quantity('1Gi'), 2 ** 30)
|
|
123
|
+
assert.strictEqual(quantity('500M'), 500e6)
|
|
124
|
+
assert.strictEqual(quantity('1.5Gi'), 1.5 * 2 ** 30)
|
|
125
|
+
assert.strictEqual(quantity(1024), 1024)
|
|
126
|
+
assert.throws(() => quantity('500m'), /not a memory quantity/)
|
|
127
|
+
assert.throws(() => quantity('big'), /not a quantity/)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
@@ -1,11 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
import { addVariables } from './variables.js'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function services (services, variables) {
|
|
3
|
+
export function services(services, variables, probe, ingress) {
|
|
6
4
|
for (const service of services) {
|
|
7
|
-
|
|
5
|
+
// one a composition runs has no deployment of its own to carry them; the composition
|
|
6
|
+
// it runs in states them, and repeating them here is dead weight in the values
|
|
7
|
+
if (service.workload === undefined) addVariables(service, variables)
|
|
8
|
+
|
|
9
|
+
if (service.probe === false) delete service.probe
|
|
10
|
+
else if (service.probe === undefined && probe !== undefined && probe !== false)
|
|
11
|
+
service.probe = probe
|
|
12
|
+
|
|
13
|
+
if (service.ingress !== undefined) expose(service, ingress)
|
|
8
14
|
}
|
|
9
15
|
}
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
/**
|
|
18
|
+
* A service declares only its own intent — a path, a port. Where that lands is
|
|
19
|
+
* the context's business, so everything else comes from its `ingress` section.
|
|
20
|
+
* What the service did declare wins.
|
|
21
|
+
*/
|
|
22
|
+
function expose(service, ingress = {}) {
|
|
23
|
+
const declared = Object.fromEntries(
|
|
24
|
+
Object.entries(service.ingress).filter(([, value]) => value !== undefined)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
service.ingress = Object.assign({}, ingress, declared)
|
|
28
|
+
|
|
29
|
+
if (service.ingress.hosts === undefined)
|
|
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
|
+
)
|
|
34
|
+
|
|
35
|
+
if (service.port === undefined)
|
|
36
|
+
throw new Error(`Service '${service.name}' declares an ingress, but no port.`)
|
|
37
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { services } from './services.js'
|
|
5
|
+
|
|
6
|
+
const service = (extra = {}) => ({
|
|
7
|
+
group: 'group',
|
|
8
|
+
name: 'group-one',
|
|
9
|
+
version: '0',
|
|
10
|
+
...extra
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should leave a service without an ingress alone', () => {
|
|
14
|
+
const list = [service()]
|
|
15
|
+
|
|
16
|
+
services(list, {}, undefined, { hosts: ['api.dev'] })
|
|
17
|
+
|
|
18
|
+
assert.strictEqual(list[0].ingress, undefined)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* A service declares only where it wants to land; the cluster-level plumbing
|
|
23
|
+
* belongs to the context.
|
|
24
|
+
*/
|
|
25
|
+
it('should supply hosts, class and annotations from the context', () => {
|
|
26
|
+
const list = [service({ port: 8002, ingress: { path: '/.introspection' } })]
|
|
27
|
+
|
|
28
|
+
services(list, {}, undefined, {
|
|
29
|
+
hosts: ['api.dev'],
|
|
30
|
+
class: 'alb',
|
|
31
|
+
annotations: { a: 'b' }
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
assert.deepStrictEqual(list[0].ingress, {
|
|
35
|
+
path: '/.introspection',
|
|
36
|
+
hosts: ['api.dev'],
|
|
37
|
+
class: 'alb',
|
|
38
|
+
annotations: { a: 'b' }
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should keep what the service declared itself', () => {
|
|
43
|
+
const list = [service({ port: 8000, ingress: { path: '/', hosts: ['own.dev'] } })]
|
|
44
|
+
|
|
45
|
+
services(list, {}, undefined, { hosts: ['api.dev'], class: 'alb' })
|
|
46
|
+
|
|
47
|
+
assert.deepStrictEqual(list[0].ingress.hosts, ['own.dev'])
|
|
48
|
+
assert.deepStrictEqual(list[0].ingress.class, 'alb')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should ignore properties the service left undefined', () => {
|
|
52
|
+
const list = [service({ port: 8000, ingress: { path: '/', class: undefined } })]
|
|
53
|
+
|
|
54
|
+
services(list, {}, undefined, { hosts: ['api.dev'], class: 'alb' })
|
|
55
|
+
|
|
56
|
+
assert.deepStrictEqual(list[0].ingress.class, 'alb')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should reject an ingress with nowhere to land', () => {
|
|
60
|
+
const list = [service({ port: 8002, ingress: { path: '/.introspection' } })]
|
|
61
|
+
|
|
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
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should reject an ingress without a port', () => {
|
|
72
|
+
const list = [service({ ingress: { path: '/.introspection' } })]
|
|
73
|
+
|
|
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
|
+
)
|
|
78
|
+
})
|
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
export function addVariables(composition, variables, keys = composition.components) {
|
|
2
|
+
composition.variables ??= []
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
const used = new Set()
|
|
5
|
-
|
|
6
|
-
deployment.variables ??= []
|
|
4
|
+
const used = new Set(composition.variables.map((variable) => variable.name))
|
|
7
5
|
|
|
8
6
|
for (const [key, set] of Object.entries(variables)) {
|
|
9
|
-
if (key !== 'global' && !
|
|
7
|
+
if (key !== 'global' && !keys?.includes(key)) continue
|
|
10
8
|
|
|
11
9
|
for (const variable of set) {
|
|
12
10
|
if (used.has(variable.name)) continue
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
composition.variables.push(variable)
|
|
15
13
|
used.add(variable.name)
|
|
16
14
|
}
|
|
17
15
|
}
|
|
18
16
|
}
|
|
19
|
-
|
|
20
|
-
exports.addVariables = addVariables
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @param {toa.norm.Context} context
|
|
5
3
|
* @param {toa.deployment.Dependency} dependency
|
|
6
4
|
* @returns {toa.deployment.Declaration}
|
|
7
5
|
*/
|
|
8
|
-
const declare = (context, dependency) => {
|
|
6
|
+
export const declare = (context, dependency) => {
|
|
9
7
|
const { references } = dependency
|
|
10
8
|
const { name, description, version } = context
|
|
11
9
|
|
|
@@ -25,5 +23,3 @@ const DECLARATION = {
|
|
|
25
23
|
apiVersion: 'v2',
|
|
26
24
|
type: 'application'
|
|
27
25
|
}
|
|
28
|
-
|
|
29
|
-
exports.declare = declare
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import * as desc from './.describe/index.js'
|
|
2
|
+
import { addVariables } from './.describe/variables.js'
|
|
3
|
+
import { addMounts } from './.describe/mounts.js'
|
|
4
|
+
import { resources } from './.describe/resources.js'
|
|
5
|
+
import { events } from './.describe/events.js'
|
|
6
|
+
import { fold } from './.describe/fold.js'
|
|
2
7
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const describe = (context, compositions, dependency) => {
|
|
8
|
+
export const describe = (context, compositions, dependency, image) => {
|
|
6
9
|
const { services } = dependency
|
|
7
10
|
|
|
8
11
|
dependency.variables.global ??= []
|
|
@@ -11,24 +14,136 @@ const describe = (context, compositions, dependency) => {
|
|
|
11
14
|
{
|
|
12
15
|
name: 'TOA_CONTEXT',
|
|
13
16
|
value: context.name
|
|
14
|
-
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
15
19
|
name: 'TOA_ENV',
|
|
16
20
|
value: context.environment
|
|
17
21
|
}
|
|
18
22
|
)
|
|
19
23
|
|
|
20
|
-
const
|
|
24
|
+
const atomicity = context.atomicity
|
|
25
|
+
|
|
26
|
+
if (atomicity?.redis !== undefined) {
|
|
27
|
+
const addresses = Array.isArray(atomicity.redis) ? atomicity.redis : [atomicity.redis]
|
|
28
|
+
|
|
29
|
+
// a lock is taken on `floor(n / 2) + 1` of them, so an even number tolerates no more
|
|
30
|
+
// losses than the odd number below it, and two tolerate fewer than one does
|
|
31
|
+
if (addresses.length % 2 === 0)
|
|
32
|
+
throw new Error(
|
|
33
|
+
`'atomicity.redis' takes an odd number of addresses, ` +
|
|
34
|
+
`${addresses.length} given`
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
dependency.variables.global.push({
|
|
38
|
+
name: 'TOA_ATOMICITY_REDIS',
|
|
39
|
+
value: addresses.join(' ')
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (atomicity?.interval !== undefined)
|
|
44
|
+
dependency.variables.global.push({
|
|
45
|
+
name: 'TOA_ATOMICITY_INTERVAL',
|
|
46
|
+
value: String(atomicity.interval)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const outbox = context.outbox
|
|
50
|
+
|
|
51
|
+
if (outbox?.interval !== undefined)
|
|
52
|
+
dependency.variables.global.push({
|
|
53
|
+
name: 'TOA_OUTBOX_INTERVAL',
|
|
54
|
+
value: String(outbox.interval)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
if (outbox?.batch !== undefined)
|
|
58
|
+
dependency.variables.global.push({
|
|
59
|
+
name: 'TOA_OUTBOX_BATCH',
|
|
60
|
+
value: String(outbox.batch)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if (outbox?.retention !== undefined)
|
|
64
|
+
dependency.variables.global.push({
|
|
65
|
+
name: 'TOA_OUTBOX_RETENTION',
|
|
66
|
+
value: String(outbox.retention)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
events(context, dependency)
|
|
70
|
+
|
|
21
71
|
const credentials = context.registry?.credentials
|
|
22
72
|
|
|
23
|
-
|
|
24
|
-
|
|
73
|
+
if (image !== undefined) {
|
|
74
|
+
const mono = unit(context, dependency)
|
|
75
|
+
|
|
76
|
+
mono.image = image.reference
|
|
77
|
+
|
|
78
|
+
const values = {
|
|
79
|
+
compositions: [],
|
|
80
|
+
components: mono.components,
|
|
81
|
+
services: [],
|
|
82
|
+
credentials,
|
|
83
|
+
mono
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
resources(context, values)
|
|
87
|
+
|
|
88
|
+
return values
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const components = desc.components(compositions)
|
|
25
92
|
|
|
26
|
-
|
|
93
|
+
desc.compositions(compositions, dependency)
|
|
94
|
+
desc.services(services, dependency.variables, dependency.probe, context.ingress)
|
|
95
|
+
|
|
96
|
+
const values = {
|
|
27
97
|
compositions,
|
|
28
98
|
components,
|
|
29
99
|
services,
|
|
30
100
|
credentials
|
|
31
101
|
}
|
|
102
|
+
|
|
103
|
+
resources(context, values)
|
|
104
|
+
|
|
105
|
+
return values
|
|
32
106
|
}
|
|
33
107
|
|
|
34
|
-
|
|
108
|
+
function unit(context, dependency) {
|
|
109
|
+
const components = (context.components ?? []).map(
|
|
110
|
+
(component) => component.locator.label
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const variables = dependency.variables ?? {}
|
|
114
|
+
const mounts = dependency.mounts ?? {}
|
|
115
|
+
|
|
116
|
+
const mono = {
|
|
117
|
+
replicas: context.mono?.replicas,
|
|
118
|
+
resources: context.mono?.resources,
|
|
119
|
+
components,
|
|
120
|
+
variables: []
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (context.ingress !== undefined) mono.ingress = Object.assign({}, context.ingress)
|
|
124
|
+
|
|
125
|
+
addVariables(mono, variables, Object.keys(variables))
|
|
126
|
+
addMounts(mono, dependency.mounts, Object.keys(mounts))
|
|
127
|
+
|
|
128
|
+
fold(mono, dependency.services ?? [], dependency)
|
|
129
|
+
|
|
130
|
+
// mono is the one workload that fronts every service it runs under a single name,
|
|
131
|
+
// so it inherits how they are reached as well as what they run
|
|
132
|
+
for (const service of dependency.services ?? []) {
|
|
133
|
+
// one Service fronts every backend, so its annotations are the union
|
|
134
|
+
if (service.annotations !== undefined)
|
|
135
|
+
mono.annotations = Object.assign(mono.annotations ?? {}, service.annotations)
|
|
136
|
+
|
|
137
|
+
if (service.ingress !== undefined) {
|
|
138
|
+
const { path, hosts, ...ingress } = service.ingress
|
|
139
|
+
|
|
140
|
+
mono.ingress = Object.assign(mono.ingress ?? {}, ingress)
|
|
141
|
+
|
|
142
|
+
// one Ingress serves every path, so its hosts are the union, not the last word
|
|
143
|
+
if (hosts !== undefined)
|
|
144
|
+
mono.ingress.hosts = [...new Set([...(mono.ingress.hosts ?? []), ...hosts])]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return mono
|
|
149
|
+
}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const { describe } = require('./describe')
|
|
5
|
-
const { merge } = require('./merge')
|
|
6
|
-
|
|
7
|
-
exports.declare = declare
|
|
8
|
-
exports.describe = describe
|
|
9
|
-
exports.merge = merge
|
|
1
|
+
export { declare } from './declare.js'
|
|
2
|
+
export { describe } from './describe.js'
|
|
3
|
+
export { merge } from './merge.js'
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const merge = (dependencies) => {
|
|
1
|
+
export const merge = (dependencies) => {
|
|
4
2
|
/** @type {toa.deployment.dependency.Reference[]} */
|
|
5
3
|
const references = []
|
|
6
4
|
|
|
@@ -13,14 +11,78 @@ const merge = (dependencies) => {
|
|
|
13
11
|
/** @type {toa.deployment.dependency.Variables} */
|
|
14
12
|
const variables = {}
|
|
15
13
|
|
|
14
|
+
/** event labels consumed by something other than a component's own receivers */
|
|
15
|
+
const events = []
|
|
16
|
+
|
|
17
|
+
const mounts = {}
|
|
18
|
+
|
|
19
|
+
/** @type {toa.deployment.dependency.Probe | false | undefined} */
|
|
20
|
+
let probe
|
|
21
|
+
|
|
16
22
|
for (const dependency of dependencies) {
|
|
17
23
|
if (dependency.references !== undefined) references.push(...dependency.references)
|
|
18
24
|
if (dependency.services !== undefined) services.push(...dependency.services)
|
|
19
25
|
if (dependency.proxies !== undefined) proxies.push(...dependency.proxies)
|
|
20
26
|
if (dependency.variables !== undefined) append(variables, dependency.variables)
|
|
27
|
+
if (dependency.events !== undefined) events.push(...dependency.events)
|
|
28
|
+
if (dependency.mounts !== undefined) append(mounts, dependency.mounts)
|
|
29
|
+
if (dependency.probe !== undefined) probe = dependency.probe
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
reserve(services, probe)
|
|
33
|
+
|
|
34
|
+
return { references, services, proxies, variables, mounts, events, probe }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A workload that hosts services puts them in one process — `mono`, a composition that
|
|
39
|
+
* declares them, a local run — so within one a port may be claimed once and only once.
|
|
40
|
+
* A service deploying on its own is a pod of its own, and shares a port with nobody.
|
|
41
|
+
*/
|
|
42
|
+
const reserve = (services, probe) => {
|
|
43
|
+
/** @type {Map<string, Map<number, string>>} */
|
|
44
|
+
const workloads = new Map()
|
|
45
|
+
|
|
46
|
+
for (const service of services)
|
|
47
|
+
// a service several compositions run binds its ports in each of their pods
|
|
48
|
+
for (const workload of service.workload ?? [service.name]) {
|
|
49
|
+
let claimed = workloads.get(workload)
|
|
50
|
+
|
|
51
|
+
if (claimed === undefined) {
|
|
52
|
+
claimed = new Map()
|
|
53
|
+
|
|
54
|
+
if (probe !== undefined && probe !== false)
|
|
55
|
+
claimed.set(probe.port, 'the readiness probe')
|
|
56
|
+
|
|
57
|
+
workloads.set(workload, claimed)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [port, claimant] of ports(service)) {
|
|
61
|
+
const conflicting = claimed.get(port)
|
|
62
|
+
|
|
63
|
+
if (conflicting !== undefined)
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Port ${port} is claimed by both ${conflicting} and ${claimant}` +
|
|
66
|
+
(service.workload === undefined ? '' : ` in '${workload}'`)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
claimed.set(port, claimant)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function* ports(service) {
|
|
75
|
+
// every service reaching here is named the way it is deployed, `<group>-<name>`
|
|
76
|
+
const name = `'${service.name}'`
|
|
77
|
+
|
|
78
|
+
if (service.port !== undefined) yield [service.port, name]
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
service.probe !== undefined &&
|
|
82
|
+
service.probe !== false &&
|
|
83
|
+
service.probe.port !== service.port
|
|
84
|
+
)
|
|
85
|
+
yield [service.probe.port, `the readiness probe of ${name}`]
|
|
24
86
|
}
|
|
25
87
|
|
|
26
88
|
const append = (merged, variables) => {
|
|
@@ -30,5 +92,3 @@ const append = (merged, variables) => {
|
|
|
30
92
|
merged[component].push(...vars)
|
|
31
93
|
}
|
|
32
94
|
}
|
|
33
|
-
|
|
34
|
-
exports.merge = merge
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { merge } from './merge.js'
|
|
5
|
+
|
|
6
|
+
// a service is named the way it is deployed by the time it reaches `merge`
|
|
7
|
+
const service = (name, extra = {}) => ({
|
|
8
|
+
group: 'group',
|
|
9
|
+
name: `group-${name}`,
|
|
10
|
+
version: '0',
|
|
11
|
+
...extra
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should merge services of all dependencies', () => {
|
|
15
|
+
const merged = merge([
|
|
16
|
+
{ services: [service('one', { port: 8000 })] },
|
|
17
|
+
{ services: [service('two', { port: 8001 })] }
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
assert.strictEqual(merged.services.length, 2)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* A workload that hosts services puts them in one process, so a port is claimed once
|
|
25
|
+
* within one. Services in separate pods share nothing.
|
|
26
|
+
*/
|
|
27
|
+
describe('port reservation', () => {
|
|
28
|
+
const hosted = (name, extra = {}) => service(name, { workload: ['mono'], ...extra })
|
|
29
|
+
|
|
30
|
+
it('should reject two services of one workload claiming one port', () => {
|
|
31
|
+
const dependencies = [
|
|
32
|
+
{ services: [hosted('one', { port: 8000 })] },
|
|
33
|
+
{ services: [hosted('two', { port: 8000 })] }
|
|
34
|
+
]
|
|
35
|
+
|
|
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
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should allow two workloads to claim one port', () => {
|
|
46
|
+
const dependencies = [
|
|
47
|
+
{ services: [service('one', { port: 8000, workload: ['edge'] })] },
|
|
48
|
+
{ services: [service('two', { port: 8000, workload: ['inner'] })] }
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
assert.doesNotThrow(() => merge(dependencies))
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should reserve the ports of a service in every workload running it', () => {
|
|
55
|
+
const dependencies = [
|
|
56
|
+
{ services: [service('one', { port: 8000, workload: ['edge', 'inner'] })] },
|
|
57
|
+
{ services: [service('two', { port: 8000, workload: ['inner'] })] }
|
|
58
|
+
]
|
|
59
|
+
|
|
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
|
+
)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should allow two services deployed on their own to claim one port', () => {
|
|
70
|
+
const dependencies = [
|
|
71
|
+
{ services: [service('one', { port: 8000 })] },
|
|
72
|
+
{ services: [service('two', { port: 8000 })] }
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
assert.doesNotThrow(() => merge(dependencies))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should reject a service claiming the port of the readiness probe', () => {
|
|
79
|
+
const dependencies = [
|
|
80
|
+
{ probe: { path: '/.ready', port: 8001 } },
|
|
81
|
+
{ services: [service('one', { port: 8001 })] }
|
|
82
|
+
]
|
|
83
|
+
|
|
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
|
+
)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should reject a probe claiming the port of another service of the workload', () => {
|
|
94
|
+
const dependencies = [
|
|
95
|
+
{ services: [hosted('one', { port: 8000 })] },
|
|
96
|
+
{
|
|
97
|
+
services: [hosted('two', { port: 8002, probe: { path: '/.ready', port: 8000 } })]
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
|
|
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
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should allow a service to probe its own port', () => {
|
|
111
|
+
const dependencies = [
|
|
112
|
+
{
|
|
113
|
+
services: [service('one', { port: 8000, probe: { path: '/.ready', port: 8000 } })]
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
assert.doesNotThrow(() => merge(dependencies))
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should ignore services without a port', () => {
|
|
121
|
+
const dependencies = [{ services: [service('one'), service('two')] }]
|
|
122
|
+
|
|
123
|
+
assert.doesNotThrow(() => merge(dependencies))
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should ignore a disabled probe', () => {
|
|
127
|
+
const dependencies = [
|
|
128
|
+
{ probe: false, services: [service('one', { port: 8000, probe: false })] }
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
assert.doesNotThrow(() => merge(dependencies))
|
|
132
|
+
})
|
|
133
|
+
})
|