@toa.io/operations 1.0.0-alpha.27 → 1.0.0-alpha.272
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 +153 -0
- package/image/runtime.Dockerfile +4 -0
- package/package.json +8 -8
- package/readme.md +16 -0
- package/src/deployment/.deployment/.describe/compositions.js +7 -2
- package/src/deployment/.deployment/.describe/events.js +67 -0
- package/src/deployment/.deployment/.describe/mounts.js +24 -0
- package/src/deployment/.deployment/.describe/resources.js +37 -0
- package/src/deployment/.deployment/.describe/services.js +28 -1
- package/src/deployment/.deployment/.describe/services.test.js +61 -0
- package/src/deployment/.deployment/.describe/variables.js +6 -5
- package/src/deployment/.deployment/describe.js +130 -6
- package/src/deployment/.deployment/merge.js +45 -1
- package/src/deployment/.deployment/merge.test.js +74 -0
- package/src/deployment/chart/templates/compositions.yaml +70 -0
- package/src/deployment/chart/templates/mono.yaml +177 -0
- package/src/deployment/chart/templates/services.yaml +71 -4
- package/src/deployment/chart/values.yaml +11 -1
- package/src/deployment/composition.js +1 -0
- package/src/deployment/deployment.js +6 -2
- package/src/deployment/factory.js +24 -8
- package/src/deployment/images/composition.Dockerfile +2 -2
- package/src/deployment/images/composition.js +16 -11
- package/src/deployment/images/factory.js +12 -3
- package/src/deployment/images/image.js +16 -7
- package/src/deployment/images/mono.Dockerfile +18 -0
- package/src/deployment/images/mono.js +71 -0
- package/src/deployment/images/runtime-base.test.js +90 -0
- package/src/deployment/images/service.Dockerfile +3 -3
- package/src/deployment/operator.js +4 -0
- package/src/deployment/registry.js +64 -16
- package/src/deployment/registry.test.js +175 -0
- package/types/_deployment/dependency.d.ts +2 -0
- package/types/_deployment/images/image.d.ts +4 -2
- package/types/_deployment/registry.d.ts +9 -7
- package/types/dependency.ts +71 -0
- package/types/dependency.d.ts +0 -39
- /package/types/{index.d.ts → index.ts} +0 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
# [1.0.0-alpha.272](https://github.com/toa-io/toa/compare/v1.0.0-alpha.271...v1.0.0-alpha.272) (2026-09-01)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
* feat(atomicity)!: take a quorum of independent servers ([862562f](https://github.com/toa-io/toa/commit/862562f24d77ec00127dbbd88d43802b208c643b))
|
|
10
|
+
* refactor(atomicity)!: take one Redis, not a cluster ([b0a4770](https://github.com/toa-io/toa/commit/b0a4770dff5dfe8fee4b9ae637a4dd297454fcd4))
|
|
11
|
+
* refactor(core)!: pump the outbox in one cycle ([bf590fe](https://github.com/toa-io/toa/commit/bf590fe8ed59c701b5fd1a91ba4874685b79242f))
|
|
12
|
+
* refactor(atomicity)!: rename the connector and free it of the outbox ([21f1a41](https://github.com/toa-io/toa/commit/21f1a41aceafcfd35c7620014062fe46b54afa95))
|
|
13
|
+
* feat(core)!: commit events with the state that produced them ([1eb68cc](https://github.com/toa-io/toa/commit/1eb68cc435dbfa03faa16009fceb866693d22e1a)), closes [#20](https://github.com/toa-io/toa/issues/20)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **atomicity:** make the interval a setting, and stop repeating n-and-i ([5c3a6d1](https://github.com/toa-io/toa/commit/5c3a6d1533751a21b7eb7d9c737f1a270bf5a5ae))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### BREAKING CHANGES
|
|
22
|
+
|
|
23
|
+
* `atomicity.redis` accepts a list again, of independent servers rather
|
|
24
|
+
than cluster nodes, and refuses an even number of them.
|
|
25
|
+
|
|
26
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
27
|
+
* `atomicity.redis` is a string. A list is refused at export.
|
|
28
|
+
|
|
29
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
30
|
+
* `Storage.outbox.pending` takes a fourth argument, the id to
|
|
31
|
+
continue from.
|
|
32
|
+
|
|
33
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
34
|
+
* `@toa.io/partitions.redis` is now `@toa.io/atomicity`, and its
|
|
35
|
+
`lanes(total)` is `slots(total)`. Redis is declared as `atomicity` in the context
|
|
36
|
+
rather than `outbox.redis`, and read from `TOA_ATOMICITY_REDIS`.
|
|
37
|
+
`TOA_OUTBOX_PARTITION_INTERVAL` is `TOA_ATOMICITY_INTERVAL`.
|
|
38
|
+
|
|
39
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
40
|
+
* `event.changeset` is removed; use `origin` and `state`. `State`
|
|
41
|
+
takes an `Outbox` in place of an `Emission`. `difference` is dropped from
|
|
42
|
+
`@toa.io/generic` along with its last caller.
|
|
43
|
+
|
|
44
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# [1.0.0-alpha.270](https://github.com/toa-io/toa/compare/v1.0.0-alpha.269...v1.0.0-alpha.270) (2026-08-31)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
### Bug Fixes
|
|
54
|
+
|
|
55
|
+
* **operations:** let a composition set its own base image ([61ba331](https://github.com/toa-io/toa/commit/61ba3319dc91915a3197d348111af411bbd5bff1))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# [1.0.0-alpha.266](https://github.com/toa-io/toa/compare/v1.0.0-alpha.265...v1.0.0-alpha.266) (2026-08-29)
|
|
62
|
+
|
|
63
|
+
**Note:** Version bump only for package @toa.io/operations
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# [1.0.0-alpha.265](https://github.com/toa-io/toa/compare/v1.0.0-alpha.264...v1.0.0-alpha.265) (2026-08-29)
|
|
70
|
+
|
|
71
|
+
**Note:** Version bump only for package @toa.io/operations
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# [1.0.0-alpha.264](https://github.com/toa-io/toa/compare/v1.0.0-alpha.263...v1.0.0-alpha.264) (2026-08-29)
|
|
78
|
+
|
|
79
|
+
**Note:** Version bump only for package @toa.io/operations
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# [1.0.0-alpha.263](https://github.com/toa-io/toa/compare/v1.0.0-alpha.262...v1.0.0-alpha.263) (2026-08-29)
|
|
86
|
+
|
|
87
|
+
**Note:** Version bump only for package @toa.io/operations
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# [1.0.0-alpha.262](https://github.com/toa-io/toa/compare/v1.0.0-alpha.261...v1.0.0-alpha.262) (2026-08-28)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
### Bug Fixes
|
|
97
|
+
|
|
98
|
+
* **cli:** boot toa mono from components and env, not context ([1518f72](https://github.com/toa-io/toa/commit/1518f729d7611ae88e0538f286231d283a7098ef))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
* feat(operations)!: require service ports to be unique ([b22cf87](https://github.com/toa-io/toa/commit/b22cf8770d24c555b5f355a5f117a9043a57799b))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
### Features
|
|
105
|
+
|
|
106
|
+
* **operations:** let a service claim a path prefix on the host ([02677d1](https://github.com/toa-io/toa/commit/02677d17fcd4222b07c3be458d93bcc496a016a8))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
### BREAKING CHANGES
|
|
110
|
+
|
|
111
|
+
* an application whose own extension runs a service on a port
|
|
112
|
+
already taken by another — 8000 by the exposition gateway, 8001 by the telemetry
|
|
113
|
+
readiness probe — must move it.
|
|
114
|
+
|
|
115
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# [1.0.0-alpha.261](https://github.com/toa-io/toa/compare/v1.0.0-alpha.260...v1.0.0-alpha.261) (2026-08-26)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
### Bug Fixes
|
|
125
|
+
|
|
126
|
+
* **operations:** include extension pointer variables in --mono ([f8be23d](https://github.com/toa-io/toa/commit/f8be23d2aaf7bbe1b0580101c8bf4d32c3cab99f))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# [1.0.0-alpha.259](https://github.com/toa-io/toa/compare/v1.0.0-alpha.258...v1.0.0-alpha.259) (2026-08-24)
|
|
133
|
+
|
|
134
|
+
**Note:** Version bump only for package @toa.io/operations
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# [1.0.0-alpha.257](https://github.com/toa-io/toa/compare/v1.0.0-alpha.256...v1.0.0-alpha.257) (2026-08-24)
|
|
141
|
+
|
|
142
|
+
**Note:** Version bump only for package @toa.io/operations
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# [1.0.0-alpha.256](https://github.com/toa-io/toa/compare/v1.0.0-alpha.255...v1.0.0-alpha.256) (2026-08-23)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
### Features
|
|
152
|
+
|
|
153
|
+
* deploy a single mono image with toa deploy --mono ([a7f14f9](https://github.com/toa-io/toa/commit/a7f14f9877a280e9c74d16c195028b35d74fb15a))
|
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.272",
|
|
4
4
|
"description": "Toa Deployment",
|
|
5
5
|
"homepage": "https://toa.io",
|
|
6
6
|
"author": {
|
|
@@ -16,23 +16,23 @@
|
|
|
16
16
|
"url": "https://github.com/toa-io/toa/issues"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
|
-
"node": ">=
|
|
19
|
+
"node": ">= 24.0.0"
|
|
20
20
|
},
|
|
21
21
|
"publishConfig": {
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"main": "src/index.js",
|
|
25
|
-
"types": "types/index.
|
|
25
|
+
"types": "types/index.ts",
|
|
26
26
|
"scripts": {
|
|
27
27
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@toa.io/filesystem": "1.0.0-alpha.
|
|
31
|
-
"@toa.io/generic": "1.0.0-alpha.
|
|
32
|
-
"@toa.io/norm": "1.0.0-alpha.
|
|
33
|
-
"@toa.io/yaml": "1.0.0-alpha.
|
|
30
|
+
"@toa.io/filesystem": "1.0.0-alpha.272",
|
|
31
|
+
"@toa.io/generic": "1.0.0-alpha.272",
|
|
32
|
+
"@toa.io/norm": "1.0.0-alpha.272",
|
|
33
|
+
"@toa.io/yaml": "1.0.0-alpha.272",
|
|
34
34
|
"execa": "5.1.1",
|
|
35
35
|
"fs-extra": "11.1.1"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "1a451755445935c04cf5b373bcc73942cac0bb10"
|
|
38
38
|
}
|
package/readme.md
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
### Container Registry
|
|
6
6
|
|
|
7
|
+
Deploy images default to `FROM ghcr.io/toa-io/runtime:<runtime.version>`
|
|
8
|
+
(published with each Toa release). The base image already has `@toa.io/runtime`
|
|
9
|
+
installed; composition/service Dockerfiles only install component dependencies.
|
|
10
|
+
|
|
11
|
+
To use a custom base image, set `registry.build.image` (or `composition.image`).
|
|
12
|
+
That image must provide the `toa` CLI, or install it via `registry.build.run`:
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
# context.toa.yaml
|
|
16
|
+
|
|
17
|
+
registry:
|
|
18
|
+
build:
|
|
19
|
+
image: node:24.14.0-alpine3.22
|
|
20
|
+
run: npm i -g @toa.io/runtime --omit=dev
|
|
21
|
+
```
|
|
22
|
+
|
|
7
23
|
#### Build Options
|
|
8
24
|
|
|
9
25
|
```yaml
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { addVariables } = require('./variables')
|
|
4
|
+
const { addMounts } = require('./mounts')
|
|
4
5
|
|
|
5
|
-
function compositions (compositions,
|
|
6
|
+
function compositions (compositions, dependency) {
|
|
6
7
|
for (const composition of compositions) {
|
|
7
|
-
addVariables(composition, variables)
|
|
8
|
+
addVariables(composition, dependency.variables)
|
|
9
|
+
addMounts(composition, dependency.mounts)
|
|
10
|
+
|
|
11
|
+
if (dependency.probe !== undefined && dependency.probe !== false)
|
|
12
|
+
composition.probe ??= dependency.probe
|
|
8
13
|
}
|
|
9
14
|
}
|
|
10
15
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tells each component which of its events something consumes. An event nobody consumes gets
|
|
5
|
+
* no emitter, no exchange and no outbox row, and a component none of whose events are consumed
|
|
6
|
+
* gets no outbox at all.
|
|
7
|
+
*
|
|
8
|
+
* A component that declares events always gets the variable, empty when nothing is consumed —
|
|
9
|
+
* an absent variable means every event, which is what a run without a deployment gets.
|
|
10
|
+
*
|
|
11
|
+
* @param {toa.norm.Context} context
|
|
12
|
+
* @param {toa.deployment.Dependency} dependency
|
|
13
|
+
*/
|
|
14
|
+
function events (context, dependency) {
|
|
15
|
+
const components = deployed(context)
|
|
16
|
+
const consumed = collect(components, context.events, dependency.events)
|
|
17
|
+
|
|
18
|
+
for (const { locator, events } of components) {
|
|
19
|
+
if (events === undefined) continue
|
|
20
|
+
|
|
21
|
+
const labels = Object.keys(events)
|
|
22
|
+
.filter((label) => consumed.has(locator.id + '.' + label))
|
|
23
|
+
|
|
24
|
+
dependency.variables[locator.label] ??= []
|
|
25
|
+
|
|
26
|
+
dependency.variables[locator.label].push({
|
|
27
|
+
name: VARIABLE + locator.uppercase,
|
|
28
|
+
value: labels.join(' ')
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Every component this context deploys, its own and those its extensions bring. `components`
|
|
35
|
+
* holds only the former; the latter reach a deployment as instances of what they depend on,
|
|
36
|
+
* which is also how they are given the rest of their variables.
|
|
37
|
+
*/
|
|
38
|
+
function deployed (context) {
|
|
39
|
+
const components = new Map()
|
|
40
|
+
|
|
41
|
+
for (const instances of Object.values(context.dependencies ?? {}))
|
|
42
|
+
for (const { component } of instances)
|
|
43
|
+
components.set(component.locator.id, component)
|
|
44
|
+
|
|
45
|
+
return [...components.values()]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Everything that consumes an event of this context: the receivers of its own components, what
|
|
50
|
+
* a dependency declares it consumes, and what the context says is consumed outside it.
|
|
51
|
+
*/
|
|
52
|
+
function collect (components, declared, contributed) {
|
|
53
|
+
const consumed = new Set(declared ?? [])
|
|
54
|
+
|
|
55
|
+
for (const label of contributed ?? []) consumed.add(label)
|
|
56
|
+
|
|
57
|
+
for (const { receivers } of components)
|
|
58
|
+
for (const [label, receiver] of Object.entries(receivers ?? {}))
|
|
59
|
+
// a receiver with a source consumes from a foreign broker, not from this context
|
|
60
|
+
if (receiver.source === undefined) consumed.add(label)
|
|
61
|
+
|
|
62
|
+
return consumed
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const VARIABLE = 'TOA_EVENTS_'
|
|
66
|
+
|
|
67
|
+
exports.events = events
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
function addMounts (composition, mounts, keys = composition.components) {
|
|
4
|
+
if (mounts === undefined)
|
|
5
|
+
return
|
|
6
|
+
|
|
7
|
+
const used = new Set()
|
|
8
|
+
|
|
9
|
+
for (const [key, mount] of Object.entries(mounts)) {
|
|
10
|
+
if (key !== 'global' && !keys?.includes(key))
|
|
11
|
+
continue
|
|
12
|
+
|
|
13
|
+
for (const { name, path, claim } of mount) {
|
|
14
|
+
if (used.has(name))
|
|
15
|
+
continue
|
|
16
|
+
|
|
17
|
+
composition.mounts ??= []
|
|
18
|
+
composition.mounts.push({ name, path, claim })
|
|
19
|
+
used.add(name)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
exports.addMounts = addMounts
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Every deployment states what it may take. One that states nothing is `BestEffort`: the
|
|
5
|
+
* first evicted under memory pressure and the last given CPU under contention — which on
|
|
6
|
+
* a busy node is slow enough for its own startup probe to kill it, in a loop, while the
|
|
7
|
+
* chart reports nothing wrong. That is not a thing to find out in production.
|
|
8
|
+
*
|
|
9
|
+
* A deployment takes its own declaration, or the context's `resources` where it has none.
|
|
10
|
+
* Deploying without any is a decision rather than an omission, and it is spelled
|
|
11
|
+
* `resources: null` — at either place.
|
|
12
|
+
*/
|
|
13
|
+
function resources (context, values) {
|
|
14
|
+
for (const unit of units(values)) {
|
|
15
|
+
// `null` is an answer and `undefined` is not one, so the fallback reads only the latter
|
|
16
|
+
if (unit.deployment.resources === undefined)
|
|
17
|
+
unit.deployment.resources = context.resources
|
|
18
|
+
|
|
19
|
+
if (unit.deployment.resources === undefined)
|
|
20
|
+
throw new Error(`${unit.subject} declares no resources. ` +
|
|
21
|
+
'Declare them on it or as the context\'s \'resources\', ' +
|
|
22
|
+
'or \'resources: null\' to deploy it without any.')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function * units (values) {
|
|
27
|
+
if (values.mono !== undefined)
|
|
28
|
+
yield { deployment: values.mono, subject: 'The mono deployment' }
|
|
29
|
+
|
|
30
|
+
for (const composition of values.compositions ?? [])
|
|
31
|
+
yield { deployment: composition, subject: `Composition '${composition.name}'` }
|
|
32
|
+
|
|
33
|
+
for (const service of values.services ?? [])
|
|
34
|
+
yield { deployment: service, subject: `Service '${service.name}'` }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
exports.resources = resources
|
|
@@ -2,10 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
const { addVariables } = require('./variables')
|
|
4
4
|
|
|
5
|
-
function services (services, variables) {
|
|
5
|
+
function services (services, variables, probe, ingress) {
|
|
6
6
|
for (const service of services) {
|
|
7
7
|
addVariables(service, variables)
|
|
8
|
+
|
|
9
|
+
if (service.probe === false)
|
|
10
|
+
delete service.probe
|
|
11
|
+
else if (service.probe === undefined && probe !== undefined && probe !== false)
|
|
12
|
+
service.probe = probe
|
|
13
|
+
|
|
14
|
+
if (service.ingress !== undefined)
|
|
15
|
+
expose(service, ingress)
|
|
8
16
|
}
|
|
9
17
|
}
|
|
10
18
|
|
|
19
|
+
/**
|
|
20
|
+
* A service declares only its own intent — a path, a port. Where that lands is
|
|
21
|
+
* the context's business, so everything else comes from its `ingress` section.
|
|
22
|
+
* What the service did declare wins.
|
|
23
|
+
*/
|
|
24
|
+
function expose (service, ingress = {}) {
|
|
25
|
+
const declared = Object.fromEntries(
|
|
26
|
+
Object.entries(service.ingress).filter(([, value]) => value !== undefined))
|
|
27
|
+
|
|
28
|
+
service.ingress = Object.assign({}, ingress, declared)
|
|
29
|
+
|
|
30
|
+
if (service.ingress.hosts === undefined)
|
|
31
|
+
throw new Error(`Service '${service.name}' declares an ingress, but no hosts are defined. ` +
|
|
32
|
+
"Declare them in the context's 'ingress' section.")
|
|
33
|
+
|
|
34
|
+
if (service.port === undefined)
|
|
35
|
+
throw new Error(`Service '${service.name}' declares an ingress, but no port.`)
|
|
36
|
+
}
|
|
37
|
+
|
|
11
38
|
exports.services = services
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { services } = require('./services')
|
|
4
|
+
|
|
5
|
+
const service = (extra = {}) => ({ group: 'group', name: 'group-one', version: '0', ...extra })
|
|
6
|
+
|
|
7
|
+
it('should leave a service without an ingress alone', () => {
|
|
8
|
+
const list = [service()]
|
|
9
|
+
|
|
10
|
+
services(list, {}, undefined, { hosts: ['api.dev'] })
|
|
11
|
+
|
|
12
|
+
expect(list[0].ingress).toBeUndefined()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* A service declares only where it wants to land; the cluster-level plumbing
|
|
17
|
+
* belongs to the context.
|
|
18
|
+
*/
|
|
19
|
+
it('should supply hosts, class and annotations from the context', () => {
|
|
20
|
+
const list = [service({ port: 8002, ingress: { path: '/.introspection' } })]
|
|
21
|
+
|
|
22
|
+
services(list, {}, undefined, { hosts: ['api.dev'], class: 'alb', annotations: { a: 'b' } })
|
|
23
|
+
|
|
24
|
+
expect(list[0].ingress).toStrictEqual({
|
|
25
|
+
path: '/.introspection',
|
|
26
|
+
hosts: ['api.dev'],
|
|
27
|
+
class: 'alb',
|
|
28
|
+
annotations: { a: 'b' }
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should keep what the service declared itself', () => {
|
|
33
|
+
const list = [service({ port: 8000, ingress: { path: '/', hosts: ['own.dev'] } })]
|
|
34
|
+
|
|
35
|
+
services(list, {}, undefined, { hosts: ['api.dev'], class: 'alb' })
|
|
36
|
+
|
|
37
|
+
expect(list[0].ingress.hosts).toStrictEqual(['own.dev'])
|
|
38
|
+
expect(list[0].ingress.class).toStrictEqual('alb')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should ignore properties the service left undefined', () => {
|
|
42
|
+
const list = [service({ port: 8000, ingress: { path: '/', class: undefined } })]
|
|
43
|
+
|
|
44
|
+
services(list, {}, undefined, { hosts: ['api.dev'], class: 'alb' })
|
|
45
|
+
|
|
46
|
+
expect(list[0].ingress.class).toStrictEqual('alb')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should reject an ingress with nowhere to land', () => {
|
|
50
|
+
const list = [service({ port: 8002, ingress: { path: '/.introspection' } })]
|
|
51
|
+
|
|
52
|
+
expect(() => services(list, {}, undefined, undefined))
|
|
53
|
+
.toThrow("Service 'group-one' declares an ingress, but no hosts are defined")
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should reject an ingress without a port', () => {
|
|
57
|
+
const list = [service({ ingress: { path: '/.introspection' } })]
|
|
58
|
+
|
|
59
|
+
expect(() => services(list, {}, undefined, { hosts: ['api.dev'] }))
|
|
60
|
+
.toThrow("Service 'group-one' declares an ingress, but no port")
|
|
61
|
+
})
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
function addVariables (
|
|
4
|
-
|
|
3
|
+
function addVariables (composition, variables, keys = composition.components) {
|
|
4
|
+
composition.variables ??= []
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const used = new Set(composition.variables.map((variable) => variable.name))
|
|
7
7
|
|
|
8
8
|
for (const [key, set] of Object.entries(variables)) {
|
|
9
|
-
if (key !== 'global' && !
|
|
9
|
+
if (key !== 'global' && !keys?.includes(key))
|
|
10
|
+
continue
|
|
10
11
|
|
|
11
12
|
for (const variable of set) {
|
|
12
13
|
if (used.has(variable.name)) continue
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
composition.variables.push(variable)
|
|
15
16
|
used.add(variable.name)
|
|
16
17
|
}
|
|
17
18
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const desc = require('./.describe')
|
|
4
|
+
const { addVariables } = require('./.describe/variables')
|
|
5
|
+
const { addMounts } = require('./.describe/mounts')
|
|
6
|
+
const { resources } = require('./.describe/resources')
|
|
7
|
+
const { events } = require('./.describe/events')
|
|
4
8
|
|
|
5
|
-
const describe = (context, compositions, dependency) => {
|
|
9
|
+
const describe = (context, compositions, dependency, image) => {
|
|
6
10
|
const { services } = dependency
|
|
7
11
|
|
|
8
12
|
dependency.variables.global ??= []
|
|
@@ -17,18 +21,138 @@ const describe = (context, compositions, dependency) => {
|
|
|
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(`'atomicity.redis' takes an odd number of addresses, ` +
|
|
33
|
+
`${addresses.length} given`)
|
|
34
|
+
|
|
35
|
+
dependency.variables.global.push({
|
|
36
|
+
name: 'TOA_ATOMICITY_REDIS',
|
|
37
|
+
value: addresses.join(' ')
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (atomicity?.interval !== undefined)
|
|
42
|
+
dependency.variables.global.push({
|
|
43
|
+
name: 'TOA_ATOMICITY_INTERVAL',
|
|
44
|
+
value: String(atomicity.interval)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const outbox = context.outbox
|
|
48
|
+
|
|
49
|
+
if (outbox?.interval !== undefined)
|
|
50
|
+
dependency.variables.global.push({
|
|
51
|
+
name: 'TOA_OUTBOX_INTERVAL',
|
|
52
|
+
value: String(outbox.interval)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (outbox?.batch !== undefined)
|
|
56
|
+
dependency.variables.global.push({
|
|
57
|
+
name: 'TOA_OUTBOX_BATCH',
|
|
58
|
+
value: String(outbox.batch)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
if (outbox?.retention !== undefined)
|
|
62
|
+
dependency.variables.global.push({
|
|
63
|
+
name: 'TOA_OUTBOX_RETENTION',
|
|
64
|
+
value: String(outbox.retention)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
events(context, dependency)
|
|
68
|
+
|
|
21
69
|
const credentials = context.registry?.credentials
|
|
22
70
|
|
|
23
|
-
|
|
24
|
-
|
|
71
|
+
if (image !== undefined) {
|
|
72
|
+
const mono = unit(context, dependency)
|
|
73
|
+
|
|
74
|
+
mono.image = image.reference
|
|
75
|
+
|
|
76
|
+
const values = {
|
|
77
|
+
compositions: [],
|
|
78
|
+
components: mono.components,
|
|
79
|
+
services: [],
|
|
80
|
+
credentials,
|
|
81
|
+
mono
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
resources(context, values)
|
|
25
85
|
|
|
26
|
-
|
|
86
|
+
return values
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const components = desc.components(compositions)
|
|
90
|
+
|
|
91
|
+
desc.compositions(compositions, dependency)
|
|
92
|
+
desc.services(services, dependency.variables, dependency.probe, context.ingress)
|
|
93
|
+
|
|
94
|
+
const values = {
|
|
27
95
|
compositions,
|
|
28
96
|
components,
|
|
29
97
|
services,
|
|
30
98
|
credentials
|
|
31
99
|
}
|
|
100
|
+
|
|
101
|
+
resources(context, values)
|
|
102
|
+
|
|
103
|
+
return values
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function unit (context, dependency) {
|
|
107
|
+
const components = (context.components ?? []).map((component) => component.locator.label)
|
|
108
|
+
|
|
109
|
+
const variables = dependency.variables ?? {}
|
|
110
|
+
const mounts = dependency.mounts ?? {}
|
|
111
|
+
|
|
112
|
+
const mono = {
|
|
113
|
+
replicas: context.mono?.replicas,
|
|
114
|
+
resources: context.mono?.resources,
|
|
115
|
+
components,
|
|
116
|
+
variables: []
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (context.ingress !== undefined)
|
|
120
|
+
mono.ingress = Object.assign({}, context.ingress)
|
|
121
|
+
|
|
122
|
+
addVariables(mono, variables, Object.keys(variables))
|
|
123
|
+
addMounts(mono, dependency.mounts, Object.keys(mounts))
|
|
124
|
+
|
|
125
|
+
for (const service of dependency.services ?? []) {
|
|
126
|
+
if (service.variables !== undefined)
|
|
127
|
+
for (const variable of service.variables)
|
|
128
|
+
if (!mono.variables.some((item) => item.name === variable.name))
|
|
129
|
+
mono.variables.push(variable)
|
|
130
|
+
|
|
131
|
+
// every declared port is bound by the single mono process, none is primary
|
|
132
|
+
if (service.port !== undefined)
|
|
133
|
+
(mono.backends ??= []).push({ port: service.port, path: service.ingress?.path ?? '/' })
|
|
134
|
+
|
|
135
|
+
if (service.ingress !== undefined) {
|
|
136
|
+
const { path, hosts, ...ingress } = service.ingress
|
|
137
|
+
|
|
138
|
+
mono.ingress = Object.assign(mono.ingress ?? {}, ingress)
|
|
139
|
+
|
|
140
|
+
// one Ingress serves every path, so its hosts are the union, not the last word
|
|
141
|
+
if (hosts !== undefined)
|
|
142
|
+
mono.ingress.hosts = [...new Set([...(mono.ingress.hosts ?? []), ...hosts])]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (service.probe !== undefined && service.probe !== false)
|
|
146
|
+
mono.probe = service.probe
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (mono.probe === undefined && dependency.probe !== undefined && dependency.probe !== false)
|
|
150
|
+
mono.probe = dependency.probe
|
|
151
|
+
|
|
152
|
+
// the more specific prefix must come first, whatever the controller's tie-break
|
|
153
|
+
mono.backends?.sort((a, b) => b.path.length - a.path.length)
|
|
154
|
+
|
|
155
|
+
return mono
|
|
32
156
|
}
|
|
33
157
|
|
|
34
158
|
exports.describe = describe
|