@toa.io/operations 1.0.0-alpha.29 → 1.0.0-alpha.290
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 +42 -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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
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.290](https://github.com/toa-io/toa/compare/v1.0.0-alpha.289...v1.0.0-alpha.290) (2026-09-07)
|
|
7
|
+
|
|
8
|
+
* fix(operations)!: an image starts as root again ([9eb0db7](https://github.com/toa-io/toa/commit/9eb0db75e3d02098fe75710280e8e15e63b73d57))
|
|
9
|
+
* The runtime's environment is not a component's (#1070), closes [#1070](https://github.com/toa-io/toa/issues/1070)
|
|
10
|
+
|
|
11
|
+
### BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* a component that read `process.env.TOA_*` reads `context` instead;
|
|
14
|
+
`echo(input)` no longer substitutes from the environment; a bash operation sees no
|
|
15
|
+
`TOA_*`; images no longer set `USER node` — see migrations/289.md.
|
|
16
|
+
* an image built by alpha.289 runs its composition as `node` and
|
|
17
|
+
leaves the deployed environment readable under /proc; rebuild on 290. Nothing in
|
|
18
|
+
a context or a manifest changes.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# [1.0.0-alpha.289](https://github.com/toa-io/toa/compare/v1.0.0-alpha.288...v1.0.0-alpha.289) (2026-09-07)
|
|
22
|
+
|
|
23
|
+
* A deploy moves only what changed, and a component sees none of the runtime's environment (#1073) ([f38e3db](https://github.com/toa-io/toa/commit/f38e3db533f24db866c57bfa1ef294eff1eaf499)), closes [#1073](https://github.com/toa-io/toa/issues/1073) [#1064](https://github.com/toa-io/toa/issues/1064) [#1066](https://github.com/toa-io/toa/issues/1066) [#1067](https://github.com/toa-io/toa/issues/1067) [#1068](https://github.com/toa-io/toa/issues/1068) [#1069](https://github.com/toa-io/toa/issues/1069) [#1071](https://github.com/toa-io/toa/issues/1071) [#1070](https://github.com/toa-io/toa/issues/1070) [#1072](https://github.com/toa-io/toa/issues/1072)
|
|
24
|
+
|
|
25
|
+
### BREAKING CHANGES
|
|
26
|
+
|
|
27
|
+
* a component that read `process.env.TOA_*` reads `context` instead;
|
|
28
|
+
`echo(input)` no longer substitutes from the environment; a bash operation sees no
|
|
29
|
+
`TOA_*`; images no longer set `USER node` — see migrations/289.md.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# [1.0.0-alpha.288](https://github.com/toa-io/toa/compare/v1.0.0-alpha.287...v1.0.0-alpha.288) (2026-09-06)
|
|
33
|
+
|
|
34
|
+
**Note:** Version bump only for package @toa.io/operations
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# [1.0.0-alpha.287](https://github.com/toa-io/toa/compare/v1.0.0-alpha.286...v1.0.0-alpha.287) (2026-09-06)
|
|
41
|
+
|
|
42
|
+
**Note:** Version bump only for package @toa.io/operations
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/operations",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.290",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Toa Deployment",
|
|
5
6
|
"homepage": "https://toa.io",
|
|
6
7
|
"author": {
|
|
@@ -16,23 +17,20 @@
|
|
|
16
17
|
"url": "https://github.com/toa-io/toa/issues"
|
|
17
18
|
},
|
|
18
19
|
"engines": {
|
|
19
|
-
"node": ">=
|
|
20
|
+
"node": ">= 24.0.0"
|
|
20
21
|
},
|
|
21
22
|
"publishConfig": {
|
|
22
23
|
"access": "public"
|
|
23
24
|
},
|
|
24
25
|
"main": "src/index.js",
|
|
25
|
-
"types": "types/index.
|
|
26
|
+
"types": "types/index.ts",
|
|
26
27
|
"scripts": {
|
|
27
28
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
|
-
"@toa.io/
|
|
31
|
-
"@toa.io/
|
|
32
|
-
"
|
|
33
|
-
"@toa.io/yaml": "1.0.0-alpha.29",
|
|
34
|
-
"execa": "5.1.1",
|
|
35
|
-
"fs-extra": "11.1.1"
|
|
31
|
+
"@toa.io/generic": "1.0.0-alpha.289",
|
|
32
|
+
"@toa.io/norm": "1.0.0-alpha.289",
|
|
33
|
+
"js-yaml": "5.4.1"
|
|
36
34
|
},
|
|
37
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "c20cb1beb8d6e2bd3194385fbd57c71617a02bfb"
|
|
38
36
|
}
|
package/readme.md
CHANGED
|
@@ -1,9 +1,102 @@
|
|
|
1
1
|
# Toa Operations
|
|
2
2
|
|
|
3
|
+
## Compositions
|
|
4
|
+
|
|
5
|
+
A composition is deployed as one pod. Beside its components it may run extension services,
|
|
6
|
+
which are otherwise each deployed on their own:
|
|
7
|
+
|
|
8
|
+
```yaml
|
|
9
|
+
# context.toa.yaml
|
|
10
|
+
|
|
11
|
+
compositions:
|
|
12
|
+
- name: edge
|
|
13
|
+
components:
|
|
14
|
+
- todos.tasks
|
|
15
|
+
services:
|
|
16
|
+
- exposition
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
See [compositions](../documentation/compositions.md).
|
|
20
|
+
|
|
3
21
|
## Context
|
|
4
22
|
|
|
5
23
|
### Container Registry
|
|
6
24
|
|
|
25
|
+
Deploy images default to `FROM ghcr.io/toa-io/runtime:<runtime.version>`
|
|
26
|
+
(published with each Toa release). The base image already has `@toa.io/runtime`
|
|
27
|
+
installed; composition/service Dockerfiles only install component dependencies.
|
|
28
|
+
|
|
29
|
+
To use a custom base image, set `registry.build.image` (or `composition.image`).
|
|
30
|
+
That image must provide the `toa` CLI, or install it via `registry.build.run`:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
# context.toa.yaml
|
|
34
|
+
|
|
35
|
+
registry:
|
|
36
|
+
build:
|
|
37
|
+
image: node:24.14.0-alpine3.22
|
|
38
|
+
run: npm i -g @toa.io/runtime --omit=dev
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
#### Composition Images
|
|
42
|
+
|
|
43
|
+
A composition is two images in one repository. `composition-<name>:deps-<hash>` is what its
|
|
44
|
+
components depend on, installed on the base image: it is tagged by everything the install
|
|
45
|
+
reads — the runtime version, the base image, the build options and each component's
|
|
46
|
+
`package.json` (and `package-lock.json`, where there is one) — and is built only when one of
|
|
47
|
+
those changes. `composition-<name>:<hash>` is the sources laid over it in a single linked
|
|
48
|
+
layer, so a deploy that changes code alone builds and pushes that layer, and neither
|
|
49
|
+
downloads nor uploads the dependencies again. The same holds for `mono`.
|
|
50
|
+
|
|
51
|
+
A pushed image builds on the `toa` container builder, whose registry exporter is what lays
|
|
52
|
+
an image over a base it never pulled; `toa build` loads on the daemon's own.
|
|
53
|
+
|
|
54
|
+
A dependency named by a moving git ref is installed when the dependencies image is built and
|
|
55
|
+
not again until its manifest changes, so pin such a dependency to a commit and bump it there.
|
|
56
|
+
|
|
57
|
+
#### Extension Service Images
|
|
58
|
+
|
|
59
|
+
`registry.services` says where an extension service's image comes from.
|
|
60
|
+
|
|
61
|
+
`build`, the default, builds one per service into `registry.base`. Its tag carries the
|
|
62
|
+
runtime version, so every Toa release rebuilds them all.
|
|
63
|
+
|
|
64
|
+
`published` deploys the image the extension ships and builds nothing:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
# context.toa.yaml
|
|
68
|
+
|
|
69
|
+
registry:
|
|
70
|
+
services: published
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The images are tagged with the runtime version:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
ghcr.io/toa-io/extension-exposition-gateway:1.0.0-alpha.285
|
|
77
|
+
ghcr.io/toa-io/extension-realtime-streams:1.0.0-alpha.285
|
|
78
|
+
ghcr.io/toa-io/extension-introspection-explorer:1.0.0-alpha.285
|
|
79
|
+
ghcr.io/toa-io/extension-configuration-values:1.0.0-alpha.285
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The cluster pulls them from `ghcr.io` rather than from `registry.base`. They are public and
|
|
83
|
+
`registry.credentials` does not apply; a cluster that cannot reach `ghcr.io` needs `build`.
|
|
84
|
+
|
|
85
|
+
The field is environment-scoped like the rest:
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
# context.toa.yaml
|
|
89
|
+
|
|
90
|
+
registry:
|
|
91
|
+
base@local: localhost:5000
|
|
92
|
+
base@production: registry.digitalocean.com/acme
|
|
93
|
+
services: published
|
|
94
|
+
services@local: build
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
An extension that states no image is built whatever this says, as is a service a
|
|
98
|
+
composition runs in its own pod.
|
|
99
|
+
|
|
7
100
|
#### Build Options
|
|
8
101
|
|
|
9
102
|
```yaml
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @param {toa.deployment.Composition[]} compositions
|
|
5
3
|
* @returns {string[]}
|
|
6
4
|
*/
|
|
7
|
-
const components = (compositions) => {
|
|
5
|
+
export const components = (compositions) => {
|
|
8
6
|
/** @type {Set<string>} */
|
|
9
7
|
const components = new Set()
|
|
10
8
|
|
|
@@ -16,5 +14,3 @@ const components = (compositions) => {
|
|
|
16
14
|
|
|
17
15
|
return Array.from(components)
|
|
18
16
|
}
|
|
19
|
-
|
|
20
|
-
exports.components = components
|
|
@@ -1,11 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
import { addVariables } from './variables.js'
|
|
2
|
+
import { addMounts } from './mounts.js'
|
|
3
|
+
import { fold } from './fold.js'
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function compositions (compositions, variables) {
|
|
5
|
+
export function compositions(compositions, dependency) {
|
|
6
6
|
for (const composition of compositions) {
|
|
7
|
-
|
|
7
|
+
const claimed = (dependency.services ?? []).filter(
|
|
8
|
+
(service) => service.workload?.includes(composition.name) === true
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// a service brings components of its own — the identity components inside the gateway —
|
|
12
|
+
// and their variables are keyed by their own labels, not by the composition's
|
|
13
|
+
const keys = composition.components.concat(
|
|
14
|
+
...claimed.map((service) => service.components ?? [])
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
addVariables(composition, dependency.variables, keys)
|
|
18
|
+
addMounts(composition, dependency.mounts, keys)
|
|
19
|
+
|
|
20
|
+
if (claimed.length > 0) {
|
|
21
|
+
// the label each hosted service's own Service selects on
|
|
22
|
+
composition.hosted = claimed.map((service) => service.name)
|
|
23
|
+
|
|
24
|
+
fold(composition, claimed, dependency)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (dependency.probe !== undefined && dependency.probe !== false)
|
|
28
|
+
composition.probe ??= dependency.probe
|
|
8
29
|
}
|
|
9
30
|
}
|
|
10
|
-
|
|
11
|
-
exports.compositions = compositions
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @param {toa.deployment.dependency.Reference[]} references
|
|
5
3
|
* @returns {*}
|
|
6
4
|
*/
|
|
7
|
-
const dependencies = (references) => {
|
|
5
|
+
export const dependencies = (references) => {
|
|
8
6
|
return references?.reduce((map, reference) => {
|
|
9
7
|
const { name, alias, values } = reference
|
|
10
8
|
|
|
@@ -13,6 +11,3 @@ const dependencies = (references) => {
|
|
|
13
11
|
return map
|
|
14
12
|
}, {})
|
|
15
13
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
exports.dependencies = dependencies
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tells each component which of its events something consumes. An event nobody consumes gets
|
|
3
|
+
* no emitter, no exchange and no outbox row, and a component none of whose events are consumed
|
|
4
|
+
* gets no outbox at all.
|
|
5
|
+
*
|
|
6
|
+
* A component that declares events always gets the variable, empty when nothing is consumed —
|
|
7
|
+
* an absent variable means every event, which is what a run without a deployment gets.
|
|
8
|
+
*
|
|
9
|
+
* @param {toa.norm.Context} context
|
|
10
|
+
* @param {toa.deployment.Dependency} dependency
|
|
11
|
+
*/
|
|
12
|
+
export function events(context, dependency) {
|
|
13
|
+
const components = deployed(context)
|
|
14
|
+
const consumed = collect(components, context.events, dependency.events)
|
|
15
|
+
|
|
16
|
+
for (const { locator, events } of components) {
|
|
17
|
+
if (events === undefined) continue
|
|
18
|
+
|
|
19
|
+
const labels = Object.keys(events).filter((label) =>
|
|
20
|
+
consumed.has(locator.id + '.' + label)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
dependency.variables[locator.label] ??= []
|
|
24
|
+
|
|
25
|
+
dependency.variables[locator.label].push({
|
|
26
|
+
name: VARIABLE + locator.uppercase,
|
|
27
|
+
value: labels.join(' ')
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Every component this context deploys, its own and those its extensions bring. `components`
|
|
34
|
+
* holds only the former; the latter reach a deployment as instances of what they depend on,
|
|
35
|
+
* which is also how they are given the rest of their variables.
|
|
36
|
+
*/
|
|
37
|
+
function deployed(context) {
|
|
38
|
+
const components = new Map()
|
|
39
|
+
|
|
40
|
+
for (const instances of Object.values(context.dependencies ?? {}))
|
|
41
|
+
for (const { component } of instances) components.set(component.locator.id, component)
|
|
42
|
+
|
|
43
|
+
return [...components.values()]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Everything that consumes an event of this context: the receivers of its own components, what
|
|
48
|
+
* a dependency declares it consumes, and what the context says is consumed outside it.
|
|
49
|
+
*/
|
|
50
|
+
function collect(components, declared, contributed) {
|
|
51
|
+
const consumed = new Set(declared ?? [])
|
|
52
|
+
|
|
53
|
+
for (const label of contributed ?? []) consumed.add(label)
|
|
54
|
+
|
|
55
|
+
for (const { receivers } of components)
|
|
56
|
+
for (const [label, receiver] of Object.entries(receivers ?? {}))
|
|
57
|
+
// a receiver with a source consumes from a foreign broker, not from this context
|
|
58
|
+
if (receiver.source === undefined) consumed.add(label)
|
|
59
|
+
|
|
60
|
+
return consumed
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const VARIABLE = 'TOA_EVENTS_'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a workload takes on by running services in its own pod: their variables, the ports
|
|
3
|
+
* they bind, and a readiness probe answered by one of them rather than by the workload.
|
|
4
|
+
*
|
|
5
|
+
* What it does *not* take on is how they are reached. A composition-hosted service keeps
|
|
6
|
+
* its own Service and Ingress — only the pods behind them change — so the ingress union
|
|
7
|
+
* belongs to `mono`, the one workload that fronts every service under a single name.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} workload
|
|
10
|
+
* @param {toa.deployment.dependency.Service[]} services
|
|
11
|
+
* @param {toa.deployment.Dependency} dependency
|
|
12
|
+
*/
|
|
13
|
+
export function fold(workload, services, dependency) {
|
|
14
|
+
workload.variables ??= []
|
|
15
|
+
|
|
16
|
+
for (const service of services) {
|
|
17
|
+
if (service.variables !== undefined)
|
|
18
|
+
for (const variable of service.variables)
|
|
19
|
+
if (!workload.variables.some((item) => item.name === variable.name))
|
|
20
|
+
workload.variables.push(variable)
|
|
21
|
+
|
|
22
|
+
// every declared port is bound by the single process, none is primary
|
|
23
|
+
if (service.port !== undefined)
|
|
24
|
+
(workload.backends ??= []).push({
|
|
25
|
+
port: service.port,
|
|
26
|
+
path: service.ingress?.path ?? '/'
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
if (service.probe !== undefined && service.probe !== false)
|
|
30
|
+
workload.probe = service.probe
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
workload.probe === undefined &&
|
|
35
|
+
dependency.probe !== undefined &&
|
|
36
|
+
dependency.probe !== false
|
|
37
|
+
)
|
|
38
|
+
workload.probe = dependency.probe
|
|
39
|
+
|
|
40
|
+
// the more specific prefix must come first, whatever the controller's tie-break
|
|
41
|
+
workload.backends?.sort((a, b) => b.path.length - a.path.length)
|
|
42
|
+
}
|
|
@@ -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,103 @@
|
|
|
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)) {
|
|
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
|
+
function* units(values) {
|
|
90
|
+
if (values.mono !== undefined)
|
|
91
|
+
yield { deployment: values.mono, subject: 'The mono deployment' }
|
|
92
|
+
|
|
93
|
+
for (const composition of values.compositions ?? [])
|
|
94
|
+
yield { deployment: composition, subject: `Composition '${composition.name}'` }
|
|
95
|
+
|
|
96
|
+
for (const service of values.services ?? []) {
|
|
97
|
+
// a service a composition runs has no deployment of its own to size; the composition
|
|
98
|
+
// it runs in states what the pod may take
|
|
99
|
+
if (service.workload !== undefined) continue
|
|
100
|
+
|
|
101
|
+
yield { deployment: service, subject: `Service '${service.name}'` }
|
|
102
|
+
}
|
|
103
|
+
}
|