@toa.io/operations 1.0.0-alpha.288 → 1.0.0-alpha.289
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/package.json +4 -6
- package/readme.md +16 -0
- package/src/deployment/.deployment/.describe/resources.js +63 -0
- package/src/deployment/.deployment/.describe/resources.test.js +60 -1
- package/src/deployment/chart/templates/compositions.yaml +10 -2
- package/src/deployment/chart/templates/mono.yaml +4 -3
- package/src/deployment/chart/templates/services.yaml +6 -5
- package/src/deployment/deployment.js +6 -2
- package/src/deployment/drain.js +70 -0
- package/src/deployment/drain.test.js +64 -0
- package/src/deployment/images/bundle.js +82 -0
- package/src/deployment/images/composition.Dockerfile +5 -14
- package/src/deployment/images/composition.js +5 -57
- package/src/deployment/images/dependencies.Dockerfile +16 -0
- package/src/deployment/images/dependencies.js +141 -0
- package/src/deployment/images/dependencies.test.js +212 -0
- package/src/deployment/images/image.js +25 -7
- package/src/deployment/images/mono.Dockerfile +5 -13
- package/src/deployment/images/mono.js +4 -62
- package/src/deployment/images/publish.js +4 -9
- package/src/deployment/images/runtime-base.test.js +4 -4
- package/src/deployment/images/service.Dockerfile +2 -1
- package/src/deployment/images/service.js +2 -2
- package/src/deployment/registry.js +59 -13
- package/src/deployment/registry.test.js +169 -13
- package/src/process.js +50 -6
- package/src/process.test.js +13 -4
- package/types/_deployment/images/image.d.ts +15 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { basename, join } from 'node:path'
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { copyFile, mkdir } from 'node:fs/promises'
|
|
4
|
+
import { createHash } from 'node:crypto'
|
|
5
|
+
|
|
6
|
+
import { Image } from './image.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What a bundle's components depend on, installed on the base: the layers that weigh the
|
|
10
|
+
* most and change the least, so they are an image of their own. Its tag digests everything
|
|
11
|
+
* the install reads, and nothing else: a change to the sources leaves it where it is, and a
|
|
12
|
+
* bundle is laid over it without building or moving it again.
|
|
13
|
+
*
|
|
14
|
+
* @implements {toa.deployment.images.Image}
|
|
15
|
+
*/
|
|
16
|
+
export class Dependencies extends Image {
|
|
17
|
+
dockerfile = join(import.meta.dirname, 'dependencies.Dockerfile')
|
|
18
|
+
arguments = true
|
|
19
|
+
|
|
20
|
+
/** @type {toa.deployment.images.Bundle} */
|
|
21
|
+
#owner
|
|
22
|
+
|
|
23
|
+
#runtime
|
|
24
|
+
#registry
|
|
25
|
+
|
|
26
|
+
/** @type {string | undefined} */
|
|
27
|
+
#version
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} scope
|
|
31
|
+
* @param {toa.norm.context.Runtime} runtime
|
|
32
|
+
* @param {toa.norm.context.Registry} registry
|
|
33
|
+
* @param {toa.deployment.images.Bundle} owner the bundle laid over this image
|
|
34
|
+
*/
|
|
35
|
+
constructor(scope, runtime, registry, owner) {
|
|
36
|
+
super(scope, runtime, registry)
|
|
37
|
+
|
|
38
|
+
this.#owner = owner
|
|
39
|
+
this.#runtime = runtime
|
|
40
|
+
this.#registry = registry
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Same repository as the bundle: a tag apart, not a name apart. */
|
|
44
|
+
get name() {
|
|
45
|
+
return this.#owner.name
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get version() {
|
|
49
|
+
if (this.#version !== undefined) return this.#version
|
|
50
|
+
|
|
51
|
+
const hash = createHash('sha256')
|
|
52
|
+
const build = this.#registry.build ?? {}
|
|
53
|
+
|
|
54
|
+
const inputs = [
|
|
55
|
+
this.#runtime.version,
|
|
56
|
+
this.#runtime.registry,
|
|
57
|
+
this.#runtime.proxy,
|
|
58
|
+
this.base,
|
|
59
|
+
build.image,
|
|
60
|
+
build.run,
|
|
61
|
+
this.run,
|
|
62
|
+
...(build.arguments ?? [])
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
for (const input of inputs) hash.update(input ?? '').update('\0')
|
|
66
|
+
|
|
67
|
+
for (const [label, files] of this.#manifests())
|
|
68
|
+
for (const file of files)
|
|
69
|
+
hash.update(label).update(basename(file)).update(readFileSync(file))
|
|
70
|
+
|
|
71
|
+
this.#version = hash.digest('hex').slice(0, 8)
|
|
72
|
+
|
|
73
|
+
return this.#version
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
digest() {
|
|
77
|
+
return PREFIX + this.version
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get base() {
|
|
81
|
+
if (this.#owner.image !== undefined) return this.#owner.image
|
|
82
|
+
|
|
83
|
+
const images = new Set(
|
|
84
|
+
this.#owner.components.map((component) => component.build?.image)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if (images.size > 1) throw new Error(this.#owner.conflict())
|
|
88
|
+
|
|
89
|
+
return images.values().next().value
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get run() {
|
|
93
|
+
const commands = []
|
|
94
|
+
|
|
95
|
+
for (const component of this.#owner.components) {
|
|
96
|
+
const run = component.build?.run
|
|
97
|
+
|
|
98
|
+
if (run !== undefined) commands.push(run)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return commands.join('\n')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async prepare(root) {
|
|
105
|
+
const context = await super.prepare(join(root, DIRECTORY))
|
|
106
|
+
|
|
107
|
+
for (const [label, files] of this.#manifests()) {
|
|
108
|
+
const target = join(context, label)
|
|
109
|
+
|
|
110
|
+
await mkdir(target, { recursive: true })
|
|
111
|
+
|
|
112
|
+
for (const file of files) await copyFile(file, join(target, basename(file)))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return context
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The files the install reads, by component label, in a fixed order.
|
|
120
|
+
*
|
|
121
|
+
* @returns {Array<[string, string[]]>}
|
|
122
|
+
*/
|
|
123
|
+
#manifests() {
|
|
124
|
+
const manifests = []
|
|
125
|
+
|
|
126
|
+
for (const component of this.#owner.components) {
|
|
127
|
+
const files = MANIFESTS.map((name) => join(component.path, name)).filter(existsSync)
|
|
128
|
+
|
|
129
|
+
if (files.length > 0) manifests.push([component.locator.label, files])
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return manifests.sort(([a], [b]) => a.localeCompare(b))
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const PREFIX = 'deps-'
|
|
137
|
+
|
|
138
|
+
/** Where the contexts land, apart from the bundles', whose names they share. */
|
|
139
|
+
const DIRECTORY = 'dependencies'
|
|
140
|
+
|
|
141
|
+
const MANIFESTS = ['package.json', 'package-lock.json']
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { mkdtemp, mkdir, writeFile, readdir, readFile } from 'node:fs/promises'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
|
|
8
|
+
import { Composition } from './composition.js'
|
|
9
|
+
import { Mono } from './mono.js'
|
|
10
|
+
|
|
11
|
+
/** @type {string} */
|
|
12
|
+
let root
|
|
13
|
+
|
|
14
|
+
/** @type {toa.norm.context.Runtime} */
|
|
15
|
+
let runtime
|
|
16
|
+
|
|
17
|
+
/** @type {toa.norm.context.Registry} */
|
|
18
|
+
let registry
|
|
19
|
+
|
|
20
|
+
/** @type {object} */
|
|
21
|
+
let composition
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
root = await mkdtemp(join(tmpdir(), 'toa-dependencies-test'))
|
|
25
|
+
runtime = { version: '1.0.0-alpha.288' }
|
|
26
|
+
registry = { base: 'example.com/reg', platforms: ['linux/amd64'] }
|
|
27
|
+
|
|
28
|
+
composition = {
|
|
29
|
+
name: 'mono',
|
|
30
|
+
components: [
|
|
31
|
+
await component('one', { dependencies: { left: '1.0.0' } }),
|
|
32
|
+
await component('two'),
|
|
33
|
+
await component('three', { devDependencies: { right: '1.0.0' } }, 'lock')
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('reference', () => {
|
|
39
|
+
it('should share the repository and stand a prefix apart', () => {
|
|
40
|
+
const image = create()
|
|
41
|
+
|
|
42
|
+
assert.match(
|
|
43
|
+
image.dependencies.reference,
|
|
44
|
+
/^example\.com\/reg\/acme\/composition-mono:deps-[0-9a-f]{8}$/
|
|
45
|
+
)
|
|
46
|
+
assert.match(
|
|
47
|
+
image.reference,
|
|
48
|
+
/^example\.com\/reg\/acme\/composition-mono:[0-9a-f]{8}$/
|
|
49
|
+
)
|
|
50
|
+
assert.strictEqual(image.base, image.dependencies.reference)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should be the same for the same inputs', () => {
|
|
54
|
+
assert.strictEqual(create().dependencies.reference, create().dependencies.reference)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should change with a manifest', async () => {
|
|
58
|
+
const before = create()
|
|
59
|
+
|
|
60
|
+
await writeFile(
|
|
61
|
+
join(composition.components[0].path, 'package.json'),
|
|
62
|
+
JSON.stringify({ dependencies: { left: '1.0.1' } })
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const after = create()
|
|
66
|
+
|
|
67
|
+
assert.notStrictEqual(after.dependencies.reference, before.dependencies.reference)
|
|
68
|
+
assert.notStrictEqual(after.reference, before.reference)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should change with a lockfile', async () => {
|
|
72
|
+
const before = create()
|
|
73
|
+
|
|
74
|
+
await writeFile(join(composition.components[2].path, 'package-lock.json'), 'other')
|
|
75
|
+
|
|
76
|
+
assert.notStrictEqual(create().dependencies.reference, before.dependencies.reference)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should change with what is run and with the runtime', () => {
|
|
80
|
+
const before = create()
|
|
81
|
+
|
|
82
|
+
composition.components[1].build = { run: 'apk add git' }
|
|
83
|
+
|
|
84
|
+
const run = create()
|
|
85
|
+
|
|
86
|
+
assert.notStrictEqual(run.dependencies.reference, before.dependencies.reference)
|
|
87
|
+
|
|
88
|
+
runtime.version = '1.0.0-alpha.289'
|
|
89
|
+
|
|
90
|
+
assert.notStrictEqual(create().dependencies.reference, run.dependencies.reference)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should change with the registry build settings', () => {
|
|
94
|
+
const before = create()
|
|
95
|
+
|
|
96
|
+
registry.build = { arguments: ['TOKEN'] }
|
|
97
|
+
|
|
98
|
+
assert.notStrictEqual(create().dependencies.reference, before.dependencies.reference)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should not change with the sources', async () => {
|
|
102
|
+
const before = create()
|
|
103
|
+
|
|
104
|
+
await writeFile(
|
|
105
|
+
join(composition.components[0].path, 'index.js'),
|
|
106
|
+
'export const changed = true'
|
|
107
|
+
)
|
|
108
|
+
composition.components[0].version = 'ffffffff'
|
|
109
|
+
|
|
110
|
+
const after = create()
|
|
111
|
+
|
|
112
|
+
assert.strictEqual(after.dependencies.reference, before.dependencies.reference)
|
|
113
|
+
assert.notStrictEqual(after.reference, before.reference)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('prepare', () => {
|
|
118
|
+
it('should hold the manifests and nothing else', async () => {
|
|
119
|
+
const image = create()
|
|
120
|
+
const context = await image.dependencies.prepare(root)
|
|
121
|
+
|
|
122
|
+
assert.strictEqual(
|
|
123
|
+
context,
|
|
124
|
+
join(root, 'dependencies', `composition-mono.${image.dependencies.version}`)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const entries = (await readdir(context, { recursive: true })).sort()
|
|
128
|
+
|
|
129
|
+
assert.deepStrictEqual(entries, [
|
|
130
|
+
'.dockerignore',
|
|
131
|
+
'Dockerfile',
|
|
132
|
+
'one',
|
|
133
|
+
'one/package.json',
|
|
134
|
+
'three',
|
|
135
|
+
'three/package-lock.json',
|
|
136
|
+
'three/package.json'
|
|
137
|
+
])
|
|
138
|
+
|
|
139
|
+
const dockerfile = await readFile(join(context, 'Dockerfile'), 'utf8')
|
|
140
|
+
|
|
141
|
+
assert.ok(dockerfile.includes('FROM ghcr.io/toa-io/runtime:1.0.0-alpha.288'))
|
|
142
|
+
assert.ok(dockerfile.includes('npm i --omit=dev'))
|
|
143
|
+
assert.ok(dockerfile.includes('WORKDIR /composition'))
|
|
144
|
+
assert.doesNotMatch(dockerfile, /USER node|CMD /)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should lay the sources over the dependencies', async () => {
|
|
148
|
+
const image = create()
|
|
149
|
+
|
|
150
|
+
image.dependencies.tag()
|
|
151
|
+
|
|
152
|
+
const context = await image.prepare(root)
|
|
153
|
+
const dockerfile = await readFile(join(context, 'Dockerfile'), 'utf8')
|
|
154
|
+
const lines = dockerfile
|
|
155
|
+
.split('\n')
|
|
156
|
+
.filter((line) => line !== '' && !line.startsWith('#'))
|
|
157
|
+
|
|
158
|
+
assert.deepStrictEqual(lines, [
|
|
159
|
+
`FROM ${image.dependencies.reference}`,
|
|
160
|
+
'COPY --link . /composition',
|
|
161
|
+
'USER node',
|
|
162
|
+
'CMD toa compose *'
|
|
163
|
+
])
|
|
164
|
+
|
|
165
|
+
const entries = (await readdir(context, { recursive: true })).sort()
|
|
166
|
+
|
|
167
|
+
assert.ok(entries.includes('one/index.js'))
|
|
168
|
+
assert.ok(entries.includes('two/package.json')) // declared for the component that had none
|
|
169
|
+
assert.ok(!entries.some((entry) => entry.includes('node_modules')))
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should run mono the same way', async () => {
|
|
173
|
+
const image = new Mono('acme', runtime, registry, composition)
|
|
174
|
+
|
|
175
|
+
image.tag()
|
|
176
|
+
|
|
177
|
+
assert.match(image.dependencies.reference, /\/mono:deps-[0-9a-f]{8}$/)
|
|
178
|
+
|
|
179
|
+
const context = await image.prepare(root)
|
|
180
|
+
const dockerfile = await readFile(join(context, 'Dockerfile'), 'utf8')
|
|
181
|
+
|
|
182
|
+
assert.ok(dockerfile.includes('CMD toa mono *'))
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
/** @returns {Composition} */
|
|
187
|
+
function create() {
|
|
188
|
+
const image = new Composition('acme', runtime, registry, composition)
|
|
189
|
+
|
|
190
|
+
image.tag()
|
|
191
|
+
|
|
192
|
+
return image
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {string} label
|
|
197
|
+
* @param {object} [manifest]
|
|
198
|
+
* @param {string} [lock]
|
|
199
|
+
*/
|
|
200
|
+
async function component(label, manifest, lock) {
|
|
201
|
+
const path = join(root, 'components', label)
|
|
202
|
+
|
|
203
|
+
await mkdir(join(path, 'node_modules', 'left'), { recursive: true })
|
|
204
|
+
await writeFile(join(path, 'index.js'), 'export const changed = false')
|
|
205
|
+
await writeFile(join(path, 'node_modules', 'left', 'index.js'), '')
|
|
206
|
+
|
|
207
|
+
if (manifest !== undefined)
|
|
208
|
+
await writeFile(join(path, 'package.json'), JSON.stringify(manifest))
|
|
209
|
+
if (lock !== undefined) await writeFile(join(path, 'package-lock.json'), lock)
|
|
210
|
+
|
|
211
|
+
return { path, version: 'abcdef12', locator: { id: `default.${label}`, label } }
|
|
212
|
+
}
|
|
@@ -14,6 +14,17 @@ export class Image {
|
|
|
14
14
|
reference
|
|
15
15
|
dockerfile
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* The image this one is laid over, built and pushed as an image of its own.
|
|
19
|
+
* Undefined stands on the base alone, which is what a service does.
|
|
20
|
+
*
|
|
21
|
+
* @type {toa.deployment.images.Image | undefined}
|
|
22
|
+
*/
|
|
23
|
+
dependencies
|
|
24
|
+
|
|
25
|
+
/** Whether the build reads `registry.build.arguments`. */
|
|
26
|
+
arguments = false
|
|
27
|
+
|
|
17
28
|
#scope
|
|
18
29
|
#registry
|
|
19
30
|
#runtime
|
|
@@ -28,18 +39,25 @@ export class Image {
|
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
tag() {
|
|
42
|
+
this.reference = posix.join(
|
|
43
|
+
this.#registry.base ?? '',
|
|
44
|
+
this.#scope,
|
|
45
|
+
`${this.name}:${this.digest()}`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What follows the colon: the runtime and the image's own version, digested.
|
|
51
|
+
*
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
digest() {
|
|
31
55
|
const hash = createHash('sha256')
|
|
32
56
|
|
|
33
57
|
hash.update(this.#runtime.version)
|
|
34
58
|
hash.update(this.version)
|
|
35
59
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.reference = posix.join(
|
|
39
|
-
this.#registry.base ?? '',
|
|
40
|
-
this.#scope,
|
|
41
|
-
`${this.name}:${tag}`
|
|
42
|
-
)
|
|
60
|
+
return hash.digest('hex').slice(0, 8)
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
/** @returns {string | undefined} */
|
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
FROM {{build.image}}
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
WORKDIR /composition
|
|
10
|
-
COPY --chown=node:node . /composition
|
|
11
|
-
|
|
12
|
-
{{build.run}}
|
|
13
|
-
|
|
14
|
-
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
15
|
-
for entry in *; do if grep -qs '"dependencies"' "$entry/package.json"; then (cd $entry && npm i --omit=dev); fi; done
|
|
3
|
+
# the one instruction that touches the filesystem, and linked: the base is not pulled to lay
|
|
4
|
+
# the sources over it, and a push carries this layer and a manifest, not the base's layers.
|
|
5
|
+
# Owned by root like the dependencies beside them: a linked layer knows no user by name,
|
|
6
|
+
# and the runtime reads its sources, it does not write them
|
|
7
|
+
COPY --link . /composition
|
|
16
8
|
|
|
17
9
|
USER node
|
|
18
10
|
CMD toa mono *
|
|
@@ -1,73 +1,15 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
|
-
import fs from 'fs-extra'
|
|
3
|
-
import { createHash } from 'node:crypto'
|
|
4
2
|
|
|
5
|
-
import {
|
|
6
|
-
import { declare } from './format.js'
|
|
3
|
+
import { Bundle } from './bundle.js'
|
|
7
4
|
|
|
8
|
-
export class Mono extends
|
|
5
|
+
export class Mono extends Bundle {
|
|
9
6
|
dockerfile = join(import.meta.dirname, 'mono.Dockerfile')
|
|
10
7
|
|
|
11
|
-
#image
|
|
12
|
-
#components
|
|
13
|
-
|
|
14
|
-
constructor(scope, runtime, registry, composition) {
|
|
15
|
-
super(scope, runtime, registry)
|
|
16
|
-
|
|
17
|
-
this.#image = composition.image
|
|
18
|
-
this.#components = composition.components
|
|
19
|
-
}
|
|
20
|
-
|
|
21
8
|
get name() {
|
|
22
9
|
return 'mono'
|
|
23
10
|
}
|
|
24
11
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
for (const component of this.#components) {
|
|
29
|
-
hash.update(component.locator.id)
|
|
30
|
-
hash.update(component.version)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return hash.digest('hex').slice(0, 8)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
get base() {
|
|
37
|
-
if (this.#image !== undefined) return this.#image
|
|
38
|
-
|
|
39
|
-
const images = new Set(this.#components.map((component) => component.build?.image))
|
|
40
|
-
|
|
41
|
-
if (images.size > 1)
|
|
42
|
-
throw new Error(
|
|
43
|
-
'Mono deployment requires different base images for its components. Specify base image for the composition in the context.'
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
return images.values().next().value
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
get run() {
|
|
50
|
-
const commands = []
|
|
51
|
-
|
|
52
|
-
for (const component of this.#components) {
|
|
53
|
-
const run = component.build?.run
|
|
54
|
-
|
|
55
|
-
if (run !== undefined) commands.push(run)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return commands.join('\n')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async prepare(root) {
|
|
62
|
-
const context = await super.prepare(root)
|
|
63
|
-
|
|
64
|
-
for (const component of this.#components) {
|
|
65
|
-
const target = join(context, component.locator.label)
|
|
66
|
-
|
|
67
|
-
await fs.copy(component.path, target)
|
|
68
|
-
await declare(component.path, target, component.locator.label)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return context
|
|
12
|
+
conflict() {
|
|
13
|
+
return 'Mono deployment requires different base images for its components. Specify base image for the composition in the context.'
|
|
72
14
|
}
|
|
73
15
|
}
|
|
@@ -4,8 +4,7 @@ import { tmpdir } from 'node:os'
|
|
|
4
4
|
import { join, resolve } from 'node:path'
|
|
5
5
|
import { pathToFileURL } from 'node:url'
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import { run } from '../../process.js'
|
|
9
8
|
import { Service } from './service.js'
|
|
10
9
|
|
|
11
10
|
/**
|
|
@@ -29,7 +28,9 @@ export async function publish(workspace, runtime, platforms) {
|
|
|
29
28
|
const { image } = await import(pathToFileURL(join(directory, manifest.main)).href)
|
|
30
29
|
|
|
31
30
|
if (image === undefined)
|
|
32
|
-
throw new Error(
|
|
31
|
+
throw new Error(
|
|
32
|
+
`'${manifest.name}' publishes no service image: it exports no 'image'`
|
|
33
|
+
)
|
|
33
34
|
|
|
34
35
|
const root = await mkdtemp(join(tmpdir(), 'toa-publish'))
|
|
35
36
|
|
|
@@ -80,12 +81,6 @@ function name(image) {
|
|
|
80
81
|
|
|
81
82
|
const read = (path) => JSON.parse(readFileSync(path, 'utf8'))
|
|
82
83
|
|
|
83
|
-
const run = async (cmd, args) => {
|
|
84
|
-
console.log('toa>', cmd, args.join(' '))
|
|
85
|
-
|
|
86
|
-
await execa(cmd, args, { stdio: 'inherit' })
|
|
87
|
-
}
|
|
88
|
-
|
|
89
84
|
// the reference is assigned rather than derived from a scope
|
|
90
85
|
const SCOPE = ''
|
|
91
86
|
|
|
@@ -7,7 +7,7 @@ import { tmpdir } from 'node:os'
|
|
|
7
7
|
|
|
8
8
|
import { Image, RUNTIME_IMAGE } from './image.js'
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const dependenciesDockerfile = join(import.meta.dirname, 'dependencies.Dockerfile')
|
|
11
11
|
const serviceDockerfile = join(import.meta.dirname, 'service.Dockerfile')
|
|
12
12
|
|
|
13
13
|
class TestImage extends Image {
|
|
@@ -47,7 +47,7 @@ describe('runtime base image', () => {
|
|
|
47
47
|
|
|
48
48
|
it('should default build.image to version-pinned GHCR runtime image', async () => {
|
|
49
49
|
const runtime = { version: '1.0.0-alpha.232' }
|
|
50
|
-
const image = new TestImage(runtime, {},
|
|
50
|
+
const image = new TestImage(runtime, {}, dependenciesDockerfile)
|
|
51
51
|
|
|
52
52
|
const path = await image.prepare(root)
|
|
53
53
|
const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
|
|
@@ -70,7 +70,7 @@ describe('runtime base image', () => {
|
|
|
70
70
|
it('should allow registry.build.image override', async () => {
|
|
71
71
|
const runtime = { version: '1.0.0-alpha.232' }
|
|
72
72
|
const registry = { build: { image: 'node:24.14.0-alpine3.22' } }
|
|
73
|
-
const image = new TestImage(runtime, registry,
|
|
73
|
+
const image = new TestImage(runtime, registry, dependenciesDockerfile)
|
|
74
74
|
|
|
75
75
|
const path = await image.prepare(root)
|
|
76
76
|
const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
|
|
@@ -84,7 +84,7 @@ describe('runtime base image', () => {
|
|
|
84
84
|
const image = new TestImage(
|
|
85
85
|
runtime,
|
|
86
86
|
{},
|
|
87
|
-
|
|
87
|
+
dependenciesDockerfile,
|
|
88
88
|
'mono',
|
|
89
89
|
'custom.example/base:1'
|
|
90
90
|
)
|
|
@@ -10,5 +10,6 @@ COPY --chown=node:node . /service
|
|
|
10
10
|
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
11
11
|
npm i --omit=dev
|
|
12
12
|
|
|
13
|
-
USER node
|
|
13
|
+
# no USER: the runtime drops to `node` itself, and only a process that started as root can
|
|
14
|
+
# close its environment under /proc — see runtime/runtime/bin/toa
|
|
14
15
|
CMD toa serve .
|
|
@@ -2,7 +2,7 @@ import { createRequire } from 'node:module'
|
|
|
2
2
|
import { join, dirname } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import { Image } from './image.js'
|
|
5
|
-
import
|
|
5
|
+
import { cp } from 'node:fs/promises'
|
|
6
6
|
|
|
7
7
|
// a service is named the way a package is, and its directory is where it lives
|
|
8
8
|
const require = createRequire(import.meta.url)
|
|
@@ -59,7 +59,7 @@ export class Service extends Image {
|
|
|
59
59
|
async prepare(root) {
|
|
60
60
|
const context = await super.prepare(root)
|
|
61
61
|
|
|
62
|
-
await
|
|
62
|
+
await cp(this.#path, context, { recursive: true })
|
|
63
63
|
|
|
64
64
|
return context
|
|
65
65
|
}
|