@toa.io/operations 1.0.0-alpha.24 → 1.0.0-alpha.243
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/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/mounts.js +24 -0
- package/src/deployment/.deployment/.describe/services.js +6 -1
- package/src/deployment/.deployment/.describe/variables.js +6 -5
- package/src/deployment/.deployment/describe.js +4 -4
- package/src/deployment/.deployment/merge.js +8 -1
- package/src/deployment/chart/templates/compositions.yaml +70 -0
- package/src/deployment/chart/templates/services.yaml +75 -1
- package/src/deployment/chart/values.yaml +16 -1
- package/src/deployment/composition.js +1 -0
- package/src/deployment/deployment.js +1 -0
- package/src/deployment/factory.js +1 -2
- package/src/deployment/images/composition.Dockerfile +2 -2
- package/src/deployment/images/composition.js +16 -6
- package/src/deployment/images/factory.js +0 -3
- package/src/deployment/images/image.js +16 -7
- 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 +59 -15
- package/src/deployment/registry.test.js +175 -0
- package/types/_deployment/images/image.d.ts +4 -2
- package/types/_deployment/registry.d.ts +9 -7
- package/types/dependency.ts +67 -0
- package/types/dependency.d.ts +0 -32
- /package/types/{index.d.ts → index.ts} +0 -0
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.243",
|
|
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": ">= 20.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.225",
|
|
31
|
+
"@toa.io/generic": "1.0.0-alpha.225",
|
|
32
|
+
"@toa.io/norm": "1.0.0-alpha.243",
|
|
33
|
+
"@toa.io/yaml": "1.0.0-alpha.225",
|
|
34
34
|
"execa": "5.1.1",
|
|
35
35
|
"fs-extra": "11.1.1"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "743e8060d35ca750f70487c04aef9698591c5edd"
|
|
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,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
function addMounts (composition, mounts) {
|
|
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' && !composition.components?.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
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
const { addVariables } = require('./variables')
|
|
4
4
|
|
|
5
|
-
function services (services, variables) {
|
|
5
|
+
function services (services, variables, probe) {
|
|
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
|
|
8
13
|
}
|
|
9
14
|
}
|
|
10
15
|
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
function addVariables (
|
|
4
|
-
|
|
3
|
+
function addVariables (composition, variables) {
|
|
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' && !composition.components?.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,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const desc = require('./.describe')
|
|
4
4
|
|
|
5
5
|
const describe = (context, compositions, dependency) => {
|
|
6
6
|
const { services } = dependency
|
|
@@ -17,11 +17,11 @@ const describe = (context, compositions, dependency) => {
|
|
|
17
17
|
}
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
const components =
|
|
20
|
+
const components = desc.components(compositions)
|
|
21
21
|
const credentials = context.registry?.credentials
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
desc.compositions(compositions, dependency)
|
|
24
|
+
desc.services(services, dependency.variables, dependency.probe)
|
|
25
25
|
|
|
26
26
|
return {
|
|
27
27
|
compositions,
|
|
@@ -13,14 +13,21 @@ const merge = (dependencies) => {
|
|
|
13
13
|
/** @type {toa.deployment.dependency.Variables} */
|
|
14
14
|
const variables = {}
|
|
15
15
|
|
|
16
|
+
const mounts = {}
|
|
17
|
+
|
|
18
|
+
/** @type {toa.deployment.dependency.Probe | false | undefined} */
|
|
19
|
+
let probe
|
|
20
|
+
|
|
16
21
|
for (const dependency of dependencies) {
|
|
17
22
|
if (dependency.references !== undefined) references.push(...dependency.references)
|
|
18
23
|
if (dependency.services !== undefined) services.push(...dependency.services)
|
|
19
24
|
if (dependency.proxies !== undefined) proxies.push(...dependency.proxies)
|
|
20
25
|
if (dependency.variables !== undefined) append(variables, dependency.variables)
|
|
26
|
+
if (dependency.mounts !== undefined) append(mounts, dependency.mounts)
|
|
27
|
+
if (dependency.probe !== undefined) probe = dependency.probe
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
return { references, services, proxies, variables }
|
|
30
|
+
return { references, services, proxies, variables, mounts, probe }
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
const append = (merged, variables) => {
|
|
@@ -5,6 +5,9 @@ metadata:
|
|
|
5
5
|
name: composition-{{ required "composition name is required" .name }}
|
|
6
6
|
spec:
|
|
7
7
|
replicas: {{ .replicas | default 2 }}
|
|
8
|
+
# A backstop for stuck rollouts only. Must stay above the `helm upgrade --timeout`
|
|
9
|
+
# the deploy runs with, so a slow but progressing rollout is not marked failed.
|
|
10
|
+
progressDeadlineSeconds: 900
|
|
8
11
|
selector:
|
|
9
12
|
matchLabels:
|
|
10
13
|
toa.io/composition: {{ .name }}
|
|
@@ -19,15 +22,82 @@ spec:
|
|
|
19
22
|
containers:
|
|
20
23
|
- name: {{ .name }}
|
|
21
24
|
image: {{ .image }}
|
|
25
|
+
{{- if .resources }}
|
|
26
|
+
resources:
|
|
27
|
+
{{- if or .resources.cpu .resources.memory }}
|
|
28
|
+
requests:
|
|
29
|
+
{{- if .resources.cpu }}
|
|
30
|
+
cpu: {{ index .resources.cpu 0 }}
|
|
31
|
+
{{- end }}
|
|
32
|
+
{{- if .resources.memory }}
|
|
33
|
+
memory: {{ index .resources.memory 0 }}
|
|
34
|
+
{{- end }}
|
|
35
|
+
limits:
|
|
36
|
+
{{- if .resources.cpu }}
|
|
37
|
+
cpu: {{ index .resources.cpu 1 }}
|
|
38
|
+
{{- end }}
|
|
39
|
+
{{- if .resources.memory }}
|
|
40
|
+
memory: {{ index .resources.memory 1 }}
|
|
41
|
+
{{- end }}
|
|
42
|
+
{{- end }}
|
|
43
|
+
{{- end }}
|
|
22
44
|
{{- if .variables }}
|
|
23
45
|
env:
|
|
24
46
|
{{- range .variables }}
|
|
25
47
|
{{- include "env.var" . | indent 12 }}
|
|
26
48
|
{{- end }}
|
|
27
49
|
{{- end }}
|
|
50
|
+
{{- if .probe }}
|
|
51
|
+
ports:
|
|
52
|
+
- containerPort: {{ .probe.port }}
|
|
53
|
+
startupProbe:
|
|
54
|
+
httpGet:
|
|
55
|
+
path: {{ .probe.path }}
|
|
56
|
+
port: {{ .probe.port }}
|
|
57
|
+
{{- if .probe.delay }}
|
|
58
|
+
initialDelaySeconds: {{ .probe.delay }}
|
|
59
|
+
{{- end }}
|
|
60
|
+
periodSeconds: 2
|
|
61
|
+
timeoutSeconds: 3
|
|
62
|
+
failureThreshold: 150
|
|
63
|
+
readinessProbe:
|
|
64
|
+
httpGet:
|
|
65
|
+
path: {{ .probe.path }}
|
|
66
|
+
port: {{ .probe.port }}
|
|
67
|
+
periodSeconds: 10
|
|
68
|
+
timeoutSeconds: 3
|
|
69
|
+
failureThreshold: 3
|
|
70
|
+
{{- end }}
|
|
71
|
+
{{- if .mounts }}
|
|
72
|
+
volumeMounts:
|
|
73
|
+
{{- range .mounts }}
|
|
74
|
+
- name: {{ .name }}
|
|
75
|
+
mountPath: {{ .path }}
|
|
76
|
+
{{- end }}
|
|
77
|
+
{{- end }}
|
|
78
|
+
lifecycle:
|
|
79
|
+
preStop:
|
|
80
|
+
exec:
|
|
81
|
+
command: [sleep, "5"]
|
|
82
|
+
terminationGracePeriodSeconds: 45
|
|
28
83
|
{{- if $.Values.credentials }}
|
|
29
84
|
imagePullSecrets:
|
|
30
85
|
- name: {{ $.Values.credentials }}
|
|
31
86
|
{{- end }}
|
|
87
|
+
{{- if .mounts }}
|
|
88
|
+
volumes:
|
|
89
|
+
{{- range .mounts }}
|
|
90
|
+
- name: {{ .name }}
|
|
91
|
+
persistentVolumeClaim:
|
|
92
|
+
claimName: {{ .claim }}
|
|
93
|
+
{{- end }}
|
|
94
|
+
{{- end }}
|
|
95
|
+
topologySpreadConstraints:
|
|
96
|
+
- maxSkew: 1
|
|
97
|
+
topologyKey: kubernetes.io/hostname
|
|
98
|
+
whenUnsatisfiable: ScheduleAnyway
|
|
99
|
+
labelSelector:
|
|
100
|
+
matchLabels:
|
|
101
|
+
toa.io/composition: {{ .name }}
|
|
32
102
|
---
|
|
33
103
|
{{- end }}
|
|
@@ -5,6 +5,16 @@ metadata:
|
|
|
5
5
|
name: extension-{{ required "deployment name is required" .name }}
|
|
6
6
|
spec:
|
|
7
7
|
replicas: {{ .replicas | default 2 }}
|
|
8
|
+
# A backstop for stuck rollouts only. Must stay above the `helm upgrade --timeout`
|
|
9
|
+
# the deploy runs with, so a slow but progressing rollout is not marked failed.
|
|
10
|
+
progressDeadlineSeconds: 900
|
|
11
|
+
strategy:
|
|
12
|
+
type: RollingUpdate
|
|
13
|
+
rollingUpdate:
|
|
14
|
+
# a replacement must pass readiness before a running replica is taken out,
|
|
15
|
+
# otherwise a single pod serves all traffic while the others start up
|
|
16
|
+
maxUnavailable: 0
|
|
17
|
+
maxSurge: 50%
|
|
8
18
|
selector:
|
|
9
19
|
matchLabels:
|
|
10
20
|
toa.io/service: extension-{{ .name }}
|
|
@@ -16,12 +26,63 @@ spec:
|
|
|
16
26
|
containers:
|
|
17
27
|
- name: extension-{{ .name }}
|
|
18
28
|
image: {{ .image }}
|
|
29
|
+
{{- if .resources }}
|
|
30
|
+
resources:
|
|
31
|
+
{{- if or .resources.cpu .resources.memory }}
|
|
32
|
+
requests:
|
|
33
|
+
{{- if .resources.cpu }}
|
|
34
|
+
cpu: {{ index .resources.cpu 0 }}
|
|
35
|
+
{{- end }}
|
|
36
|
+
{{- if .resources.memory }}
|
|
37
|
+
memory: {{ index .resources.memory 0 }}
|
|
38
|
+
{{- end }}
|
|
39
|
+
limits:
|
|
40
|
+
{{- if .resources.cpu }}
|
|
41
|
+
cpu: {{ index .resources.cpu 1 }}
|
|
42
|
+
{{- end }}
|
|
43
|
+
{{- if .resources.memory }}
|
|
44
|
+
memory: {{ index .resources.memory 1 }}
|
|
45
|
+
{{- end }}
|
|
46
|
+
{{- end }}
|
|
47
|
+
{{- end }}
|
|
19
48
|
{{- if .variables }}
|
|
20
49
|
env:
|
|
21
50
|
{{- range .variables }}
|
|
22
51
|
{{- include "env.var" . | indent 12 }}
|
|
23
52
|
{{- end }}
|
|
24
53
|
{{- end }}
|
|
54
|
+
{{- if .probe }}
|
|
55
|
+
startupProbe:
|
|
56
|
+
httpGet:
|
|
57
|
+
path: {{ .probe.path }}
|
|
58
|
+
port: {{ .probe.port }}
|
|
59
|
+
{{- if .probe.delay }}
|
|
60
|
+
initialDelaySeconds: {{ .probe.delay }}
|
|
61
|
+
{{- end }}
|
|
62
|
+
periodSeconds: 2
|
|
63
|
+
timeoutSeconds: 3
|
|
64
|
+
failureThreshold: 150
|
|
65
|
+
readinessProbe:
|
|
66
|
+
httpGet:
|
|
67
|
+
path: {{ .probe.path }}
|
|
68
|
+
port: {{ .probe.port }}
|
|
69
|
+
periodSeconds: 10
|
|
70
|
+
timeoutSeconds: 3
|
|
71
|
+
failureThreshold: 3
|
|
72
|
+
{{- end }}
|
|
73
|
+
lifecycle:
|
|
74
|
+
preStop:
|
|
75
|
+
exec:
|
|
76
|
+
command: [sleep, "5"]
|
|
77
|
+
terminationGracePeriodSeconds: 45
|
|
78
|
+
topologySpreadConstraints:
|
|
79
|
+
- maxSkew: 1
|
|
80
|
+
topologyKey: kubernetes.io/hostname
|
|
81
|
+
whenUnsatisfiable: ScheduleAnyway
|
|
82
|
+
labelSelector:
|
|
83
|
+
matchLabels:
|
|
84
|
+
toa.io/service: extension-{{ .name }}
|
|
85
|
+
{{- if .port }}
|
|
25
86
|
---
|
|
26
87
|
apiVersion: v1
|
|
27
88
|
kind: Service
|
|
@@ -36,9 +97,10 @@ spec:
|
|
|
36
97
|
protocol: TCP
|
|
37
98
|
port: {{ .port }}
|
|
38
99
|
targetPort: {{ .port }}
|
|
39
|
-
|
|
100
|
+
{{- end }}
|
|
40
101
|
{{- if .ingress }}
|
|
41
102
|
{{- $service := .name }}
|
|
103
|
+
---
|
|
42
104
|
apiVersion: networking.k8s.io/v1
|
|
43
105
|
kind: Ingress
|
|
44
106
|
metadata:
|
|
@@ -64,5 +126,17 @@ spec:
|
|
|
64
126
|
port:
|
|
65
127
|
number: 8000
|
|
66
128
|
{{- end }}
|
|
129
|
+
{{- if .ingress.default }}
|
|
130
|
+
- http:
|
|
131
|
+
paths:
|
|
132
|
+
- path: /
|
|
133
|
+
pathType: Prefix
|
|
134
|
+
backend:
|
|
135
|
+
service:
|
|
136
|
+
name: extension-{{ $service }}
|
|
137
|
+
port:
|
|
138
|
+
number: 8000
|
|
139
|
+
{{- end }}
|
|
67
140
|
{{- end }}
|
|
141
|
+
---
|
|
68
142
|
{{- end }}
|
|
@@ -3,9 +3,16 @@ compositions:
|
|
|
3
3
|
- name: todos
|
|
4
4
|
image: localhost:5000/composition-todos:0.0.0
|
|
5
5
|
replicas: 2
|
|
6
|
+
mounts:
|
|
7
|
+
- name: mount-name
|
|
8
|
+
path: /storage
|
|
9
|
+
claim: storage-pvc
|
|
6
10
|
components:
|
|
7
11
|
- todos-tasks
|
|
8
12
|
- todos-stats
|
|
13
|
+
resources:
|
|
14
|
+
cpu: [100m, 1]
|
|
15
|
+
memory: [100Mi, 1Gi]
|
|
9
16
|
variables:
|
|
10
17
|
- name: TOA_CONFIGURATION_TODOS_TASKS
|
|
11
18
|
value: foo
|
|
@@ -18,7 +25,6 @@ compositions:
|
|
|
18
25
|
replicas: 3
|
|
19
26
|
components:
|
|
20
27
|
- users-users
|
|
21
|
-
# TODO: create component services only if sync binding is being used
|
|
22
28
|
components:
|
|
23
29
|
- todos-tasks
|
|
24
30
|
- todos-stats
|
|
@@ -28,7 +34,11 @@ services:
|
|
|
28
34
|
image: localhost:5000/resources-gateway:0.0.0
|
|
29
35
|
port: 8000
|
|
30
36
|
replicas: 2
|
|
37
|
+
resources:
|
|
38
|
+
cpu: [100m, 1]
|
|
39
|
+
memory: [100Mi, 1Gi]
|
|
31
40
|
ingress:
|
|
41
|
+
default: true
|
|
32
42
|
hosts: [dummies.toa.io]
|
|
33
43
|
class: alb
|
|
34
44
|
annotations:
|
|
@@ -41,6 +51,11 @@ services:
|
|
|
41
51
|
secret:
|
|
42
52
|
name: secret-name
|
|
43
53
|
key: secret-key
|
|
54
|
+
probe:
|
|
55
|
+
port: 8000
|
|
56
|
+
path: /.ready
|
|
57
|
+
delay: 1
|
|
58
|
+
|
|
44
59
|
proxies:
|
|
45
60
|
- name: storage-proxy
|
|
46
61
|
target: host.docker.internal
|
|
@@ -42,6 +42,7 @@ class Deployment {
|
|
|
42
42
|
|
|
43
43
|
if (options.namespace !== undefined) args.push('-n', options.namespace)
|
|
44
44
|
if (options.wait === true) args.push('--wait')
|
|
45
|
+
if (options.timeout !== undefined) args.push('--timeout', options.timeout)
|
|
45
46
|
|
|
46
47
|
await this.#process.execute('helm', ['dependency', 'update', this.#target])
|
|
47
48
|
await this.#process.execute('helm', ['upgrade', this.#chart.name, '-i', ...args, this.#target])
|
|
@@ -15,7 +15,6 @@ class Factory {
|
|
|
15
15
|
#dependencies
|
|
16
16
|
#registry
|
|
17
17
|
#process
|
|
18
|
-
#extensionComponents = []
|
|
19
18
|
|
|
20
19
|
constructor (context) {
|
|
21
20
|
this.#context = context
|
|
@@ -23,7 +22,7 @@ class Factory {
|
|
|
23
22
|
|
|
24
23
|
const imagesFactory = new ImagesFactory(context.name, context.runtime, context.registry)
|
|
25
24
|
|
|
26
|
-
this.#registry = new Registry(context.registry, imagesFactory, this.#process)
|
|
25
|
+
this.#registry = new Registry(context.name, context.registry, imagesFactory, this.#process)
|
|
27
26
|
this.#compositions = context.compositions.map((composition) => this.#composition(composition))
|
|
28
27
|
this.#dependencies = this.#getDependencies()
|
|
29
28
|
}
|
|
@@ -5,7 +5,6 @@ FROM {{build.image}}
|
|
|
5
5
|
ENV NODE_ENV=production
|
|
6
6
|
RUN if [ "{{runtime.registry}}" != "" ]; then npm set registry {{runtime.registry}}; fi
|
|
7
7
|
RUN if [ "{{runtime.proxy}}" != "" ]; then npm set proxy {{runtime.proxy}}; fi
|
|
8
|
-
RUN npm i -g @toa.io/runtime@{{runtime.version}} --omit=dev
|
|
9
8
|
|
|
10
9
|
WORKDIR /composition
|
|
11
10
|
COPY --chown=node:node . /composition
|
|
@@ -13,7 +12,8 @@ COPY --chown=node:node . /composition
|
|
|
13
12
|
{{build.run}}
|
|
14
13
|
|
|
15
14
|
# run 'npm i' in each component
|
|
16
|
-
RUN
|
|
15
|
+
RUN --mount=type=cache,target=/root/.npm \
|
|
16
|
+
for entry in *; do if [ -f "$entry/package.json" ]; then (cd $entry && npm i --omit=dev); fi; done
|
|
17
17
|
|
|
18
18
|
USER node
|
|
19
19
|
CMD toa compose *
|
|
@@ -5,6 +5,7 @@ const fs = require('fs-extra')
|
|
|
5
5
|
const { createHash } = require('node:crypto')
|
|
6
6
|
|
|
7
7
|
const { Image } = require('./image')
|
|
8
|
+
const { undef } = require('@toa.io/concise/source/expressions/undefined')
|
|
8
9
|
|
|
9
10
|
class Composition extends Image {
|
|
10
11
|
dockerfile = join(__dirname, 'composition.Dockerfile')
|
|
@@ -37,11 +38,7 @@ class Composition extends Image {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
get base () {
|
|
40
|
-
|
|
41
|
-
return this.#image
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let image = null
|
|
41
|
+
let image = this.#image
|
|
45
42
|
|
|
46
43
|
for (const component of this.#components) {
|
|
47
44
|
const value = component.build?.image
|
|
@@ -53,7 +50,20 @@ class Composition extends Image {
|
|
|
53
50
|
image = value
|
|
54
51
|
}
|
|
55
52
|
|
|
56
|
-
return image
|
|
53
|
+
return image
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get run () {
|
|
57
|
+
const commands = []
|
|
58
|
+
|
|
59
|
+
for (const component of this.#components) {
|
|
60
|
+
const run = component.build?.run
|
|
61
|
+
|
|
62
|
+
if (run !== undefined)
|
|
63
|
+
commands.push(run)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return commands.join('\n')
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
async prepare (root) {
|
|
@@ -26,9 +26,7 @@ class Image {
|
|
|
26
26
|
#registry
|
|
27
27
|
#runtime
|
|
28
28
|
#values = {
|
|
29
|
-
build: {
|
|
30
|
-
image: 'node:20.9.0-alpine3.18'
|
|
31
|
-
}
|
|
29
|
+
build: {}
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
constructor (scope, runtime, registry) {
|
|
@@ -54,6 +52,8 @@ class Image {
|
|
|
54
52
|
|
|
55
53
|
get base () {}
|
|
56
54
|
|
|
55
|
+
get run () {}
|
|
56
|
+
|
|
57
57
|
async prepare (root) {
|
|
58
58
|
if (this.dockerfile === undefined) throw new Error('Dockerfile isn\'t specified')
|
|
59
59
|
|
|
@@ -65,7 +65,7 @@ class Image {
|
|
|
65
65
|
|
|
66
66
|
const template = await read(this.dockerfile, 'utf-8')
|
|
67
67
|
const contents = template.replace(/{{(\S{1,32})}}/g, (_, key) => this.#value(key))
|
|
68
|
-
const ignore = 'Dockerfile'
|
|
68
|
+
const ignore = ['Dockerfile', '**/node_modules'].join('\r\n')
|
|
69
69
|
|
|
70
70
|
await write(join(path, 'Dockerfile'), contents)
|
|
71
71
|
await write(join(path, '.dockerignore'), ignore)
|
|
@@ -77,13 +77,19 @@ class Image {
|
|
|
77
77
|
|
|
78
78
|
#setValues () {
|
|
79
79
|
this.#values.runtime = this.#runtime
|
|
80
|
-
this.#values.build = overwrite(
|
|
80
|
+
this.#values.build = overwrite({
|
|
81
|
+
image: `${RUNTIME_IMAGE}:${this.#runtime.version}`
|
|
82
|
+
}, this.#registry.build)
|
|
81
83
|
|
|
82
84
|
const image = this.base
|
|
83
85
|
|
|
84
|
-
if (image !== undefined)
|
|
86
|
+
if (image !== undefined)
|
|
85
87
|
this.#values.build.image = image
|
|
86
|
-
|
|
88
|
+
|
|
89
|
+
const run = this.run
|
|
90
|
+
|
|
91
|
+
if (run !== undefined)
|
|
92
|
+
this.#values.build.run = (this.#values.build.run === undefined ? '' : this.#values.build.run + '\n') + run
|
|
87
93
|
|
|
88
94
|
if (this.#values.build.arguments !== undefined) this.#values.build.arguments = createArguments(this.#values.build.arguments)
|
|
89
95
|
if (this.#values.build.run !== undefined) this.#values.build.run = createRunCommands(this.#values.build.run)
|
|
@@ -121,4 +127,7 @@ function createArguments (variables) {
|
|
|
121
127
|
return args.join('\n')
|
|
122
128
|
}
|
|
123
129
|
|
|
130
|
+
const RUNTIME_IMAGE = 'ghcr.io/toa-io/runtime'
|
|
131
|
+
|
|
124
132
|
exports.Image = Image
|
|
133
|
+
exports.RUNTIME_IMAGE = RUNTIME_IMAGE
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { join } = require('node:path')
|
|
4
|
+
const { readFile } = require('node:fs/promises')
|
|
5
|
+
const { directory } = require('@toa.io/filesystem')
|
|
6
|
+
|
|
7
|
+
const { Image, RUNTIME_IMAGE } = require('./image')
|
|
8
|
+
|
|
9
|
+
const compositionDockerfile = join(__dirname, 'composition.Dockerfile')
|
|
10
|
+
const serviceDockerfile = join(__dirname, 'service.Dockerfile')
|
|
11
|
+
|
|
12
|
+
class TestImage extends Image {
|
|
13
|
+
dockerfile
|
|
14
|
+
|
|
15
|
+
#name
|
|
16
|
+
#base
|
|
17
|
+
|
|
18
|
+
constructor (runtime, registry, dockerfile, name = 'test', base) {
|
|
19
|
+
super('acme', runtime, registry)
|
|
20
|
+
|
|
21
|
+
this.dockerfile = dockerfile
|
|
22
|
+
this.#name = name
|
|
23
|
+
this.#base = base
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get name () {
|
|
27
|
+
return this.#name
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get version () {
|
|
31
|
+
return 'abcdef12'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get base () {
|
|
35
|
+
return this.#base
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('runtime base image', () => {
|
|
40
|
+
/** @type {string} */
|
|
41
|
+
let root
|
|
42
|
+
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
root = await directory.temp('toa-runtime-base-test')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should default build.image to version-pinned GHCR runtime image', async () => {
|
|
48
|
+
const runtime = { version: '1.0.0-alpha.232' }
|
|
49
|
+
const image = new TestImage(runtime, {}, compositionDockerfile)
|
|
50
|
+
|
|
51
|
+
const path = await image.prepare(root)
|
|
52
|
+
const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
|
|
53
|
+
|
|
54
|
+
expect(dockerfile).toContain(`FROM ${RUNTIME_IMAGE}:1.0.0-alpha.232`)
|
|
55
|
+
expect(dockerfile).not.toMatch(/npm i -g @toa\.io\/runtime/)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should not install runtime in service Dockerfile template', async () => {
|
|
59
|
+
const runtime = { version: '1.0.0-alpha.99' }
|
|
60
|
+
const image = new TestImage(runtime, {}, serviceDockerfile, 'service-test')
|
|
61
|
+
|
|
62
|
+
const path = await image.prepare(root)
|
|
63
|
+
const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
|
|
64
|
+
|
|
65
|
+
expect(dockerfile).toContain(`FROM ${RUNTIME_IMAGE}:1.0.0-alpha.99`)
|
|
66
|
+
expect(dockerfile).not.toMatch(/npm i -g @toa\.io\/runtime/)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should allow registry.build.image override', async () => {
|
|
70
|
+
const runtime = { version: '1.0.0-alpha.232' }
|
|
71
|
+
const registry = { build: { image: 'node:24.14.0-alpine3.22' } }
|
|
72
|
+
const image = new TestImage(runtime, registry, compositionDockerfile)
|
|
73
|
+
|
|
74
|
+
const path = await image.prepare(root)
|
|
75
|
+
const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
|
|
76
|
+
|
|
77
|
+
expect(dockerfile).toContain('FROM node:24.14.0-alpine3.22')
|
|
78
|
+
expect(dockerfile).not.toContain(RUNTIME_IMAGE)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should allow composition.image override via base', async () => {
|
|
82
|
+
const runtime = { version: '1.0.0-alpha.232' }
|
|
83
|
+
const image = new TestImage(runtime, {}, compositionDockerfile, 'mono', 'custom.example/base:1')
|
|
84
|
+
|
|
85
|
+
const path = await image.prepare(root)
|
|
86
|
+
const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
|
|
87
|
+
|
|
88
|
+
expect(dockerfile).toContain('FROM custom.example/base:1')
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
FROM
|
|
1
|
+
FROM {{build.image}}
|
|
2
2
|
|
|
3
3
|
ENV NODE_ENV=production
|
|
4
4
|
RUN if [ "{{runtime.registry}}" != "" ]; then npm set registry {{runtime.registry}}; fi
|
|
5
5
|
RUN if [ "{{runtime.proxy}}" != "" ]; then npm set proxy {{runtime.proxy}}; fi
|
|
6
|
-
RUN npm i -g @toa.io/runtime@{{runtime.version}} --omit=dev
|
|
7
6
|
|
|
8
7
|
WORKDIR /service
|
|
9
8
|
COPY --chown=node:node . /service
|
|
10
9
|
|
|
11
|
-
RUN npm
|
|
10
|
+
RUN --mount=type=cache,target=/root/.npm \
|
|
11
|
+
npm i --omit=dev
|
|
12
12
|
|
|
13
13
|
USER node
|
|
14
14
|
CMD toa serve .
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { posix } = require('node:path')
|
|
3
4
|
const workspace = require('./workspace')
|
|
4
|
-
const { newid } = require('@toa.io/generic')
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @implements {toa.deployment.Registry}
|
|
8
8
|
*/
|
|
9
9
|
class Registry {
|
|
10
|
+
#scope
|
|
11
|
+
|
|
10
12
|
#registry
|
|
11
13
|
|
|
12
14
|
#factory
|
|
@@ -15,7 +17,11 @@ class Registry {
|
|
|
15
17
|
|
|
16
18
|
#images = []
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
/** @type {string | undefined} */
|
|
21
|
+
#builder
|
|
22
|
+
|
|
23
|
+
constructor (scope, registry, factory, process) {
|
|
24
|
+
this.#scope = scope
|
|
19
25
|
this.#registry = registry
|
|
20
26
|
this.#factory = factory
|
|
21
27
|
this.#process = process
|
|
@@ -40,9 +46,8 @@ class Registry {
|
|
|
40
46
|
async build () {
|
|
41
47
|
await this.prepare()
|
|
42
48
|
|
|
43
|
-
for (const image of this.#images)
|
|
49
|
+
for (const image of this.#images)
|
|
44
50
|
await this.#build(image)
|
|
45
|
-
}
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
async push () {
|
|
@@ -51,6 +56,10 @@ class Registry {
|
|
|
51
56
|
for (const image of this.#images) await this.#push(image)
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
tags () {
|
|
60
|
+
return this.#images.map((image) => image.reference)
|
|
61
|
+
}
|
|
62
|
+
|
|
54
63
|
/**
|
|
55
64
|
* @param {'composition' | 'service'} type
|
|
56
65
|
* @param {...any} args
|
|
@@ -70,13 +79,17 @@ class Registry {
|
|
|
70
79
|
* @returns {Promise<void>}
|
|
71
80
|
*/
|
|
72
81
|
async #build (image, push = false) {
|
|
82
|
+
if (await this.exists(image.reference)) {
|
|
83
|
+
console.log('Image already exists, skipping:', image.reference)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
73
87
|
const args = ['--context=default', 'buildx', 'build']
|
|
74
88
|
|
|
75
|
-
if (push)
|
|
89
|
+
if (push)
|
|
76
90
|
args.push('--push')
|
|
77
|
-
|
|
91
|
+
else
|
|
78
92
|
args.push('--load')
|
|
79
|
-
}
|
|
80
93
|
|
|
81
94
|
args.push('--tag', image.reference, image.context)
|
|
82
95
|
|
|
@@ -88,14 +101,15 @@ class Registry {
|
|
|
88
101
|
|
|
89
102
|
if (multiarch) {
|
|
90
103
|
const platform = this.#registry.platforms.join(',')
|
|
91
|
-
const builder = await this.#
|
|
104
|
+
const builder = await this.#ensureBuilder()
|
|
92
105
|
|
|
93
106
|
args.push('--platform', platform)
|
|
94
107
|
args.push('--builder', builder)
|
|
95
108
|
|
|
96
|
-
|
|
109
|
+
if (this.#registry.base !== undefined)
|
|
110
|
+
this.#appendCache(args)
|
|
111
|
+
} else
|
|
97
112
|
args.push('--builder', 'default')
|
|
98
|
-
}
|
|
99
113
|
|
|
100
114
|
args.push('--progress', 'plain')
|
|
101
115
|
|
|
@@ -106,13 +120,43 @@ class Registry {
|
|
|
106
120
|
await this.#build(image, true)
|
|
107
121
|
}
|
|
108
122
|
|
|
109
|
-
async
|
|
110
|
-
const
|
|
111
|
-
|
|
123
|
+
async exists (tag) {
|
|
124
|
+
const args = ['manifest', 'inspect', tag]
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await this.#process.execute('docker', args, { silently: true })
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.log(error.message)
|
|
130
|
+
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async #ensureBuilder () {
|
|
138
|
+
if (this.#builder !== undefined)
|
|
139
|
+
return this.#builder
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await this.#process.execute('docker', ['buildx', 'inspect', BUILDER], { silently: true })
|
|
143
|
+
} catch {
|
|
144
|
+
await this.#process.execute('docker', ['buildx', 'create', '--name', BUILDER, '--bootstrap'])
|
|
145
|
+
}
|
|
112
146
|
|
|
113
|
-
|
|
147
|
+
this.#builder = BUILDER
|
|
148
|
+
|
|
149
|
+
return BUILDER
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {string[]} args
|
|
154
|
+
*/
|
|
155
|
+
#appendCache (args) {
|
|
156
|
+
const ref = posix.join(this.#registry.base, this.#scope, 'buildcache')
|
|
114
157
|
|
|
115
|
-
|
|
158
|
+
args.push('--cache-from', `type=registry,ref=${ref}`)
|
|
159
|
+
args.push('--cache-to', `type=registry,ref=${ref},mode=max,image-manifest=true`)
|
|
116
160
|
}
|
|
117
161
|
}
|
|
118
162
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { Registry } = require('./registry')
|
|
4
|
+
|
|
5
|
+
/** @type {toa.operations.Process} */
|
|
6
|
+
let process
|
|
7
|
+
|
|
8
|
+
/** @type {toa.deployment.images.Factory} */
|
|
9
|
+
let factory
|
|
10
|
+
|
|
11
|
+
/** @type {toa.deployment.images.Image[]} */
|
|
12
|
+
let images
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
images = []
|
|
16
|
+
process = /** @type {toa.operations.Process} */ {
|
|
17
|
+
execute: jest.fn(async (cmd, args) => {
|
|
18
|
+
if (args[0] === 'manifest')
|
|
19
|
+
throw new Error('manifest unknown')
|
|
20
|
+
|
|
21
|
+
if (args[0] === 'buildx' && args[1] === 'inspect')
|
|
22
|
+
throw new Error('builder not found')
|
|
23
|
+
|
|
24
|
+
return ''
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
factory = /** @type {toa.deployment.images.Factory} */ {
|
|
29
|
+
composition: () => createImage('composition-mono'),
|
|
30
|
+
service: () => createImage('extension-realtime')
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should reuse named builder across images', async () => {
|
|
35
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
36
|
+
|
|
37
|
+
registry.composition(/** @type {any} */ ({}))
|
|
38
|
+
registry.service('.', /** @type {any} */ ({}))
|
|
39
|
+
|
|
40
|
+
await registry.build()
|
|
41
|
+
|
|
42
|
+
const creates = process.execute.mock.calls.filter(([, args]) =>
|
|
43
|
+
args[0] === 'buildx' && args[1] === 'create')
|
|
44
|
+
|
|
45
|
+
expect(creates).toHaveLength(1)
|
|
46
|
+
expect(creates[0][1]).toEqual(['buildx', 'create', '--name', 'toa', '--bootstrap'])
|
|
47
|
+
|
|
48
|
+
const builds = process.execute.mock.calls.filter(([, args]) =>
|
|
49
|
+
args[0] === '--context=default' && args[1] === 'buildx' && args[2] === 'build')
|
|
50
|
+
|
|
51
|
+
expect(builds).toHaveLength(2)
|
|
52
|
+
|
|
53
|
+
for (const [, args] of builds) {
|
|
54
|
+
expect(args).toContain('--builder')
|
|
55
|
+
expect(args[args.indexOf('--builder') + 1]).toBe('toa')
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should not create builder when it already exists', async () => {
|
|
60
|
+
process.execute = jest.fn(async (cmd, args) => {
|
|
61
|
+
if (args[0] === 'manifest')
|
|
62
|
+
throw new Error('manifest unknown')
|
|
63
|
+
|
|
64
|
+
return ''
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
68
|
+
|
|
69
|
+
registry.composition(/** @type {any} */ ({}))
|
|
70
|
+
|
|
71
|
+
await registry.build()
|
|
72
|
+
|
|
73
|
+
const creates = process.execute.mock.calls.filter(([, args]) =>
|
|
74
|
+
args[0] === 'buildx' && args[1] === 'create')
|
|
75
|
+
|
|
76
|
+
expect(creates).toHaveLength(0)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should add shared registry cache flags when base is set', async () => {
|
|
80
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
81
|
+
|
|
82
|
+
registry.composition(/** @type {any} */ ({}))
|
|
83
|
+
registry.service('.', /** @type {any} */ ({}))
|
|
84
|
+
|
|
85
|
+
await registry.build()
|
|
86
|
+
|
|
87
|
+
const builds = process.execute.mock.calls.filter(([, args]) =>
|
|
88
|
+
args[0] === '--context=default' && args[2] === 'build')
|
|
89
|
+
|
|
90
|
+
expect(builds).toHaveLength(2)
|
|
91
|
+
|
|
92
|
+
for (const [, args] of builds) {
|
|
93
|
+
expect(args).toContain('--cache-from')
|
|
94
|
+
expect(args).toContain('type=registry,ref=example.com/reg/acme/buildcache')
|
|
95
|
+
expect(args).toContain('--cache-to')
|
|
96
|
+
expect(args).toContain('type=registry,ref=example.com/reg/acme/buildcache,mode=max,image-manifest=true')
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should omit cache flags when base is not set', async () => {
|
|
101
|
+
const registry = createRegistry({ platforms: ['linux/amd64'] })
|
|
102
|
+
|
|
103
|
+
registry.composition(/** @type {any} */ ({}))
|
|
104
|
+
|
|
105
|
+
await registry.build()
|
|
106
|
+
|
|
107
|
+
const builds = process.execute.mock.calls.filter(([, args]) =>
|
|
108
|
+
args[0] === '--context=default' && args[2] === 'build')
|
|
109
|
+
|
|
110
|
+
expect(builds).toHaveLength(1)
|
|
111
|
+
|
|
112
|
+
const [, args] = builds[0]
|
|
113
|
+
|
|
114
|
+
expect(args).not.toContain('--cache-from')
|
|
115
|
+
expect(args).not.toContain('--cache-to')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should use default builder when platforms is null', async () => {
|
|
119
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: null })
|
|
120
|
+
|
|
121
|
+
registry.composition(/** @type {any} */ ({}))
|
|
122
|
+
|
|
123
|
+
await registry.build()
|
|
124
|
+
|
|
125
|
+
const builds = process.execute.mock.calls.filter(([, args]) =>
|
|
126
|
+
args[0] === '--context=default' && args[2] === 'build')
|
|
127
|
+
|
|
128
|
+
expect(builds).toHaveLength(1)
|
|
129
|
+
|
|
130
|
+
const [, args] = builds[0]
|
|
131
|
+
|
|
132
|
+
expect(args[args.indexOf('--builder') + 1]).toBe('default')
|
|
133
|
+
expect(args).not.toContain('--cache-from')
|
|
134
|
+
expect(args).not.toContain('--platform')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should skip build when image already exists', async () => {
|
|
138
|
+
process.execute = jest.fn(async () => '')
|
|
139
|
+
|
|
140
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
141
|
+
|
|
142
|
+
registry.composition(/** @type {any} */ ({}))
|
|
143
|
+
|
|
144
|
+
await registry.build()
|
|
145
|
+
|
|
146
|
+
const builds = process.execute.mock.calls.filter(([, args]) =>
|
|
147
|
+
args[0] === '--context=default' && args[2] === 'build')
|
|
148
|
+
|
|
149
|
+
expect(builds).toHaveLength(0)
|
|
150
|
+
expect(process.execute).toHaveBeenCalledWith('docker', ['manifest', 'inspect', images[0].reference], { silently: true })
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {object} [registry]
|
|
155
|
+
* @returns {Registry}
|
|
156
|
+
*/
|
|
157
|
+
function createRegistry (registry = {}) {
|
|
158
|
+
return new Registry('acme', registry, factory, process)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} name
|
|
163
|
+
* @returns {toa.deployment.images.Image}
|
|
164
|
+
*/
|
|
165
|
+
function createImage (name) {
|
|
166
|
+
const image = /** @type {toa.deployment.images.Image} */ {
|
|
167
|
+
reference: `example.com/reg/acme/${name}:abcdef12`,
|
|
168
|
+
context: `/tmp/${name}`,
|
|
169
|
+
prepare: jest.fn(async () => `/tmp/${name}`)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
images.push(image)
|
|
173
|
+
|
|
174
|
+
return image
|
|
175
|
+
}
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import type * as _norm from '@toa.io/norm/types'
|
|
2
2
|
import type * as _dependency from './dependency'
|
|
3
|
-
import type * as _image from
|
|
3
|
+
import type * as _image from './images/image'
|
|
4
4
|
|
|
5
5
|
declare namespace toa.deployment {
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
interface Registry {
|
|
8
|
-
composition(composition: _norm.Composition): _image.Image
|
|
8
|
+
composition (composition: _norm.Composition): _image.Image
|
|
9
|
+
|
|
10
|
+
service (path: string, service: _dependency.Service): _image.Image
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
prepare (path: string): Promise<string>
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
build (): Promise<void>
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
push (): Promise<void>
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
tags (): string[]
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Manifest } from '@toa.io/norm'
|
|
2
|
+
import type { Locator } from '@toa.io/core'
|
|
3
|
+
|
|
4
|
+
export interface Service {
|
|
5
|
+
group: string
|
|
6
|
+
name: string
|
|
7
|
+
version: string
|
|
8
|
+
port?: number
|
|
9
|
+
ingress?: Ingress
|
|
10
|
+
resources?: Resources
|
|
11
|
+
variables?: Variable[]
|
|
12
|
+
components?: string[]
|
|
13
|
+
probe?: Probe | false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Variable {
|
|
17
|
+
name: string
|
|
18
|
+
value?: string
|
|
19
|
+
secret?: {
|
|
20
|
+
name: string
|
|
21
|
+
key: string
|
|
22
|
+
optional?: boolean
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Instance<T> {
|
|
27
|
+
locator: Locator
|
|
28
|
+
manifest: T
|
|
29
|
+
component: Manifest
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type Instances<T> = Array<Instance<T>>
|
|
33
|
+
|
|
34
|
+
export type Variables = Record<'global' | string, Variable[]>
|
|
35
|
+
export type Mounts = Record<'global' | string, Mount[]>
|
|
36
|
+
|
|
37
|
+
export interface Dependency {
|
|
38
|
+
services?: Service[]
|
|
39
|
+
variables?: Variables
|
|
40
|
+
mounts?: Mounts
|
|
41
|
+
/** Default probe for compositions and services without their own probe. `false` disables. */
|
|
42
|
+
probe?: Probe | false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface Ingress {
|
|
46
|
+
default?: boolean
|
|
47
|
+
hosts?: string[]
|
|
48
|
+
class?: string
|
|
49
|
+
annotations?: object
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Probe {
|
|
53
|
+
port: number
|
|
54
|
+
path: string
|
|
55
|
+
delay?: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface Mount {
|
|
59
|
+
name: string
|
|
60
|
+
path: string
|
|
61
|
+
claim: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface Resources {
|
|
65
|
+
cpu: string[]
|
|
66
|
+
memory: string[]
|
|
67
|
+
}
|
package/types/dependency.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export type Service = {
|
|
2
|
-
group: string
|
|
3
|
-
name: string
|
|
4
|
-
version: string
|
|
5
|
-
port: number
|
|
6
|
-
ingress: Ingress
|
|
7
|
-
variables: Variable[]
|
|
8
|
-
components?: string[]
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type Variable = {
|
|
12
|
-
name: string
|
|
13
|
-
value?: string
|
|
14
|
-
secret?: {
|
|
15
|
-
name: string,
|
|
16
|
-
key: string
|
|
17
|
-
optional?: boolean
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type Variables = Record<'global' | string, Variable[]>
|
|
22
|
-
|
|
23
|
-
export type Dependency = {
|
|
24
|
-
services?: Service[]
|
|
25
|
-
variables?: Variables
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
type Ingress = {
|
|
29
|
-
hosts: string[]
|
|
30
|
-
class?: string
|
|
31
|
-
annotations?: object
|
|
32
|
-
}
|
|
File without changes
|