@toa.io/operations 1.0.0-alpha.32 → 1.0.0-alpha.321

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.
Files changed (78) hide show
  1. package/CHANGELOG.md +299 -0
  2. package/image/runtime.Dockerfile +20 -0
  3. package/package.json +10 -10
  4. package/readme.md +224 -0
  5. package/src/deployment/.deployment/.describe/components.js +1 -5
  6. package/src/deployment/.deployment/.describe/compositions.js +26 -7
  7. package/src/deployment/.deployment/.describe/dependencies.js +1 -6
  8. package/src/deployment/.deployment/.describe/events.js +63 -0
  9. package/src/deployment/.deployment/.describe/fold.js +42 -0
  10. package/src/deployment/.deployment/.describe/fold.test.js +108 -0
  11. package/src/deployment/.deployment/.describe/index.js +4 -13
  12. package/src/deployment/.deployment/.describe/mounts.js +17 -0
  13. package/src/deployment/.deployment/.describe/resources.js +129 -0
  14. package/src/deployment/.deployment/.describe/resources.test.js +129 -0
  15. package/src/deployment/.deployment/.describe/services.js +32 -6
  16. package/src/deployment/.deployment/.describe/services.test.js +78 -0
  17. package/src/deployment/.deployment/.describe/variables.js +5 -9
  18. package/src/deployment/.deployment/declare.js +1 -5
  19. package/src/deployment/.deployment/describe.js +160 -10
  20. package/src/deployment/.deployment/index.js +3 -9
  21. package/src/deployment/.deployment/merge.js +66 -6
  22. package/src/deployment/.deployment/merge.test.js +133 -0
  23. package/src/deployment/chart/templates/components.configmap.yaml +13 -0
  24. package/src/deployment/chart/templates/components.yaml +4 -3
  25. package/src/deployment/chart/templates/compositions.yaml +109 -4
  26. package/src/deployment/chart/templates/mono.yaml +195 -0
  27. package/src/deployment/chart/templates/services.yaml +97 -7
  28. package/src/deployment/chart/values.yaml +23 -1
  29. package/src/deployment/composition.js +8 -6
  30. package/src/deployment/deployment.js +172 -34
  31. package/src/deployment/drain.js +70 -0
  32. package/src/deployment/drain.test.js +64 -0
  33. package/src/deployment/factory.js +169 -37
  34. package/src/deployment/images/bundle.js +89 -0
  35. package/src/deployment/images/composition.Dockerfile +9 -17
  36. package/src/deployment/images/composition.js +9 -56
  37. package/src/deployment/images/dependencies.Dockerfile +23 -0
  38. package/src/deployment/images/dependencies.js +171 -0
  39. package/src/deployment/images/dependencies.test.js +268 -0
  40. package/src/deployment/images/factory.js +31 -15
  41. package/src/deployment/images/format.js +66 -0
  42. package/src/deployment/images/format.test.js +149 -0
  43. package/src/deployment/images/image.fixtures.js +17 -19
  44. package/src/deployment/images/image.js +79 -38
  45. package/src/deployment/images/image.test.js +11 -5
  46. package/src/deployment/images/index.js +1 -5
  47. package/src/deployment/images/mono.Dockerfile +11 -0
  48. package/src/deployment/images/mono.js +15 -0
  49. package/src/deployment/images/publish.js +91 -0
  50. package/src/deployment/images/runtime-base.test.js +97 -0
  51. package/src/deployment/images/service.Dockerfile +17 -5
  52. package/src/deployment/images/service.js +63 -14
  53. package/src/deployment/index.js +1 -5
  54. package/src/deployment/operator.js +25 -16
  55. package/src/deployment/operator.test.js +98 -7
  56. package/src/deployment/registry.js +210 -46
  57. package/src/deployment/registry.test.js +539 -0
  58. package/src/deployment/secrets.js +92 -0
  59. package/src/deployment/secrets.test.js +172 -0
  60. package/src/deployment/service.js +4 -6
  61. package/src/deployment/workspace.js +13 -8
  62. package/src/index.js +3 -2
  63. package/src/process.js +51 -13
  64. package/src/process.test.js +33 -0
  65. package/types/_deployment/composition.d.ts +2 -3
  66. package/types/_deployment/dependency.d.ts +15 -7
  67. package/types/_deployment/deployment.d.ts +10 -8
  68. package/types/_deployment/factory.d.ts +2 -4
  69. package/types/_deployment/images/factory.d.ts +6 -8
  70. package/types/_deployment/images/image.d.ts +16 -1
  71. package/types/_deployment/images/registry.d.ts +8 -11
  72. package/types/_deployment/operator.d.ts +10 -9
  73. package/types/_deployment/registry.d.ts +7 -4
  74. package/types/_deployment/service.d.ts +4 -3
  75. package/types/dependency.ts +79 -0
  76. package/types/index.ts +1 -0
  77. package/types/dependency.d.ts +0 -39
  78. 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}}'