@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,61 +1,89 @@
|
|
|
1
|
-
|
|
1
|
+
import { join, posix } from 'node:path'
|
|
2
|
+
import { readFile as read, writeFile as write } from 'node:fs/promises'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
posix
|
|
6
|
-
} = require('node:path')
|
|
7
|
-
const {
|
|
8
|
-
readFile: read,
|
|
9
|
-
writeFile: write
|
|
10
|
-
} = require('node:fs/promises')
|
|
11
|
-
const { createHash } = require('node:crypto')
|
|
12
|
-
|
|
13
|
-
const { overwrite } = require('@toa.io/generic')
|
|
14
|
-
const { mkdir } = require('node:fs/promises')
|
|
5
|
+
import { overwrite } from '@toa.io/generic'
|
|
6
|
+
import { mkdir } from 'node:fs/promises'
|
|
15
7
|
|
|
16
8
|
/**
|
|
17
9
|
* @implements {toa.deployment.images.Image}
|
|
18
10
|
* @abstract
|
|
19
11
|
*/
|
|
20
|
-
class Image {
|
|
12
|
+
export class Image {
|
|
21
13
|
context
|
|
22
14
|
reference
|
|
23
15
|
dockerfile
|
|
24
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
|
+
|
|
25
28
|
#scope
|
|
26
29
|
#registry
|
|
27
30
|
#runtime
|
|
28
31
|
#values = {
|
|
29
|
-
build: {
|
|
30
|
-
image: 'node:20.9.0-alpine3.18'
|
|
31
|
-
}
|
|
32
|
+
build: {}
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
constructor
|
|
35
|
+
constructor(scope, runtime, registry) {
|
|
35
36
|
this.#scope = scope
|
|
36
37
|
this.#registry = registry
|
|
37
38
|
this.#runtime = runtime
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
tag
|
|
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() {
|
|
41
55
|
const hash = createHash('sha256')
|
|
42
56
|
|
|
43
57
|
hash.update(this.#runtime.version)
|
|
44
58
|
hash.update(this.version)
|
|
45
59
|
|
|
46
|
-
|
|
60
|
+
return hash.digest('hex').slice(0, 8)
|
|
61
|
+
}
|
|
47
62
|
|
|
48
|
-
|
|
63
|
+
/** @returns {string | undefined} */
|
|
64
|
+
get name() {
|
|
65
|
+
return undefined
|
|
49
66
|
}
|
|
50
67
|
|
|
51
|
-
|
|
68
|
+
/** @returns {string | undefined} */
|
|
69
|
+
get version() {
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
52
72
|
|
|
53
|
-
|
|
73
|
+
/** The image to build `FROM`. Undefined takes the runtime's, which is what a service does.
|
|
74
|
+
* @returns {string | undefined} */
|
|
75
|
+
get base() {
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
54
78
|
|
|
55
|
-
|
|
79
|
+
/** Build commands to add. Undefined adds none, which is what a service does.
|
|
80
|
+
* @returns {string | undefined} */
|
|
81
|
+
get run() {
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
56
84
|
|
|
57
|
-
async prepare
|
|
58
|
-
if (this.dockerfile === undefined) throw new Error(
|
|
85
|
+
async prepare(root) {
|
|
86
|
+
if (this.dockerfile === undefined) throw new Error("Dockerfile isn't specified")
|
|
59
87
|
|
|
60
88
|
this.#setValues()
|
|
61
89
|
|
|
@@ -65,7 +93,7 @@ class Image {
|
|
|
65
93
|
|
|
66
94
|
const template = await read(this.dockerfile, 'utf-8')
|
|
67
95
|
const contents = template.replace(/{{(\S{1,32})}}/g, (_, key) => this.#value(key))
|
|
68
|
-
const ignore = 'Dockerfile'
|
|
96
|
+
const ignore = ['Dockerfile', '**/node_modules'].join('\r\n')
|
|
69
97
|
|
|
70
98
|
await write(join(path, 'Dockerfile'), contents)
|
|
71
99
|
await write(join(path, '.dockerignore'), ignore)
|
|
@@ -75,32 +103,43 @@ class Image {
|
|
|
75
103
|
return path
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
#setValues
|
|
106
|
+
#setValues() {
|
|
79
107
|
this.#values.runtime = this.#runtime
|
|
80
|
-
this.#values.build = overwrite(
|
|
108
|
+
this.#values.build = overwrite(
|
|
109
|
+
{
|
|
110
|
+
image: `${RUNTIME_IMAGE}:${this.#runtime.version}`
|
|
111
|
+
},
|
|
112
|
+
this.#registry.build
|
|
113
|
+
)
|
|
81
114
|
|
|
82
115
|
const image = this.base
|
|
83
116
|
|
|
84
|
-
if (image !== undefined)
|
|
85
|
-
|
|
86
|
-
|
|
117
|
+
if (image !== undefined) this.#values.build.image = image
|
|
118
|
+
|
|
119
|
+
const run = this.run
|
|
120
|
+
|
|
121
|
+
if (run !== undefined)
|
|
122
|
+
this.#values.build.run =
|
|
123
|
+
(this.#values.build.run === undefined ? '' : this.#values.build.run + '\n') + run
|
|
87
124
|
|
|
88
|
-
if (this.#values.build.arguments !== undefined)
|
|
89
|
-
|
|
125
|
+
if (this.#values.build.arguments !== undefined)
|
|
126
|
+
this.#values.build.arguments = createArguments(this.#values.build.arguments)
|
|
127
|
+
if (this.#values.build.run !== undefined)
|
|
128
|
+
this.#values.build.run = createRunCommands(this.#values.build.run)
|
|
90
129
|
}
|
|
91
130
|
|
|
92
131
|
/**
|
|
93
132
|
* @param key {string}
|
|
94
133
|
* @returns {string}
|
|
95
134
|
*/
|
|
96
|
-
#value
|
|
135
|
+
#value(key) {
|
|
97
136
|
const [source, property] = key.split('.')
|
|
98
137
|
|
|
99
138
|
return this.#values[source]?.[property] ?? ''
|
|
100
139
|
}
|
|
101
140
|
}
|
|
102
141
|
|
|
103
|
-
function createRunCommands
|
|
142
|
+
function createRunCommands(input) {
|
|
104
143
|
const lines = input.split('\n')
|
|
105
144
|
|
|
106
145
|
return lines.reduce((commands, command) => {
|
|
@@ -110,7 +149,7 @@ function createRunCommands (input) {
|
|
|
110
149
|
}, '')
|
|
111
150
|
}
|
|
112
151
|
|
|
113
|
-
function createArguments
|
|
152
|
+
function createArguments(variables) {
|
|
114
153
|
const args = []
|
|
115
154
|
|
|
116
155
|
for (const variable of variables) {
|
|
@@ -121,4 +160,4 @@ function createArguments (variables) {
|
|
|
121
160
|
return args.join('\n')
|
|
122
161
|
}
|
|
123
162
|
|
|
124
|
-
|
|
163
|
+
export const RUNTIME_IMAGE = 'ghcr.io/toa-io/runtime'
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { describe, it, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
import * as fixtures from './image.fixtures.js'
|
|
5
|
+
import { generate } from 'randomstring'
|
|
5
6
|
|
|
6
7
|
/** @type {toa.deployment.images.Image} */
|
|
7
8
|
let instance
|
|
@@ -13,11 +14,16 @@ beforeEach(() => {
|
|
|
13
14
|
it('should assign url', () => {
|
|
14
15
|
instance.tag()
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
assert.deepStrictEqual(
|
|
18
|
+
instance.reference,
|
|
19
|
+
`${fixtures.registry.base}/${fixtures.scope}/${fixtures.name}:${fixtures.version}`
|
|
20
|
+
)
|
|
17
21
|
})
|
|
18
22
|
|
|
19
23
|
describe('prepare', () => {
|
|
20
24
|
it('should throw error if no dockerfile specified', async () => {
|
|
21
|
-
await
|
|
25
|
+
await assert.rejects(instance.prepare(generate()), (error) =>
|
|
26
|
+
/Dockerfile isn't specified/.test(error.message)
|
|
27
|
+
)
|
|
22
28
|
})
|
|
23
29
|
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
FROM {{build.image}}
|
|
2
|
+
|
|
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
|
|
8
|
+
|
|
9
|
+
# no USER: the runtime drops to `node` itself, and only a process that started as root can
|
|
10
|
+
# close its environment under /proc — see runtime/runtime/bin/toa
|
|
11
|
+
CMD toa mono *
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { Bundle } from './bundle.js'
|
|
4
|
+
|
|
5
|
+
export class Mono extends Bundle {
|
|
6
|
+
dockerfile = join(import.meta.dirname, 'mono.Dockerfile')
|
|
7
|
+
|
|
8
|
+
get name() {
|
|
9
|
+
return 'mono'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
conflict() {
|
|
13
|
+
return 'Mono deployment requires different base images for its components. Specify base image for the composition in the context.'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { mkdtemp, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join, resolve } from 'node:path'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { run } from '../../process.js'
|
|
8
|
+
import { Service } from './service.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds and pushes the image an extension ships for its service, so that an application
|
|
12
|
+
* with `registry.services: published` takes it instead of building one.
|
|
13
|
+
*
|
|
14
|
+
* The image is built from the package as npm publishes it, not from the workspace: what
|
|
15
|
+
* `.npmignore` leaves out is not in the tarball, and an application builds from the tarball.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} workspace path to the extension's directory in this repository
|
|
18
|
+
* @param {string} runtime the runtime version, which is the tag and the base image
|
|
19
|
+
* @param {string[]} platforms
|
|
20
|
+
* @returns {Promise<string>} the reference pushed
|
|
21
|
+
*/
|
|
22
|
+
export async function publish(workspace, runtime, platforms) {
|
|
23
|
+
const directory = resolve(workspace)
|
|
24
|
+
const manifest = read(join(directory, 'package.json'))
|
|
25
|
+
|
|
26
|
+
// read from the workspace, where the extension's own dependencies resolve; what is
|
|
27
|
+
// installed below carries the same constant but not everything it takes to load it
|
|
28
|
+
const { image } = await import(pathToFileURL(join(directory, manifest.main)).href)
|
|
29
|
+
|
|
30
|
+
if (image === undefined)
|
|
31
|
+
throw new Error(
|
|
32
|
+
`'${manifest.name}' publishes no service image: it exports no 'image'`
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const root = await mkdtemp(join(tmpdir(), 'toa-publish'))
|
|
36
|
+
|
|
37
|
+
await writeFile(join(root, 'package.json'), '{"private":true}')
|
|
38
|
+
await run('npm', [
|
|
39
|
+
'i',
|
|
40
|
+
'--prefix',
|
|
41
|
+
root,
|
|
42
|
+
'--omit=dev',
|
|
43
|
+
`${manifest.name}@${manifest.version}`
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
const path = join(root, 'node_modules', manifest.name)
|
|
47
|
+
|
|
48
|
+
const service = new Service(SCOPE, { version: runtime }, {}, path, {
|
|
49
|
+
...name(image),
|
|
50
|
+
version: manifest.version
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
service.reference = `${image}:${runtime}`
|
|
54
|
+
|
|
55
|
+
await service.prepare(await mkdtemp(join(tmpdir(), 'toa-images')))
|
|
56
|
+
|
|
57
|
+
await run('docker', [
|
|
58
|
+
'buildx',
|
|
59
|
+
'build',
|
|
60
|
+
'--platform',
|
|
61
|
+
platforms.join(','),
|
|
62
|
+
'--tag',
|
|
63
|
+
service.reference,
|
|
64
|
+
'--provenance=false',
|
|
65
|
+
'--push',
|
|
66
|
+
service.context
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
return service.reference
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The repository's last segment is what `Service` names an image, so it reads back. */
|
|
73
|
+
function name(image) {
|
|
74
|
+
const match = /\/extension-([^-/]+)-([^/]+)$/.exec(image)
|
|
75
|
+
|
|
76
|
+
if (match === null)
|
|
77
|
+
throw new Error(`'${image}' is not named 'extension-<group>-<name>'`)
|
|
78
|
+
|
|
79
|
+
return { group: match[1], name: match[2] }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const read = (path) => JSON.parse(readFileSync(path, 'utf8'))
|
|
83
|
+
|
|
84
|
+
// the reference is assigned rather than derived from a scope
|
|
85
|
+
const SCOPE = ''
|
|
86
|
+
|
|
87
|
+
const PLATFORMS = 'linux/amd64,linux/arm64'
|
|
88
|
+
|
|
89
|
+
if (import.meta.main) {
|
|
90
|
+
const [workspace, runtime, platforms = PLATFORMS] = process.argv.slice(2)
|
|
91
|
+
|
|
92
|
+
console.log(await publish(workspace, runtime, platforms.split(',')))
|
|
93
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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, readFile } from 'node:fs/promises'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
|
|
8
|
+
import { Image, RUNTIME_IMAGE } from './image.js'
|
|
9
|
+
|
|
10
|
+
const dependenciesDockerfile = join(import.meta.dirname, 'dependencies.Dockerfile')
|
|
11
|
+
const serviceDockerfile = join(import.meta.dirname, 'service.Dockerfile')
|
|
12
|
+
|
|
13
|
+
class TestImage extends Image {
|
|
14
|
+
dockerfile
|
|
15
|
+
|
|
16
|
+
#name
|
|
17
|
+
#base
|
|
18
|
+
|
|
19
|
+
constructor(runtime, registry, dockerfile, name = 'test', base) {
|
|
20
|
+
super('acme', runtime, registry)
|
|
21
|
+
|
|
22
|
+
this.dockerfile = dockerfile
|
|
23
|
+
this.#name = name
|
|
24
|
+
this.#base = base
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get name() {
|
|
28
|
+
return this.#name
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get version() {
|
|
32
|
+
return 'abcdef12'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get base() {
|
|
36
|
+
return this.#base
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('runtime base image', () => {
|
|
41
|
+
/** @type {string} */
|
|
42
|
+
let root
|
|
43
|
+
|
|
44
|
+
beforeEach(async () => {
|
|
45
|
+
root = await mkdtemp(join(tmpdir(), 'toa-runtime-base-test'))
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should default build.image to version-pinned GHCR runtime image', async () => {
|
|
49
|
+
const runtime = { version: '1.0.0-alpha.232' }
|
|
50
|
+
const image = new TestImage(runtime, {}, dependenciesDockerfile)
|
|
51
|
+
|
|
52
|
+
const path = await image.prepare(root)
|
|
53
|
+
const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
|
|
54
|
+
|
|
55
|
+
assert.ok(dockerfile.includes(`FROM ${RUNTIME_IMAGE}:1.0.0-alpha.232`))
|
|
56
|
+
assert.doesNotMatch(dockerfile, /npm i -g @toa\.io\/runtime/)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should not install runtime in service Dockerfile template', async () => {
|
|
60
|
+
const runtime = { version: '1.0.0-alpha.99' }
|
|
61
|
+
const image = new TestImage(runtime, {}, serviceDockerfile, 'service-test')
|
|
62
|
+
|
|
63
|
+
const path = await image.prepare(root)
|
|
64
|
+
const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
|
|
65
|
+
|
|
66
|
+
assert.ok(dockerfile.includes(`FROM ${RUNTIME_IMAGE}:1.0.0-alpha.99`))
|
|
67
|
+
assert.doesNotMatch(dockerfile, /npm i -g @toa\.io\/runtime/)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should allow registry.build.image override', async () => {
|
|
71
|
+
const runtime = { version: '1.0.0-alpha.232' }
|
|
72
|
+
const registry = { build: { image: 'node:24.14.0-alpine3.22' } }
|
|
73
|
+
const image = new TestImage(runtime, registry, dependenciesDockerfile)
|
|
74
|
+
|
|
75
|
+
const path = await image.prepare(root)
|
|
76
|
+
const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
|
|
77
|
+
|
|
78
|
+
assert.ok(dockerfile.includes('FROM node:24.14.0-alpine3.22'))
|
|
79
|
+
assert.ok(!dockerfile.includes(RUNTIME_IMAGE))
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should allow composition.image override via base', async () => {
|
|
83
|
+
const runtime = { version: '1.0.0-alpha.232' }
|
|
84
|
+
const image = new TestImage(
|
|
85
|
+
runtime,
|
|
86
|
+
{},
|
|
87
|
+
dependenciesDockerfile,
|
|
88
|
+
'mono',
|
|
89
|
+
'custom.example/base:1'
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const path = await image.prepare(root)
|
|
93
|
+
const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
|
|
94
|
+
|
|
95
|
+
assert.ok(dockerfile.includes('FROM custom.example/base:1'))
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
FROM
|
|
1
|
+
FROM {{build.image}}
|
|
2
2
|
|
|
3
3
|
ENV NODE_ENV=production
|
|
4
4
|
RUN if [ "{{runtime.registry}}" != "" ]; then npm set registry {{runtime.registry}}; fi
|
|
5
5
|
RUN if [ "{{runtime.proxy}}" != "" ]; then npm set proxy {{runtime.proxy}}; fi
|
|
6
|
-
RUN npm i -g @toa.io/runtime@{{runtime.version}} --omit=dev
|
|
7
6
|
|
|
8
7
|
WORKDIR /service
|
|
9
8
|
COPY --chown=node:node . /service
|
|
10
9
|
|
|
11
|
-
RUN npm
|
|
10
|
+
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
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 .
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { join, dirname } from 'node:path'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import { Image } from './image.js'
|
|
5
|
+
import { cp } from 'node:fs/promises'
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
+
// a service is named the way a package is, and its directory is where it lives
|
|
8
|
+
const require = createRequire(import.meta.url)
|
|
7
9
|
|
|
8
|
-
class Service extends Image {
|
|
9
|
-
dockerfile = join(
|
|
10
|
+
export class Service extends Image {
|
|
11
|
+
dockerfile = join(import.meta.dirname, 'service.Dockerfile')
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Used by Dockerfile
|
|
@@ -35,7 +37,7 @@ class Service extends Image {
|
|
|
35
37
|
* @param {string} reference
|
|
36
38
|
* @param {toa.deployment.dependency.Service} service
|
|
37
39
|
*/
|
|
38
|
-
constructor
|
|
40
|
+
constructor(scope, runtime, registry, reference, service) {
|
|
39
41
|
super(scope, runtime, registry)
|
|
40
42
|
|
|
41
43
|
this.service = service.name
|
|
@@ -46,18 +48,18 @@ class Service extends Image {
|
|
|
46
48
|
this.#version = service.version
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
get name
|
|
51
|
+
get name() {
|
|
50
52
|
return 'extension-' + this.#group + '-' + this.#name
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
get version
|
|
55
|
+
get version() {
|
|
54
56
|
return this.#version
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
async prepare
|
|
59
|
+
async prepare(root) {
|
|
58
60
|
const context = await super.prepare(root)
|
|
59
61
|
|
|
60
|
-
await
|
|
62
|
+
await cp(this.#path, context, { recursive: true })
|
|
61
63
|
|
|
62
64
|
return context
|
|
63
65
|
}
|
|
@@ -70,5 +72,3 @@ class Service extends Image {
|
|
|
70
72
|
const find = (reference) => {
|
|
71
73
|
return dirname(require.resolve(join(reference, 'package.json')))
|
|
72
74
|
}
|
|
73
|
-
|
|
74
|
-
exports.Service = Service
|
package/src/deployment/index.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
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
|
|
|
@@ -13,12 +11,12 @@ class Operator {
|
|
|
13
11
|
* @param deployment {toa.deployment.Deployment}
|
|
14
12
|
* @param registry {toa.deployment.Registry}
|
|
15
13
|
*/
|
|
16
|
-
constructor
|
|
14
|
+
constructor(deployment, registry) {
|
|
17
15
|
this.#deployment = deployment
|
|
18
16
|
this.#registry = registry
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
async export
|
|
19
|
+
async export(path) {
|
|
22
20
|
const target = await workspace.create('deployment', path)
|
|
23
21
|
|
|
24
22
|
await this.#deployment.export(target)
|
|
@@ -26,33 +24,35 @@ class Operator {
|
|
|
26
24
|
return target
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
async prepare
|
|
27
|
+
async prepare(path) {
|
|
30
28
|
return await this.#registry.prepare(path)
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
async push
|
|
31
|
+
async push() {
|
|
34
32
|
await this.#registry.push()
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
async install
|
|
35
|
+
async install(options = {}) {
|
|
38
36
|
options = Object.assign({}, OPTIONS, options)
|
|
39
37
|
|
|
40
38
|
await Promise.all([this.export(), this.push()])
|
|
41
39
|
await this.#deployment.install(options)
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
async template
|
|
42
|
+
async template(options = {}) {
|
|
45
43
|
await this.export()
|
|
46
44
|
|
|
47
45
|
return await this.#deployment.template(options)
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
variables
|
|
48
|
+
variables() {
|
|
51
49
|
return this.#deployment.variables()
|
|
52
50
|
}
|
|
51
|
+
|
|
52
|
+
tags() {
|
|
53
|
+
return this.#registry.tags()
|
|
54
|
+
}
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/** @type {toa.deployment.installation.Options} */
|
|
56
58
|
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,12 @@ 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)
|
|
43
44
|
})
|
|
44
45
|
})
|