@toa.io/operations 1.0.0-alpha.31 → 1.0.0-alpha.310
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +213 -0
- package/image/runtime.Dockerfile +14 -0
- package/package.json +10 -10
- package/readme.md +201 -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 +129 -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 +153 -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.configmap.yaml +14 -0
- package/src/deployment/chart/templates/components.yaml +1 -1
- package/src/deployment/chart/templates/compositions.yaml +109 -4
- package/src/deployment/chart/templates/mono.yaml +195 -0
- package/src/deployment/chart/templates/services.yaml +97 -7
- package/src/deployment/chart/values.yaml +23 -1
- package/src/deployment/composition.js +8 -6
- package/src/deployment/deployment.js +147 -34
- package/src/deployment/drain.js +70 -0
- package/src/deployment/drain.test.js +64 -0
- package/src/deployment/factory.js +169 -37
- package/src/deployment/images/bundle.js +88 -0
- package/src/deployment/images/composition.Dockerfile +9 -17
- package/src/deployment/images/composition.js +9 -56
- package/src/deployment/images/dependencies.Dockerfile +23 -0
- package/src/deployment/images/dependencies.js +171 -0
- package/src/deployment/images/dependencies.test.js +268 -0
- package/src/deployment/images/factory.js +31 -15
- package/src/deployment/images/format.js +66 -0
- package/src/deployment/images/format.test.js +149 -0
- package/src/deployment/images/image.fixtures.js +17 -19
- package/src/deployment/images/image.js +79 -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 +91 -0
- package/src/deployment/images/runtime-base.test.js +97 -0
- package/src/deployment/images/service.Dockerfile +17 -5
- package/src/deployment/images/service.js +63 -14
- package/src/deployment/index.js +1 -5
- package/src/deployment/operator.js +22 -16
- package/src/deployment/operator.test.js +57 -7
- package/src/deployment/registry.js +210 -46
- package/src/deployment/registry.test.js +539 -0
- package/src/deployment/service.js +4 -6
- package/src/deployment/workspace.js +13 -8
- package/src/index.js +3 -2
- package/src/process.js +51 -13
- package/src/process.test.js +33 -0
- package/types/_deployment/composition.d.ts +2 -3
- package/types/_deployment/dependency.d.ts +13 -7
- package/types/_deployment/deployment.d.ts +7 -8
- 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 +10 -9
- package/types/_deployment/registry.d.ts +7 -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,12 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { join, dirname } from 'node:path'
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
import { Image } from './image.js'
|
|
6
|
+
import { cp, writeFile } from 'node:fs/promises'
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
const
|
|
8
|
+
// a service is named the way a package is, and its directory is where it lives
|
|
9
|
+
const require = createRequire(import.meta.url)
|
|
7
10
|
|
|
8
|
-
class Service extends Image {
|
|
9
|
-
dockerfile = join(
|
|
11
|
+
export class Service extends Image {
|
|
12
|
+
dockerfile = join(import.meta.dirname, 'service.Dockerfile')
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* Used by Dockerfile
|
|
@@ -35,7 +38,7 @@ class Service extends Image {
|
|
|
35
38
|
* @param {string} reference
|
|
36
39
|
* @param {toa.deployment.dependency.Service} service
|
|
37
40
|
*/
|
|
38
|
-
constructor
|
|
41
|
+
constructor(scope, runtime, registry, reference, service) {
|
|
39
42
|
super(scope, runtime, registry)
|
|
40
43
|
|
|
41
44
|
this.service = service.name
|
|
@@ -46,29 +49,75 @@ class Service extends Image {
|
|
|
46
49
|
this.#version = service.version
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
get name
|
|
52
|
+
get name() {
|
|
50
53
|
return 'extension-' + this.#group + '-' + this.#name
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
get version
|
|
56
|
+
get version() {
|
|
54
57
|
return this.#version
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
async prepare
|
|
60
|
+
async prepare(root) {
|
|
58
61
|
const context = await super.prepare(root)
|
|
59
62
|
|
|
60
|
-
await
|
|
63
|
+
await cp(this.#path, context, { recursive: true })
|
|
64
|
+
await writeFile(join(context, PACKAGES), this.#packages().join('\n'))
|
|
61
65
|
|
|
62
66
|
return context
|
|
63
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* What the components this service runs declare, as `name@version`.
|
|
71
|
+
*
|
|
72
|
+
* Each is installed beside its component, where the component's own modules import it, and
|
|
73
|
+
* beside Toa, where an extension that imports on a component's behalf is — a storage
|
|
74
|
+
* provider's SDK is imported by `@toa.io/extensions.storages`, not by the component that
|
|
75
|
+
* declares the storage.
|
|
76
|
+
*
|
|
77
|
+
* @returns {string[]}
|
|
78
|
+
*/
|
|
79
|
+
#packages() {
|
|
80
|
+
const directory = join(this.#path, 'components')
|
|
81
|
+
|
|
82
|
+
if (!existsSync(directory)) return []
|
|
83
|
+
|
|
84
|
+
/** @type {Record<string, string>} */
|
|
85
|
+
const packages = {}
|
|
86
|
+
|
|
87
|
+
for (const label of readdirSync(directory)) {
|
|
88
|
+
const manifest = join(directory, label, 'package.json')
|
|
89
|
+
|
|
90
|
+
if (!existsSync(manifest)) continue
|
|
91
|
+
|
|
92
|
+
Object.assign(packages, JSON.parse(readFileSync(manifest, 'utf8')).dependencies)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return Object.entries(packages)
|
|
96
|
+
.map(([name, version]) => `${name}@${version}`)
|
|
97
|
+
.sort()
|
|
98
|
+
}
|
|
64
99
|
}
|
|
65
100
|
|
|
101
|
+
/** Where the install reads what the components need, one `name@version` per line. */
|
|
102
|
+
const PACKAGES = '.packages'
|
|
103
|
+
|
|
66
104
|
/**
|
|
105
|
+
* Where the extension is installed, which is what its image is built from. A deploy install
|
|
106
|
+
* carries what an extension declares and not the extension, so a service is built only where
|
|
107
|
+
* the runtime is installed beside the deployment library; elsewhere its image is published.
|
|
108
|
+
*
|
|
67
109
|
* @param {string} reference
|
|
68
110
|
* @returns {string}
|
|
69
111
|
*/
|
|
70
112
|
const find = (reference) => {
|
|
71
|
-
|
|
113
|
+
try {
|
|
114
|
+
return dirname(require.resolve(join(reference, 'package.json')))
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`'${reference}' is not installed, and \`registry.services: build\` builds its image ` +
|
|
118
|
+
'from where it is: install the runtime beside @toa.io/operations, or take the ' +
|
|
119
|
+
'image the release publishes with `registry.services: published`',
|
|
120
|
+
{ cause: error }
|
|
121
|
+
)
|
|
122
|
+
}
|
|
72
123
|
}
|
|
73
|
-
|
|
74
|
-
exports.Service = Service
|
package/src/deployment/index.js
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
import * as workspace from './workspace.js'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class Operator {
|
|
3
|
+
export class Operator {
|
|
6
4
|
/** @type {toa.deployment.Deployment} */
|
|
7
5
|
#deployment
|
|
8
6
|
|
|
9
7
|
/** @type {toa.deployment.Registry} */
|
|
10
8
|
#registry
|
|
11
9
|
|
|
10
|
+
/** @type {string | undefined} */
|
|
11
|
+
#environment
|
|
12
|
+
|
|
12
13
|
/**
|
|
13
|
-
* @param
|
|
14
|
-
* @param
|
|
14
|
+
* @param {toa.deployment.Deployment} deployment
|
|
15
|
+
* @param {toa.deployment.Registry} registry
|
|
16
|
+
* @param {string} [environment]
|
|
15
17
|
*/
|
|
16
|
-
constructor
|
|
18
|
+
constructor(deployment, registry, environment) {
|
|
17
19
|
this.#deployment = deployment
|
|
18
20
|
this.#registry = registry
|
|
21
|
+
this.#environment = environment
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
async export
|
|
24
|
+
async export(path) {
|
|
22
25
|
const target = await workspace.create('deployment', path)
|
|
23
26
|
|
|
24
27
|
await this.#deployment.export(target)
|
|
@@ -26,33 +29,36 @@ class Operator {
|
|
|
26
29
|
return target
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
async prepare
|
|
32
|
+
async prepare(path) {
|
|
30
33
|
return await this.#registry.prepare(path)
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
async push
|
|
36
|
+
async push() {
|
|
34
37
|
await this.#registry.push()
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
async install
|
|
40
|
+
async install(options = {}) {
|
|
38
41
|
options = Object.assign({}, OPTIONS, options)
|
|
39
42
|
|
|
40
43
|
await Promise.all([this.export(), this.push()])
|
|
44
|
+
await this.#registry.alias(this.#environment)
|
|
41
45
|
await this.#deployment.install(options)
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
async template
|
|
48
|
+
async template(options = {}) {
|
|
45
49
|
await this.export()
|
|
46
50
|
|
|
47
51
|
return await this.#deployment.template(options)
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
variables
|
|
51
|
-
return this.#deployment.variables()
|
|
54
|
+
variables(options) {
|
|
55
|
+
return this.#deployment.variables(options)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
tags() {
|
|
59
|
+
return this.#registry.tags()
|
|
52
60
|
}
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
/** @type {toa.deployment.installation.Options} */
|
|
56
64
|
const OPTIONS = { wait: false }
|
|
57
|
-
|
|
58
|
-
exports.Operator = Operator
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import { describe, it, beforeEach, mock } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
import { Operator } from '../../src/deployment/operator.js'
|
|
5
|
+
import { generate } from 'randomstring'
|
|
5
6
|
|
|
6
7
|
it('should be', async () => {
|
|
7
|
-
|
|
8
|
+
assert.ok(Operator instanceof Function)
|
|
8
9
|
})
|
|
9
10
|
|
|
10
11
|
/** @type {toa.deployment.Operator} */
|
|
@@ -25,7 +26,7 @@ describe('env', () => {
|
|
|
25
26
|
it('should be', async () => {
|
|
26
27
|
operator = new Operator(deployment, registry)
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
assert.ok(operator.variables instanceof Function)
|
|
29
30
|
})
|
|
30
31
|
|
|
31
32
|
it('should return variables', async () => {
|
|
@@ -33,12 +34,61 @@ describe('env', () => {
|
|
|
33
34
|
|
|
34
35
|
deployment.variables =
|
|
35
36
|
/** @type {typeof toa.deployment.Operator.variables} */
|
|
36
|
-
|
|
37
|
+
mock.fn(() => variables)
|
|
37
38
|
|
|
38
39
|
operator = new Operator(deployment, registry)
|
|
39
40
|
|
|
40
41
|
const output = operator.variables()
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
assert.deepStrictEqual(output, variables)
|
|
44
|
+
})
|
|
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)
|
|
43
93
|
})
|
|
44
94
|
})
|
|
@@ -1,62 +1,83 @@
|
|
|
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
|
|
45
|
+
async build() {
|
|
46
|
+
await this.#run(false)
|
|
47
|
+
}
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
49
|
+
async push() {
|
|
50
|
+
await this.#run(true)
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
}
|
|
50
70
|
|
|
51
|
-
|
|
71
|
+
tags() {
|
|
72
|
+
return this.#images.map((image) => image.reference)
|
|
52
73
|
}
|
|
53
74
|
|
|
54
75
|
/**
|
|
55
|
-
* @param {'composition' | 'service'} type
|
|
76
|
+
* @param {'composition' | 'service' | 'mono'} type
|
|
56
77
|
* @param {...any} args
|
|
57
78
|
* @returns {toa.deployment.images.Image}
|
|
58
79
|
*/
|
|
59
|
-
#create
|
|
80
|
+
#create(type, ...args) {
|
|
60
81
|
const image = this.#factory[type](...args)
|
|
61
82
|
|
|
62
83
|
this.#images.push(image)
|
|
@@ -64,58 +85,201 @@ class Registry {
|
|
|
64
85
|
return image
|
|
65
86
|
}
|
|
66
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Every image and what it is laid over.
|
|
90
|
+
*
|
|
91
|
+
* @returns {toa.deployment.images.Image[]}
|
|
92
|
+
*/
|
|
93
|
+
#all() {
|
|
94
|
+
return this.#images.flatMap((image) =>
|
|
95
|
+
image.dependencies === undefined ? [image] : [image.dependencies, image]
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Every image is probed at once, and what is missing is prepared and built concurrently:
|
|
101
|
+
* a build is bound by the registry it uploads to, not by the runner, so one at a time
|
|
102
|
+
* leaves both idle. What an image is laid over is built before it.
|
|
103
|
+
*
|
|
104
|
+
* @param {boolean} push
|
|
105
|
+
* @returns {Promise<void>}
|
|
106
|
+
*/
|
|
107
|
+
async #run(push) {
|
|
108
|
+
// a push builds on the container driver, whose bootstrap is spent while the probes are out
|
|
109
|
+
if (push) this.#ensureBuilder().catch(() => undefined)
|
|
110
|
+
|
|
111
|
+
const images = this.#all()
|
|
112
|
+
|
|
113
|
+
const existing = await Promise.all(
|
|
114
|
+
images.map((image) => this.exists(image.reference))
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const missing = images.filter((image, index) => {
|
|
118
|
+
if (existing[index]) console.log('Image already exists, skipping:', image.reference)
|
|
119
|
+
|
|
120
|
+
return !existing[index]
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (missing.length === 0) return
|
|
124
|
+
|
|
125
|
+
const path = await workspace.create('images')
|
|
126
|
+
|
|
127
|
+
await Promise.all(missing.map((image) => image.prepare(path)))
|
|
128
|
+
|
|
129
|
+
const bases = new Set(this.#images.map((image) => image.dependencies))
|
|
130
|
+
const build = (image) => this.#build(image, push)
|
|
131
|
+
|
|
132
|
+
await pool(
|
|
133
|
+
missing.filter((image) => bases.has(image)),
|
|
134
|
+
build
|
|
135
|
+
)
|
|
136
|
+
await pool(
|
|
137
|
+
missing.filter((image) => !bases.has(image)),
|
|
138
|
+
build
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
67
142
|
/**
|
|
68
143
|
* @param {toa.deployment.images.Image} image
|
|
69
144
|
* @param {boolean} [push]
|
|
70
145
|
* @returns {Promise<void>}
|
|
71
146
|
*/
|
|
72
|
-
async #build
|
|
147
|
+
async #build(image, push = false) {
|
|
73
148
|
const args = ['--context=default', 'buildx', 'build']
|
|
74
149
|
|
|
75
|
-
if (push)
|
|
76
|
-
|
|
77
|
-
} else {
|
|
78
|
-
args.push('--load')
|
|
79
|
-
}
|
|
150
|
+
if (push) args.push('--push')
|
|
151
|
+
else args.push('--load')
|
|
80
152
|
|
|
81
153
|
args.push('--tag', image.reference, image.context)
|
|
82
154
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
for (const arg of this.#registry.build.arguments) args.push('--build-arg', `${arg}=${process.env[arg]}`)
|
|
155
|
+
if (image.arguments && this.#registry.build?.arguments !== undefined) {
|
|
156
|
+
for (const arg of this.#registry.build.arguments)
|
|
157
|
+
args.push('--build-arg', `${arg}=${process.env[arg]}`)
|
|
87
158
|
}
|
|
88
159
|
|
|
89
|
-
|
|
90
|
-
const platform = this.#registry.platforms.join(',')
|
|
91
|
-
const builder = await this.#createBuilder()
|
|
160
|
+
const platforms = this.#registry.platforms
|
|
92
161
|
|
|
93
|
-
|
|
94
|
-
args.push('--builder', builder)
|
|
162
|
+
if (platforms !== null) args.push('--platform', platforms.join(','))
|
|
95
163
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
164
|
+
// a push takes the container driver: its registry exporter is what lays an image over a
|
|
165
|
+
// base it never pulled. A load takes the daemon's own, unless it builds for a platform
|
|
166
|
+
// the runner is not, which the daemon's cannot
|
|
167
|
+
if (push || (platforms !== null && platforms.length > 1)) {
|
|
168
|
+
args.push('--builder', await this.#ensureBuilder())
|
|
169
|
+
|
|
170
|
+
// the driver that produces attestations is this one, and nothing reads them;
|
|
171
|
+
// each is a manifest list to export and push per image
|
|
172
|
+
args.push('--provenance=false')
|
|
173
|
+
} else args.push('--builder', 'default')
|
|
99
174
|
|
|
100
175
|
args.push('--progress', 'plain')
|
|
101
176
|
|
|
102
177
|
await this.#process.execute('docker', args)
|
|
103
178
|
}
|
|
104
179
|
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
|
|
196
|
+
async exists(tag) {
|
|
197
|
+
const args = ['manifest', 'inspect']
|
|
198
|
+
|
|
199
|
+
// a registry on this machine speaks plain HTTP, which the probe has to be told
|
|
200
|
+
if (LOCAL.test(tag)) args.push('--insecure')
|
|
201
|
+
|
|
202
|
+
args.push(tag)
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
await this.#process.execute('docker', args, { silently: true })
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.log(error.message)
|
|
208
|
+
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return true
|
|
107
213
|
}
|
|
108
214
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
215
|
+
/** @returns {Promise<string>} */
|
|
216
|
+
#ensureBuilder() {
|
|
217
|
+
// the promise is what is memoized, not its value: concurrent builds ask before any answers
|
|
218
|
+
this.#builder ??= this.#createBuilder()
|
|
112
219
|
|
|
113
|
-
|
|
220
|
+
return this.#builder
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** @returns {Promise<string>} */
|
|
224
|
+
async #createBuilder() {
|
|
225
|
+
try {
|
|
226
|
+
await this.#process.execute('docker', ['buildx', 'inspect', BUILDER], {
|
|
227
|
+
silently: true
|
|
228
|
+
})
|
|
229
|
+
} catch {
|
|
230
|
+
// on the host's network, a registry the host reaches as `localhost` is reached
|
|
231
|
+
await this.#process.execute('docker', [
|
|
232
|
+
'buildx',
|
|
233
|
+
'create',
|
|
234
|
+
'--name',
|
|
235
|
+
BUILDER,
|
|
236
|
+
'--driver',
|
|
237
|
+
'docker-container',
|
|
238
|
+
'--driver-opt',
|
|
239
|
+
'network=host',
|
|
240
|
+
'--bootstrap'
|
|
241
|
+
])
|
|
242
|
+
}
|
|
114
243
|
|
|
115
|
-
return
|
|
244
|
+
return BUILDER
|
|
116
245
|
}
|
|
117
246
|
}
|
|
118
247
|
|
|
248
|
+
/**
|
|
249
|
+
* @template T
|
|
250
|
+
* @param {T[]} items
|
|
251
|
+
* @param {(item: T) => Promise<void>} work
|
|
252
|
+
* @returns {Promise<void>}
|
|
253
|
+
*/
|
|
254
|
+
async function pool(items, work) {
|
|
255
|
+
const queue = items.slice()
|
|
256
|
+
|
|
257
|
+
const workers = Array.from(
|
|
258
|
+
{ length: Math.min(CONCURRENCY, queue.length) },
|
|
259
|
+
async () => {
|
|
260
|
+
while (queue.length > 0) await work(queue.shift())
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
await Promise.all(workers)
|
|
265
|
+
}
|
|
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
|
+
|
|
119
276
|
const BUILDER = 'toa'
|
|
120
277
|
|
|
121
|
-
|
|
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
|
+
|
|
281
|
+
/** A reference into a registry on this machine, by any of the names it goes by. */
|
|
282
|
+
const LOCAL = /^(localhost|127\.\d+\.\d+\.\d+|host\.docker\.internal)(:\d+)?\//
|
|
283
|
+
|
|
284
|
+
/** How many builds run at once. Past this the uploads contend for the same uplink. */
|
|
285
|
+
const CONCURRENCY = 3
|