@toa.io/operations 1.0.0-alpha.31 → 1.0.0-alpha.310
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +213 -0
- package/image/runtime.Dockerfile +14 -0
- package/package.json +10 -10
- package/readme.md +201 -0
- package/src/deployment/.deployment/.describe/components.js +1 -5
- package/src/deployment/.deployment/.describe/compositions.js +26 -7
- package/src/deployment/.deployment/.describe/dependencies.js +1 -6
- package/src/deployment/.deployment/.describe/events.js +63 -0
- package/src/deployment/.deployment/.describe/fold.js +42 -0
- package/src/deployment/.deployment/.describe/fold.test.js +108 -0
- package/src/deployment/.deployment/.describe/index.js +4 -13
- package/src/deployment/.deployment/.describe/mounts.js +17 -0
- package/src/deployment/.deployment/.describe/resources.js +129 -0
- package/src/deployment/.deployment/.describe/resources.test.js +129 -0
- package/src/deployment/.deployment/.describe/services.js +32 -6
- package/src/deployment/.deployment/.describe/services.test.js +78 -0
- package/src/deployment/.deployment/.describe/variables.js +5 -9
- package/src/deployment/.deployment/declare.js +1 -5
- package/src/deployment/.deployment/describe.js +153 -10
- package/src/deployment/.deployment/index.js +3 -9
- package/src/deployment/.deployment/merge.js +66 -6
- package/src/deployment/.deployment/merge.test.js +133 -0
- package/src/deployment/chart/templates/components.configmap.yaml +14 -0
- package/src/deployment/chart/templates/components.yaml +1 -1
- package/src/deployment/chart/templates/compositions.yaml +109 -4
- package/src/deployment/chart/templates/mono.yaml +195 -0
- package/src/deployment/chart/templates/services.yaml +97 -7
- package/src/deployment/chart/values.yaml +23 -1
- package/src/deployment/composition.js +8 -6
- package/src/deployment/deployment.js +147 -34
- package/src/deployment/drain.js +70 -0
- package/src/deployment/drain.test.js +64 -0
- package/src/deployment/factory.js +169 -37
- package/src/deployment/images/bundle.js +88 -0
- package/src/deployment/images/composition.Dockerfile +9 -17
- package/src/deployment/images/composition.js +9 -56
- package/src/deployment/images/dependencies.Dockerfile +23 -0
- package/src/deployment/images/dependencies.js +171 -0
- package/src/deployment/images/dependencies.test.js +268 -0
- package/src/deployment/images/factory.js +31 -15
- package/src/deployment/images/format.js +66 -0
- package/src/deployment/images/format.test.js +149 -0
- package/src/deployment/images/image.fixtures.js +17 -19
- package/src/deployment/images/image.js +79 -38
- package/src/deployment/images/image.test.js +11 -5
- package/src/deployment/images/index.js +1 -5
- package/src/deployment/images/mono.Dockerfile +11 -0
- package/src/deployment/images/mono.js +15 -0
- package/src/deployment/images/publish.js +91 -0
- package/src/deployment/images/runtime-base.test.js +97 -0
- package/src/deployment/images/service.Dockerfile +17 -5
- package/src/deployment/images/service.js +63 -14
- package/src/deployment/index.js +1 -5
- package/src/deployment/operator.js +22 -16
- package/src/deployment/operator.test.js +57 -7
- package/src/deployment/registry.js +210 -46
- package/src/deployment/registry.test.js +539 -0
- package/src/deployment/service.js +4 -6
- package/src/deployment/workspace.js +13 -8
- package/src/index.js +3 -2
- package/src/process.js +51 -13
- package/src/process.test.js +33 -0
- package/types/_deployment/composition.d.ts +2 -3
- package/types/_deployment/dependency.d.ts +13 -7
- package/types/_deployment/deployment.d.ts +7 -8
- package/types/_deployment/factory.d.ts +2 -4
- package/types/_deployment/images/factory.d.ts +6 -8
- package/types/_deployment/images/image.d.ts +16 -1
- package/types/_deployment/images/registry.d.ts +8 -11
- package/types/_deployment/operator.d.ts +10 -9
- package/types/_deployment/registry.d.ts +7 -4
- package/types/_deployment/service.d.ts +4 -3
- package/types/dependency.ts +79 -0
- package/types/index.ts +1 -0
- package/types/dependency.d.ts +0 -39
- package/types/index.d.ts +0 -1
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { it, beforeEach, mock } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
4
|
+
|
|
5
|
+
import { Registry } from './registry.js'
|
|
6
|
+
|
|
7
|
+
/** @type {toa.operations.Process} */
|
|
8
|
+
let process
|
|
9
|
+
|
|
10
|
+
/** @type {toa.deployment.images.Factory} */
|
|
11
|
+
let factory
|
|
12
|
+
|
|
13
|
+
/** @type {toa.deployment.images.Image[]} */
|
|
14
|
+
let images
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
images = []
|
|
18
|
+
process = execute()
|
|
19
|
+
|
|
20
|
+
factory = /** @type {toa.deployment.images.Factory} */ {
|
|
21
|
+
composition: () => createImage('composition-mono', true),
|
|
22
|
+
service: () => createImage('extension-realtime'),
|
|
23
|
+
mono: () => createImage('mono', true)
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should reuse named builder across images', async () => {
|
|
28
|
+
const registry = createRegistry({
|
|
29
|
+
base: 'example.com/reg',
|
|
30
|
+
platforms: ['linux/amd64', 'linux/arm64']
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
registry.composition(/** @type {any} */ ({}))
|
|
34
|
+
registry.service('.', /** @type {any} */ ({}))
|
|
35
|
+
|
|
36
|
+
await registry.build()
|
|
37
|
+
|
|
38
|
+
const creates = process.execute.mock.calls.filter(
|
|
39
|
+
({ arguments: [, args] }) => args[0] === 'buildx' && args[1] === 'create'
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
assert.strictEqual(creates.length, 1)
|
|
43
|
+
assert.deepStrictEqual(creates[0].arguments[1], [
|
|
44
|
+
'buildx',
|
|
45
|
+
'create',
|
|
46
|
+
'--name',
|
|
47
|
+
'toa',
|
|
48
|
+
'--driver',
|
|
49
|
+
'docker-container',
|
|
50
|
+
'--driver-opt',
|
|
51
|
+
'network=host',
|
|
52
|
+
'--bootstrap'
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
const builds = process.execute.mock.calls.filter(
|
|
56
|
+
({ arguments: [, args] }) =>
|
|
57
|
+
args[0] === '--context=default' && args[1] === 'buildx' && args[2] === 'build'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
assert.strictEqual(builds.length, 3)
|
|
61
|
+
|
|
62
|
+
for (const {
|
|
63
|
+
arguments: [, args]
|
|
64
|
+
} of builds) {
|
|
65
|
+
assert.ok(args.includes('--builder'))
|
|
66
|
+
assert.strictEqual(args[args.indexOf('--builder') + 1], 'toa')
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should not create builder when it already exists', async () => {
|
|
71
|
+
process.execute = mock.fn(async (cmd, args) => {
|
|
72
|
+
if (args[0] === 'manifest') throw new Error('manifest unknown')
|
|
73
|
+
|
|
74
|
+
return ''
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const registry = createRegistry({
|
|
78
|
+
base: 'example.com/reg',
|
|
79
|
+
platforms: ['linux/amd64', 'linux/arm64']
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
registry.composition(/** @type {any} */ ({}))
|
|
83
|
+
|
|
84
|
+
await registry.build()
|
|
85
|
+
|
|
86
|
+
const creates = process.execute.mock.calls.filter(
|
|
87
|
+
({ arguments: [, args] }) => args[0] === 'buildx' && args[1] === 'create'
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
assert.strictEqual(creates.length, 0)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should not export a build cache', async () => {
|
|
94
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
95
|
+
|
|
96
|
+
registry.composition(/** @type {any} */ ({}))
|
|
97
|
+
registry.service('.', /** @type {any} */ ({}))
|
|
98
|
+
|
|
99
|
+
await registry.build()
|
|
100
|
+
|
|
101
|
+
const builds = process.execute.mock.calls.filter(
|
|
102
|
+
({ arguments: [, args] }) => args[0] === '--context=default' && args[2] === 'build'
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
assert.strictEqual(builds.length, 3)
|
|
106
|
+
|
|
107
|
+
for (const {
|
|
108
|
+
arguments: [, args]
|
|
109
|
+
} of builds) {
|
|
110
|
+
assert.ok(!args.includes('--cache-from'))
|
|
111
|
+
assert.ok(!args.includes('--cache-to'))
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should use the default builder for a single platform', async () => {
|
|
116
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
117
|
+
|
|
118
|
+
registry.composition(/** @type {any} */ ({}))
|
|
119
|
+
|
|
120
|
+
await registry.build()
|
|
121
|
+
|
|
122
|
+
const creates = process.execute.mock.calls.filter(
|
|
123
|
+
({ arguments: [, args] }) => args[0] === 'buildx' && args[1] === 'create'
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
assert.strictEqual(creates.length, 0)
|
|
127
|
+
|
|
128
|
+
const {
|
|
129
|
+
arguments: [, args]
|
|
130
|
+
} = process.execute.mock.calls.find(
|
|
131
|
+
({ arguments: [, args] }) => args[0] === '--context=default' && args[2] === 'build'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
assert.strictEqual(args[args.indexOf('--builder') + 1], 'default')
|
|
135
|
+
assert.ok(args.includes('--platform'))
|
|
136
|
+
assert.strictEqual(args[args.indexOf('--platform') + 1], 'linux/amd64')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('should create the builder once when images build concurrently', async () => {
|
|
140
|
+
const registry = createRegistry({
|
|
141
|
+
base: 'example.com/reg',
|
|
142
|
+
platforms: ['linux/amd64', 'linux/arm64']
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
registry.composition(/** @type {any} */ ({}))
|
|
146
|
+
registry.service('.', /** @type {any} */ ({}))
|
|
147
|
+
registry.mono(/** @type {any} */ ({}))
|
|
148
|
+
|
|
149
|
+
await registry.build()
|
|
150
|
+
|
|
151
|
+
const creates = process.execute.mock.calls.filter(
|
|
152
|
+
({ arguments: [, args] }) => args[0] === 'buildx' && args[1] === 'create'
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
assert.strictEqual(creates.length, 1)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should probe every image before building any', async () => {
|
|
159
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
160
|
+
|
|
161
|
+
registry.composition(/** @type {any} */ ({}))
|
|
162
|
+
registry.service('.', /** @type {any} */ ({}))
|
|
163
|
+
|
|
164
|
+
await registry.build()
|
|
165
|
+
|
|
166
|
+
const calls = process.execute.mock.calls.map(({ arguments: [, args] }) => args[0])
|
|
167
|
+
const build = calls.indexOf('--context=default')
|
|
168
|
+
const probed = calls.slice(0, build).filter((argument) => argument === 'manifest')
|
|
169
|
+
|
|
170
|
+
// every probe is spent before the first build starts, rather than one before each:
|
|
171
|
+
// the composition, what it is laid over, and the service
|
|
172
|
+
assert.strictEqual(probed.length, 3)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should build what an image is laid over before the image', async () => {
|
|
176
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
177
|
+
|
|
178
|
+
registry.composition(/** @type {any} */ ({}))
|
|
179
|
+
registry.service('.', /** @type {any} */ ({}))
|
|
180
|
+
|
|
181
|
+
await registry.build()
|
|
182
|
+
|
|
183
|
+
const tags = builds().map((args) => args[args.indexOf('--tag') + 1])
|
|
184
|
+
const [composition] = images
|
|
185
|
+
|
|
186
|
+
assert.strictEqual(tags.length, 3)
|
|
187
|
+
assert.ok(
|
|
188
|
+
tags.indexOf(composition.dependencies.reference) < tags.indexOf(composition.reference)
|
|
189
|
+
)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should lay the image over what already exists', async () => {
|
|
193
|
+
process.execute = mock.fn(async (cmd, args) => {
|
|
194
|
+
if (args[0] === 'manifest' && !args[2].includes(':deps-'))
|
|
195
|
+
throw new Error('manifest unknown')
|
|
196
|
+
|
|
197
|
+
return ''
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
201
|
+
|
|
202
|
+
registry.composition(/** @type {any} */ ({}))
|
|
203
|
+
|
|
204
|
+
await registry.build()
|
|
205
|
+
|
|
206
|
+
const tags = builds().map((args) => args[args.indexOf('--tag') + 1])
|
|
207
|
+
const [composition] = images
|
|
208
|
+
|
|
209
|
+
assert.deepStrictEqual(tags, [composition.reference])
|
|
210
|
+
assert.strictEqual(composition.dependencies.prepare.mock.callCount(), 0)
|
|
211
|
+
assert.strictEqual(composition.prepare.mock.callCount(), 1)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should prepare only what is missing', async () => {
|
|
215
|
+
process.execute = mock.fn(async () => '')
|
|
216
|
+
|
|
217
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
218
|
+
|
|
219
|
+
registry.composition(/** @type {any} */ ({}))
|
|
220
|
+
registry.service('.', /** @type {any} */ ({}))
|
|
221
|
+
|
|
222
|
+
await registry.build()
|
|
223
|
+
|
|
224
|
+
for (const image of images) assert.strictEqual(image.prepare.mock.callCount(), 0)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('should pass build arguments to the images that read them', async () => {
|
|
228
|
+
const registry = createRegistry({
|
|
229
|
+
base: 'example.com/reg',
|
|
230
|
+
platforms: ['linux/amd64'],
|
|
231
|
+
build: { arguments: ['TOKEN'] }
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
registry.composition(/** @type {any} */ ({}))
|
|
235
|
+
registry.service('.', /** @type {any} */ ({}))
|
|
236
|
+
|
|
237
|
+
await registry.build()
|
|
238
|
+
|
|
239
|
+
const [composition] = images
|
|
240
|
+
|
|
241
|
+
for (const args of builds()) {
|
|
242
|
+
const tag = args[args.indexOf('--tag') + 1]
|
|
243
|
+
const reads = tag === composition.dependencies.reference
|
|
244
|
+
|
|
245
|
+
assert.strictEqual(args.includes('--build-arg'), reads, tag)
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('should push on the container builder', async () => {
|
|
250
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
251
|
+
|
|
252
|
+
registry.composition(/** @type {any} */ ({}))
|
|
253
|
+
|
|
254
|
+
await registry.push()
|
|
255
|
+
|
|
256
|
+
for (const args of builds()) {
|
|
257
|
+
assert.ok(args.includes('--push'))
|
|
258
|
+
assert.strictEqual(args[args.indexOf('--builder') + 1], 'toa')
|
|
259
|
+
assert.ok(args.includes('--provenance=false'))
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should not create the builder for a push when nothing is missing', async () => {
|
|
264
|
+
process.execute = mock.fn(async (cmd, args) => {
|
|
265
|
+
if (args[0] === 'buildx' && args[1] === 'inspect')
|
|
266
|
+
throw new Error('builder not found')
|
|
267
|
+
|
|
268
|
+
return ''
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
272
|
+
|
|
273
|
+
registry.composition(/** @type {any} */ ({}))
|
|
274
|
+
|
|
275
|
+
await registry.push()
|
|
276
|
+
|
|
277
|
+
assert.strictEqual(builds().length, 0)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('should use default builder when platforms is null', async () => {
|
|
281
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: null })
|
|
282
|
+
|
|
283
|
+
registry.composition(/** @type {any} */ ({}))
|
|
284
|
+
|
|
285
|
+
await registry.build()
|
|
286
|
+
|
|
287
|
+
const builds = process.execute.mock.calls.filter(
|
|
288
|
+
({ arguments: [, args] }) => args[0] === '--context=default' && args[2] === 'build'
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
assert.strictEqual(builds.length, 2)
|
|
292
|
+
|
|
293
|
+
for (const {
|
|
294
|
+
arguments: [, args]
|
|
295
|
+
} of builds) {
|
|
296
|
+
assert.strictEqual(args[args.indexOf('--builder') + 1], 'default')
|
|
297
|
+
assert.ok(!args.includes('--platform'))
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('should skip build when image already exists', async () => {
|
|
302
|
+
process.execute = mock.fn(async () => '')
|
|
303
|
+
|
|
304
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
305
|
+
|
|
306
|
+
registry.composition(/** @type {any} */ ({}))
|
|
307
|
+
|
|
308
|
+
await registry.build()
|
|
309
|
+
|
|
310
|
+
const builds = process.execute.mock.calls.filter(
|
|
311
|
+
({ arguments: [, args] }) => args[0] === '--context=default' && args[2] === 'build'
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
assert.strictEqual(builds.length, 0)
|
|
315
|
+
assert.ok(
|
|
316
|
+
process.execute.mock.calls.some(
|
|
317
|
+
(call) =>
|
|
318
|
+
call.arguments.length === 3 &&
|
|
319
|
+
isDeepStrictEqual(call.arguments[0], 'docker') &&
|
|
320
|
+
isDeepStrictEqual(call.arguments[1], [
|
|
321
|
+
'manifest',
|
|
322
|
+
'inspect',
|
|
323
|
+
images[0].reference
|
|
324
|
+
]) &&
|
|
325
|
+
isDeepStrictEqual(call.arguments[2], { silently: true })
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('should tag the environment on each workload image', async () => {
|
|
331
|
+
const proc = execute()
|
|
332
|
+
const registry = createRegistry(
|
|
333
|
+
{ base: 'example.com/reg', platforms: ['linux/amd64'] },
|
|
334
|
+
proc
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
registry.composition(/** @type {any} */ ({}))
|
|
338
|
+
registry.service('.', /** @type {any} */ ({}))
|
|
339
|
+
|
|
340
|
+
await registry.alias('production')
|
|
341
|
+
|
|
342
|
+
const aliases = imagetools(proc)
|
|
343
|
+
const tags = aliases.map((args) => args[args.indexOf('--tag') + 1]).sort()
|
|
344
|
+
const sources = aliases.map((args) => args.at(-1)).sort()
|
|
345
|
+
|
|
346
|
+
assert.strictEqual(aliases.length, 2)
|
|
347
|
+
assert.deepStrictEqual(tags, [
|
|
348
|
+
'example.com/reg/acme/composition-mono:production',
|
|
349
|
+
'example.com/reg/acme/extension-realtime:production'
|
|
350
|
+
])
|
|
351
|
+
assert.deepStrictEqual(sources, [
|
|
352
|
+
'example.com/reg/acme/composition-mono:abcdef12',
|
|
353
|
+
'example.com/reg/acme/extension-realtime:abcdef12'
|
|
354
|
+
])
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('should not tag deps images with the environment', async () => {
|
|
358
|
+
const proc = execute()
|
|
359
|
+
const registry = createRegistry(
|
|
360
|
+
{ base: 'example.com/reg', platforms: ['linux/amd64'] },
|
|
361
|
+
proc
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
registry.composition(/** @type {any} */ ({}))
|
|
365
|
+
|
|
366
|
+
await registry.alias('production')
|
|
367
|
+
|
|
368
|
+
const aliases = imagetools(proc)
|
|
369
|
+
|
|
370
|
+
assert.strictEqual(aliases.length, 1)
|
|
371
|
+
assert.ok(!aliases[0].some((arg) => String(arg).includes(':deps-')))
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('should not tag the environment on push', async () => {
|
|
375
|
+
const proc = execute()
|
|
376
|
+
const registry = createRegistry(
|
|
377
|
+
{ base: 'example.com/reg', platforms: ['linux/amd64'] },
|
|
378
|
+
proc
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
registry.composition(/** @type {any} */ ({}))
|
|
382
|
+
|
|
383
|
+
await registry.push()
|
|
384
|
+
|
|
385
|
+
assert.deepStrictEqual(imagetools(proc), [])
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it('should not move a content tag as the environment', async () => {
|
|
389
|
+
const proc = execute()
|
|
390
|
+
const registry = createRegistry(
|
|
391
|
+
{ base: 'example.com/reg', platforms: ['linux/amd64'] },
|
|
392
|
+
proc
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
registry.composition(/** @type {any} */ ({}))
|
|
396
|
+
|
|
397
|
+
await registry.alias('abcdef12')
|
|
398
|
+
await registry.alias('deps-12345678')
|
|
399
|
+
|
|
400
|
+
assert.deepStrictEqual(imagetools(proc), [])
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('should not tag without an environment', async () => {
|
|
404
|
+
const proc = execute()
|
|
405
|
+
const registry = createRegistry(
|
|
406
|
+
{ base: 'example.com/reg', platforms: ['linux/amd64'] },
|
|
407
|
+
proc
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
registry.composition(/** @type {any} */ ({}))
|
|
411
|
+
|
|
412
|
+
await registry.alias()
|
|
413
|
+
await registry.alias('')
|
|
414
|
+
|
|
415
|
+
assert.deepStrictEqual(imagetools(proc), [])
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('should probe a local registry as insecure', async () => {
|
|
419
|
+
const proc = execute()
|
|
420
|
+
|
|
421
|
+
proc.execute = mock.fn(async () => '')
|
|
422
|
+
|
|
423
|
+
const image = createImage('composition-mono', true)
|
|
424
|
+
|
|
425
|
+
image.reference = 'localhost:5000/acme/composition-mono:abcdef12'
|
|
426
|
+
image.dependencies.reference = 'localhost:5000/acme/composition-mono:deps-12345678'
|
|
427
|
+
|
|
428
|
+
const registry = new Registry(
|
|
429
|
+
{ base: 'localhost:5000', platforms: null },
|
|
430
|
+
{ composition: () => image, service: factory.service, mono: factory.mono },
|
|
431
|
+
proc
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
registry.composition(/** @type {any} */ ({}))
|
|
435
|
+
|
|
436
|
+
await registry.build()
|
|
437
|
+
|
|
438
|
+
const probes = proc.execute.mock.calls
|
|
439
|
+
.filter(({ arguments: [, args] }) => args[0] === 'manifest')
|
|
440
|
+
.map(({ arguments: [, args] }) => args)
|
|
441
|
+
|
|
442
|
+
assert.strictEqual(probes.length, 2)
|
|
443
|
+
|
|
444
|
+
for (const args of probes)
|
|
445
|
+
assert.deepStrictEqual(args.slice(0, 3), ['manifest', 'inspect', '--insecure'])
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('should tag a local registry as insecure', async () => {
|
|
449
|
+
const proc = execute()
|
|
450
|
+
const image = createImage('composition-mono', true)
|
|
451
|
+
|
|
452
|
+
image.reference = 'localhost:5000/acme/composition-mono:abcdef12'
|
|
453
|
+
image.dependencies.reference = 'localhost:5000/acme/composition-mono:deps-12345678'
|
|
454
|
+
|
|
455
|
+
const registry = new Registry(
|
|
456
|
+
{ base: 'localhost:5000', platforms: null },
|
|
457
|
+
{ composition: () => image, service: factory.service, mono: factory.mono },
|
|
458
|
+
proc
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
registry.composition(/** @type {any} */ ({}))
|
|
462
|
+
|
|
463
|
+
await registry.alias('production')
|
|
464
|
+
|
|
465
|
+
assert.deepStrictEqual(imagetools(proc), [
|
|
466
|
+
[
|
|
467
|
+
'buildx',
|
|
468
|
+
'imagetools',
|
|
469
|
+
'create',
|
|
470
|
+
'--tag',
|
|
471
|
+
'localhost:5000/acme/composition-mono:production',
|
|
472
|
+
'--insecure',
|
|
473
|
+
'localhost:5000/acme/composition-mono:abcdef12'
|
|
474
|
+
]
|
|
475
|
+
])
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* @param {object} [registry]
|
|
480
|
+
* @returns {Registry}
|
|
481
|
+
*/
|
|
482
|
+
function createRegistry(registry = {}, proc = process) {
|
|
483
|
+
return new Registry(registry, factory, proc)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** @returns {toa.operations.Process} */
|
|
487
|
+
function execute() {
|
|
488
|
+
return /** @type {toa.operations.Process} */ {
|
|
489
|
+
execute: mock.fn(async (cmd, args) => {
|
|
490
|
+
if (args[0] === 'manifest') throw new Error('manifest unknown')
|
|
491
|
+
|
|
492
|
+
if (args[0] === 'buildx' && args[1] === 'inspect')
|
|
493
|
+
throw new Error('builder not found')
|
|
494
|
+
|
|
495
|
+
return ''
|
|
496
|
+
})
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* @param {string} name
|
|
502
|
+
* @param {boolean} [bundle] laid over an image of its dependencies
|
|
503
|
+
* @returns {toa.deployment.images.Image}
|
|
504
|
+
*/
|
|
505
|
+
function createImage(name, bundle = false) {
|
|
506
|
+
const image = /** @type {toa.deployment.images.Image} */ {
|
|
507
|
+
reference: `example.com/reg/acme/${name}:abcdef12`,
|
|
508
|
+
context: `/tmp/${name}`,
|
|
509
|
+
prepare: mock.fn(async () => `/tmp/${name}`)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (bundle)
|
|
513
|
+
image.dependencies = /** @type {toa.deployment.images.Image} */ {
|
|
514
|
+
reference: `example.com/reg/acme/${name}:deps-12345678`,
|
|
515
|
+
context: `/tmp/dependencies/${name}`,
|
|
516
|
+
arguments: true,
|
|
517
|
+
prepare: mock.fn(async () => `/tmp/dependencies/${name}`)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
images.push(image)
|
|
521
|
+
|
|
522
|
+
return image
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** @returns {string[][]} the arguments of every imagetools run */
|
|
526
|
+
function imagetools(proc = process) {
|
|
527
|
+
return proc.execute.mock.calls
|
|
528
|
+
.filter(({ arguments: [, args] }) => args[0] === 'buildx' && args[1] === 'imagetools')
|
|
529
|
+
.map(({ arguments: [, args] }) => args)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** @returns {string[][]} the arguments of every build run */
|
|
533
|
+
function builds() {
|
|
534
|
+
return process.execute.mock.calls
|
|
535
|
+
.filter(
|
|
536
|
+
({ arguments: [, args] }) => args[0] === '--context=default' && args[2] === 'build'
|
|
537
|
+
)
|
|
538
|
+
.map(({ arguments: [, args] }) => args)
|
|
539
|
+
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
class Service {
|
|
4
|
-
constructor
|
|
1
|
+
// a service is what a deployment carries, and it is constructed
|
|
2
|
+
// oxlint-disable-next-line typescript/no-extraneous-class
|
|
3
|
+
export class Service {
|
|
4
|
+
constructor(service, image) {
|
|
5
5
|
Object.assign(this, service)
|
|
6
6
|
|
|
7
7
|
this.name = service.group + '-' + service.name
|
|
8
8
|
this.image = image.reference
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
exports.Service = Service
|
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { mkdir, mkdtemp, readdir } from 'node:fs/promises'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @param {string} type
|
|
7
7
|
* @param {string} [path]
|
|
8
8
|
* @return {Promise<string>}
|
|
9
9
|
*/
|
|
10
|
-
async function create
|
|
11
|
-
if (path === undefined)
|
|
12
|
-
|
|
10
|
+
export async function create(type, path) {
|
|
11
|
+
if (path === undefined) return await mkdtemp(join(tmpdir(), 'toa-' + type))
|
|
12
|
+
|
|
13
|
+
path = resolve(path)
|
|
14
|
+
|
|
15
|
+
await mkdir(path, { recursive: true })
|
|
16
|
+
|
|
17
|
+
const entries = await readdir(path)
|
|
18
|
+
|
|
19
|
+
if (entries.length > 0) throw new Error('Target directory must be empty')
|
|
13
20
|
|
|
14
21
|
return path
|
|
15
22
|
}
|
|
16
|
-
|
|
17
|
-
exports.create = create
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export * as deployment from './deployment/index.js'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// what conceals a secret in the cluster a deployment goes to; the CLI needs no package of its own for it
|
|
4
|
+
export * as kubernetes from '@toa.io/kubernetes'
|
package/src/process.js
CHANGED
|
@@ -1,27 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const execa = require('execa')
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* @implements {toa.operations.Process}
|
|
7
5
|
*/
|
|
8
|
-
class Process {
|
|
9
|
-
async execute
|
|
6
|
+
export class Process {
|
|
7
|
+
async execute(cmd, args, options = {}) {
|
|
10
8
|
console.log('toa>', cmd, args.join(' '))
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
const
|
|
10
|
+
const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
11
|
+
const stdout = []
|
|
12
|
+
const stderr = []
|
|
13
|
+
|
|
14
|
+
child.stdout.on('data', (chunk) => stdout.push(chunk))
|
|
15
|
+
child.stderr.on('data', (chunk) => stderr.push(chunk))
|
|
14
16
|
|
|
15
17
|
if (options.silently !== true) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
child.stdout.pipe(process.stdout)
|
|
19
|
+
child.stderr.pipe(process.stderr)
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
const result = await command
|
|
22
|
+
await exit(child, cmd, args, () => Buffer.concat(stderr).toString())
|
|
22
23
|
|
|
23
|
-
return
|
|
24
|
+
return Buffer.concat(stdout).toString().replace(/\n$/, '')
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Runs a command on this process's own streams, and answers once it has exited.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} cmd
|
|
32
|
+
* @param {string[]} args
|
|
33
|
+
* @returns {Promise<void>}
|
|
34
|
+
*/
|
|
35
|
+
export async function run(cmd, args) {
|
|
36
|
+
console.log('toa>', cmd, args.join(' '))
|
|
37
|
+
|
|
38
|
+
await exit(spawn(cmd, args, { stdio: 'inherit' }), cmd, args)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {import('node:child_process').ChildProcess} child
|
|
43
|
+
* @param {string} cmd
|
|
44
|
+
* @param {string[]} args
|
|
45
|
+
* @param {() => string} stderr
|
|
46
|
+
* @returns {Promise<void>}
|
|
47
|
+
*/
|
|
48
|
+
const exit = (child, cmd, args, stderr = () => '') =>
|
|
49
|
+
new Promise((resolve, reject) => {
|
|
50
|
+
child.once('error', reject)
|
|
51
|
+
|
|
52
|
+
child.once('close', (code, signal) => {
|
|
53
|
+
if (code === 0) return resolve()
|
|
54
|
+
|
|
55
|
+
const output = stderr()
|
|
56
|
+
const reason = signal === null ? `exit code ${code}` : signal
|
|
57
|
+
|
|
58
|
+
reject(
|
|
59
|
+
new Error(
|
|
60
|
+
`Command failed with ${reason}: ${cmd} ${args.join(' ')}` +
|
|
61
|
+
(output === '' ? '' : '\n' + output.trimEnd())
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
})
|
|
65
|
+
})
|