@toa.io/operations 1.0.0-alpha.297 → 1.0.0-alpha.299
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 +15 -0
- package/image/runtime.Dockerfile +2 -6
- package/package.json +4 -3
- package/readme.md +47 -1
- package/src/deployment/factory.js +1 -1
- package/src/deployment/images/dependencies.Dockerfile +1 -3
- package/src/deployment/images/dependencies.test.js +1 -3
- package/src/deployment/images/format.test.js +1 -1
- package/src/deployment/images/service.Dockerfile +2 -2
- package/src/deployment/operator.js +9 -3
- package/src/deployment/operator.test.js +49 -0
- package/src/deployment/registry.js +46 -0
- package/src/deployment/registry.test.js +153 -21
- package/types/_deployment/registry.d.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [1.0.0-alpha.299](https://github.com/toa-io/toa/compare/v1.0.0-alpha.298...v1.0.0-alpha.299) (2026-09-10)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @toa.io/operations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# [1.0.0-alpha.298](https://github.com/toa-io/toa/compare/v1.0.0-alpha.297...v1.0.0-alpha.298) (2026-09-08)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **operations:** tag a deploy with the environment so registries can prune ([7247c35](https://github.com/toa-io/toa/commit/7247c35f33fb34dcfb3ffa04b196c14bfb3d5620))
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# [1.0.0-alpha.297](https://github.com/toa-io/toa/compare/v1.0.0-alpha.296...v1.0.0-alpha.297) (2026-09-08)
|
|
7
22
|
|
|
8
23
|
**Note:** Version bump only for package @toa.io/operations
|
package/image/runtime.Dockerfile
CHANGED
|
@@ -7,12 +7,8 @@ ARG VERSION
|
|
|
7
7
|
WORKDIR /toa
|
|
8
8
|
|
|
9
9
|
# mounted rather than written: npm's cache is a hundred megabytes, and a layer keeps whatever
|
|
10
|
-
# the command left behind
|
|
11
|
-
# `--legacy-peer-deps` installs no peer this does not ask for: `promex`, `reretry` and
|
|
12
|
-
# `matchacho` declare `typescript` a peer without marking it optional, and nothing here imports
|
|
13
|
-
# it. Every other peer in the closure is a dependency of the runtime's already. Drop the flag
|
|
14
|
-
# once those three are published with `peerDependenciesMeta`.
|
|
10
|
+
# the command left behind
|
|
15
11
|
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
16
|
-
npm i @toa.io/runtime@${VERSION} --omit=dev
|
|
12
|
+
npm i @toa.io/runtime@${VERSION} --omit=dev
|
|
17
13
|
|
|
18
14
|
ENV PATH=/toa/node_modules/.bin:$PATH
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/operations",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.299",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Toa Deployment",
|
|
6
6
|
"homepage": "https://toa.io",
|
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@toa.io/generic": "1.0.0-alpha.292",
|
|
32
32
|
"@toa.io/kubernetes": "1.0.0-alpha.293",
|
|
33
|
-
"@toa.io/norm": "1.0.0-alpha.
|
|
33
|
+
"@toa.io/norm": "1.0.0-alpha.299",
|
|
34
|
+
"oxc-parser": "0.149.0",
|
|
34
35
|
"paseto": "4.0.0"
|
|
35
36
|
},
|
|
36
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "2ba95b16f2cf06f82d6ff8d436761e843e5e4b43"
|
|
37
38
|
}
|
package/readme.md
CHANGED
|
@@ -79,7 +79,10 @@ downloads nor uploads the dependencies again. The same holds for `mono`.
|
|
|
79
79
|
Beside each component's sources the layer carries `manifest.toa.json`, the manifest as this
|
|
80
80
|
build normalised it. A composition reads it instead of normalising again, so it loads no
|
|
81
81
|
bridge to read what a module declares; a workspace has no such file and is read as it always
|
|
82
|
-
was.
|
|
82
|
+
was. A component an extension ships has none either — its package is published long before any
|
|
83
|
+
image is built — and is read from the digest `@toa.io/definitions` carries, which a deploy has
|
|
84
|
+
always read. So nothing that runs reads a module's source, and `oxc-parser` is installed only
|
|
85
|
+
where a manifest is normalised: a workspace and a deploy, both through `@toa.io/operations`. What a build cannot express there — a prototype in a directory of the application's own
|
|
83
86
|
rather than in a package — fails the build, where before it failed the container.
|
|
84
87
|
|
|
85
88
|
A pushed image builds on the `toa` container builder, whose registry exporter is what lays
|
|
@@ -175,3 +178,46 @@ a secret containing required credentials can be specified using `registry.creden
|
|
|
175
178
|
registry:
|
|
176
179
|
credentials: docker-credentials-secret-name
|
|
177
180
|
```
|
|
181
|
+
|
|
182
|
+
#### Toa images
|
|
183
|
+
|
|
184
|
+
Each release publishes `ghcr.io/toa-io/runtime:<version>` and, beside it, `:alpha` or `:latest`.
|
|
185
|
+
The extension services share that version:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
ghcr.io/toa-io/extension-exposition-gateway:1.0.0-alpha.285
|
|
189
|
+
ghcr.io/toa-io/extension-realtime-streams:1.0.0-alpha.285
|
|
190
|
+
ghcr.io/toa-io/extension-introspection-explorer:1.0.0-alpha.285
|
|
191
|
+
ghcr.io/toa-io/extension-configuration-values:1.0.0-alpha.285
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Untagged manifests are deleted. A prerelease older than 90 days, and not among the last 30, is
|
|
195
|
+
deleted with the four service images of that version. `:alpha`, `:latest`, and a version without
|
|
196
|
+
a pre-release suffix stay.
|
|
197
|
+
|
|
198
|
+
An application still on a deleted prerelease cannot pull `registry.services: published` and cannot
|
|
199
|
+
rebuild `FROM` that runtime. Images already in `registry.base` keep running. Stay inside the
|
|
200
|
+
window, or pin a graduated release.
|
|
201
|
+
|
|
202
|
+
#### Userspace images
|
|
203
|
+
|
|
204
|
+
`toa deploy production` writes two tags on the same composition image (`mono` and a service
|
|
205
|
+
`registry.services: build` built are the same):
|
|
206
|
+
|
|
207
|
+
- `<name>:<hash>` — what the chart pins. A hash of the runtime and the sources. `toa push` writes
|
|
208
|
+
this tag and nothing else.
|
|
209
|
+
- `<name>:production` — moved onto that image. The next production deploy moves it again.
|
|
210
|
+
|
|
211
|
+
`deps-<hash>` is not tagged with the environment. Do not name an environment eight hex digits, or
|
|
212
|
+
`deps-` plus eight: a retention job would treat it as a content tag.
|
|
213
|
+
|
|
214
|
+
A job the application runs deletes every `<hash>` and `deps-<hash>` older than 7 days, except the
|
|
215
|
+
last 5, except the image `:production` (or `:staging`, or `:latest`) currently names. Then:
|
|
216
|
+
|
|
217
|
+
```shell
|
|
218
|
+
$ doctl registries garbage-collection start --include-untagged-manifests <registry>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Rollback is only those 7 days, those last 5, and whatever the environment tag still names. Turn
|
|
222
|
+
the job on after the first deploy that writes that tag. The first garbage collection returns the
|
|
223
|
+
space the old tags held, and blocks pushes while it runs.
|
|
@@ -19,7 +19,5 @@ RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
|
19
19
|
# what the extensions need for what these components declare: installed beside the extension
|
|
20
20
|
# that reads the declaration, in Toa's own install, because that is where Node resolves an
|
|
21
21
|
# extension's imports from — and one copy for the composition rather than one per component
|
|
22
|
-
# `--legacy-peer-deps` for the reason the base image installs with it: what is added here
|
|
23
|
-
# re-resolves the tree, and a peer nothing imports comes back with it — 23 MB of `typescript`
|
|
24
22
|
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
25
|
-
if [ -s .packages ]; then npm i --prefix /toa --omit=dev
|
|
23
|
+
if [ -s .packages ]; then npm i --prefix /toa --omit=dev $(cat .packages); fi
|
|
@@ -176,9 +176,7 @@ describe('prepare', () => {
|
|
|
176
176
|
|
|
177
177
|
const dockerfile = await readFile(join(context, 'Dockerfile'), 'utf8')
|
|
178
178
|
|
|
179
|
-
assert.ok(
|
|
180
|
-
dockerfile.includes('npm i --prefix /toa --omit=dev --legacy-peer-deps $(cat .packages)')
|
|
181
|
-
)
|
|
179
|
+
assert.ok(dockerfile.includes('npm i --prefix /toa --omit=dev $(cat .packages)'))
|
|
182
180
|
})
|
|
183
181
|
|
|
184
182
|
it('should hold what a service the composition runs brings', async () => {
|
|
@@ -144,6 +144,6 @@ describe('a service image', () => {
|
|
|
144
144
|
|
|
145
145
|
const dockerfile = await readFile(join(context, 'Dockerfile'), 'utf8')
|
|
146
146
|
|
|
147
|
-
assert.ok(dockerfile.includes('npm i --prefix /toa --omit=dev
|
|
147
|
+
assert.ok(dockerfile.includes('npm i --prefix /toa --omit=dev $(cat .packages)'))
|
|
148
148
|
})
|
|
149
149
|
})
|
|
@@ -14,12 +14,12 @@ RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
|
14
14
|
# component rather than beside the extension: an application that runs none of them, and the
|
|
15
15
|
# runtime image every application is built on, carry nothing for them
|
|
16
16
|
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
17
|
-
for entry in components/*; do if grep -qs '"dependencies"' "$entry/package.json"; then (cd $entry && npm i --omit=dev
|
|
17
|
+
for entry in components/*; do if grep -qs '"dependencies"' "$entry/package.json"; then (cd $entry && npm i --omit=dev); fi; done
|
|
18
18
|
|
|
19
19
|
# and beside Toa as well, because what an extension imports on a component's behalf — a storage
|
|
20
20
|
# provider's SDK — is imported from where the extension is, which is `/toa` and not here
|
|
21
21
|
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
22
|
-
if [ -s .packages ]; then npm i --prefix /toa --omit=dev
|
|
22
|
+
if [ -s .packages ]; then npm i --prefix /toa --omit=dev $(cat .packages); fi
|
|
23
23
|
|
|
24
24
|
# no USER: the runtime drops to `node` itself, and only a process that started as root can
|
|
25
25
|
# close its environment under /proc — see runtime/runtime/bin/toa
|
|
@@ -7,13 +7,18 @@ export class Operator {
|
|
|
7
7
|
/** @type {toa.deployment.Registry} */
|
|
8
8
|
#registry
|
|
9
9
|
|
|
10
|
+
/** @type {string | undefined} */
|
|
11
|
+
#environment
|
|
12
|
+
|
|
10
13
|
/**
|
|
11
|
-
* @param
|
|
12
|
-
* @param
|
|
14
|
+
* @param {toa.deployment.Deployment} deployment
|
|
15
|
+
* @param {toa.deployment.Registry} registry
|
|
16
|
+
* @param {string} [environment]
|
|
13
17
|
*/
|
|
14
|
-
constructor(deployment, registry) {
|
|
18
|
+
constructor(deployment, registry, environment) {
|
|
15
19
|
this.#deployment = deployment
|
|
16
20
|
this.#registry = registry
|
|
21
|
+
this.#environment = environment
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
async export(path) {
|
|
@@ -36,6 +41,7 @@ export class Operator {
|
|
|
36
41
|
options = Object.assign({}, OPTIONS, options)
|
|
37
42
|
|
|
38
43
|
await Promise.all([this.export(), this.push()])
|
|
44
|
+
await this.#registry.alias(this.#environment)
|
|
39
45
|
await this.#deployment.install(options)
|
|
40
46
|
}
|
|
41
47
|
|
|
@@ -43,3 +43,52 @@ describe('env', () => {
|
|
|
43
43
|
assert.deepStrictEqual(output, variables)
|
|
44
44
|
})
|
|
45
45
|
})
|
|
46
|
+
|
|
47
|
+
describe('install', () => {
|
|
48
|
+
/** @type {toa.deployment.Deployment} */
|
|
49
|
+
let deployment
|
|
50
|
+
|
|
51
|
+
/** @type {toa.deployment.Registry} */
|
|
52
|
+
let registry
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
deployment = /** @type {toa.deployment.Deployment} */ {
|
|
56
|
+
export: mock.fn(async () => {}),
|
|
57
|
+
install: mock.fn(async () => {})
|
|
58
|
+
}
|
|
59
|
+
registry = /** @type {toa.deployment.Registry} */ {
|
|
60
|
+
push: mock.fn(async () => {}),
|
|
61
|
+
alias: mock.fn(async () => {})
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should tag the environment after a push', async () => {
|
|
66
|
+
const order = []
|
|
67
|
+
|
|
68
|
+
registry.push = mock.fn(async () => {
|
|
69
|
+
order.push('push')
|
|
70
|
+
})
|
|
71
|
+
registry.alias = mock.fn(async () => {
|
|
72
|
+
order.push('alias')
|
|
73
|
+
})
|
|
74
|
+
deployment.install = mock.fn(async () => {
|
|
75
|
+
order.push('install')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
operator = new Operator(deployment, registry, 'production')
|
|
79
|
+
|
|
80
|
+
await operator.install()
|
|
81
|
+
|
|
82
|
+
assert.deepStrictEqual(order, ['push', 'alias', 'install'])
|
|
83
|
+
assert.deepStrictEqual(registry.alias.mock.calls[0].arguments, ['production'])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should not tag the environment on push', async () => {
|
|
87
|
+
operator = new Operator(deployment, registry, 'production')
|
|
88
|
+
|
|
89
|
+
await operator.push()
|
|
90
|
+
|
|
91
|
+
assert.strictEqual(registry.push.mock.callCount(), 1)
|
|
92
|
+
assert.strictEqual(registry.alias.mock.callCount(), 0)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -50,6 +50,24 @@ export class Registry {
|
|
|
50
50
|
await this.#run(true)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* A second tag on each workload image, the environment, so a retention job can see what
|
|
55
|
+
* a deploy left running. `deps-*` is not in `#images`.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} [environment]
|
|
58
|
+
* @returns {Promise<void>}
|
|
59
|
+
*/
|
|
60
|
+
async alias(environment) {
|
|
61
|
+
if (
|
|
62
|
+
typeof environment !== 'string' ||
|
|
63
|
+
environment === '' ||
|
|
64
|
+
CONTENT_TAG.test(environment)
|
|
65
|
+
)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
await Promise.all(this.#images.map((image) => this.#alias(image, environment)))
|
|
69
|
+
}
|
|
70
|
+
|
|
53
71
|
tags() {
|
|
54
72
|
return this.#images.map((image) => image.reference)
|
|
55
73
|
}
|
|
@@ -159,6 +177,22 @@ export class Registry {
|
|
|
159
177
|
await this.#process.execute('docker', args)
|
|
160
178
|
}
|
|
161
179
|
|
|
180
|
+
/**
|
|
181
|
+
* @param {toa.deployment.images.Image} image
|
|
182
|
+
* @param {string} environment
|
|
183
|
+
* @returns {Promise<void>}
|
|
184
|
+
*/
|
|
185
|
+
async #alias(image, environment) {
|
|
186
|
+
const target = retag(image.reference, environment)
|
|
187
|
+
const args = ['buildx', 'imagetools', 'create', '--tag', target]
|
|
188
|
+
|
|
189
|
+
if (LOCAL.test(image.reference)) args.push('--insecure')
|
|
190
|
+
|
|
191
|
+
args.push(image.reference)
|
|
192
|
+
|
|
193
|
+
await this.#process.execute('docker', args)
|
|
194
|
+
}
|
|
195
|
+
|
|
162
196
|
async exists(tag) {
|
|
163
197
|
const args = ['manifest', 'inspect']
|
|
164
198
|
|
|
@@ -230,8 +264,20 @@ async function pool(items, work) {
|
|
|
230
264
|
await Promise.all(workers)
|
|
231
265
|
}
|
|
232
266
|
|
|
267
|
+
/**
|
|
268
|
+
* @param {string} reference
|
|
269
|
+
* @param {string} tag
|
|
270
|
+
* @returns {string}
|
|
271
|
+
*/
|
|
272
|
+
function retag(reference, tag) {
|
|
273
|
+
return reference.slice(0, reference.lastIndexOf(':') + 1) + tag
|
|
274
|
+
}
|
|
275
|
+
|
|
233
276
|
const BUILDER = 'toa'
|
|
234
277
|
|
|
278
|
+
/** An environment named this way is a content tag, and must not be moved as one. */
|
|
279
|
+
const CONTENT_TAG = /^(deps-)?[0-9a-f]{8}$/
|
|
280
|
+
|
|
235
281
|
/** A reference into a registry on this machine, by any of the names it goes by. */
|
|
236
282
|
const LOCAL = /^(localhost|127\.\d+\.\d+\.\d+|host\.docker\.internal)(:\d+)?\//
|
|
237
283
|
|
|
@@ -15,16 +15,7 @@ let images
|
|
|
15
15
|
|
|
16
16
|
beforeEach(() => {
|
|
17
17
|
images = []
|
|
18
|
-
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
|
-
}
|
|
18
|
+
process = execute()
|
|
28
19
|
|
|
29
20
|
factory = /** @type {toa.deployment.images.Factory} */ {
|
|
30
21
|
composition: () => createImage('composition-mono', true),
|
|
@@ -336,25 +327,115 @@ it('should skip build when image already exists', async () => {
|
|
|
336
327
|
)
|
|
337
328
|
})
|
|
338
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
|
+
|
|
339
418
|
it('should probe a local registry as insecure', async () => {
|
|
340
|
-
|
|
419
|
+
const proc = execute()
|
|
341
420
|
|
|
342
|
-
|
|
421
|
+
proc.execute = mock.fn(async () => '')
|
|
343
422
|
|
|
344
|
-
|
|
345
|
-
const image = createImage('composition-mono', true)
|
|
423
|
+
const image = createImage('composition-mono', true)
|
|
346
424
|
|
|
347
|
-
|
|
348
|
-
|
|
425
|
+
image.reference = 'localhost:5000/acme/composition-mono:abcdef12'
|
|
426
|
+
image.dependencies.reference = 'localhost:5000/acme/composition-mono:deps-12345678'
|
|
349
427
|
|
|
350
|
-
|
|
351
|
-
|
|
428
|
+
const registry = new Registry(
|
|
429
|
+
{ base: 'localhost:5000', platforms: null },
|
|
430
|
+
{ composition: () => image, service: factory.service, mono: factory.mono },
|
|
431
|
+
proc
|
|
432
|
+
)
|
|
352
433
|
|
|
353
434
|
registry.composition(/** @type {any} */ ({}))
|
|
354
435
|
|
|
355
436
|
await registry.build()
|
|
356
437
|
|
|
357
|
-
const probes =
|
|
438
|
+
const probes = proc.execute.mock.calls
|
|
358
439
|
.filter(({ arguments: [, args] }) => args[0] === 'manifest')
|
|
359
440
|
.map(({ arguments: [, args] }) => args)
|
|
360
441
|
|
|
@@ -364,12 +445,56 @@ it('should probe a local registry as insecure', async () => {
|
|
|
364
445
|
assert.deepStrictEqual(args.slice(0, 3), ['manifest', 'inspect', '--insecure'])
|
|
365
446
|
})
|
|
366
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
|
+
|
|
367
478
|
/**
|
|
368
479
|
* @param {object} [registry]
|
|
369
480
|
* @returns {Registry}
|
|
370
481
|
*/
|
|
371
|
-
function createRegistry(registry = {}) {
|
|
372
|
-
return new Registry(registry, factory,
|
|
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
|
+
}
|
|
373
498
|
}
|
|
374
499
|
|
|
375
500
|
/**
|
|
@@ -397,6 +522,13 @@ function createImage(name, bundle = false) {
|
|
|
397
522
|
return image
|
|
398
523
|
}
|
|
399
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
|
+
|
|
400
532
|
/** @returns {string[][]} the arguments of every build run */
|
|
401
533
|
function builds() {
|
|
402
534
|
return process.execute.mock.calls
|