@toa.io/operations 1.0.0-alpha.291 → 1.0.0-alpha.293
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 +16 -0
- package/image/runtime.Dockerfile +15 -1
- package/package.json +7 -5
- package/readme.md +47 -5
- package/src/deployment/factory.js +6 -15
- package/src/deployment/images/bundle.js +7 -1
- package/src/deployment/images/dependencies.Dockerfile +8 -1
- package/src/deployment/images/dependencies.js +31 -1
- package/src/deployment/images/dependencies.test.js +54 -0
- package/src/deployment/images/format.js +14 -0
- package/src/deployment/images/format.test.js +40 -1
- package/src/deployment/images/publish.js +3 -5
- package/src/deployment/images/service.Dockerfile +6 -0
- package/src/deployment/images/service.js +14 -1
- package/src/index.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [1.0.0-alpha.293](https://github.com/toa-io/toa/compare/v1.0.0-alpha.292...v1.0.0-alpha.293) (2026-09-07)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @toa.io/operations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# [1.0.0-alpha.292](https://github.com/toa-io/toa/compare/v1.0.0-alpha.291...v1.0.0-alpha.292) (2026-09-07)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **cli:** the deployment library is a peer, and the binary is the CLI's ([a923f81](https://github.com/toa-io/toa/commit/a923f8173a55ed1c091213a7c7db1863ca1d0ba6))
|
|
19
|
+
* **norm:** a package's definition is read by one resolver ([bffd41c](https://github.com/toa-io/toa/commit/bffd41c8d18c42786b0bf321623088b9dabc4724))
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# [1.0.0-alpha.291](https://github.com/toa-io/toa/compare/v1.0.0-alpha.290...v1.0.0-alpha.291) (2026-09-07)
|
|
7
23
|
|
|
8
24
|
**Note:** Version bump only for package @toa.io/operations
|
package/image/runtime.Dockerfile
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
FROM node:24.14.0-alpine3.22
|
|
2
2
|
|
|
3
3
|
ARG VERSION
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
# installed locally rather than globally: the binary is @toa.io/cli's, which the runtime depends
|
|
6
|
+
# on, and npm links a dependency's binaries only into a local `node_modules/.bin`
|
|
7
|
+
WORKDIR /toa
|
|
8
|
+
|
|
9
|
+
# mounted rather than written: npm's cache is a hundred megabytes, and a layer keeps whatever
|
|
10
|
+
# the command left behind.
|
|
11
|
+
# `--legacy-peer-deps` installs no peer this does not ask for: `promex`, `reretry` and
|
|
12
|
+
# `matchacho` declare `typescript` a peer without marking it optional, and nothing here imports
|
|
13
|
+
# it. Every other peer in the closure is a dependency of the runtime's already. Drop the flag
|
|
14
|
+
# once those three are published with `peerDependenciesMeta`.
|
|
15
|
+
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
16
|
+
npm i @toa.io/runtime@${VERSION} --omit=dev --legacy-peer-deps
|
|
17
|
+
|
|
18
|
+
ENV PATH=/toa/node_modules/.bin:$PATH
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/operations",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.293",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Toa Deployment",
|
|
6
6
|
"homepage": "https://toa.io",
|
|
@@ -28,9 +28,11 @@
|
|
|
28
28
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@toa.io/generic": "1.0.0-alpha.
|
|
32
|
-
"@toa.io/
|
|
33
|
-
"
|
|
31
|
+
"@toa.io/generic": "1.0.0-alpha.292",
|
|
32
|
+
"@toa.io/kubernetes": "1.0.0-alpha.293",
|
|
33
|
+
"@toa.io/norm": "1.0.0-alpha.293",
|
|
34
|
+
"oxc-parser": "0.149.0",
|
|
35
|
+
"paseto": "4.0.0"
|
|
34
36
|
},
|
|
35
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "1e05b3ab64758324c58c425f1f8e52e9f35f1da8"
|
|
36
38
|
}
|
package/readme.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Toa Operations
|
|
2
2
|
|
|
3
|
+
## Installing
|
|
4
|
+
|
|
5
|
+
`toa deploy`, `toa build`, `toa push`, `toa env`, `toa export` and `toa conceal` need this package
|
|
6
|
+
beside the CLI; the runtime does not carry it, and a container that runs a composition cannot
|
|
7
|
+
deploy one. An application that deploys lists it with the runtime:
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
$ npm i -D @toa.io/runtime @toa.io/operations
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
A machine that only deploys needs no runtime and no extension:
|
|
14
|
+
|
|
15
|
+
```shell
|
|
16
|
+
$ npm i @toa.io/cli @toa.io/operations
|
|
17
|
+
$ npx toa deploy production -p application
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
What the extensions declare is read from `@toa.io/definitions`, which the CLI brings; what they run
|
|
21
|
+
is not needed to render a chart. `registry.services: build` is the exception: it builds an
|
|
22
|
+
extension's service image from the installed package, so it needs the runtime installed beside
|
|
23
|
+
this one. `published` does not.
|
|
24
|
+
|
|
3
25
|
## Compositions
|
|
4
26
|
|
|
5
27
|
A composition is deployed as one pod. Beside its components it may run extension services,
|
|
@@ -27,7 +49,8 @@ Deploy images default to `FROM ghcr.io/toa-io/runtime:<runtime.version>`
|
|
|
27
49
|
installed; composition/service Dockerfiles only install component dependencies.
|
|
28
50
|
|
|
29
51
|
To use a custom base image, set `registry.build.image` (or `composition.image`).
|
|
30
|
-
That image must provide the `toa` CLI, or install it via `registry.build.run
|
|
52
|
+
That image must provide the `toa` CLI, or install it via `registry.build.run`. The binary is
|
|
53
|
+
`@toa.io/cli`'s, which the runtime depends on, so a local install puts it on the `PATH`:
|
|
31
54
|
|
|
32
55
|
```yaml
|
|
33
56
|
# context.toa.yaml
|
|
@@ -35,25 +58,44 @@ That image must provide the `toa` CLI, or install it via `registry.build.run`:
|
|
|
35
58
|
registry:
|
|
36
59
|
build:
|
|
37
60
|
image: node:24.14.0-alpine3.22
|
|
38
|
-
run:
|
|
61
|
+
run: |
|
|
62
|
+
npm i --prefix /toa @toa.io/runtime --omit=dev
|
|
63
|
+
ln -s /toa/node_modules/.bin/toa /usr/local/bin/toa
|
|
39
64
|
```
|
|
40
65
|
|
|
66
|
+
`/toa` is where it goes: what an extension needs for what a component declares is installed
|
|
67
|
+
there, beside the extension that reads the declaration.
|
|
68
|
+
|
|
41
69
|
#### Composition Images
|
|
42
70
|
|
|
43
71
|
A composition is two images in one repository. `composition-<name>:deps-<hash>` is what its
|
|
44
72
|
components depend on, installed on the base image: it is tagged by everything the install
|
|
45
|
-
reads — the runtime version, the base image, the build options
|
|
46
|
-
`package.json` (and `package-lock.json`, where there is one)
|
|
47
|
-
those changes. `composition-<name>:<hash>` is the sources laid over it in a single linked
|
|
73
|
+
reads — the runtime version, the base image, the build options, each component's
|
|
74
|
+
`package.json` (and `package-lock.json`, where there is one), and what the extensions install
|
|
75
|
+
for what the components declare — and is built only when one of those changes. `composition-<name>:<hash>` is the sources laid over it in a single linked
|
|
48
76
|
layer, so a deploy that changes code alone builds and pushes that layer, and neither
|
|
49
77
|
downloads nor uploads the dependencies again. The same holds for `mono`.
|
|
50
78
|
|
|
79
|
+
Beside each component's sources the layer carries `manifest.toa.json`, the manifest as this
|
|
80
|
+
build normalised it. A composition reads it instead of normalising again, so it loads no
|
|
81
|
+
bridge to read what a module declares; a workspace has no such file and is read as it always
|
|
82
|
+
was. What a build cannot express there — a prototype in a directory of the application's own
|
|
83
|
+
rather than in a package — fails the build, where before it failed the container.
|
|
84
|
+
|
|
51
85
|
A pushed image builds on the `toa` container builder, whose registry exporter is what lays
|
|
52
86
|
an image over a base it never pulled; `toa build` loads on the daemon's own.
|
|
53
87
|
|
|
54
88
|
A dependency named by a moving git ref is installed when the dependencies image is built and
|
|
55
89
|
not again until its manifest changes, so pin such a dependency to a commit and bump it there.
|
|
56
90
|
|
|
91
|
+
An extension's heavy dependency is not installed with the extension: `@toa.io/extensions.storages`
|
|
92
|
+
declares the AWS and Cloudinary SDKs as optional peers, so the base image carries neither. What a
|
|
93
|
+
component declares is what a deploy installs — the storages named in its `manifest.toa.yaml`,
|
|
94
|
+
resolved through the context's annotation to their providers' packages, installed into `/toa`
|
|
95
|
+
beside the extension that reads them. A composition whose components declare no `s3` storage
|
|
96
|
+
carries no AWS SDK, whatever the context declares for another composition. A workspace installs
|
|
97
|
+
the same set with `toa npm`.
|
|
98
|
+
|
|
57
99
|
#### Extension Service Images
|
|
58
100
|
|
|
59
101
|
`registry.services` says where an extension service's image comes from.
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { createRequire } from 'node:module'
|
|
4
|
-
import { pathToFileURL } from 'node:url'
|
|
5
|
-
import { context as load } from '@toa.io/norm'
|
|
1
|
+
import { context as load, definition } from '@toa.io/norm'
|
|
6
2
|
import { Process } from '../process.js'
|
|
7
3
|
import { Operator } from './operator.js'
|
|
8
4
|
import { Factory as ImagesFactory } from './images/index.js'
|
|
9
5
|
import { Deployment } from './deployment.js'
|
|
10
6
|
|
|
11
|
-
// a dependency is resolved the way a package is
|
|
12
|
-
const require = createRequire(import.meta.url)
|
|
13
7
|
import { Registry } from './registry.js'
|
|
14
8
|
import { Composition } from './composition.js'
|
|
15
9
|
import { Service } from './service.js'
|
|
@@ -47,7 +41,9 @@ export class Factory {
|
|
|
47
41
|
|
|
48
42
|
if (this.#mono)
|
|
49
43
|
this.#image = this.#registry.mono({
|
|
50
|
-
components: context.components
|
|
44
|
+
components: context.components,
|
|
45
|
+
// mono runs every service, so it installs what every one of them brings
|
|
46
|
+
packages: context.packages
|
|
51
47
|
})
|
|
52
48
|
else
|
|
53
49
|
this.#compositions = context.compositions.map((composition) =>
|
|
@@ -110,16 +106,11 @@ export class Factory {
|
|
|
110
106
|
}
|
|
111
107
|
|
|
112
108
|
async #getDependency(reference, instances) {
|
|
113
|
-
|
|
114
|
-
// and a module is loaded by file
|
|
115
|
-
const module = await import(pathToFileURL(require.resolve(reference)).href)
|
|
116
|
-
const pkg = JSON.parse(
|
|
117
|
-
readFileSync(require.resolve(join(reference, 'package.json')), 'utf8')
|
|
118
|
-
)
|
|
109
|
+
const { name, module } = await definition(reference)
|
|
119
110
|
|
|
120
111
|
if (module.deployment === undefined) return
|
|
121
112
|
|
|
122
|
-
const annotation = this.#context.annotations?.[
|
|
113
|
+
const annotation = this.#context.annotations?.[name]
|
|
123
114
|
|
|
124
115
|
/** @type {toa.deployment.dependency.Declaration} */
|
|
125
116
|
const dependency = module.deployment(instances, annotation)
|
|
@@ -4,7 +4,7 @@ import { createHash } from 'node:crypto'
|
|
|
4
4
|
|
|
5
5
|
import { Image } from './image.js'
|
|
6
6
|
import { Dependencies } from './dependencies.js'
|
|
7
|
-
import { declare } from './format.js'
|
|
7
|
+
import { declare, normalized } from './format.js'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Components in one image: their sources laid over their dependencies, which are an image
|
|
@@ -23,11 +23,16 @@ export class Bundle extends Image {
|
|
|
23
23
|
/** @type {toa.norm.Component[]} */
|
|
24
24
|
components
|
|
25
25
|
|
|
26
|
+
/** What the services this workload runs install, which no component of it declares.
|
|
27
|
+
* @type {Record<string, string> | undefined} */
|
|
28
|
+
packages
|
|
29
|
+
|
|
26
30
|
constructor(scope, runtime, registry, composition) {
|
|
27
31
|
super(scope, runtime, registry)
|
|
28
32
|
|
|
29
33
|
this.image = composition.image
|
|
30
34
|
this.components = composition.components
|
|
35
|
+
this.packages = composition.packages
|
|
31
36
|
this.dependencies = new Dependencies(scope, runtime, registry, this)
|
|
32
37
|
}
|
|
33
38
|
|
|
@@ -73,6 +78,7 @@ export class Bundle extends Image {
|
|
|
73
78
|
// what was installed in the workspace is not what the image installs
|
|
74
79
|
await cp(component.path, target, { recursive: true, filter: sources })
|
|
75
80
|
await declare(component.path, target, component.locator.label)
|
|
81
|
+
await normalized(component, target)
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
return context
|
|
@@ -11,6 +11,13 @@ COPY --chown=node:node . /composition
|
|
|
11
11
|
|
|
12
12
|
{{build.run}}
|
|
13
13
|
|
|
14
|
-
# the context holds the manifests alone, so this is every dependency and none of the sources
|
|
14
|
+
# the context holds the manifests alone, so this is every dependency and none of the sources.
|
|
15
|
+
# `.packages` is a dotfile, so the glob does not reach it
|
|
15
16
|
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
16
17
|
for entry in *; do if grep -qs '"dependencies"' "$entry/package.json"; then (cd $entry && npm i --omit=dev); fi; done
|
|
18
|
+
|
|
19
|
+
# what the extensions need for what these components declare: installed beside the extension
|
|
20
|
+
# that reads the declaration, in Toa's own install, because that is where Node resolves an
|
|
21
|
+
# extension's imports from — and one copy for the composition rather than one per component
|
|
22
|
+
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
23
|
+
if [ -s .packages ]; then npm i --prefix /toa --omit=dev $(cat .packages); fi
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename, join } from 'node:path'
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
-
import { copyFile, mkdir } from 'node:fs/promises'
|
|
3
|
+
import { copyFile, mkdir, writeFile } from 'node:fs/promises'
|
|
4
4
|
import { createHash } from 'node:crypto'
|
|
5
5
|
|
|
6
6
|
import { Image } from './image.js'
|
|
@@ -68,6 +68,8 @@ export class Dependencies extends Image {
|
|
|
68
68
|
for (const file of files)
|
|
69
69
|
hash.update(label).update(basename(file)).update(readFileSync(file))
|
|
70
70
|
|
|
71
|
+
hash.update(this.#packages().join('\n'))
|
|
72
|
+
|
|
71
73
|
this.#version = hash.digest('hex').slice(0, 8)
|
|
72
74
|
|
|
73
75
|
return this.#version
|
|
@@ -112,9 +114,34 @@ export class Dependencies extends Image {
|
|
|
112
114
|
for (const file of files) await copyFile(file, join(target, basename(file)))
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
const packages = this.#packages()
|
|
118
|
+
|
|
119
|
+
// written whether or not there are any, so the file is one thing the Dockerfile reads
|
|
120
|
+
await writeFile(join(context, PACKAGES), packages.join('\n'))
|
|
121
|
+
|
|
115
122
|
return context
|
|
116
123
|
}
|
|
117
124
|
|
|
125
|
+
/**
|
|
126
|
+
* What the extensions install for what this workload runs, as `name@version`: what its
|
|
127
|
+
* components declare, and what the services it hosts bring. A component states its own
|
|
128
|
+
* dependencies in its manifest; this is what the packages that read its declaration need,
|
|
129
|
+
* which its manifest has no way to say.
|
|
130
|
+
*
|
|
131
|
+
* @returns {string[]}
|
|
132
|
+
*/
|
|
133
|
+
#packages() {
|
|
134
|
+
/** @type {Record<string, string>} */
|
|
135
|
+
const packages = { ...this.#owner.packages }
|
|
136
|
+
|
|
137
|
+
for (const component of this.#owner.components)
|
|
138
|
+
Object.assign(packages, component.packages)
|
|
139
|
+
|
|
140
|
+
return Object.entries(packages)
|
|
141
|
+
.map(([name, version]) => `${name}@${version}`)
|
|
142
|
+
.sort()
|
|
143
|
+
}
|
|
144
|
+
|
|
118
145
|
/**
|
|
119
146
|
* The files the install reads, by component label, in a fixed order.
|
|
120
147
|
*
|
|
@@ -139,3 +166,6 @@ const PREFIX = 'deps-'
|
|
|
139
166
|
const DIRECTORY = 'dependencies'
|
|
140
167
|
|
|
141
168
|
const MANIFESTS = ['package.json', 'package-lock.json']
|
|
169
|
+
|
|
170
|
+
/** Where the install reads what the extensions need, one `name@version` per line. */
|
|
171
|
+
const PACKAGES = '.packages'
|
|
@@ -90,6 +90,20 @@ describe('reference', () => {
|
|
|
90
90
|
assert.notStrictEqual(create().dependencies.reference, run.dependencies.reference)
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
+
it('should change with what an extension installs', () => {
|
|
94
|
+
const before = create()
|
|
95
|
+
|
|
96
|
+
composition.components[1].packages = { cloudinary: '2.11.0' }
|
|
97
|
+
|
|
98
|
+
const component = create()
|
|
99
|
+
|
|
100
|
+
assert.notStrictEqual(component.dependencies.reference, before.dependencies.reference)
|
|
101
|
+
|
|
102
|
+
composition.packages = { 'lru-cache': '11.5.2' }
|
|
103
|
+
|
|
104
|
+
assert.notStrictEqual(create().dependencies.reference, component.dependencies.reference)
|
|
105
|
+
})
|
|
106
|
+
|
|
93
107
|
it('should change with the registry build settings', () => {
|
|
94
108
|
const before = create()
|
|
95
109
|
|
|
@@ -128,6 +142,7 @@ describe('prepare', () => {
|
|
|
128
142
|
|
|
129
143
|
assert.deepStrictEqual(entries, [
|
|
130
144
|
'.dockerignore',
|
|
145
|
+
'.packages',
|
|
131
146
|
'Dockerfile',
|
|
132
147
|
'one',
|
|
133
148
|
'one/package.json',
|
|
@@ -144,6 +159,44 @@ describe('prepare', () => {
|
|
|
144
159
|
assert.doesNotMatch(dockerfile, /USER node|CMD /)
|
|
145
160
|
})
|
|
146
161
|
|
|
162
|
+
it('should hold what the extensions install, deduplicated and ordered', async () => {
|
|
163
|
+
composition.components[0].packages = { cloudinary: '2.11.0' }
|
|
164
|
+
composition.components[1].packages = {
|
|
165
|
+
cloudinary: '2.11.0',
|
|
166
|
+
'@aws-sdk/client-s3': '3.1125.0'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const image = create()
|
|
170
|
+
const context = await image.dependencies.prepare(root)
|
|
171
|
+
|
|
172
|
+
assert.strictEqual(
|
|
173
|
+
await readFile(join(context, '.packages'), 'utf8'),
|
|
174
|
+
'@aws-sdk/client-s3@3.1125.0\ncloudinary@2.11.0'
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
const dockerfile = await readFile(join(context, 'Dockerfile'), 'utf8')
|
|
178
|
+
|
|
179
|
+
assert.ok(dockerfile.includes('npm i --prefix /toa --omit=dev $(cat .packages)'))
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should hold what a service the composition runs brings', async () => {
|
|
183
|
+
composition.packages = { 'lru-cache': '11.5.2' }
|
|
184
|
+
composition.components[0].packages = { cloudinary: '2.11.0' }
|
|
185
|
+
|
|
186
|
+
const context = await create().dependencies.prepare(root)
|
|
187
|
+
|
|
188
|
+
assert.strictEqual(
|
|
189
|
+
await readFile(join(context, '.packages'), 'utf8'),
|
|
190
|
+
'cloudinary@2.11.0\nlru-cache@11.5.2'
|
|
191
|
+
)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should hold an empty list where nothing is declared', async () => {
|
|
195
|
+
const context = await create().dependencies.prepare(root)
|
|
196
|
+
|
|
197
|
+
assert.strictEqual(await readFile(join(context, '.packages'), 'utf8'), '')
|
|
198
|
+
})
|
|
199
|
+
|
|
147
200
|
it('should lay the sources over the dependencies', async () => {
|
|
148
201
|
const image = create()
|
|
149
202
|
|
|
@@ -167,6 +220,7 @@ describe('prepare', () => {
|
|
|
167
220
|
|
|
168
221
|
assert.ok(entries.includes('one/index.js'))
|
|
169
222
|
assert.ok(entries.includes('two/package.json')) // declared for the component that had none
|
|
223
|
+
assert.ok(entries.includes('one/manifest.toa.json')) // what the process reads instead of normalizing
|
|
170
224
|
assert.ok(!entries.some((entry) => entry.includes('node_modules')))
|
|
171
225
|
})
|
|
172
226
|
|
|
@@ -2,6 +2,8 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
2
2
|
import { writeFile } from 'node:fs/promises'
|
|
3
3
|
import { dirname, join, parse } from 'node:path'
|
|
4
4
|
|
|
5
|
+
import { NORMALIZED, plain } from '@toa.io/norm'
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* A component is copied into the image on its own, away from the package that
|
|
7
9
|
* declared what its files are. Node reads a `.js` as CommonJS unless a manifest
|
|
@@ -23,6 +25,18 @@ export async function declare(source, target, name) {
|
|
|
23
25
|
)
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The manifest as this build read it, written beside the component so that the process which
|
|
30
|
+
* runs it does not read it again. See `plain` in `@toa.io/norm` for what is left out and why
|
|
31
|
+
* a build's answer is the one to trust.
|
|
32
|
+
*
|
|
33
|
+
* @param {toa.norm.Component} component
|
|
34
|
+
* @param {string} target where it was copied
|
|
35
|
+
*/
|
|
36
|
+
export async function normalized(component, target) {
|
|
37
|
+
await writeFile(join(target, NORMALIZED), JSON.stringify(plain(component)))
|
|
38
|
+
}
|
|
39
|
+
|
|
26
40
|
/**
|
|
27
41
|
* The module format a directory's files are read as, which the nearest manifest
|
|
28
42
|
* above it decides.
|
|
@@ -4,7 +4,10 @@ import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'
|
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { Locator } from '@toa.io/core'
|
|
8
|
+
import { revive } from '@toa.io/norm'
|
|
9
|
+
|
|
10
|
+
import { declare, format, normalized } from './format.js'
|
|
8
11
|
|
|
9
12
|
let root
|
|
10
13
|
|
|
@@ -72,3 +75,39 @@ describe('declare', () => {
|
|
|
72
75
|
assert.strictEqual(await readFile(join(target, 'package.json'), 'utf8'), own)
|
|
73
76
|
})
|
|
74
77
|
})
|
|
78
|
+
|
|
79
|
+
describe('normalized', () => {
|
|
80
|
+
it('should write what a process reads instead of normalizing', async () => {
|
|
81
|
+
const target = join(root, 'image', 'normalized')
|
|
82
|
+
|
|
83
|
+
await mkdir(target, { recursive: true })
|
|
84
|
+
|
|
85
|
+
const component = {
|
|
86
|
+
name: 'tasks',
|
|
87
|
+
namespace: 'todos',
|
|
88
|
+
version: 'abcdef12',
|
|
89
|
+
operations: { compute: { type: 'computation' } },
|
|
90
|
+
path: '/workspace/components/todos.tasks',
|
|
91
|
+
locator: new Locator('tasks', 'todos'),
|
|
92
|
+
packages: { cloudinary: '2.11.0' }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await normalized(component, target)
|
|
96
|
+
|
|
97
|
+
const carried = JSON.parse(
|
|
98
|
+
await readFile(join(target, 'manifest.toa.json'), 'utf8')
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
// where the manifest is, and what only a deploy reads, are not what it declares
|
|
102
|
+
assert.ok(!('path' in carried))
|
|
103
|
+
assert.ok(!('locator' in carried))
|
|
104
|
+
assert.ok(!('packages' in carried))
|
|
105
|
+
|
|
106
|
+
const read = revive(carried, '/composition/todos-tasks')
|
|
107
|
+
|
|
108
|
+
assert.deepStrictEqual(read.operations, component.operations)
|
|
109
|
+
assert.strictEqual(read.version, component.version)
|
|
110
|
+
assert.strictEqual(read.path, '/composition/todos-tasks')
|
|
111
|
+
assert.strictEqual(read.locator.id, 'todos.tasks')
|
|
112
|
+
})
|
|
113
|
+
})
|
|
@@ -2,7 +2,7 @@ import { mkdtemp, writeFile } from 'node:fs/promises'
|
|
|
2
2
|
import { readFileSync } from 'node:fs'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join, resolve } from 'node:path'
|
|
5
|
-
import {
|
|
5
|
+
import { definition } from '@toa.io/norm'
|
|
6
6
|
|
|
7
7
|
import { run } from '../../process.js'
|
|
8
8
|
import { Service } from './service.js'
|
|
@@ -12,7 +12,7 @@ import { Service } from './service.js'
|
|
|
12
12
|
* with `registry.services: published` takes it instead of building one.
|
|
13
13
|
*
|
|
14
14
|
* The image is built from the package as npm publishes it, not from the workspace: what
|
|
15
|
-
*
|
|
15
|
+
* `files` leaves out is not in the tarball, and an application builds from the tarball.
|
|
16
16
|
*
|
|
17
17
|
* @param {string} workspace path to the extension's directory in this repository
|
|
18
18
|
* @param {string} runtime the runtime version, which is the tag and the base image
|
|
@@ -23,9 +23,7 @@ export async function publish(workspace, runtime, platforms) {
|
|
|
23
23
|
const directory = resolve(workspace)
|
|
24
24
|
const manifest = read(join(directory, 'package.json'))
|
|
25
25
|
|
|
26
|
-
|
|
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)
|
|
26
|
+
const { image } = (await definition(manifest.name)).module
|
|
29
27
|
|
|
30
28
|
if (image === undefined)
|
|
31
29
|
throw new Error(
|
|
@@ -10,6 +10,12 @@ 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
|
+
# a component of the extension's own declares what it imports, and it is installed beside the
|
|
14
|
+
# component rather than beside the extension: an application that runs none of them, and the
|
|
15
|
+
# runtime image every application is built on, carry nothing for them
|
|
16
|
+
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
|
|
17
|
+
for entry in components/*; do if grep -qs '"dependencies"' "$entry/package.json"; then (cd $entry && npm i --omit=dev); fi; done
|
|
18
|
+
|
|
13
19
|
# no USER: the runtime drops to `node` itself, and only a process that started as root can
|
|
14
20
|
# close its environment under /proc — see runtime/runtime/bin/toa
|
|
15
21
|
CMD toa serve .
|
|
@@ -66,9 +66,22 @@ export class Service extends Image {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
+
* Where the extension is installed, which is what its image is built from. A deploy install
|
|
70
|
+
* carries what an extension declares and not the extension, so a service is built only where
|
|
71
|
+
* the runtime is installed beside the deployment library; elsewhere its image is published.
|
|
72
|
+
*
|
|
69
73
|
* @param {string} reference
|
|
70
74
|
* @returns {string}
|
|
71
75
|
*/
|
|
72
76
|
const find = (reference) => {
|
|
73
|
-
|
|
77
|
+
try {
|
|
78
|
+
return dirname(require.resolve(join(reference, 'package.json')))
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`'${reference}' is not installed, and \`registry.services: build\` builds its image ` +
|
|
82
|
+
'from where it is: install the runtime beside @toa.io/operations, or take the ' +
|
|
83
|
+
'image the release publishes with `registry.services: published`',
|
|
84
|
+
{ cause: error }
|
|
85
|
+
)
|
|
86
|
+
}
|
|
74
87
|
}
|
package/src/index.js
CHANGED