@toa.io/operations 1.0.0-alpha.31 → 1.0.0-alpha.310
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 +213 -0
- package/image/runtime.Dockerfile +14 -0
- package/package.json +10 -10
- package/readme.md +201 -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 +129 -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 +153 -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.configmap.yaml +14 -0
- package/src/deployment/chart/templates/components.yaml +1 -1
- package/src/deployment/chart/templates/compositions.yaml +109 -4
- package/src/deployment/chart/templates/mono.yaml +195 -0
- package/src/deployment/chart/templates/services.yaml +97 -7
- package/src/deployment/chart/values.yaml +23 -1
- package/src/deployment/composition.js +8 -6
- package/src/deployment/deployment.js +147 -34
- package/src/deployment/drain.js +70 -0
- package/src/deployment/drain.test.js +64 -0
- package/src/deployment/factory.js +169 -37
- package/src/deployment/images/bundle.js +88 -0
- package/src/deployment/images/composition.Dockerfile +9 -17
- package/src/deployment/images/composition.js +9 -56
- package/src/deployment/images/dependencies.Dockerfile +23 -0
- package/src/deployment/images/dependencies.js +171 -0
- package/src/deployment/images/dependencies.test.js +268 -0
- package/src/deployment/images/factory.js +31 -15
- package/src/deployment/images/format.js +66 -0
- package/src/deployment/images/format.test.js +149 -0
- package/src/deployment/images/image.fixtures.js +17 -19
- package/src/deployment/images/image.js +79 -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 +91 -0
- package/src/deployment/images/runtime-base.test.js +97 -0
- package/src/deployment/images/service.Dockerfile +17 -5
- package/src/deployment/images/service.js +63 -14
- package/src/deployment/index.js +1 -5
- package/src/deployment/operator.js +22 -16
- package/src/deployment/operator.test.js +57 -7
- package/src/deployment/registry.js +210 -46
- package/src/deployment/registry.test.js +539 -0
- package/src/deployment/service.js +4 -6
- package/src/deployment/workspace.js +13 -8
- package/src/index.js +3 -2
- package/src/process.js +51 -13
- package/src/process.test.js +33 -0
- package/types/_deployment/composition.d.ts +2 -3
- package/types/_deployment/dependency.d.ts +13 -7
- package/types/_deployment/deployment.d.ts +7 -8
- 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 +10 -9
- package/types/_deployment/registry.d.ts +7 -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,108 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { fold } from './fold.js'
|
|
5
|
+
|
|
6
|
+
const service = (name, extra = {}) => ({ group: 'group', name, version: '0', ...extra })
|
|
7
|
+
|
|
8
|
+
it('should collect the ports of every service', () => {
|
|
9
|
+
const workload = {}
|
|
10
|
+
|
|
11
|
+
fold(workload, [service('one', { port: 8000 }), service('two', { port: 8002 })], {})
|
|
12
|
+
|
|
13
|
+
assert.deepStrictEqual(workload.backends, [
|
|
14
|
+
{ port: 8000, path: '/' },
|
|
15
|
+
{ port: 8002, path: '/' }
|
|
16
|
+
])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should take the path a service declares', () => {
|
|
20
|
+
const workload = {}
|
|
21
|
+
|
|
22
|
+
fold(workload, [service('one', { port: 8002, ingress: { path: '/explorer' } })], {})
|
|
23
|
+
|
|
24
|
+
assert.deepStrictEqual(workload.backends, [{ port: 8002, path: '/explorer' }])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should put the more specific prefix first', () => {
|
|
28
|
+
const workload = {}
|
|
29
|
+
|
|
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
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should leave a service without a port out of the backends', () => {
|
|
46
|
+
const workload = {}
|
|
47
|
+
|
|
48
|
+
fold(workload, [service('one')], {})
|
|
49
|
+
|
|
50
|
+
assert.strictEqual(workload.backends, undefined)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('variables', () => {
|
|
54
|
+
it('should take those of every service', () => {
|
|
55
|
+
const workload = {}
|
|
56
|
+
|
|
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
|
+
])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should keep the first of a name', () => {
|
|
73
|
+
const workload = { variables: [{ name: 'A', value: 'own' }] }
|
|
74
|
+
|
|
75
|
+
fold(workload, [service('one', { variables: [{ name: 'A', value: 'theirs' }] })], {})
|
|
76
|
+
|
|
77
|
+
assert.deepStrictEqual(workload.variables, [{ name: 'A', value: 'own' }])
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('probe', () => {
|
|
82
|
+
const probe = { path: '/.ready', port: 8004 }
|
|
83
|
+
const fallback = { path: '/.ready', port: 8001 }
|
|
84
|
+
|
|
85
|
+
it('should take the probe of a service over the default', () => {
|
|
86
|
+
const workload = {}
|
|
87
|
+
|
|
88
|
+
fold(workload, [service('one', { port: 8000, probe })], { probe: fallback })
|
|
89
|
+
|
|
90
|
+
assert.deepStrictEqual(workload.probe, probe)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should fall back to the default', () => {
|
|
94
|
+
const workload = {}
|
|
95
|
+
|
|
96
|
+
fold(workload, [service('one', { port: 8000 })], { probe: fallback })
|
|
97
|
+
|
|
98
|
+
assert.deepStrictEqual(workload.probe, fallback)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should ignore a disabled probe', () => {
|
|
102
|
+
const workload = {}
|
|
103
|
+
|
|
104
|
+
fold(workload, [service('one', { port: 8000, probe: false })], { probe: false })
|
|
105
|
+
|
|
106
|
+
assert.strictEqual(workload.probe, undefined)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const { compositions } = require('./compositions')
|
|
6
|
-
const { variables } = require('./variables')
|
|
7
|
-
const { services } = require('./services')
|
|
8
|
-
|
|
9
|
-
exports.dependencies = dependencies
|
|
10
|
-
exports.components = components
|
|
11
|
-
exports.compositions = compositions
|
|
12
|
-
exports.variables = variables
|
|
13
|
-
exports.services = services
|
|
1
|
+
export { dependencies } from './dependencies.js'
|
|
2
|
+
export { components } from './components.js'
|
|
3
|
+
export { compositions } from './compositions.js'
|
|
4
|
+
export { services } from './services.js'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function addMounts(composition, mounts, keys = composition.components) {
|
|
2
|
+
if (mounts === undefined) return
|
|
3
|
+
|
|
4
|
+
const used = new Set()
|
|
5
|
+
|
|
6
|
+
for (const [key, mount] of Object.entries(mounts)) {
|
|
7
|
+
if (key !== 'global' && !keys?.includes(key)) continue
|
|
8
|
+
|
|
9
|
+
for (const { name, path, claim } of mount) {
|
|
10
|
+
if (used.has(name)) continue
|
|
11
|
+
|
|
12
|
+
composition.mounts ??= []
|
|
13
|
+
composition.mounts.push({ name, path, claim })
|
|
14
|
+
used.add(name)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every deployment states what it may take. One that states nothing is `BestEffort`: the
|
|
3
|
+
* first evicted under memory pressure and the last given CPU under contention — which on
|
|
4
|
+
* a busy node is slow enough for its own startup probe to kill it, in a loop, while the
|
|
5
|
+
* chart reports nothing wrong. That is not a thing to find out in production.
|
|
6
|
+
*
|
|
7
|
+
* A deployment takes its own declaration, or the context's `resources` where it has none.
|
|
8
|
+
* Deploying without any is a decision rather than an omission, and it is spelled
|
|
9
|
+
* `resources: null` — at either place.
|
|
10
|
+
*/
|
|
11
|
+
export function resources(context, values) {
|
|
12
|
+
for (const unit of units(values, evicted(context))) {
|
|
13
|
+
// `null` is an answer and `undefined` is not one, so the fallback reads only the latter
|
|
14
|
+
if (unit.deployment.resources === undefined)
|
|
15
|
+
unit.deployment.resources = context.resources
|
|
16
|
+
|
|
17
|
+
if (unit.deployment.resources === undefined)
|
|
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
|
+
)
|
|
23
|
+
|
|
24
|
+
heap(unit.deployment)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The memory limit sizes the heap. Node reads the machine's memory rather than the
|
|
30
|
+
* container's, so without this the heap grows past the limit and the pod is killed instead
|
|
31
|
+
* of collected. What is left of the limit is the process itself: its code, its buffers and
|
|
32
|
+
* its threads. A deployment that states `NODE_OPTIONS` of its own keeps them.
|
|
33
|
+
*/
|
|
34
|
+
function heap(deployment) {
|
|
35
|
+
const limit = deployment.resources?.memory?.[1]
|
|
36
|
+
|
|
37
|
+
if (limit === undefined) return
|
|
38
|
+
|
|
39
|
+
deployment.variables ??= []
|
|
40
|
+
|
|
41
|
+
if (deployment.variables.some((variable) => variable.name === NODE_OPTIONS)) return
|
|
42
|
+
|
|
43
|
+
const megabytes = Math.floor((quantity(limit) * HEAP_SHARE) / 2 ** 20)
|
|
44
|
+
|
|
45
|
+
deployment.variables.push({
|
|
46
|
+
name: NODE_OPTIONS,
|
|
47
|
+
value: `--max-old-space-size=${megabytes}`
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A Kubernetes quantity, in bytes.
|
|
53
|
+
*
|
|
54
|
+
* @param {string | number} value
|
|
55
|
+
* @returns {number}
|
|
56
|
+
*/
|
|
57
|
+
export function quantity(value) {
|
|
58
|
+
const match = String(value).match(/^(\d+(?:\.\d+)?)([KMGTPE]i?|[kmun])?$/)
|
|
59
|
+
|
|
60
|
+
if (match === null) throw new Error(`'${value}' is not a quantity`)
|
|
61
|
+
|
|
62
|
+
const [, number, suffix = ''] = match
|
|
63
|
+
const scale = SUFFIXES[suffix]
|
|
64
|
+
|
|
65
|
+
if (scale === undefined) throw new Error(`'${value}' is not a memory quantity`)
|
|
66
|
+
|
|
67
|
+
return Number(number) * scale
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const NODE_OPTIONS = 'NODE_OPTIONS'
|
|
71
|
+
const HEAP_SHARE = 0.75
|
|
72
|
+
|
|
73
|
+
const SUFFIXES = {
|
|
74
|
+
'': 1,
|
|
75
|
+
K: 1e3,
|
|
76
|
+
M: 1e6,
|
|
77
|
+
G: 1e9,
|
|
78
|
+
T: 1e12,
|
|
79
|
+
P: 1e15,
|
|
80
|
+
E: 1e18,
|
|
81
|
+
Ki: 2 ** 10,
|
|
82
|
+
Mi: 2 ** 20,
|
|
83
|
+
Gi: 2 ** 30,
|
|
84
|
+
Ti: 2 ** 40,
|
|
85
|
+
Pi: 2 ** 50,
|
|
86
|
+
Ei: 2 ** 60
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The labels of the components the context evicts, whether or not the eviction was applied:
|
|
91
|
+
* `toa env -c` does not apply it, since a local run of an evicted component still needs its
|
|
92
|
+
* variables.
|
|
93
|
+
*
|
|
94
|
+
* @returns {Set<string>}
|
|
95
|
+
*/
|
|
96
|
+
function evicted(context) {
|
|
97
|
+
const ids = new Set(context.evicted?.components ?? [])
|
|
98
|
+
|
|
99
|
+
return new Set(
|
|
100
|
+
(context.components ?? [])
|
|
101
|
+
.filter(({ locator }) => ids.has(locator.id))
|
|
102
|
+
.map(({ locator }) => locator.label)
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function* units(values, evicted) {
|
|
107
|
+
if (values.mono !== undefined)
|
|
108
|
+
yield { deployment: values.mono, subject: 'The mono deployment' }
|
|
109
|
+
|
|
110
|
+
for (const composition of values.compositions ?? []) {
|
|
111
|
+
// a composition of what the context evicts is here only for a local run of it, and Toa
|
|
112
|
+
// deploys none of it, so nothing asks what it may take
|
|
113
|
+
if (
|
|
114
|
+
composition.components?.length > 0 &&
|
|
115
|
+
composition.components.every((label) => evicted.has(label))
|
|
116
|
+
)
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
yield { deployment: composition, subject: `Composition '${composition.name}'` }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const service of values.services ?? []) {
|
|
123
|
+
// a service a composition runs has no deployment of its own to size; the composition
|
|
124
|
+
// it runs in states what the pod may take
|
|
125
|
+
if (service.workload !== undefined) continue
|
|
126
|
+
|
|
127
|
+
yield { deployment: service, subject: `Service '${service.name}'` }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -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
|