@toa.io/operations 1.0.0-alpha.32 → 1.0.0-alpha.320
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 +291 -0
- package/image/runtime.Dockerfile +20 -0
- package/package.json +10 -10
- package/readme.md +224 -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 +160 -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 +13 -0
- package/src/deployment/chart/templates/components.yaml +4 -3
- 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 +172 -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 +89 -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 +25 -16
- package/src/deployment/operator.test.js +98 -7
- package/src/deployment/registry.js +210 -46
- package/src/deployment/registry.test.js +539 -0
- package/src/deployment/secrets.js +92 -0
- package/src/deployment/secrets.test.js +172 -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 +15 -7
- package/types/_deployment/deployment.d.ts +10 -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
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the cluster has to hold for a deployment to run, and the refusal where it does not.
|
|
3
|
+
*
|
|
4
|
+
* A pod whose secret is not there is never created, and nothing the chart carries says so: Helm
|
|
5
|
+
* reports a release, the rollout stalls, and the name of what is missing is in the events of a pod
|
|
6
|
+
* that does not exist. The cluster answers the question before any of that, for one call.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Every secret the workloads read, as `<name>/<key>`, and the image pull secret by name alone —
|
|
11
|
+
* what it holds is the registry's affair, and its absence stops a pod just the same.
|
|
12
|
+
*
|
|
13
|
+
* @param {toa.deployment.dependency.Variable[]} variables
|
|
14
|
+
* @param {string} [credentials] `registry.credentials`
|
|
15
|
+
* @returns {string[]}
|
|
16
|
+
*/
|
|
17
|
+
export function references(variables, credentials) {
|
|
18
|
+
/** @type {string[]} */
|
|
19
|
+
const references = []
|
|
20
|
+
|
|
21
|
+
// as the chart renders it: `imagePullSecrets` is there where the name is, and a context
|
|
22
|
+
// that states the key and no name names no secret
|
|
23
|
+
if (credentials) references.push(credentials)
|
|
24
|
+
|
|
25
|
+
for (const { secret } of variables) {
|
|
26
|
+
// a key the workload starts without is not one the cluster has to hold
|
|
27
|
+
if (secret === undefined || secret.optional === true) continue
|
|
28
|
+
|
|
29
|
+
const reference = `${secret.name}/${secret.key}`
|
|
30
|
+
|
|
31
|
+
if (!references.includes(reference)) references.push(reference)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return references
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {toa.operations.Process} process
|
|
39
|
+
* @param {string[]} references
|
|
40
|
+
* @param {toa.deployment.installation.Options} options
|
|
41
|
+
* @returns {Promise<void>}
|
|
42
|
+
*/
|
|
43
|
+
export async function verify(process, references, options) {
|
|
44
|
+
if (references.length === 0) return
|
|
45
|
+
|
|
46
|
+
const deployed = await read(process, options)
|
|
47
|
+
const missing = references.filter((reference) => !deployed.has(reference))
|
|
48
|
+
|
|
49
|
+
if (missing.length > 0)
|
|
50
|
+
throw new Error(`Secrets are not deployed: ${missing.join(', ')}`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* What the namespace holds, as the names of its secrets and the `<name>/<key>` of every key in
|
|
55
|
+
* them. The namespace is read whole: a call per secret would be a process per secret, and the
|
|
56
|
+
* wrapper for a single one cannot tell a secret that is absent from a cluster that cannot be
|
|
57
|
+
* reached — which a preflight is the worst place to guess at. Listing asks no permission a
|
|
58
|
+
* deploy does not already have, Helm keeping its own release state in secrets of this namespace.
|
|
59
|
+
*
|
|
60
|
+
* Only the keys are read. What is under them is the cluster's and stays there.
|
|
61
|
+
*
|
|
62
|
+
* @param {toa.operations.Process} process
|
|
63
|
+
* @param {toa.deployment.installation.Options} options
|
|
64
|
+
* @returns {Promise<Set<string>>}
|
|
65
|
+
*/
|
|
66
|
+
async function read(process, options) {
|
|
67
|
+
const args = ['get', 'secrets', '-o', TEMPLATE]
|
|
68
|
+
|
|
69
|
+
if (options.namespace !== undefined) args.push('-n', options.namespace)
|
|
70
|
+
|
|
71
|
+
const output = await process.execute('kubectl', args, { silently: true })
|
|
72
|
+
|
|
73
|
+
/** @type {Set<string>} */
|
|
74
|
+
const deployed = new Set()
|
|
75
|
+
|
|
76
|
+
for (const line of output.split('\n')) {
|
|
77
|
+
if (line === '') continue
|
|
78
|
+
|
|
79
|
+
const [name, ...keys] = line.split(' ')
|
|
80
|
+
|
|
81
|
+
deployed.add(name)
|
|
82
|
+
|
|
83
|
+
for (const key of keys) deployed.add(`${name}/${key}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return deployed
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A secret's name and the keys in it, one secret a line. Neither may hold a space. */
|
|
90
|
+
const TEMPLATE =
|
|
91
|
+
'go-template={{range .items}}{{.metadata.name}}' +
|
|
92
|
+
'{{range $key, $value := .data}} {{$key}}{{end}}{{"\\n"}}{{end}}'
|