@toa.io/operations 1.0.0-alpha.284 → 1.0.0-alpha.287
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 +1 -224
- package/package.json +4 -4
- package/readme.md +43 -0
- package/src/deployment/.deployment/.describe/compositions.js +7 -5
- package/src/deployment/.deployment/.describe/events.js +7 -7
- package/src/deployment/.deployment/.describe/fold.js +10 -3
- package/src/deployment/.deployment/.describe/fold.test.js +30 -14
- package/src/deployment/.deployment/.describe/mounts.js +4 -7
- package/src/deployment/.deployment/.describe/resources.js +7 -5
- package/src/deployment/.deployment/.describe/resources.test.js +13 -6
- package/src/deployment/.deployment/.describe/services.js +10 -9
- package/src/deployment/.deployment/.describe/services.test.js +22 -5
- package/src/deployment/.deployment/.describe/variables.js +2 -3
- package/src/deployment/.deployment/describe.js +11 -7
- package/src/deployment/.deployment/merge.js +11 -6
- package/src/deployment/.deployment/merge.test.js +41 -10
- package/src/deployment/composition.js +1 -1
- package/src/deployment/deployment.js +35 -16
- package/src/deployment/factory.js +39 -19
- package/src/deployment/images/composition.Dockerfile +1 -1
- package/src/deployment/images/composition.js +10 -9
- package/src/deployment/images/factory.js +17 -6
- package/src/deployment/images/format.js +9 -5
- package/src/deployment/images/image.fixtures.js +2 -2
- package/src/deployment/images/image.js +38 -21
- package/src/deployment/images/image.test.js +7 -2
- package/src/deployment/images/mono.Dockerfile +1 -1
- package/src/deployment/images/mono.js +10 -9
- package/src/deployment/images/publish.js +98 -0
- package/src/deployment/images/runtime-base.test.js +12 -6
- package/src/deployment/images/service.Dockerfile +1 -1
- package/src/deployment/images/service.js +4 -4
- package/src/deployment/operator.js +8 -8
- package/src/deployment/registry.js +89 -61
- package/src/deployment/registry.test.js +117 -43
- package/src/deployment/service.js +3 -1
- package/src/deployment/workspace.js +1 -1
- package/src/process.js +1 -1
- package/src/process.test.js +8 -4
- package/types/_deployment/composition.d.ts +0 -2
- package/types/_deployment/dependency.d.ts +10 -6
- package/types/_deployment/deployment.d.ts +0 -4
- package/types/_deployment/factory.d.ts +0 -2
- package/types/_deployment/images/factory.d.ts +4 -6
- package/types/_deployment/images/image.d.ts +2 -4
- package/types/_deployment/images/registry.d.ts +7 -10
- package/types/_deployment/operator.d.ts +5 -7
- package/types/_deployment/registry.d.ts +6 -8
- package/types/_deployment/service.d.ts +0 -2
- package/types/dependency.ts +3 -0
|
@@ -21,13 +21,13 @@ export class Image {
|
|
|
21
21
|
build: {}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
constructor
|
|
24
|
+
constructor(scope, runtime, registry) {
|
|
25
25
|
this.#scope = scope
|
|
26
26
|
this.#registry = registry
|
|
27
27
|
this.#runtime = runtime
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
tag
|
|
30
|
+
tag() {
|
|
31
31
|
const hash = createHash('sha256')
|
|
32
32
|
|
|
33
33
|
hash.update(this.#runtime.version)
|
|
@@ -35,25 +35,37 @@ export class Image {
|
|
|
35
35
|
|
|
36
36
|
const tag = hash.digest('hex').slice(0, 8)
|
|
37
37
|
|
|
38
|
-
this.reference = posix.join(
|
|
38
|
+
this.reference = posix.join(
|
|
39
|
+
this.#registry.base ?? '',
|
|
40
|
+
this.#scope,
|
|
41
|
+
`${this.name}:${tag}`
|
|
42
|
+
)
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
/** @returns {string | undefined} */
|
|
42
|
-
get name
|
|
46
|
+
get name() {
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
43
49
|
|
|
44
50
|
/** @returns {string | undefined} */
|
|
45
|
-
get version
|
|
51
|
+
get version() {
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
46
54
|
|
|
47
55
|
/** The image to build `FROM`. Undefined takes the runtime's, which is what a service does.
|
|
48
56
|
* @returns {string | undefined} */
|
|
49
|
-
get base
|
|
57
|
+
get base() {
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
50
60
|
|
|
51
61
|
/** Build commands to add. Undefined adds none, which is what a service does.
|
|
52
62
|
* @returns {string | undefined} */
|
|
53
|
-
get run
|
|
63
|
+
get run() {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
54
66
|
|
|
55
|
-
async prepare
|
|
56
|
-
if (this.dockerfile === undefined) throw new Error(
|
|
67
|
+
async prepare(root) {
|
|
68
|
+
if (this.dockerfile === undefined) throw new Error("Dockerfile isn't specified")
|
|
57
69
|
|
|
58
70
|
this.#setValues()
|
|
59
71
|
|
|
@@ -73,38 +85,43 @@ export class Image {
|
|
|
73
85
|
return path
|
|
74
86
|
}
|
|
75
87
|
|
|
76
|
-
#setValues
|
|
88
|
+
#setValues() {
|
|
77
89
|
this.#values.runtime = this.#runtime
|
|
78
|
-
this.#values.build = overwrite(
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
this.#values.build = overwrite(
|
|
91
|
+
{
|
|
92
|
+
image: `${RUNTIME_IMAGE}:${this.#runtime.version}`
|
|
93
|
+
},
|
|
94
|
+
this.#registry.build
|
|
95
|
+
)
|
|
81
96
|
|
|
82
97
|
const image = this.base
|
|
83
98
|
|
|
84
|
-
if (image !== undefined)
|
|
85
|
-
this.#values.build.image = image
|
|
99
|
+
if (image !== undefined) this.#values.build.image = image
|
|
86
100
|
|
|
87
101
|
const run = this.run
|
|
88
102
|
|
|
89
103
|
if (run !== undefined)
|
|
90
|
-
this.#values.build.run =
|
|
104
|
+
this.#values.build.run =
|
|
105
|
+
(this.#values.build.run === undefined ? '' : this.#values.build.run + '\n') + run
|
|
91
106
|
|
|
92
|
-
if (this.#values.build.arguments !== undefined)
|
|
93
|
-
|
|
107
|
+
if (this.#values.build.arguments !== undefined)
|
|
108
|
+
this.#values.build.arguments = createArguments(this.#values.build.arguments)
|
|
109
|
+
if (this.#values.build.run !== undefined)
|
|
110
|
+
this.#values.build.run = createRunCommands(this.#values.build.run)
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
/**
|
|
97
114
|
* @param key {string}
|
|
98
115
|
* @returns {string}
|
|
99
116
|
*/
|
|
100
|
-
#value
|
|
117
|
+
#value(key) {
|
|
101
118
|
const [source, property] = key.split('.')
|
|
102
119
|
|
|
103
120
|
return this.#values[source]?.[property] ?? ''
|
|
104
121
|
}
|
|
105
122
|
}
|
|
106
123
|
|
|
107
|
-
function createRunCommands
|
|
124
|
+
function createRunCommands(input) {
|
|
108
125
|
const lines = input.split('\n')
|
|
109
126
|
|
|
110
127
|
return lines.reduce((commands, command) => {
|
|
@@ -114,7 +131,7 @@ function createRunCommands (input) {
|
|
|
114
131
|
}, '')
|
|
115
132
|
}
|
|
116
133
|
|
|
117
|
-
function createArguments
|
|
134
|
+
function createArguments(variables) {
|
|
118
135
|
const args = []
|
|
119
136
|
|
|
120
137
|
for (const variable of variables) {
|
|
@@ -14,11 +14,16 @@ beforeEach(() => {
|
|
|
14
14
|
it('should assign url', () => {
|
|
15
15
|
instance.tag()
|
|
16
16
|
|
|
17
|
-
assert.deepStrictEqual(
|
|
17
|
+
assert.deepStrictEqual(
|
|
18
|
+
instance.reference,
|
|
19
|
+
`${fixtures.registry.base}/${fixtures.scope}/${fixtures.name}:${fixtures.version}`
|
|
20
|
+
)
|
|
18
21
|
})
|
|
19
22
|
|
|
20
23
|
describe('prepare', () => {
|
|
21
24
|
it('should throw error if no dockerfile specified', async () => {
|
|
22
|
-
await assert.rejects(instance.prepare(generate()), (error) =>
|
|
25
|
+
await assert.rejects(instance.prepare(generate()), (error) =>
|
|
26
|
+
/Dockerfile isn't specified/.test(error.message)
|
|
27
|
+
)
|
|
23
28
|
})
|
|
24
29
|
})
|
|
@@ -11,7 +11,7 @@ COPY --chown=node:node . /composition
|
|
|
11
11
|
|
|
12
12
|
{{build.run}}
|
|
13
13
|
|
|
14
|
-
RUN --mount=type=cache,target=/root/.npm \
|
|
14
|
+
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
15
15
|
for entry in *; do if grep -qs '"dependencies"' "$entry/package.json"; then (cd $entry && npm i --omit=dev); fi; done
|
|
16
16
|
|
|
17
17
|
USER node
|
|
@@ -11,18 +11,18 @@ export class Mono extends Image {
|
|
|
11
11
|
#image
|
|
12
12
|
#components
|
|
13
13
|
|
|
14
|
-
constructor
|
|
14
|
+
constructor(scope, runtime, registry, composition) {
|
|
15
15
|
super(scope, runtime, registry)
|
|
16
16
|
|
|
17
17
|
this.#image = composition.image
|
|
18
18
|
this.#components = composition.components
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
get name
|
|
21
|
+
get name() {
|
|
22
22
|
return 'mono'
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
get version
|
|
25
|
+
get version() {
|
|
26
26
|
const hash = createHash('sha256')
|
|
27
27
|
|
|
28
28
|
for (const component of this.#components) {
|
|
@@ -33,31 +33,32 @@ export class Mono extends Image {
|
|
|
33
33
|
return hash.digest('hex').slice(0, 8)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
get base
|
|
36
|
+
get base() {
|
|
37
37
|
if (this.#image !== undefined) return this.#image
|
|
38
38
|
|
|
39
39
|
const images = new Set(this.#components.map((component) => component.build?.image))
|
|
40
40
|
|
|
41
41
|
if (images.size > 1)
|
|
42
|
-
throw new Error(
|
|
42
|
+
throw new Error(
|
|
43
|
+
'Mono deployment requires different base images for its components. Specify base image for the composition in the context.'
|
|
44
|
+
)
|
|
43
45
|
|
|
44
46
|
return images.values().next().value
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
get run
|
|
49
|
+
get run() {
|
|
48
50
|
const commands = []
|
|
49
51
|
|
|
50
52
|
for (const component of this.#components) {
|
|
51
53
|
const run = component.build?.run
|
|
52
54
|
|
|
53
|
-
if (run !== undefined)
|
|
54
|
-
commands.push(run)
|
|
55
|
+
if (run !== undefined) commands.push(run)
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
return commands.join('\n')
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
async prepare
|
|
61
|
+
async prepare(root) {
|
|
61
62
|
const context = await super.prepare(root)
|
|
62
63
|
|
|
63
64
|
for (const component of this.#components) {
|
|
@@ -0,0 +1,98 @@
|
|
|
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 { execa } from 'execa'
|
|
8
|
+
|
|
9
|
+
import { Service } from './service.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds and pushes the image an extension ships for its service, so that an application
|
|
13
|
+
* with `registry.services: published` takes it instead of building one.
|
|
14
|
+
*
|
|
15
|
+
* The image is built from the package as npm publishes it, not from the workspace: what
|
|
16
|
+
* `.npmignore` leaves out is not in the tarball, and an application builds from the tarball.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} workspace path to the extension's directory in this repository
|
|
19
|
+
* @param {string} runtime the runtime version, which is the tag and the base image
|
|
20
|
+
* @param {string[]} platforms
|
|
21
|
+
* @returns {Promise<string>} the reference pushed
|
|
22
|
+
*/
|
|
23
|
+
export async function publish(workspace, runtime, platforms) {
|
|
24
|
+
const directory = resolve(workspace)
|
|
25
|
+
const manifest = read(join(directory, 'package.json'))
|
|
26
|
+
|
|
27
|
+
// read from the workspace, where the extension's own dependencies resolve; what is
|
|
28
|
+
// installed below carries the same constant but not everything it takes to load it
|
|
29
|
+
const { image } = await import(pathToFileURL(join(directory, manifest.main)).href)
|
|
30
|
+
|
|
31
|
+
if (image === undefined)
|
|
32
|
+
throw new Error(`'${manifest.name}' publishes no service image: it exports no 'image'`)
|
|
33
|
+
|
|
34
|
+
const root = await mkdtemp(join(tmpdir(), 'toa-publish'))
|
|
35
|
+
|
|
36
|
+
await writeFile(join(root, 'package.json'), '{"private":true}')
|
|
37
|
+
await run('npm', [
|
|
38
|
+
'i',
|
|
39
|
+
'--prefix',
|
|
40
|
+
root,
|
|
41
|
+
'--omit=dev',
|
|
42
|
+
`${manifest.name}@${manifest.version}`
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
const path = join(root, 'node_modules', manifest.name)
|
|
46
|
+
|
|
47
|
+
const service = new Service(SCOPE, { version: runtime }, {}, path, {
|
|
48
|
+
...name(image),
|
|
49
|
+
version: manifest.version
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
service.reference = `${image}:${runtime}`
|
|
53
|
+
|
|
54
|
+
await service.prepare(await mkdtemp(join(tmpdir(), 'toa-images')))
|
|
55
|
+
|
|
56
|
+
await run('docker', [
|
|
57
|
+
'buildx',
|
|
58
|
+
'build',
|
|
59
|
+
'--platform',
|
|
60
|
+
platforms.join(','),
|
|
61
|
+
'--tag',
|
|
62
|
+
service.reference,
|
|
63
|
+
'--provenance=false',
|
|
64
|
+
'--push',
|
|
65
|
+
service.context
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
return service.reference
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The repository's last segment is what `Service` names an image, so it reads back. */
|
|
72
|
+
function name(image) {
|
|
73
|
+
const match = /\/extension-([^-/]+)-([^/]+)$/.exec(image)
|
|
74
|
+
|
|
75
|
+
if (match === null)
|
|
76
|
+
throw new Error(`'${image}' is not named 'extension-<group>-<name>'`)
|
|
77
|
+
|
|
78
|
+
return { group: match[1], name: match[2] }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const read = (path) => JSON.parse(readFileSync(path, 'utf8'))
|
|
82
|
+
|
|
83
|
+
const run = async (cmd, args) => {
|
|
84
|
+
console.log('toa>', cmd, args.join(' '))
|
|
85
|
+
|
|
86
|
+
await execa(cmd, args, { stdio: 'inherit' })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// the reference is assigned rather than derived from a scope
|
|
90
|
+
const SCOPE = ''
|
|
91
|
+
|
|
92
|
+
const PLATFORMS = 'linux/amd64,linux/arm64'
|
|
93
|
+
|
|
94
|
+
if (import.meta.main) {
|
|
95
|
+
const [workspace, runtime, platforms = PLATFORMS] = process.argv.slice(2)
|
|
96
|
+
|
|
97
|
+
console.log(await publish(workspace, runtime, platforms.split(',')))
|
|
98
|
+
}
|
|
@@ -16,7 +16,7 @@ class TestImage extends Image {
|
|
|
16
16
|
#name
|
|
17
17
|
#base
|
|
18
18
|
|
|
19
|
-
constructor
|
|
19
|
+
constructor(runtime, registry, dockerfile, name = 'test', base) {
|
|
20
20
|
super('acme', runtime, registry)
|
|
21
21
|
|
|
22
22
|
this.dockerfile = dockerfile
|
|
@@ -24,15 +24,15 @@ class TestImage extends Image {
|
|
|
24
24
|
this.#base = base
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
get name
|
|
27
|
+
get name() {
|
|
28
28
|
return this.#name
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
get version
|
|
31
|
+
get version() {
|
|
32
32
|
return 'abcdef12'
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
get base
|
|
35
|
+
get base() {
|
|
36
36
|
return this.#base
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -76,12 +76,18 @@ describe('runtime base image', () => {
|
|
|
76
76
|
const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
|
|
77
77
|
|
|
78
78
|
assert.ok(dockerfile.includes('FROM node:24.14.0-alpine3.22'))
|
|
79
|
-
assert.ok(!
|
|
79
|
+
assert.ok(!dockerfile.includes(RUNTIME_IMAGE))
|
|
80
80
|
})
|
|
81
81
|
|
|
82
82
|
it('should allow composition.image override via base', async () => {
|
|
83
83
|
const runtime = { version: '1.0.0-alpha.232' }
|
|
84
|
-
const image = new TestImage(
|
|
84
|
+
const image = new TestImage(
|
|
85
|
+
runtime,
|
|
86
|
+
{},
|
|
87
|
+
compositionDockerfile,
|
|
88
|
+
'mono',
|
|
89
|
+
'custom.example/base:1'
|
|
90
|
+
)
|
|
85
91
|
|
|
86
92
|
const path = await image.prepare(root)
|
|
87
93
|
const dockerfile = await readFile(join(path, 'Dockerfile'), 'utf8')
|
|
@@ -7,7 +7,7 @@ RUN if [ "{{runtime.proxy}}" != "" ]; then npm set proxy {{runtime.proxy}}; fi
|
|
|
7
7
|
WORKDIR /service
|
|
8
8
|
COPY --chown=node:node . /service
|
|
9
9
|
|
|
10
|
-
RUN --mount=type=cache,target=/root/.npm \
|
|
10
|
+
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
11
11
|
npm i --omit=dev
|
|
12
12
|
|
|
13
13
|
USER node
|
|
@@ -37,7 +37,7 @@ export class Service extends Image {
|
|
|
37
37
|
* @param {string} reference
|
|
38
38
|
* @param {toa.deployment.dependency.Service} service
|
|
39
39
|
*/
|
|
40
|
-
constructor
|
|
40
|
+
constructor(scope, runtime, registry, reference, service) {
|
|
41
41
|
super(scope, runtime, registry)
|
|
42
42
|
|
|
43
43
|
this.service = service.name
|
|
@@ -48,15 +48,15 @@ export class Service extends Image {
|
|
|
48
48
|
this.#version = service.version
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
get name
|
|
51
|
+
get name() {
|
|
52
52
|
return 'extension-' + this.#group + '-' + this.#name
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
get version
|
|
55
|
+
get version() {
|
|
56
56
|
return this.#version
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
async prepare
|
|
59
|
+
async prepare(root) {
|
|
60
60
|
const context = await super.prepare(root)
|
|
61
61
|
|
|
62
62
|
await fs.copy(this.#path, context)
|
|
@@ -11,12 +11,12 @@ export class Operator {
|
|
|
11
11
|
* @param deployment {toa.deployment.Deployment}
|
|
12
12
|
* @param registry {toa.deployment.Registry}
|
|
13
13
|
*/
|
|
14
|
-
constructor
|
|
14
|
+
constructor(deployment, registry) {
|
|
15
15
|
this.#deployment = deployment
|
|
16
16
|
this.#registry = registry
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
async export
|
|
19
|
+
async export(path) {
|
|
20
20
|
const target = await workspace.create('deployment', path)
|
|
21
21
|
|
|
22
22
|
await this.#deployment.export(target)
|
|
@@ -24,32 +24,32 @@ export class Operator {
|
|
|
24
24
|
return target
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
async prepare
|
|
27
|
+
async prepare(path) {
|
|
28
28
|
return await this.#registry.prepare(path)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
async push
|
|
31
|
+
async push() {
|
|
32
32
|
await this.#registry.push()
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
async install
|
|
35
|
+
async install(options = {}) {
|
|
36
36
|
options = Object.assign({}, OPTIONS, options)
|
|
37
37
|
|
|
38
38
|
await Promise.all([this.export(), this.push()])
|
|
39
39
|
await this.#deployment.install(options)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
async template
|
|
42
|
+
async template(options = {}) {
|
|
43
43
|
await this.export()
|
|
44
44
|
|
|
45
45
|
return await this.#deployment.template(options)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
variables
|
|
48
|
+
variables() {
|
|
49
49
|
return this.#deployment.variables()
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
tags
|
|
52
|
+
tags() {
|
|
53
53
|
return this.#registry.tags()
|
|
54
54
|
}
|
|
55
55
|
}
|