@toa.io/operations 1.0.0-alpha.29 → 1.0.0-alpha.290

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 (75) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/image/runtime.Dockerfile +4 -0
  3. package/package.json +8 -10
  4. package/readme.md +93 -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 +103 -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 +125 -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.yaml +1 -1
  24. package/src/deployment/chart/templates/compositions.yaml +96 -4
  25. package/src/deployment/chart/templates/mono.yaml +182 -0
  26. package/src/deployment/chart/templates/services.yaml +82 -7
  27. package/src/deployment/chart/values.yaml +23 -1
  28. package/src/deployment/composition.js +6 -6
  29. package/src/deployment/deployment.js +55 -25
  30. package/src/deployment/drain.js +70 -0
  31. package/src/deployment/drain.test.js +64 -0
  32. package/src/deployment/factory.js +121 -32
  33. package/src/deployment/images/bundle.js +82 -0
  34. package/src/deployment/images/composition.Dockerfile +8 -16
  35. package/src/deployment/images/composition.js +9 -56
  36. package/src/deployment/images/dependencies.Dockerfile +16 -0
  37. package/src/deployment/images/dependencies.js +141 -0
  38. package/src/deployment/images/dependencies.test.js +214 -0
  39. package/src/deployment/images/factory.js +31 -15
  40. package/src/deployment/images/format.js +52 -0
  41. package/src/deployment/images/format.test.js +74 -0
  42. package/src/deployment/images/image.fixtures.js +13 -13
  43. package/src/deployment/images/image.js +77 -38
  44. package/src/deployment/images/image.test.js +11 -5
  45. package/src/deployment/images/index.js +1 -5
  46. package/src/deployment/images/mono.Dockerfile +11 -0
  47. package/src/deployment/images/mono.js +15 -0
  48. package/src/deployment/images/publish.js +93 -0
  49. package/src/deployment/images/runtime-base.test.js +97 -0
  50. package/src/deployment/images/service.Dockerfile +5 -4
  51. package/src/deployment/images/service.js +13 -13
  52. package/src/deployment/index.js +1 -5
  53. package/src/deployment/operator.js +13 -13
  54. package/src/deployment/operator.test.js +8 -7
  55. package/src/deployment/registry.js +165 -47
  56. package/src/deployment/registry.test.js +407 -0
  57. package/src/deployment/service.js +4 -6
  58. package/src/deployment/workspace.js +13 -8
  59. package/src/index.js +1 -3
  60. package/src/process.js +51 -13
  61. package/src/process.test.js +33 -0
  62. package/types/_deployment/composition.d.ts +1 -3
  63. package/types/_deployment/dependency.d.ts +13 -7
  64. package/types/_deployment/deployment.d.ts +3 -7
  65. package/types/_deployment/factory.d.ts +2 -4
  66. package/types/_deployment/images/factory.d.ts +6 -8
  67. package/types/_deployment/images/image.d.ts +16 -1
  68. package/types/_deployment/images/registry.d.ts +8 -11
  69. package/types/_deployment/operator.d.ts +7 -9
  70. package/types/_deployment/registry.d.ts +4 -4
  71. package/types/_deployment/service.d.ts +4 -3
  72. package/types/dependency.ts +79 -0
  73. package/types/index.ts +1 -0
  74. package/types/dependency.d.ts +0 -39
  75. package/types/index.d.ts +0 -1
@@ -1,62 +1,65 @@
1
- 'use strict'
2
-
3
- const workspace = require('./workspace')
4
- const { newid } = require('@toa.io/generic')
1
+ import * as workspace from './workspace.js'
5
2
 
6
3
  /**
7
4
  * @implements {toa.deployment.Registry}
8
5
  */
9
- class Registry {
6
+ export class Registry {
10
7
  #registry
11
8
 
12
9
  #factory
13
10
 
14
11
  #process
15
12
 
13
+ /** @type {toa.deployment.images.Image[]} */
16
14
  #images = []
17
15
 
18
- constructor (registry, factory, process) {
16
+ /** @type {Promise<string> | undefined} */
17
+ #builder
18
+
19
+ constructor(registry, factory, process) {
19
20
  this.#registry = registry
20
21
  this.#factory = factory
21
22
  this.#process = process
22
23
  }
23
24
 
24
- composition (composition) {
25
+ composition(composition) {
25
26
  return this.#create('composition', composition)
26
27
  }
27
28
 
28
- service (path, service) {
29
+ service(path, service) {
29
30
  return this.#create('service', path, service)
30
31
  }
31
32
 
32
- async prepare (root) {
33
+ mono(composition) {
34
+ return this.#create('mono', composition)
35
+ }
36
+
37
+ async prepare(root) {
33
38
  const path = await workspace.create('images', root)
34
39
 
35
- await Promise.all(this.#images.map((image) => image.prepare(path)))
40
+ await Promise.all(this.#all().map((image) => image.prepare(path)))
36
41
 
37
42
  return path
38
43
  }
39
44
 
40
- async build () {
41
- await this.prepare()
42
-
43
- for (const image of this.#images) {
44
- await this.#build(image)
45
- }
45
+ async build() {
46
+ await this.#run(false)
46
47
  }
47
48
 
48
- async push () {
49
- await this.prepare()
49
+ async push() {
50
+ await this.#run(true)
51
+ }
50
52
 
51
- for (const image of this.#images) await this.#push(image)
53
+ tags() {
54
+ return this.#images.map((image) => image.reference)
52
55
  }
53
56
 
54
57
  /**
55
- * @param {'composition' | 'service'} type
58
+ * @param {'composition' | 'service' | 'mono'} type
56
59
  * @param {...any} args
57
60
  * @returns {toa.deployment.images.Image}
58
61
  */
59
- #create (type, ...args) {
62
+ #create(type, ...args) {
60
63
  const image = this.#factory[type](...args)
61
64
 
62
65
  this.#images.push(image)
@@ -64,58 +67,173 @@ class Registry {
64
67
  return image
65
68
  }
66
69
 
70
+ /**
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.
85
+ *
86
+ * @param {boolean} push
87
+ * @returns {Promise<void>}
88
+ */
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
+
95
+ const existing = await Promise.all(
96
+ images.map((image) => this.exists(image.reference))
97
+ )
98
+
99
+ const missing = images.filter((image, index) => {
100
+ if (existing[index]) console.log('Image already exists, skipping:', image.reference)
101
+
102
+ return !existing[index]
103
+ })
104
+
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
+ )
122
+ }
123
+
67
124
  /**
68
125
  * @param {toa.deployment.images.Image} image
69
126
  * @param {boolean} [push]
70
127
  * @returns {Promise<void>}
71
128
  */
72
- async #build (image, push = false) {
129
+ async #build(image, push = false) {
73
130
  const args = ['--context=default', 'buildx', 'build']
74
131
 
75
- if (push) {
76
- args.push('--push')
77
- } else {
78
- args.push('--load')
79
- }
132
+ if (push) args.push('--push')
133
+ else args.push('--load')
80
134
 
81
135
  args.push('--tag', image.reference, image.context)
82
136
 
83
- const multiarch = this.#registry.platforms !== null
84
-
85
- if (this.#registry.build?.arguments !== undefined) {
86
- for (const arg of this.#registry.build.arguments) args.push('--build-arg', `${arg}=${process.env[arg]}`)
137
+ if (image.arguments && this.#registry.build?.arguments !== undefined) {
138
+ for (const arg of this.#registry.build.arguments)
139
+ args.push('--build-arg', `${arg}=${process.env[arg]}`)
87
140
  }
88
141
 
89
- if (multiarch) {
90
- const platform = this.#registry.platforms.join(',')
91
- const builder = await this.#createBuilder()
142
+ const platforms = this.#registry.platforms
92
143
 
93
- args.push('--platform', platform)
94
- args.push('--builder', builder)
144
+ if (platforms !== null) args.push('--platform', platforms.join(','))
95
145
 
96
- } else {
97
- args.push('--builder', 'default')
98
- }
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)) {
150
+ args.push('--builder', await this.#ensureBuilder())
151
+
152
+ // the driver that produces attestations is this one, and nothing reads them;
153
+ // each is a manifest list to export and push per image
154
+ args.push('--provenance=false')
155
+ } else args.push('--builder', 'default')
99
156
 
100
157
  args.push('--progress', 'plain')
101
158
 
102
159
  await this.#process.execute('docker', args)
103
160
  }
104
161
 
105
- async #push (image) {
106
- await this.#build(image, true)
162
+ async exists(tag) {
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)
169
+
170
+ try {
171
+ await this.#process.execute('docker', args, { silently: true })
172
+ } catch (error) {
173
+ console.log(error.message)
174
+
175
+ return false
176
+ }
177
+
178
+ return true
107
179
  }
108
180
 
109
- async #createBuilder () {
110
- const name = `toa-${newid()}`
111
- const create = `buildx create --name ${name} --bootstrap --use`.split(' ')
181
+ /** @returns {Promise<string>} */
182
+ #ensureBuilder() {
183
+ // the promise is what is memoized, not its value: concurrent builds ask before any answers
184
+ this.#builder ??= this.#createBuilder()
112
185
 
113
- await this.#process.execute('docker', create)
186
+ return this.#builder
187
+ }
188
+
189
+ /** @returns {Promise<string>} */
190
+ async #createBuilder() {
191
+ try {
192
+ await this.#process.execute('docker', ['buildx', 'inspect', BUILDER], {
193
+ silently: true
194
+ })
195
+ } catch {
196
+ // on the host's network, a registry the host reaches as `localhost` is reached
197
+ await this.#process.execute('docker', [
198
+ 'buildx',
199
+ 'create',
200
+ '--name',
201
+ BUILDER,
202
+ '--driver',
203
+ 'docker-container',
204
+ '--driver-opt',
205
+ 'network=host',
206
+ '--bootstrap'
207
+ ])
208
+ }
114
209
 
115
- return name
210
+ return BUILDER
116
211
  }
117
212
  }
118
213
 
214
+ /**
215
+ * @template T
216
+ * @param {T[]} items
217
+ * @param {(item: T) => Promise<void>} work
218
+ * @returns {Promise<void>}
219
+ */
220
+ async function pool(items, work) {
221
+ const queue = items.slice()
222
+
223
+ const workers = Array.from(
224
+ { length: Math.min(CONCURRENCY, queue.length) },
225
+ async () => {
226
+ while (queue.length > 0) await work(queue.shift())
227
+ }
228
+ )
229
+
230
+ await Promise.all(workers)
231
+ }
232
+
119
233
  const BUILDER = 'toa'
120
234
 
121
- exports.Registry = Registry
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
+
238
+ /** How many builds run at once. Past this the uploads contend for the same uplink. */
239
+ const CONCURRENCY = 3
@@ -0,0 +1,407 @@
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 = /** @type {toa.operations.Process} */ {
19
+ execute: mock.fn(async (cmd, args) => {
20
+ if (args[0] === 'manifest') throw new Error('manifest unknown')
21
+
22
+ if (args[0] === 'buildx' && args[1] === 'inspect')
23
+ throw new Error('builder not found')
24
+
25
+ return ''
26
+ })
27
+ }
28
+
29
+ factory = /** @type {toa.deployment.images.Factory} */ {
30
+ composition: () => createImage('composition-mono', true),
31
+ service: () => createImage('extension-realtime'),
32
+ mono: () => createImage('mono', true)
33
+ }
34
+ })
35
+
36
+ it('should reuse named builder across images', async () => {
37
+ const registry = createRegistry({
38
+ base: 'example.com/reg',
39
+ platforms: ['linux/amd64', 'linux/arm64']
40
+ })
41
+
42
+ registry.composition(/** @type {any} */ ({}))
43
+ registry.service('.', /** @type {any} */ ({}))
44
+
45
+ await registry.build()
46
+
47
+ const creates = process.execute.mock.calls.filter(
48
+ ({ arguments: [, args] }) => args[0] === 'buildx' && args[1] === 'create'
49
+ )
50
+
51
+ assert.strictEqual(creates.length, 1)
52
+ assert.deepStrictEqual(creates[0].arguments[1], [
53
+ 'buildx',
54
+ 'create',
55
+ '--name',
56
+ 'toa',
57
+ '--driver',
58
+ 'docker-container',
59
+ '--driver-opt',
60
+ 'network=host',
61
+ '--bootstrap'
62
+ ])
63
+
64
+ const builds = process.execute.mock.calls.filter(
65
+ ({ arguments: [, args] }) =>
66
+ args[0] === '--context=default' && args[1] === 'buildx' && args[2] === 'build'
67
+ )
68
+
69
+ assert.strictEqual(builds.length, 3)
70
+
71
+ for (const {
72
+ arguments: [, args]
73
+ } of builds) {
74
+ assert.ok(args.includes('--builder'))
75
+ assert.strictEqual(args[args.indexOf('--builder') + 1], 'toa')
76
+ }
77
+ })
78
+
79
+ it('should not create builder when it already exists', async () => {
80
+ process.execute = mock.fn(async (cmd, args) => {
81
+ if (args[0] === 'manifest') throw new Error('manifest unknown')
82
+
83
+ return ''
84
+ })
85
+
86
+ const registry = createRegistry({
87
+ base: 'example.com/reg',
88
+ platforms: ['linux/amd64', 'linux/arm64']
89
+ })
90
+
91
+ registry.composition(/** @type {any} */ ({}))
92
+
93
+ await registry.build()
94
+
95
+ const creates = process.execute.mock.calls.filter(
96
+ ({ arguments: [, args] }) => args[0] === 'buildx' && args[1] === 'create'
97
+ )
98
+
99
+ assert.strictEqual(creates.length, 0)
100
+ })
101
+
102
+ it('should not export a build cache', async () => {
103
+ const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
104
+
105
+ registry.composition(/** @type {any} */ ({}))
106
+ registry.service('.', /** @type {any} */ ({}))
107
+
108
+ await registry.build()
109
+
110
+ const builds = process.execute.mock.calls.filter(
111
+ ({ arguments: [, args] }) => args[0] === '--context=default' && args[2] === 'build'
112
+ )
113
+
114
+ assert.strictEqual(builds.length, 3)
115
+
116
+ for (const {
117
+ arguments: [, args]
118
+ } of builds) {
119
+ assert.ok(!args.includes('--cache-from'))
120
+ assert.ok(!args.includes('--cache-to'))
121
+ }
122
+ })
123
+
124
+ it('should use the default builder for a single platform', async () => {
125
+ const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
126
+
127
+ registry.composition(/** @type {any} */ ({}))
128
+
129
+ await registry.build()
130
+
131
+ const creates = process.execute.mock.calls.filter(
132
+ ({ arguments: [, args] }) => args[0] === 'buildx' && args[1] === 'create'
133
+ )
134
+
135
+ assert.strictEqual(creates.length, 0)
136
+
137
+ const {
138
+ arguments: [, args]
139
+ } = process.execute.mock.calls.find(
140
+ ({ arguments: [, args] }) => args[0] === '--context=default' && args[2] === 'build'
141
+ )
142
+
143
+ assert.strictEqual(args[args.indexOf('--builder') + 1], 'default')
144
+ assert.ok(args.includes('--platform'))
145
+ assert.strictEqual(args[args.indexOf('--platform') + 1], 'linux/amd64')
146
+ })
147
+
148
+ it('should create the builder once when images build concurrently', async () => {
149
+ const registry = createRegistry({
150
+ base: 'example.com/reg',
151
+ platforms: ['linux/amd64', 'linux/arm64']
152
+ })
153
+
154
+ registry.composition(/** @type {any} */ ({}))
155
+ registry.service('.', /** @type {any} */ ({}))
156
+ registry.mono(/** @type {any} */ ({}))
157
+
158
+ await registry.build()
159
+
160
+ const creates = process.execute.mock.calls.filter(
161
+ ({ arguments: [, args] }) => args[0] === 'buildx' && args[1] === 'create'
162
+ )
163
+
164
+ assert.strictEqual(creates.length, 1)
165
+ })
166
+
167
+ it('should probe every image before building any', async () => {
168
+ const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
169
+
170
+ registry.composition(/** @type {any} */ ({}))
171
+ registry.service('.', /** @type {any} */ ({}))
172
+
173
+ await registry.build()
174
+
175
+ const calls = process.execute.mock.calls.map(({ arguments: [, args] }) => args[0])
176
+ const build = calls.indexOf('--context=default')
177
+ const probed = calls.slice(0, build).filter((argument) => argument === 'manifest')
178
+
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)
287
+ })
288
+
289
+ it('should use default builder when platforms is null', async () => {
290
+ const registry = createRegistry({ base: 'example.com/reg', platforms: null })
291
+
292
+ registry.composition(/** @type {any} */ ({}))
293
+
294
+ await registry.build()
295
+
296
+ const builds = process.execute.mock.calls.filter(
297
+ ({ arguments: [, args] }) => args[0] === '--context=default' && args[2] === 'build'
298
+ )
299
+
300
+ assert.strictEqual(builds.length, 2)
301
+
302
+ for (const {
303
+ arguments: [, args]
304
+ } of builds) {
305
+ assert.strictEqual(args[args.indexOf('--builder') + 1], 'default')
306
+ assert.ok(!args.includes('--platform'))
307
+ }
308
+ })
309
+
310
+ it('should skip build when image already exists', async () => {
311
+ process.execute = mock.fn(async () => '')
312
+
313
+ const registry = createRegistry({ base: 'example.com/reg', platforms: ['linux/amd64'] })
314
+
315
+ registry.composition(/** @type {any} */ ({}))
316
+
317
+ await registry.build()
318
+
319
+ const builds = process.execute.mock.calls.filter(
320
+ ({ arguments: [, args] }) => args[0] === '--context=default' && args[2] === 'build'
321
+ )
322
+
323
+ assert.strictEqual(builds.length, 0)
324
+ assert.ok(
325
+ process.execute.mock.calls.some(
326
+ (call) =>
327
+ call.arguments.length === 3 &&
328
+ isDeepStrictEqual(call.arguments[0], 'docker') &&
329
+ isDeepStrictEqual(call.arguments[1], [
330
+ 'manifest',
331
+ 'inspect',
332
+ images[0].reference
333
+ ]) &&
334
+ isDeepStrictEqual(call.arguments[2], { silently: true })
335
+ )
336
+ )
337
+ })
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
+
367
+ /**
368
+ * @param {object} [registry]
369
+ * @returns {Registry}
370
+ */
371
+ function createRegistry(registry = {}) {
372
+ return new Registry(registry, factory, process)
373
+ }
374
+
375
+ /**
376
+ * @param {string} name
377
+ * @param {boolean} [bundle] laid over an image of its dependencies
378
+ * @returns {toa.deployment.images.Image}
379
+ */
380
+ function createImage(name, bundle = false) {
381
+ const image = /** @type {toa.deployment.images.Image} */ {
382
+ reference: `example.com/reg/acme/${name}:abcdef12`,
383
+ context: `/tmp/${name}`,
384
+ prepare: mock.fn(async () => `/tmp/${name}`)
385
+ }
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
+
395
+ images.push(image)
396
+
397
+ return image
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
+ }