@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +213 -0
  2. package/image/runtime.Dockerfile +14 -0
  3. package/package.json +10 -10
  4. package/readme.md +201 -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 +153 -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 +14 -0
  24. package/src/deployment/chart/templates/components.yaml +1 -1
  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 +147 -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 +88 -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 +22 -16
  55. package/src/deployment/operator.test.js +57 -7
  56. package/src/deployment/registry.js +210 -46
  57. package/src/deployment/registry.test.js +539 -0
  58. package/src/deployment/service.js +4 -6
  59. package/src/deployment/workspace.js +13 -8
  60. package/src/index.js +3 -2
  61. package/src/process.js +51 -13
  62. package/src/process.test.js +33 -0
  63. package/types/_deployment/composition.d.ts +2 -3
  64. package/types/_deployment/dependency.d.ts +13 -7
  65. package/types/_deployment/deployment.d.ts +7 -8
  66. package/types/_deployment/factory.d.ts +2 -4
  67. package/types/_deployment/images/factory.d.ts +6 -8
  68. package/types/_deployment/images/image.d.ts +16 -1
  69. package/types/_deployment/images/registry.d.ts +8 -11
  70. package/types/_deployment/operator.d.ts +10 -9
  71. package/types/_deployment/registry.d.ts +7 -4
  72. package/types/_deployment/service.d.ts +4 -3
  73. package/types/dependency.ts +79 -0
  74. package/types/index.ts +1 -0
  75. package/types/dependency.d.ts +0 -39
  76. 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
- 'use strict'
2
-
3
- class Service {
4
- constructor (service, image) {
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
- 'use strict'
2
-
3
- const { directory } = require('@toa.io/filesystem')
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 (type, path) {
11
- if (path === undefined) path = await directory.temp('toa-' + type)
12
- else path = await directory.ensure(path)
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
- 'use strict'
1
+ export * as deployment from './deployment/index.js'
2
2
 
3
- exports.deployment = require('./deployment')
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
- 'use strict'
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 (cmd, args, options = {}) {
6
+ export class Process {
7
+ async execute(cmd, args, options = {}) {
10
8
  console.log('toa>', cmd, args.join(' '))
11
9
 
12
- /** @type {execa.ExecaReturnValue<import('stream').Stream>} */
13
- const command = execa(cmd, args)
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
- command.stdout.pipe(process.stdout)
17
- command.stderr.pipe(process.stderr)
18
+ child.stdout.pipe(process.stdout)
19
+ child.stderr.pipe(process.stderr)
18
20
  }
19
21
 
20
- /** @type {execa.ExecaReturnValue<string>} */
21
- const result = await command
22
+ await exit(child, cmd, args, () => Buffer.concat(stderr).toString())
22
23
 
23
- return result.stdout
24
+ return Buffer.concat(stdout).toString().replace(/\n$/, '')
24
25
  }
25
26
  }
26
27
 
27
- exports.Process = Process
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
+ })