@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.
- package/CHANGELOG.md +42 -0
- package/image/runtime.Dockerfile +4 -0
- package/package.json +8 -10
- package/readme.md +93 -0
- package/src/deployment/.deployment/.describe/components.js +1 -5
- package/src/deployment/.deployment/.describe/compositions.js +26 -7
- package/src/deployment/.deployment/.describe/dependencies.js +1 -6
- package/src/deployment/.deployment/.describe/events.js +63 -0
- package/src/deployment/.deployment/.describe/fold.js +42 -0
- package/src/deployment/.deployment/.describe/fold.test.js +108 -0
- package/src/deployment/.deployment/.describe/index.js +4 -13
- package/src/deployment/.deployment/.describe/mounts.js +17 -0
- package/src/deployment/.deployment/.describe/resources.js +103 -0
- package/src/deployment/.deployment/.describe/resources.test.js +129 -0
- package/src/deployment/.deployment/.describe/services.js +32 -6
- package/src/deployment/.deployment/.describe/services.test.js +78 -0
- package/src/deployment/.deployment/.describe/variables.js +5 -9
- package/src/deployment/.deployment/declare.js +1 -5
- package/src/deployment/.deployment/describe.js +125 -10
- package/src/deployment/.deployment/index.js +3 -9
- package/src/deployment/.deployment/merge.js +66 -6
- package/src/deployment/.deployment/merge.test.js +133 -0
- package/src/deployment/chart/templates/components.yaml +1 -1
- package/src/deployment/chart/templates/compositions.yaml +96 -4
- package/src/deployment/chart/templates/mono.yaml +182 -0
- package/src/deployment/chart/templates/services.yaml +82 -7
- package/src/deployment/chart/values.yaml +23 -1
- package/src/deployment/composition.js +6 -6
- package/src/deployment/deployment.js +55 -25
- package/src/deployment/drain.js +70 -0
- package/src/deployment/drain.test.js +64 -0
- package/src/deployment/factory.js +121 -32
- package/src/deployment/images/bundle.js +82 -0
- package/src/deployment/images/composition.Dockerfile +8 -16
- package/src/deployment/images/composition.js +9 -56
- package/src/deployment/images/dependencies.Dockerfile +16 -0
- package/src/deployment/images/dependencies.js +141 -0
- package/src/deployment/images/dependencies.test.js +214 -0
- package/src/deployment/images/factory.js +31 -15
- package/src/deployment/images/format.js +52 -0
- package/src/deployment/images/format.test.js +74 -0
- package/src/deployment/images/image.fixtures.js +13 -13
- package/src/deployment/images/image.js +77 -38
- package/src/deployment/images/image.test.js +11 -5
- package/src/deployment/images/index.js +1 -5
- package/src/deployment/images/mono.Dockerfile +11 -0
- package/src/deployment/images/mono.js +15 -0
- package/src/deployment/images/publish.js +93 -0
- package/src/deployment/images/runtime-base.test.js +97 -0
- package/src/deployment/images/service.Dockerfile +5 -4
- package/src/deployment/images/service.js +13 -13
- package/src/deployment/index.js +1 -5
- package/src/deployment/operator.js +13 -13
- package/src/deployment/operator.test.js +8 -7
- package/src/deployment/registry.js +165 -47
- package/src/deployment/registry.test.js +407 -0
- package/src/deployment/service.js +4 -6
- package/src/deployment/workspace.js +13 -8
- package/src/index.js +1 -3
- package/src/process.js +51 -13
- package/src/process.test.js +33 -0
- package/types/_deployment/composition.d.ts +1 -3
- package/types/_deployment/dependency.d.ts +13 -7
- package/types/_deployment/deployment.d.ts +3 -7
- package/types/_deployment/factory.d.ts +2 -4
- package/types/_deployment/images/factory.d.ts +6 -8
- package/types/_deployment/images/image.d.ts +16 -1
- package/types/_deployment/images/registry.d.ts +8 -11
- package/types/_deployment/operator.d.ts +7 -9
- package/types/_deployment/registry.d.ts +4 -4
- package/types/_deployment/service.d.ts +4 -3
- package/types/dependency.ts +79 -0
- package/types/index.ts +1 -0
- package/types/dependency.d.ts +0 -39
- package/types/index.d.ts +0 -1
|
@@ -1,62 +1,65 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
|
25
|
+
composition(composition) {
|
|
25
26
|
return this.#create('composition', composition)
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
service
|
|
29
|
+
service(path, service) {
|
|
29
30
|
return this.#create('service', path, service)
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
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.#
|
|
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
|
|
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
|
|
49
|
+
async push() {
|
|
50
|
+
await this.#run(true)
|
|
51
|
+
}
|
|
50
52
|
|
|
51
|
-
|
|
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
|
|
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
|
|
129
|
+
async #build(image, push = false) {
|
|
73
130
|
const args = ['--context=default', 'buildx', 'build']
|
|
74
131
|
|
|
75
|
-
if (push)
|
|
76
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
const platform = this.#registry.platforms.join(',')
|
|
91
|
-
const builder = await this.#createBuilder()
|
|
142
|
+
const platforms = this.#registry.platforms
|
|
92
143
|
|
|
93
|
-
|
|
94
|
-
args.push('--builder', builder)
|
|
144
|
+
if (platforms !== null) args.push('--platform', platforms.join(','))
|
|
95
145
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|