@toa.io/operations 1.0.0-alpha.288 → 1.0.0-alpha.289
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 +11 -0
- package/package.json +4 -6
- package/readme.md +16 -0
- package/src/deployment/.deployment/.describe/resources.js +63 -0
- package/src/deployment/.deployment/.describe/resources.test.js +60 -1
- package/src/deployment/chart/templates/compositions.yaml +10 -2
- package/src/deployment/chart/templates/mono.yaml +4 -3
- package/src/deployment/chart/templates/services.yaml +6 -5
- package/src/deployment/deployment.js +6 -2
- package/src/deployment/drain.js +70 -0
- package/src/deployment/drain.test.js +64 -0
- package/src/deployment/images/bundle.js +82 -0
- package/src/deployment/images/composition.Dockerfile +5 -14
- package/src/deployment/images/composition.js +5 -57
- package/src/deployment/images/dependencies.Dockerfile +16 -0
- package/src/deployment/images/dependencies.js +141 -0
- package/src/deployment/images/dependencies.test.js +212 -0
- package/src/deployment/images/image.js +25 -7
- package/src/deployment/images/mono.Dockerfile +5 -13
- package/src/deployment/images/mono.js +4 -62
- package/src/deployment/images/publish.js +4 -9
- package/src/deployment/images/runtime-base.test.js +4 -4
- package/src/deployment/images/service.Dockerfile +2 -1
- package/src/deployment/images/service.js +2 -2
- package/src/deployment/registry.js +59 -13
- package/src/deployment/registry.test.js +169 -13
- package/src/process.js +50 -6
- package/src/process.test.js +13 -4
- package/types/_deployment/images/image.d.ts +15 -0
|
@@ -10,6 +10,7 @@ export class Registry {
|
|
|
10
10
|
|
|
11
11
|
#process
|
|
12
12
|
|
|
13
|
+
/** @type {toa.deployment.images.Image[]} */
|
|
13
14
|
#images = []
|
|
14
15
|
|
|
15
16
|
/** @type {Promise<string> | undefined} */
|
|
@@ -36,18 +37,16 @@ export class Registry {
|
|
|
36
37
|
async prepare(root) {
|
|
37
38
|
const path = await workspace.create('images', root)
|
|
38
39
|
|
|
39
|
-
await Promise.all(this.#
|
|
40
|
+
await Promise.all(this.#all().map((image) => image.prepare(path)))
|
|
40
41
|
|
|
41
42
|
return path
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
async build() {
|
|
45
|
-
await this.prepare()
|
|
46
46
|
await this.#run(false)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async push() {
|
|
50
|
-
await this.prepare()
|
|
51
50
|
await this.#run(true)
|
|
52
51
|
}
|
|
53
52
|
|
|
@@ -69,24 +68,57 @@ export class Registry {
|
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
/**
|
|
72
|
-
* Every image
|
|
73
|
-
*
|
|
71
|
+
* Every image and what it is laid over.
|
|
72
|
+
*
|
|
73
|
+
* @returns {toa.deployment.images.Image[]}
|
|
74
|
+
*/
|
|
75
|
+
#all() {
|
|
76
|
+
return this.#images.flatMap((image) =>
|
|
77
|
+
image.dependencies === undefined ? [image] : [image.dependencies, image]
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Every image is probed at once, and what is missing is prepared and built concurrently:
|
|
83
|
+
* a build is bound by the registry it uploads to, not by the runner, so one at a time
|
|
84
|
+
* leaves both idle. What an image is laid over is built before it.
|
|
74
85
|
*
|
|
75
86
|
* @param {boolean} push
|
|
76
87
|
* @returns {Promise<void>}
|
|
77
88
|
*/
|
|
78
89
|
async #run(push) {
|
|
90
|
+
// a push builds on the container driver, whose bootstrap is spent while the probes are out
|
|
91
|
+
if (push) this.#ensureBuilder().catch(() => undefined)
|
|
92
|
+
|
|
93
|
+
const images = this.#all()
|
|
94
|
+
|
|
79
95
|
const existing = await Promise.all(
|
|
80
|
-
|
|
96
|
+
images.map((image) => this.exists(image.reference))
|
|
81
97
|
)
|
|
82
98
|
|
|
83
|
-
const missing =
|
|
99
|
+
const missing = images.filter((image, index) => {
|
|
84
100
|
if (existing[index]) console.log('Image already exists, skipping:', image.reference)
|
|
85
101
|
|
|
86
102
|
return !existing[index]
|
|
87
103
|
})
|
|
88
104
|
|
|
89
|
-
|
|
105
|
+
if (missing.length === 0) return
|
|
106
|
+
|
|
107
|
+
const path = await workspace.create('images')
|
|
108
|
+
|
|
109
|
+
await Promise.all(missing.map((image) => image.prepare(path)))
|
|
110
|
+
|
|
111
|
+
const bases = new Set(this.#images.map((image) => image.dependencies))
|
|
112
|
+
const build = (image) => this.#build(image, push)
|
|
113
|
+
|
|
114
|
+
await pool(
|
|
115
|
+
missing.filter((image) => bases.has(image)),
|
|
116
|
+
build
|
|
117
|
+
)
|
|
118
|
+
await pool(
|
|
119
|
+
missing.filter((image) => !bases.has(image)),
|
|
120
|
+
build
|
|
121
|
+
)
|
|
90
122
|
}
|
|
91
123
|
|
|
92
124
|
/**
|
|
@@ -102,7 +134,7 @@ export class Registry {
|
|
|
102
134
|
|
|
103
135
|
args.push('--tag', image.reference, image.context)
|
|
104
136
|
|
|
105
|
-
if (this.#registry.build?.arguments !== undefined) {
|
|
137
|
+
if (image.arguments && this.#registry.build?.arguments !== undefined) {
|
|
106
138
|
for (const arg of this.#registry.build.arguments)
|
|
107
139
|
args.push('--build-arg', `${arg}=${process.env[arg]}`)
|
|
108
140
|
}
|
|
@@ -111,9 +143,10 @@ export class Registry {
|
|
|
111
143
|
|
|
112
144
|
if (platforms !== null) args.push('--platform', platforms.join(','))
|
|
113
145
|
|
|
114
|
-
// the container driver
|
|
115
|
-
//
|
|
116
|
-
|
|
146
|
+
// a push takes the container driver: its registry exporter is what lays an image over a
|
|
147
|
+
// base it never pulled. A load takes the daemon's own, unless it builds for a platform
|
|
148
|
+
// the runner is not, which the daemon's cannot
|
|
149
|
+
if (push || (platforms !== null && platforms.length > 1)) {
|
|
117
150
|
args.push('--builder', await this.#ensureBuilder())
|
|
118
151
|
|
|
119
152
|
// the driver that produces attestations is this one, and nothing reads them;
|
|
@@ -127,7 +160,12 @@ export class Registry {
|
|
|
127
160
|
}
|
|
128
161
|
|
|
129
162
|
async exists(tag) {
|
|
130
|
-
const args = ['manifest', 'inspect'
|
|
163
|
+
const args = ['manifest', 'inspect']
|
|
164
|
+
|
|
165
|
+
// a registry on this machine speaks plain HTTP, which the probe has to be told
|
|
166
|
+
if (LOCAL.test(tag)) args.push('--insecure')
|
|
167
|
+
|
|
168
|
+
args.push(tag)
|
|
131
169
|
|
|
132
170
|
try {
|
|
133
171
|
await this.#process.execute('docker', args, { silently: true })
|
|
@@ -155,11 +193,16 @@ export class Registry {
|
|
|
155
193
|
silently: true
|
|
156
194
|
})
|
|
157
195
|
} catch {
|
|
196
|
+
// on the host's network, a registry the host reaches as `localhost` is reached
|
|
158
197
|
await this.#process.execute('docker', [
|
|
159
198
|
'buildx',
|
|
160
199
|
'create',
|
|
161
200
|
'--name',
|
|
162
201
|
BUILDER,
|
|
202
|
+
'--driver',
|
|
203
|
+
'docker-container',
|
|
204
|
+
'--driver-opt',
|
|
205
|
+
'network=host',
|
|
163
206
|
'--bootstrap'
|
|
164
207
|
])
|
|
165
208
|
}
|
|
@@ -189,5 +232,8 @@ async function pool(items, work) {
|
|
|
189
232
|
|
|
190
233
|
const BUILDER = 'toa'
|
|
191
234
|
|
|
235
|
+
/** A reference into a registry on this machine, by any of the names it goes by. */
|
|
236
|
+
const LOCAL = /^(localhost|127\.\d+\.\d+\.\d+|host\.docker\.internal)(:\d+)?\//
|
|
237
|
+
|
|
192
238
|
/** How many builds run at once. Past this the uploads contend for the same uplink. */
|
|
193
239
|
const CONCURRENCY = 3
|
|
@@ -27,9 +27,9 @@ beforeEach(() => {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
factory = /** @type {toa.deployment.images.Factory} */ {
|
|
30
|
-
composition: () => createImage('composition-mono'),
|
|
30
|
+
composition: () => createImage('composition-mono', true),
|
|
31
31
|
service: () => createImage('extension-realtime'),
|
|
32
|
-
mono: () => createImage('mono')
|
|
32
|
+
mono: () => createImage('mono', true)
|
|
33
33
|
}
|
|
34
34
|
})
|
|
35
35
|
|
|
@@ -54,6 +54,10 @@ it('should reuse named builder across images', async () => {
|
|
|
54
54
|
'create',
|
|
55
55
|
'--name',
|
|
56
56
|
'toa',
|
|
57
|
+
'--driver',
|
|
58
|
+
'docker-container',
|
|
59
|
+
'--driver-opt',
|
|
60
|
+
'network=host',
|
|
57
61
|
'--bootstrap'
|
|
58
62
|
])
|
|
59
63
|
|
|
@@ -62,7 +66,7 @@ it('should reuse named builder across images', async () => {
|
|
|
62
66
|
args[0] === '--context=default' && args[1] === 'buildx' && args[2] === 'build'
|
|
63
67
|
)
|
|
64
68
|
|
|
65
|
-
assert.strictEqual(builds.length,
|
|
69
|
+
assert.strictEqual(builds.length, 3)
|
|
66
70
|
|
|
67
71
|
for (const {
|
|
68
72
|
arguments: [, args]
|
|
@@ -107,7 +111,7 @@ it('should not export a build cache', async () => {
|
|
|
107
111
|
({ arguments: [, args] }) => args[0] === '--context=default' && args[2] === 'build'
|
|
108
112
|
)
|
|
109
113
|
|
|
110
|
-
assert.strictEqual(builds.length,
|
|
114
|
+
assert.strictEqual(builds.length, 3)
|
|
111
115
|
|
|
112
116
|
for (const {
|
|
113
117
|
arguments: [, args]
|
|
@@ -172,8 +176,114 @@ it('should probe every image before building any', async () => {
|
|
|
172
176
|
const build = calls.indexOf('--context=default')
|
|
173
177
|
const probed = calls.slice(0, build).filter((argument) => argument === 'manifest')
|
|
174
178
|
|
|
175
|
-
//
|
|
176
|
-
|
|
179
|
+
// every probe is spent before the first build starts, rather than one before each:
|
|
180
|
+
// the composition, what it is laid over, and the service
|
|
181
|
+
assert.strictEqual(probed.length, 3)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should build what an image is laid over before the image', async () => {
|
|
185
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
186
|
+
|
|
187
|
+
registry.composition(/** @type {any} */ ({}))
|
|
188
|
+
registry.service('.', /** @type {any} */ ({}))
|
|
189
|
+
|
|
190
|
+
await registry.build()
|
|
191
|
+
|
|
192
|
+
const tags = builds().map((args) => args[args.indexOf('--tag') + 1])
|
|
193
|
+
const [composition] = images
|
|
194
|
+
|
|
195
|
+
assert.strictEqual(tags.length, 3)
|
|
196
|
+
assert.ok(
|
|
197
|
+
tags.indexOf(composition.dependencies.reference) < tags.indexOf(composition.reference)
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('should lay the image over what already exists', async () => {
|
|
202
|
+
process.execute = mock.fn(async (cmd, args) => {
|
|
203
|
+
if (args[0] === 'manifest' && !args[2].includes(':deps-'))
|
|
204
|
+
throw new Error('manifest unknown')
|
|
205
|
+
|
|
206
|
+
return ''
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
210
|
+
|
|
211
|
+
registry.composition(/** @type {any} */ ({}))
|
|
212
|
+
|
|
213
|
+
await registry.build()
|
|
214
|
+
|
|
215
|
+
const tags = builds().map((args) => args[args.indexOf('--tag') + 1])
|
|
216
|
+
const [composition] = images
|
|
217
|
+
|
|
218
|
+
assert.deepStrictEqual(tags, [composition.reference])
|
|
219
|
+
assert.strictEqual(composition.dependencies.prepare.mock.callCount(), 0)
|
|
220
|
+
assert.strictEqual(composition.prepare.mock.callCount(), 1)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should prepare only what is missing', async () => {
|
|
224
|
+
process.execute = mock.fn(async () => '')
|
|
225
|
+
|
|
226
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
227
|
+
|
|
228
|
+
registry.composition(/** @type {any} */ ({}))
|
|
229
|
+
registry.service('.', /** @type {any} */ ({}))
|
|
230
|
+
|
|
231
|
+
await registry.build()
|
|
232
|
+
|
|
233
|
+
for (const image of images) assert.strictEqual(image.prepare.mock.callCount(), 0)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('should pass build arguments to the images that read them', async () => {
|
|
237
|
+
const registry = createRegistry({
|
|
238
|
+
base: 'example.com/reg',
|
|
239
|
+
platforms: ['linux/amd64'],
|
|
240
|
+
build: { arguments: ['TOKEN'] }
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
registry.composition(/** @type {any} */ ({}))
|
|
244
|
+
registry.service('.', /** @type {any} */ ({}))
|
|
245
|
+
|
|
246
|
+
await registry.build()
|
|
247
|
+
|
|
248
|
+
const [composition] = images
|
|
249
|
+
|
|
250
|
+
for (const args of builds()) {
|
|
251
|
+
const tag = args[args.indexOf('--tag') + 1]
|
|
252
|
+
const reads = tag === composition.dependencies.reference
|
|
253
|
+
|
|
254
|
+
assert.strictEqual(args.includes('--build-arg'), reads, tag)
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should push on the container builder', async () => {
|
|
259
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
260
|
+
|
|
261
|
+
registry.composition(/** @type {any} */ ({}))
|
|
262
|
+
|
|
263
|
+
await registry.push()
|
|
264
|
+
|
|
265
|
+
for (const args of builds()) {
|
|
266
|
+
assert.ok(args.includes('--push'))
|
|
267
|
+
assert.strictEqual(args[args.indexOf('--builder') + 1], 'toa')
|
|
268
|
+
assert.ok(args.includes('--provenance=false'))
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('should not create the builder for a push when nothing is missing', async () => {
|
|
273
|
+
process.execute = mock.fn(async (cmd, args) => {
|
|
274
|
+
if (args[0] === 'buildx' && args[1] === 'inspect')
|
|
275
|
+
throw new Error('builder not found')
|
|
276
|
+
|
|
277
|
+
return ''
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
|
|
281
|
+
|
|
282
|
+
registry.composition(/** @type {any} */ ({}))
|
|
283
|
+
|
|
284
|
+
await registry.push()
|
|
285
|
+
|
|
286
|
+
assert.strictEqual(builds().length, 0)
|
|
177
287
|
})
|
|
178
288
|
|
|
179
289
|
it('should use default builder when platforms is null', async () => {
|
|
@@ -187,14 +297,14 @@ it('should use default builder when platforms is null', async () => {
|
|
|
187
297
|
({ arguments: [, args] }) => args[0] === '--context=default' && args[2] === 'build'
|
|
188
298
|
)
|
|
189
299
|
|
|
190
|
-
assert.strictEqual(builds.length,
|
|
300
|
+
assert.strictEqual(builds.length, 2)
|
|
191
301
|
|
|
192
|
-
const {
|
|
302
|
+
for (const {
|
|
193
303
|
arguments: [, args]
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
304
|
+
} of builds) {
|
|
305
|
+
assert.strictEqual(args[args.indexOf('--builder') + 1], 'default')
|
|
306
|
+
assert.ok(!args.includes('--platform'))
|
|
307
|
+
}
|
|
198
308
|
})
|
|
199
309
|
|
|
200
310
|
it('should skip build when image already exists', async () => {
|
|
@@ -226,6 +336,34 @@ it('should skip build when image already exists', async () => {
|
|
|
226
336
|
)
|
|
227
337
|
})
|
|
228
338
|
|
|
339
|
+
it('should probe a local registry as insecure', async () => {
|
|
340
|
+
process.execute = mock.fn(async () => '')
|
|
341
|
+
|
|
342
|
+
const registry = createRegistry({ base: 'localhost:5000', platforms: null })
|
|
343
|
+
|
|
344
|
+
factory.composition = () => {
|
|
345
|
+
const image = createImage('composition-mono', true)
|
|
346
|
+
|
|
347
|
+
image.reference = 'localhost:5000/acme/composition-mono:abcdef12'
|
|
348
|
+
image.dependencies.reference = 'localhost:5000/acme/composition-mono:deps-12345678'
|
|
349
|
+
|
|
350
|
+
return image
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
registry.composition(/** @type {any} */ ({}))
|
|
354
|
+
|
|
355
|
+
await registry.build()
|
|
356
|
+
|
|
357
|
+
const probes = process.execute.mock.calls
|
|
358
|
+
.filter(({ arguments: [, args] }) => args[0] === 'manifest')
|
|
359
|
+
.map(({ arguments: [, args] }) => args)
|
|
360
|
+
|
|
361
|
+
assert.strictEqual(probes.length, 2)
|
|
362
|
+
|
|
363
|
+
for (const args of probes)
|
|
364
|
+
assert.deepStrictEqual(args.slice(0, 3), ['manifest', 'inspect', '--insecure'])
|
|
365
|
+
})
|
|
366
|
+
|
|
229
367
|
/**
|
|
230
368
|
* @param {object} [registry]
|
|
231
369
|
* @returns {Registry}
|
|
@@ -236,16 +374,34 @@ function createRegistry(registry = {}) {
|
|
|
236
374
|
|
|
237
375
|
/**
|
|
238
376
|
* @param {string} name
|
|
377
|
+
* @param {boolean} [bundle] laid over an image of its dependencies
|
|
239
378
|
* @returns {toa.deployment.images.Image}
|
|
240
379
|
*/
|
|
241
|
-
function createImage(name) {
|
|
380
|
+
function createImage(name, bundle = false) {
|
|
242
381
|
const image = /** @type {toa.deployment.images.Image} */ {
|
|
243
382
|
reference: `example.com/reg/acme/${name}:abcdef12`,
|
|
244
383
|
context: `/tmp/${name}`,
|
|
245
384
|
prepare: mock.fn(async () => `/tmp/${name}`)
|
|
246
385
|
}
|
|
247
386
|
|
|
387
|
+
if (bundle)
|
|
388
|
+
image.dependencies = /** @type {toa.deployment.images.Image} */ {
|
|
389
|
+
reference: `example.com/reg/acme/${name}:deps-12345678`,
|
|
390
|
+
context: `/tmp/dependencies/${name}`,
|
|
391
|
+
arguments: true,
|
|
392
|
+
prepare: mock.fn(async () => `/tmp/dependencies/${name}`)
|
|
393
|
+
}
|
|
394
|
+
|
|
248
395
|
images.push(image)
|
|
249
396
|
|
|
250
397
|
return image
|
|
251
398
|
}
|
|
399
|
+
|
|
400
|
+
/** @returns {string[][]} the arguments of every build run */
|
|
401
|
+
function builds() {
|
|
402
|
+
return process.execute.mock.calls
|
|
403
|
+
.filter(
|
|
404
|
+
({ arguments: [, args] }) => args[0] === '--context=default' && args[2] === 'build'
|
|
405
|
+
)
|
|
406
|
+
.map(({ arguments: [, args] }) => args)
|
|
407
|
+
}
|
package/src/process.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @implements {toa.operations.Process}
|
|
@@ -7,15 +7,59 @@ export class Process {
|
|
|
7
7
|
async execute(cmd, args, options = {}) {
|
|
8
8
|
console.log('toa>', cmd, args.join(' '))
|
|
9
9
|
|
|
10
|
-
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))
|
|
11
16
|
|
|
12
17
|
if (options.silently !== true) {
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
child.stdout.pipe(process.stdout)
|
|
19
|
+
child.stderr.pipe(process.stderr)
|
|
15
20
|
}
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
await exit(child, cmd, args, () => Buffer.concat(stderr).toString())
|
|
18
23
|
|
|
19
|
-
return
|
|
24
|
+
return Buffer.concat(stdout).toString().replace(/\n$/, '')
|
|
20
25
|
}
|
|
21
26
|
}
|
|
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
|
+
})
|
package/src/process.test.js
CHANGED
|
@@ -5,8 +5,6 @@ import { Process } from './process.js'
|
|
|
5
5
|
|
|
6
6
|
describe('execute', () => {
|
|
7
7
|
it('should answer what the command wrote', async () => {
|
|
8
|
-
// `execa` is a named export, and calling the module namespace instead answers
|
|
9
|
-
// `execa is not a function` — where only a deployment would have found out
|
|
10
8
|
const output = await new Process().execute(
|
|
11
9
|
'node',
|
|
12
10
|
['-e', 'process.stdout.write("ok")'],
|
|
@@ -16,9 +14,20 @@ describe('execute', () => {
|
|
|
16
14
|
assert.strictEqual(output, 'ok')
|
|
17
15
|
})
|
|
18
16
|
|
|
19
|
-
it('should reject what the command failed at', async () => {
|
|
17
|
+
it('should reject what the command failed at, with what it wrote', async () => {
|
|
20
18
|
await assert.rejects(
|
|
21
|
-
new Process().execute(
|
|
19
|
+
new Process().execute(
|
|
20
|
+
'node',
|
|
21
|
+
['-e', 'process.stderr.write("broken"); process.exit(2)'],
|
|
22
|
+
{ silently: true }
|
|
23
|
+
),
|
|
24
|
+
(error) => /exit code 2: node/.test(error.message) && /broken/.test(error.message)
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should reject a command that does not exist', async () => {
|
|
29
|
+
await assert.rejects(
|
|
30
|
+
new Process().execute('toa-no-such-command', [], { silently: true })
|
|
22
31
|
)
|
|
23
32
|
})
|
|
24
33
|
})
|
|
@@ -3,12 +3,27 @@ declare namespace toa.deployment.images {
|
|
|
3
3
|
readonly reference: string
|
|
4
4
|
readonly context: string
|
|
5
5
|
|
|
6
|
+
/** The image this one is laid over, built and pushed as an image of its own. */
|
|
7
|
+
readonly dependencies?: Image
|
|
8
|
+
|
|
9
|
+
/** Whether the build reads `registry.build.arguments`. */
|
|
10
|
+
readonly arguments?: boolean
|
|
11
|
+
|
|
6
12
|
name: string
|
|
7
13
|
|
|
8
14
|
tag(): void
|
|
9
15
|
|
|
10
16
|
prepare(root: string): Promise<string>
|
|
11
17
|
}
|
|
18
|
+
|
|
19
|
+
/** Components in one image, laid over their dependencies. */
|
|
20
|
+
export interface Bundle extends Image {
|
|
21
|
+
readonly image?: string
|
|
22
|
+
readonly components: any[]
|
|
23
|
+
|
|
24
|
+
conflict(): string
|
|
25
|
+
}
|
|
12
26
|
}
|
|
13
27
|
|
|
14
28
|
export type Image = toa.deployment.images.Image
|
|
29
|
+
export type Bundle = toa.deployment.images.Bundle
|