@toa.io/operations 1.0.0-alpha.28 → 1.0.0-alpha.280
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 +204 -0
- package/image/runtime.Dockerfile +4 -0
- package/package.json +10 -10
- package/readme.md +16 -0
- package/src/deployment/.deployment/.describe/components.js +1 -5
- package/src/deployment/.deployment/.describe/compositions.js +8 -7
- package/src/deployment/.deployment/.describe/dependencies.js +1 -6
- package/src/deployment/.deployment/.describe/events.js +63 -0
- package/src/deployment/.deployment/.describe/index.js +4 -13
- package/src/deployment/.deployment/.describe/mounts.js +20 -0
- package/src/deployment/.deployment/.describe/resources.js +33 -0
- package/src/deployment/.deployment/.describe/services.js +28 -5
- package/src/deployment/.deployment/.describe/services.test.js +61 -0
- package/src/deployment/.deployment/.describe/variables.js +6 -9
- package/src/deployment/.deployment/declare.js +1 -5
- package/src/deployment/.deployment/describe.js +133 -9
- package/src/deployment/.deployment/index.js +3 -9
- package/src/deployment/.deployment/merge.js +46 -6
- package/src/deployment/.deployment/merge.test.js +72 -0
- package/src/deployment/chart/templates/compositions.yaml +70 -0
- package/src/deployment/chart/templates/mono.yaml +181 -0
- package/src/deployment/chart/templates/services.yaml +75 -4
- package/src/deployment/chart/values.yaml +11 -1
- package/src/deployment/composition.js +2 -5
- package/src/deployment/deployment.js +19 -12
- package/src/deployment/factory.js +50 -28
- package/src/deployment/images/composition.Dockerfile +2 -2
- package/src/deployment/images/composition.js +26 -22
- package/src/deployment/images/factory.js +15 -10
- package/src/deployment/images/format.js +48 -0
- package/src/deployment/images/format.test.js +74 -0
- package/src/deployment/images/image.fixtures.js +11 -11
- package/src/deployment/images/image.js +20 -22
- package/src/deployment/images/image.test.js +6 -5
- package/src/deployment/images/index.js +1 -5
- package/src/deployment/images/mono.Dockerfile +18 -0
- package/src/deployment/images/mono.js +72 -0
- package/src/deployment/images/runtime-base.test.js +91 -0
- package/src/deployment/images/service.Dockerfile +3 -3
- package/src/deployment/images/service.js +8 -8
- package/src/deployment/index.js +1 -5
- package/src/deployment/operator.js +6 -6
- package/src/deployment/operator.test.js +8 -7
- package/src/deployment/registry.js +66 -22
- package/src/deployment/registry.test.js +177 -0
- package/src/deployment/service.js +1 -5
- package/src/deployment/workspace.js +13 -8
- package/src/index.js +1 -3
- package/src/process.js +2 -8
- package/src/process.test.js +20 -0
- package/types/_deployment/composition.d.ts +1 -1
- package/types/_deployment/dependency.d.ts +3 -1
- package/types/_deployment/deployment.d.ts +3 -3
- package/types/_deployment/factory.d.ts +2 -2
- package/types/_deployment/images/factory.d.ts +2 -2
- package/types/_deployment/images/image.d.ts +4 -2
- package/types/_deployment/images/registry.d.ts +2 -2
- package/types/_deployment/operator.d.ts +2 -2
- package/types/_deployment/registry.d.ts +10 -8
- package/types/_deployment/service.d.ts +4 -1
- package/types/dependency.ts +73 -0
- package/types/index.ts +1 -0
- package/types/dependency.d.ts +0 -39
- package/types/index.d.ts +0 -1
|
@@ -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,6 +26,25 @@ 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 }}
|
|
@@ -23,19 +52,46 @@ spec:
|
|
|
23
52
|
{{- end }}
|
|
24
53
|
{{- end }}
|
|
25
54
|
{{- if .probe }}
|
|
26
|
-
|
|
55
|
+
startupProbe:
|
|
27
56
|
httpGet:
|
|
28
57
|
path: {{ .probe.path }}
|
|
29
58
|
port: {{ .probe.port }}
|
|
30
59
|
{{- if .probe.delay }}
|
|
31
60
|
initialDelaySeconds: {{ .probe.delay }}
|
|
32
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
|
|
33
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 }}
|
|
34
86
|
---
|
|
35
87
|
apiVersion: v1
|
|
36
88
|
kind: Service
|
|
37
89
|
metadata:
|
|
38
90
|
name: extension-{{ .name }}
|
|
91
|
+
{{- if .annotations }}
|
|
92
|
+
annotations:
|
|
93
|
+
{{ toYaml .annotations | indent 4 }}
|
|
94
|
+
{{- end }}
|
|
39
95
|
spec:
|
|
40
96
|
type: ClusterIP
|
|
41
97
|
selector:
|
|
@@ -45,9 +101,12 @@ spec:
|
|
|
45
101
|
protocol: TCP
|
|
46
102
|
port: {{ .port }}
|
|
47
103
|
targetPort: {{ .port }}
|
|
48
|
-
|
|
104
|
+
{{- end }}
|
|
49
105
|
{{- if .ingress }}
|
|
50
106
|
{{- $service := .name }}
|
|
107
|
+
{{- $port := .port }}
|
|
108
|
+
{{- $path := .ingress.path | default "/" }}
|
|
109
|
+
---
|
|
51
110
|
apiVersion: networking.k8s.io/v1
|
|
52
111
|
kind: Ingress
|
|
53
112
|
metadata:
|
|
@@ -65,13 +124,25 @@ spec:
|
|
|
65
124
|
- host: {{ . }}
|
|
66
125
|
http:
|
|
67
126
|
paths:
|
|
68
|
-
- path:
|
|
127
|
+
- path: {{ $path }}
|
|
128
|
+
pathType: Prefix
|
|
129
|
+
backend:
|
|
130
|
+
service:
|
|
131
|
+
name: extension-{{ $service }}
|
|
132
|
+
port:
|
|
133
|
+
number: {{ $port }}
|
|
134
|
+
{{- end }}
|
|
135
|
+
{{- if .ingress.default }}
|
|
136
|
+
- http:
|
|
137
|
+
paths:
|
|
138
|
+
- path: {{ $path }}
|
|
69
139
|
pathType: Prefix
|
|
70
140
|
backend:
|
|
71
141
|
service:
|
|
72
142
|
name: extension-{{ $service }}
|
|
73
143
|
port:
|
|
74
|
-
number:
|
|
144
|
+
number: {{ $port }}
|
|
75
145
|
{{- end }}
|
|
76
146
|
{{- end }}
|
|
147
|
+
---
|
|
77
148
|
{{- 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:
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
class Composition {
|
|
1
|
+
export class Composition {
|
|
4
2
|
name
|
|
5
3
|
image
|
|
6
4
|
/** @type {string[]} */
|
|
@@ -10,9 +8,8 @@ class Composition {
|
|
|
10
8
|
this.name = composition.name
|
|
11
9
|
this.image = image.reference
|
|
12
10
|
this.components = composition.components.map(component)
|
|
11
|
+
this.resources = composition.resources
|
|
13
12
|
}
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
const component = (component) => component.locator.label
|
|
17
|
-
|
|
18
|
-
exports.Composition = Composition
|
|
@@ -1,23 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { writeFile as write } from 'node:fs/promises'
|
|
3
|
+
import { yaml as jsyaml } from '@toa.io/generic'
|
|
4
|
+
import fs from 'fs-extra'
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
const { writeFile: write } = require('node:fs/promises')
|
|
5
|
-
const { dump } = require('@toa.io/yaml')
|
|
6
|
-
const fs = require('fs-extra')
|
|
6
|
+
import { merge, declare, describe } from './.deployment/index.js'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class Deployment {
|
|
8
|
+
export class Deployment {
|
|
11
9
|
#chart
|
|
12
10
|
#values
|
|
13
11
|
#process
|
|
14
12
|
#target
|
|
15
13
|
|
|
16
|
-
constructor (context, compositions, dependencies, process) {
|
|
14
|
+
constructor (context, compositions, dependencies, process, image) {
|
|
17
15
|
const dependency = merge(dependencies)
|
|
18
16
|
|
|
19
17
|
this.#chart = declare(context, dependency)
|
|
20
|
-
this.#values = describe(context, compositions, dependency)
|
|
18
|
+
this.#values = describe(context, compositions, dependency, image)
|
|
21
19
|
this.#process = process
|
|
22
20
|
}
|
|
23
21
|
|
|
@@ -42,6 +40,7 @@ class Deployment {
|
|
|
42
40
|
|
|
43
41
|
if (options.namespace !== undefined) args.push('-n', options.namespace)
|
|
44
42
|
if (options.wait === true) args.push('--wait')
|
|
43
|
+
if (options.timeout !== undefined) args.push('--timeout', options.timeout)
|
|
45
44
|
|
|
46
45
|
await this.#process.execute('helm', ['dependency', 'update', this.#target])
|
|
47
46
|
await this.#process.execute('helm', ['upgrade', this.#chart.name, '-i', ...args, this.#target])
|
|
@@ -68,6 +67,9 @@ class Deployment {
|
|
|
68
67
|
addVariables(this.#values.compositions, variables, used)
|
|
69
68
|
addVariables(this.#values.services, variables, used)
|
|
70
69
|
|
|
70
|
+
if (this.#values.mono !== undefined)
|
|
71
|
+
addVariables([this.#values.mono], variables, used)
|
|
72
|
+
|
|
71
73
|
return variables
|
|
72
74
|
}
|
|
73
75
|
}
|
|
@@ -85,6 +87,11 @@ function addVariables (list, variables, used = new Set()) {
|
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
const TEMPLATES = join(
|
|
90
|
+
const TEMPLATES = join(import.meta.dirname, 'chart/templates')
|
|
91
|
+
|
|
89
92
|
|
|
90
|
-
|
|
93
|
+
|
|
94
|
+
function dump (object) {
|
|
95
|
+
// js-yaml writes plain objects only, and the values carry locators and images
|
|
96
|
+
return jsyaml.dump(JSON.parse(JSON.stringify(object)), { noRefs: true, lineWidth: -1 })
|
|
97
|
+
}
|
|
@@ -1,35 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import { context as load } from '@toa.io/norm'
|
|
6
|
+
import { Process } from '../process.js'
|
|
7
|
+
import { Operator } from './operator.js'
|
|
8
|
+
import { Factory as ImagesFactory } from './images/index.js'
|
|
9
|
+
import { Deployment } from './deployment.js'
|
|
10
|
+
|
|
11
|
+
// a dependency is resolved the way a package is
|
|
12
|
+
const require = createRequire(import.meta.url)
|
|
13
|
+
import { Registry } from './registry.js'
|
|
14
|
+
import { Composition } from './composition.js'
|
|
15
|
+
import { Service } from './service.js'
|
|
16
|
+
|
|
17
|
+
export class Factory {
|
|
13
18
|
#context
|
|
19
|
+
#mono
|
|
14
20
|
#compositions
|
|
15
21
|
#dependencies
|
|
16
22
|
#registry
|
|
17
23
|
#process
|
|
18
|
-
#
|
|
24
|
+
#image
|
|
19
25
|
|
|
20
|
-
constructor (context) {
|
|
26
|
+
constructor (context, options = {}) {
|
|
21
27
|
this.#context = context
|
|
28
|
+
this.#mono = options.mono === true
|
|
22
29
|
this.#process = new Process()
|
|
23
30
|
|
|
24
31
|
const imagesFactory = new ImagesFactory(context.name, context.runtime, context.registry)
|
|
25
32
|
|
|
26
|
-
this.#registry = new Registry(context.registry, imagesFactory, this.#process)
|
|
27
|
-
this.#compositions = context.compositions.map((composition) => this.#composition(composition))
|
|
33
|
+
this.#registry = new Registry(context.name, context.registry, imagesFactory, this.#process)
|
|
28
34
|
this.#dependencies = this.#getDependencies()
|
|
35
|
+
this.#compositions = []
|
|
36
|
+
|
|
37
|
+
if (this.#mono)
|
|
38
|
+
this.#image = this.#registry.mono({
|
|
39
|
+
components: context.components
|
|
40
|
+
})
|
|
41
|
+
else
|
|
42
|
+
this.#compositions = context.compositions.map((composition) => this.#composition(composition))
|
|
29
43
|
}
|
|
30
44
|
|
|
31
|
-
operator () {
|
|
32
|
-
const deployment = new Deployment(
|
|
45
|
+
async operator () {
|
|
46
|
+
const deployment = new Deployment(
|
|
47
|
+
this.#context,
|
|
48
|
+
this.#compositions,
|
|
49
|
+
// the constructor cannot await, so what it started is settled here
|
|
50
|
+
await this.#dependencies,
|
|
51
|
+
this.#process,
|
|
52
|
+
this.#image
|
|
53
|
+
)
|
|
33
54
|
|
|
34
55
|
return new Operator(deployment, this.#registry)
|
|
35
56
|
}
|
|
@@ -44,14 +65,14 @@ class Factory {
|
|
|
44
65
|
return new Composition(composition, image)
|
|
45
66
|
}
|
|
46
67
|
|
|
47
|
-
#getDependencies () {
|
|
68
|
+
async #getDependencies () {
|
|
48
69
|
/** @type {toa.deployment.Dependency[]} */
|
|
49
70
|
const dependencies = []
|
|
50
71
|
|
|
51
72
|
if (this.#context.dependencies === undefined) return dependencies
|
|
52
73
|
|
|
53
74
|
for (const [reference, instances] of Object.entries(this.#context.dependencies)) {
|
|
54
|
-
const dependency = this.#getDependency(reference, instances)
|
|
75
|
+
const dependency = await this.#getDependency(reference, instances)
|
|
55
76
|
|
|
56
77
|
if (dependency !== undefined) dependencies.push(dependency)
|
|
57
78
|
}
|
|
@@ -59,9 +80,11 @@ class Factory {
|
|
|
59
80
|
return dependencies
|
|
60
81
|
}
|
|
61
82
|
|
|
62
|
-
#getDependency (
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
async #getDependency (reference, instances) {
|
|
84
|
+
// a dependency may be named the way a package is or written as a directory,
|
|
85
|
+
// and a module is loaded by file
|
|
86
|
+
const module = await import(pathToFileURL(require.resolve(reference)).href)
|
|
87
|
+
const pkg = JSON.parse(readFileSync(require.resolve(join(reference, 'package.json')), 'utf8'))
|
|
65
88
|
|
|
66
89
|
if (module.deployment === undefined) return
|
|
67
90
|
|
|
@@ -71,7 +94,8 @@ class Factory {
|
|
|
71
94
|
const dependency = module.deployment(instances, annotation)
|
|
72
95
|
|
|
73
96
|
/** @type {toa.deployment.Service[]} */
|
|
74
|
-
const services = dependency.services?.map((service) =>
|
|
97
|
+
const services = dependency.services?.map((service) =>
|
|
98
|
+
this.#mono ? service : this.#service(reference, service))
|
|
75
99
|
|
|
76
100
|
return { ...dependency, services }
|
|
77
101
|
}
|
|
@@ -87,11 +111,9 @@ class Factory {
|
|
|
87
111
|
return new Service(service, image)
|
|
88
112
|
}
|
|
89
113
|
|
|
90
|
-
static async create (path, environment) {
|
|
114
|
+
static async create (path, environment, options = {}) {
|
|
91
115
|
const context = await load(path, environment)
|
|
92
116
|
|
|
93
|
-
return new Factory(context)
|
|
117
|
+
return new Factory(context, options)
|
|
94
118
|
}
|
|
95
119
|
}
|
|
96
|
-
|
|
97
|
-
exports.Factory = Factory
|
|
@@ -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 grep -qs '"dependencies"' "$entry/package.json"; then (cd $entry && npm i --omit=dev); fi; done
|
|
17
17
|
|
|
18
18
|
USER node
|
|
19
19
|
CMD toa compose *
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import fs from 'fs-extra'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const { createHash } = require('node:crypto')
|
|
5
|
+
import { Image } from './image.js'
|
|
6
|
+
import { declare } from './format.js'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Composition extends Image {
|
|
10
|
-
dockerfile = join(__dirname, 'composition.Dockerfile')
|
|
8
|
+
export class Composition extends Image {
|
|
9
|
+
dockerfile = join(import.meta.dirname, 'composition.Dockerfile')
|
|
11
10
|
|
|
12
11
|
#name
|
|
13
12
|
#image
|
|
@@ -37,34 +36,39 @@ class Composition extends Image {
|
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
get base () {
|
|
40
|
-
if (this.#image !== undefined)
|
|
41
|
-
return this.#image
|
|
42
|
-
}
|
|
39
|
+
if (this.#image !== undefined) return this.#image
|
|
43
40
|
|
|
44
|
-
|
|
41
|
+
const images = new Set(this.#components.map((component) => component.build?.image))
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
if (images.size > 1)
|
|
44
|
+
throw new Error(`Composition '${this.#name}' requires different base images for its components. Specify base image for the composition in the context.`)
|
|
45
|
+
|
|
46
|
+
return images.values().next().value
|
|
47
|
+
}
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
49
|
+
get run () {
|
|
50
|
+
const commands = []
|
|
52
51
|
|
|
53
|
-
|
|
52
|
+
for (const component of this.#components) {
|
|
53
|
+
const run = component.build?.run
|
|
54
|
+
|
|
55
|
+
if (run !== undefined)
|
|
56
|
+
commands.push(run)
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
return
|
|
59
|
+
return commands.join('\n')
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
async prepare (root) {
|
|
60
63
|
const context = await super.prepare(root)
|
|
61
64
|
|
|
62
65
|
for (const component of this.#components) {
|
|
63
|
-
|
|
66
|
+
const target = join(context, component.locator.label)
|
|
67
|
+
|
|
68
|
+
await fs.copy(component.path, target)
|
|
69
|
+
await declare(component.path, target, component.locator.label)
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
return context
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
|
-
|
|
70
|
-
exports.Composition = Composition
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { Composition } from './composition.js'
|
|
2
|
+
import { Service } from './service.js'
|
|
3
|
+
import { Mono } from './mono.js'
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
const { Service } = require('./service')
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @implements {toa.deployment.images.Factory}
|
|
8
|
-
*/
|
|
9
|
-
class Factory {
|
|
5
|
+
export class Factory {
|
|
10
6
|
/** @type {string} */
|
|
11
7
|
#scope
|
|
12
8
|
|
|
@@ -48,6 +44,15 @@ class Factory {
|
|
|
48
44
|
|
|
49
45
|
return instance
|
|
50
46
|
}
|
|
51
|
-
}
|
|
52
47
|
|
|
53
|
-
|
|
48
|
+
/**
|
|
49
|
+
* @returns {Mono}
|
|
50
|
+
*/
|
|
51
|
+
mono (composition) {
|
|
52
|
+
const instance = new Mono(this.#scope, this.#runtime, this.#registry, composition)
|
|
53
|
+
|
|
54
|
+
instance.tag()
|
|
55
|
+
|
|
56
|
+
return instance
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { writeFile } from 'node:fs/promises'
|
|
3
|
+
import { dirname, join, parse } from 'node:path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A component is copied into the image on its own, away from the package that
|
|
7
|
+
* declared what its files are. Node reads a `.js` as CommonJS unless a manifest
|
|
8
|
+
* beside it says otherwise, so the one the component was written under is
|
|
9
|
+
* restated where it lands.
|
|
10
|
+
*
|
|
11
|
+
* A component that ships its own manifest already says so, and is left alone.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} source the component in the workspace
|
|
14
|
+
* @param {string} target where it was copied
|
|
15
|
+
* @param {string} name
|
|
16
|
+
*/
|
|
17
|
+
export async function declare (source, target, name) {
|
|
18
|
+
if (existsSync(join(target, MANIFEST))) return
|
|
19
|
+
|
|
20
|
+
await writeFile(join(target, MANIFEST),
|
|
21
|
+
JSON.stringify({ name, private: true, type: format(source) }, null, 2) + '\n')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The module format a directory's files are read as, which the nearest manifest
|
|
26
|
+
* above it decides.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} directory
|
|
29
|
+
* @returns {'module' | 'commonjs'}
|
|
30
|
+
*/
|
|
31
|
+
export function format (directory) {
|
|
32
|
+
const { root } = parse(directory)
|
|
33
|
+
|
|
34
|
+
let current = directory
|
|
35
|
+
|
|
36
|
+
while (current !== root) {
|
|
37
|
+
const manifest = join(current, MANIFEST)
|
|
38
|
+
|
|
39
|
+
if (existsSync(manifest))
|
|
40
|
+
return JSON.parse(readFileSync(manifest, 'utf8')).type === 'module' ? 'module' : 'commonjs'
|
|
41
|
+
|
|
42
|
+
current = dirname(current)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return 'commonjs'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const MANIFEST = 'package.json'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { it, describe, before, after } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import { declare, format } from './format.js'
|
|
8
|
+
|
|
9
|
+
let root
|
|
10
|
+
|
|
11
|
+
before(async () => {
|
|
12
|
+
root = await mkdtemp(join(tmpdir(), 'format-'))
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
after(async () => {
|
|
16
|
+
await rm(root, { recursive: true, force: true })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('format', () => {
|
|
20
|
+
it('should read the nearest manifest above', async () => {
|
|
21
|
+
const workspace = join(root, 'modules')
|
|
22
|
+
const component = join(workspace, 'components', 'one')
|
|
23
|
+
|
|
24
|
+
await mkdir(component, { recursive: true })
|
|
25
|
+
await writeFile(join(workspace, 'package.json'), JSON.stringify({ type: 'module' }))
|
|
26
|
+
|
|
27
|
+
assert.strictEqual(format(component), 'module')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should default to commonjs', async () => {
|
|
31
|
+
const workspace = join(root, 'scripts')
|
|
32
|
+
const component = join(workspace, 'components', 'one')
|
|
33
|
+
|
|
34
|
+
await mkdir(component, { recursive: true })
|
|
35
|
+
await writeFile(join(workspace, 'package.json'), JSON.stringify({ name: 'scripts' }))
|
|
36
|
+
|
|
37
|
+
assert.strictEqual(format(component), 'commonjs')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('declare', () => {
|
|
42
|
+
it('should state the format the component was written under', async () => {
|
|
43
|
+
const workspace = join(root, 'stated')
|
|
44
|
+
const source = join(workspace, 'components', 'one')
|
|
45
|
+
const target = join(root, 'image', 'one')
|
|
46
|
+
|
|
47
|
+
await mkdir(source, { recursive: true })
|
|
48
|
+
await mkdir(target, { recursive: true })
|
|
49
|
+
await writeFile(join(workspace, 'package.json'), JSON.stringify({ type: 'module' }))
|
|
50
|
+
|
|
51
|
+
await declare(source, target, 'default-one')
|
|
52
|
+
|
|
53
|
+
const manifest = JSON.parse(await readFile(join(target, 'package.json'), 'utf8'))
|
|
54
|
+
|
|
55
|
+
assert.strictEqual(manifest.type, 'module')
|
|
56
|
+
assert.strictEqual(manifest.name, 'default-one')
|
|
57
|
+
assert.strictEqual(manifest.private, true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should leave a manifest the component ships alone', async () => {
|
|
61
|
+
const source = join(root, 'own', 'components', 'one')
|
|
62
|
+
const target = join(root, 'image', 'own')
|
|
63
|
+
|
|
64
|
+
await mkdir(source, { recursive: true })
|
|
65
|
+
await mkdir(target, { recursive: true })
|
|
66
|
+
|
|
67
|
+
const own = JSON.stringify({ name: 'own', dependencies: { matchacho: '0.6.0' } })
|
|
68
|
+
|
|
69
|
+
await writeFile(join(target, 'package.json'), own)
|
|
70
|
+
await declare(source, target, 'default-own')
|
|
71
|
+
|
|
72
|
+
assert.strictEqual(await readFile(join(target, 'package.json'), 'utf8'), own)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const { Image } = require('./image')
|
|
4
|
-
const { generate } = require('randomstring')
|
|
1
|
+
import { Image } from './image.js'
|
|
2
|
+
import { generate } from 'randomstring'
|
|
5
3
|
|
|
6
4
|
const version = '168b04ff'
|
|
7
5
|
const name = generate()
|
|
@@ -29,10 +27,12 @@ const registry = {
|
|
|
29
27
|
base: 'node:alpine'
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
export const scope = generate()
|
|
31
|
+
|
|
32
|
+
// distinct from the module's own `version` above
|
|
33
|
+
const published = 'ba2409fc'
|
|
34
|
+
|
|
35
|
+
// the fixture stands in for the global process
|
|
36
|
+
const current = process
|
|
37
|
+
|
|
38
|
+
export { name, Class, runtime, registry, current as process, published as version }
|