@forgezero/agent 0.1.73 → 0.1.75
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/README.md +565 -61
- package/dist/agent-heartbeat.d.ts +25 -1
- package/dist/agent-heartbeat.js +45 -5
- package/dist/bootstrap.d.ts +2 -0
- package/dist/bootstrap.js +22 -4
- package/dist/community-rehearsal-host.js +208 -9
- package/dist/credential-schema.d.ts +1 -1
- package/dist/credential-schema.js +1 -1
- package/dist/definition.js +206 -7
- package/dist/deploy-actions.d.ts +7 -0
- package/dist/deploy-compiler.js +191 -10
- package/dist/deploy-file.js +206 -7
- package/dist/deploy-plan-runner.d.ts +3 -0
- package/dist/deploy-plan-runner.js +190 -9
- package/dist/deploy-plan.js +187 -8
- package/dist/deploy-providers.d.ts +19 -0
- package/dist/deploy.d.ts +89 -3
- package/dist/deploy.js +49 -2
- package/dist/deployment-connectivity.d.ts +63 -0
- package/dist/deployment-connectivity.js +148 -0
- package/dist/deployment-pull.d.ts +2 -0
- package/dist/deployment-targets.d.ts +9 -0
- package/dist/deployment-targets.js +141 -0
- package/dist/deployment.d.ts +69 -0
- package/dist/fz-agent.js +1112 -302
- package/dist/fz.js +246 -18
- package/dist/index.d.ts +1 -0
- package/dist/metal-bootstrap.js +1 -1
- package/dist/metal-helper-socket.js +210 -11
- package/dist/metal-provision.js +210 -11
- package/dist/operator-bootstrap.js +22 -4
- package/dist/platform-bootstrap-runtime.d.ts +1 -0
- package/dist/platform-bootstrap-runtime.js +216 -10
- package/dist/platform-fleet-verification.js +523 -150
- package/dist/platform-genesis.js +206 -7
- package/dist/provision.js +465 -99
- package/dist/service-supervisor.d.ts +6 -0
- package/dist/software-helper.d.ts +3 -0
- package/dist/software-helper.js +465 -98
- package/dist/software.d.ts +4 -1
- package/dist/software.js +210 -8
- package/dist/ubuntu.js +206 -7
- package/dist/version.d.ts +1 -1
- package/package.json +12 -4
package/dist/software-helper.js
CHANGED
|
@@ -4,22 +4,30 @@ import {
|
|
|
4
4
|
accessSync,
|
|
5
5
|
chmodSync,
|
|
6
6
|
copyFileSync,
|
|
7
|
+
createReadStream,
|
|
7
8
|
existsSync,
|
|
8
9
|
mkdtempSync,
|
|
9
10
|
mkdirSync,
|
|
10
11
|
readFileSync,
|
|
11
12
|
renameSync,
|
|
12
13
|
rmSync,
|
|
14
|
+
statSync,
|
|
13
15
|
symlinkSync,
|
|
14
16
|
unlinkSync,
|
|
15
17
|
writeFileSync
|
|
16
18
|
} from "node:fs";
|
|
17
19
|
import { tmpdir } from "node:os";
|
|
18
|
-
import { join } from "node:path";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
19
21
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
20
22
|
var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
|
|
21
23
|
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
22
24
|
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
25
|
+
var NERDCTL_VERSION = "2.3.5";
|
|
26
|
+
var BUILDKIT_VERSION = "0.32.2";
|
|
27
|
+
var KATA_CONTAINERS_VERSION = "4.0.0";
|
|
28
|
+
var NERDCTL_SHA256 = "de3206aeb7cbd5f20f5fb1f55c1e3bf2db1be567812a8a3f5e65eba2488347ee";
|
|
29
|
+
var BUILDKIT_SHA256 = "2975d0f651ad96ba8b80b9992ae1f9a964f4408569af5b6dc36544165c3926af";
|
|
30
|
+
var KATA_SHA256 = "2c3b9dfeba355582b40aee462b12916c9740654d0230f696adf719d67b063a8c";
|
|
23
31
|
var OS_CATALOG = [
|
|
24
32
|
{
|
|
25
33
|
id: "ubuntu",
|
|
@@ -49,6 +57,8 @@ var OS_CATALOG = [
|
|
|
49
57
|
var SOFTWARE_CATALOG = [
|
|
50
58
|
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
51
59
|
{ id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
60
|
+
{ id: "containerd", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
61
|
+
{ id: "kata-containers", version: "4.0.0", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
52
62
|
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
53
63
|
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
54
64
|
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -57,6 +67,7 @@ var SOFTWARE_CATALOG = [
|
|
|
57
67
|
{ id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
58
68
|
{ id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
59
69
|
{ id: "docker", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
70
|
+
{ id: "containerd", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
60
71
|
{ id: "nginx", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
61
72
|
{ id: "ufw", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
62
73
|
{ id: "openssh-client", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -65,6 +76,8 @@ var SOFTWARE_CATALOG = [
|
|
|
65
76
|
var UBUNTU_2604_X64 = [
|
|
66
77
|
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
67
78
|
{ requirement: { id: "docker", version: "ubuntu-26.04" } },
|
|
79
|
+
{ requirement: { id: "containerd", version: "ubuntu-26.04" } },
|
|
80
|
+
{ requirement: { id: "kata-containers", version: "4.0.0" } },
|
|
68
81
|
{ requirement: { id: "nginx", version: "ubuntu-26.04" } },
|
|
69
82
|
{ requirement: { id: "arangodb", version: "3.11.14" } },
|
|
70
83
|
{ requirement: { id: "cloudflared", version: "2026.7.3" } },
|
|
@@ -76,6 +89,7 @@ var UBUNTU_2604_X64 = [
|
|
|
76
89
|
var UBUNTU_2404_X64 = [
|
|
77
90
|
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
78
91
|
{ requirement: { id: "docker", version: "ubuntu-24.04" } },
|
|
92
|
+
{ requirement: { id: "containerd", version: "ubuntu-24.04" } },
|
|
79
93
|
{ requirement: { id: "nginx", version: "ubuntu-24.04" } },
|
|
80
94
|
{ requirement: { id: "ufw", version: "ubuntu-24.04" } },
|
|
81
95
|
{ requirement: { id: "openssh-client", version: "ubuntu-24.04" } },
|
|
@@ -92,6 +106,46 @@ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
|
92
106
|
"log-opts": { "max-size": "10m", "max-file": "3" }
|
|
93
107
|
}, null, 2)}
|
|
94
108
|
`;
|
|
109
|
+
var CONTAINERD_CONFIG_PATH = "/etc/containerd/config.toml";
|
|
110
|
+
var KATA_SNP_CONFIG_PATH = "/opt/kata/share/defaults/kata-containers/configuration-qemu-snp.toml";
|
|
111
|
+
var KATA_ACTIVE_CONFIG_PATH = "/etc/kata-containers/configuration.toml";
|
|
112
|
+
var CONTAINERD_CONFIG = (kata) => `version = 2
|
|
113
|
+
root = "/var/lib/forgezero/containerd"
|
|
114
|
+
state = "/run/forgezero/containerd"
|
|
115
|
+
|
|
116
|
+
[grpc]
|
|
117
|
+
address = "/run/containerd/containerd.sock"
|
|
118
|
+
|
|
119
|
+
[plugins."io.containerd.grpc.v1.cri".containerd]
|
|
120
|
+
snapshotter = "overlayfs"
|
|
121
|
+
default_runtime_name = "runc"
|
|
122
|
+
|
|
123
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
|
|
124
|
+
runtime_type = "io.containerd.runc.v2"
|
|
125
|
+
${kata ? `
|
|
126
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-qemu-snp]
|
|
127
|
+
runtime_type = "io.containerd.kata.v2"
|
|
128
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-qemu-snp.options]
|
|
129
|
+
ConfigPath = "${KATA_ACTIVE_CONFIG_PATH}"
|
|
130
|
+
` : ""}`;
|
|
131
|
+
var BUILDKIT_UNIT = `[Unit]
|
|
132
|
+
Description=ForgeZero BuildKit daemon
|
|
133
|
+
After=network-online.target containerd.service
|
|
134
|
+
Wants=network-online.target
|
|
135
|
+
Requires=containerd.service
|
|
136
|
+
|
|
137
|
+
[Service]
|
|
138
|
+
Type=notify
|
|
139
|
+
ExecStart=/usr/local/bin/buildkitd --addr unix:///run/buildkit/buildkitd.sock --root /var/lib/forgezero/buildkit
|
|
140
|
+
Restart=on-failure
|
|
141
|
+
RestartSec=2
|
|
142
|
+
NoNewPrivileges=yes
|
|
143
|
+
ProtectSystem=strict
|
|
144
|
+
ReadWritePaths=/var/lib/forgezero/buildkit /run/buildkit
|
|
145
|
+
|
|
146
|
+
[Install]
|
|
147
|
+
WantedBy=multi-user.target
|
|
148
|
+
`;
|
|
95
149
|
var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
|
|
96
150
|
var runSoftwareCommand = async (argv, env = {}) => {
|
|
97
151
|
let child;
|
|
@@ -111,14 +165,28 @@ var runSoftwareCommand = async (argv, env = {}) => {
|
|
|
111
165
|
return { exitCode, output: `${stdout}${stderr}` };
|
|
112
166
|
};
|
|
113
167
|
var run = runSoftwareCommand;
|
|
114
|
-
var download = async (url, destination, sha256) => {
|
|
115
|
-
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(
|
|
168
|
+
var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
|
|
169
|
+
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30 * 60000) });
|
|
116
170
|
if (!response.ok)
|
|
117
171
|
throw new Error(`download failed with HTTP ${response.status}`);
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
172
|
+
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
173
|
+
if (declared > maximumBytes)
|
|
174
|
+
throw new Error("download exceeds reviewed size bound");
|
|
175
|
+
if (existsSync(destination))
|
|
176
|
+
throw new Error("download destination already exists");
|
|
177
|
+
await Bun.write(destination, response);
|
|
178
|
+
chmodSync(destination, 384);
|
|
179
|
+
if (statSync(destination).size > maximumBytes) {
|
|
180
|
+
rmSync(destination, { force: true });
|
|
181
|
+
throw new Error("download exceeds reviewed size bound");
|
|
182
|
+
}
|
|
183
|
+
const digest = createHash("sha256");
|
|
184
|
+
for await (const chunk of createReadStream(destination))
|
|
185
|
+
digest.update(chunk);
|
|
186
|
+
if (digest.digest("hex") !== sha256) {
|
|
187
|
+
rmSync(destination, { force: true });
|
|
120
188
|
throw new Error("download checksum mismatch");
|
|
121
|
-
|
|
189
|
+
}
|
|
122
190
|
};
|
|
123
191
|
var successful = (result, pattern) => result.exitCode === 0 && (!pattern || pattern.test(result.output));
|
|
124
192
|
var aptInstall = async (name) => {
|
|
@@ -126,6 +194,106 @@ var aptInstall = async (name) => {
|
|
|
126
194
|
const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
|
|
127
195
|
return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", name], environment) : update;
|
|
128
196
|
};
|
|
197
|
+
var aptInstallMany = async (names) => {
|
|
198
|
+
const environment = { DEBIAN_FRONTEND: "noninteractive" };
|
|
199
|
+
const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
|
|
200
|
+
return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", ...names], environment) : update;
|
|
201
|
+
};
|
|
202
|
+
var writeOwnedPolicy = (pathValue, content, mode = 420) => {
|
|
203
|
+
if (existsSync(pathValue)) {
|
|
204
|
+
if (readFileSync(pathValue, "utf8") !== content)
|
|
205
|
+
throw new Error(`refusing to overwrite non-ForgeZero policy at ${pathValue}`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
mkdirSync(dirname(pathValue), { recursive: true, mode: 493 });
|
|
209
|
+
writeFileSync(pathValue, content, { mode, flag: "wx" });
|
|
210
|
+
};
|
|
211
|
+
var installContainerdRuntime = async (directory, osVersion) => {
|
|
212
|
+
const installed = await aptInstallMany(["containerd", "runc", "containernetworking-plugins"]);
|
|
213
|
+
if (installed.exitCode !== 0)
|
|
214
|
+
return installed;
|
|
215
|
+
const nerdctl = join(directory, "nerdctl.tar.gz");
|
|
216
|
+
await download(`https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz`, nerdctl, NERDCTL_SHA256);
|
|
217
|
+
const nerdctlExtract = await run(["/usr/bin/tar", "-xzf", nerdctl, "-C", "/usr/local/bin", "nerdctl"]);
|
|
218
|
+
if (nerdctlExtract.exitCode !== 0)
|
|
219
|
+
return nerdctlExtract;
|
|
220
|
+
chmodSync("/usr/local/bin/nerdctl", 493);
|
|
221
|
+
const buildkit = join(directory, "buildkit.tar.gz");
|
|
222
|
+
await download(`https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-amd64.tar.gz`, buildkit, BUILDKIT_SHA256);
|
|
223
|
+
const unpacked = join(directory, "buildkit");
|
|
224
|
+
mkdirSync(unpacked, { mode: 448 });
|
|
225
|
+
const buildkitExtract = await run(["/usr/bin/tar", "-xzf", buildkit, "-C", unpacked]);
|
|
226
|
+
if (buildkitExtract.exitCode !== 0)
|
|
227
|
+
return buildkitExtract;
|
|
228
|
+
for (const binary of ["buildctl", "buildkitd"]) {
|
|
229
|
+
copyFileSync(join(unpacked, "bin", binary), `/usr/local/bin/${binary}`);
|
|
230
|
+
chmodSync(`/usr/local/bin/${binary}`, 493);
|
|
231
|
+
}
|
|
232
|
+
mkdirSync("/etc/containerd", { recursive: true, mode: 493 });
|
|
233
|
+
const expected = CONTAINERD_CONFIG(false);
|
|
234
|
+
const kataExpected = CONTAINERD_CONFIG(true);
|
|
235
|
+
if (existsSync(CONTAINERD_CONFIG_PATH) && ![expected, kataExpected].includes(readFileSync(CONTAINERD_CONFIG_PATH, "utf8"))) {
|
|
236
|
+
return { exitCode: 2, output: "refusing to overwrite an existing containerd configuration that differs from the reviewed ForgeZero policy" };
|
|
237
|
+
}
|
|
238
|
+
if (!existsSync(CONTAINERD_CONFIG_PATH))
|
|
239
|
+
writeFileSync(CONTAINERD_CONFIG_PATH, expected, { mode: 420, flag: "wx" });
|
|
240
|
+
writeOwnedPolicy("/etc/systemd/system/forgezero-buildkit.service", BUILDKIT_UNIT);
|
|
241
|
+
mkdirSync("/var/lib/forgezero/containerd", { recursive: true, mode: 448 });
|
|
242
|
+
mkdirSync("/var/lib/forgezero/buildkit", { recursive: true, mode: 448 });
|
|
243
|
+
const reload = await run(["/usr/bin/systemctl", "daemon-reload"]);
|
|
244
|
+
if (reload.exitCode !== 0)
|
|
245
|
+
return reload;
|
|
246
|
+
const enabled = await run(["/usr/bin/systemctl", "enable", "--now", "containerd.service", "forgezero-buildkit.service"]);
|
|
247
|
+
return enabled.exitCode === 0 ? { exitCode: 0, output: `containerd ${osVersion} with nerdctl ${NERDCTL_VERSION} and BuildKit ${BUILDKIT_VERSION}` } : enabled;
|
|
248
|
+
};
|
|
249
|
+
var installKataRuntime = async (directory) => {
|
|
250
|
+
for (const pathValue of ["/dev/kvm", "/dev/sev"])
|
|
251
|
+
if (!existsSync(pathValue))
|
|
252
|
+
return { exitCode: 2, output: `${pathValue} is required for Kata SEV-SNP` };
|
|
253
|
+
for (const parameter of ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"]) {
|
|
254
|
+
if (!existsSync(parameter) || !/^(1|Y)$/i.test(readFileSync(parameter, "utf8").trim()))
|
|
255
|
+
return { exitCode: 2, output: `${parameter} does not enable SEV-SNP` };
|
|
256
|
+
}
|
|
257
|
+
const dependencies = await aptInstallMany(["zstd"]);
|
|
258
|
+
if (dependencies.exitCode !== 0)
|
|
259
|
+
return dependencies;
|
|
260
|
+
const archive = join(directory, "kata.tar.zst");
|
|
261
|
+
await download(`https://github.com/kata-containers/kata-containers/releases/download/${KATA_CONTAINERS_VERSION}/kata-static-${KATA_CONTAINERS_VERSION}-amd64.tar.zst`, archive, KATA_SHA256, 2200000000);
|
|
262
|
+
const tar = join(directory, "kata.tar");
|
|
263
|
+
const expanded = await run(["/usr/bin/unzstd", "--quiet", archive, "--output", tar]);
|
|
264
|
+
if (expanded.exitCode !== 0)
|
|
265
|
+
return expanded;
|
|
266
|
+
const extracted = await run(["/usr/bin/tar", "-xf", tar, "-C", "/"]);
|
|
267
|
+
if (extracted.exitCode !== 0)
|
|
268
|
+
return extracted;
|
|
269
|
+
if (!existsSync("/opt/kata/bin/containerd-shim-kata-v2") || !existsSync(KATA_SNP_CONFIG_PATH)) {
|
|
270
|
+
return { exitCode: 2, output: "Kata archive does not contain the reviewed QEMU SNP runtime and configuration" };
|
|
271
|
+
}
|
|
272
|
+
const snpConfig = readFileSync(KATA_SNP_CONFIG_PATH, "utf8");
|
|
273
|
+
if (!/^\s*confidential_guest\s*=\s*true\s*$/m.test(snpConfig) || !/^\s*sev_snp_guest\s*=\s*true\s*$/m.test(snpConfig)) {
|
|
274
|
+
return { exitCode: 2, output: "Kata QEMU configuration does not enable confidential SEV-SNP guests" };
|
|
275
|
+
}
|
|
276
|
+
mkdirSync("/etc/kata-containers", { recursive: true, mode: 493 });
|
|
277
|
+
if (existsSync(KATA_ACTIVE_CONFIG_PATH) && readFileSync(KATA_ACTIVE_CONFIG_PATH, "utf8") !== snpConfig) {
|
|
278
|
+
return { exitCode: 2, output: "refusing to overwrite a non-ForgeZero Kata runtime configuration" };
|
|
279
|
+
}
|
|
280
|
+
if (!existsSync(KATA_ACTIVE_CONFIG_PATH))
|
|
281
|
+
copyFileSync(KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH);
|
|
282
|
+
chmodSync(KATA_ACTIVE_CONFIG_PATH, 420);
|
|
283
|
+
try {
|
|
284
|
+
unlinkSync("/usr/local/bin/containerd-shim-kata-v2");
|
|
285
|
+
} catch {}
|
|
286
|
+
symlinkSync("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
|
|
287
|
+
const base = CONTAINERD_CONFIG(false);
|
|
288
|
+
const kata = CONTAINERD_CONFIG(true);
|
|
289
|
+
if (!existsSync(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync(CONTAINERD_CONFIG_PATH, "utf8"))) {
|
|
290
|
+
return { exitCode: 2, output: "containerd must use the reviewed ForgeZero policy before Kata is installed" };
|
|
291
|
+
}
|
|
292
|
+
if (readFileSync(CONTAINERD_CONFIG_PATH, "utf8") !== kata)
|
|
293
|
+
writeFileSync(CONTAINERD_CONFIG_PATH, kata, { mode: 420 });
|
|
294
|
+
const restarted = await run(["/usr/bin/systemctl", "restart", "containerd.service"]);
|
|
295
|
+
return restarted.exitCode === 0 ? { exitCode: 0, output: `Kata Containers ${KATA_CONTAINERS_VERSION} QEMU SNP` } : restarted;
|
|
296
|
+
};
|
|
129
297
|
async function executeSoftwareOperation(operation) {
|
|
130
298
|
const { software, version } = operation;
|
|
131
299
|
if (![...UBUNTU_2604_X64, ...UBUNTU_2404_X64].some(({ requirement }) => requirement.id === software && requirement.version === version)) {
|
|
@@ -143,6 +311,26 @@ async function executeSoftwareOperation(operation) {
|
|
|
143
311
|
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
|
|
144
312
|
return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
|
|
145
313
|
}
|
|
314
|
+
if (software === "containerd") {
|
|
315
|
+
const [containerd, nerdctl, buildkit, active, builder] = await Promise.all([
|
|
316
|
+
run(["/usr/bin/containerd", "--version"]),
|
|
317
|
+
run(["/usr/local/bin/nerdctl", "--version"]),
|
|
318
|
+
run(["/usr/local/bin/buildkitd", "--version"]),
|
|
319
|
+
run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]),
|
|
320
|
+
run(["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-buildkit.service"])
|
|
321
|
+
]);
|
|
322
|
+
const policy = existsSync(CONTAINERD_CONFIG_PATH) ? readFileSync(CONTAINERD_CONFIG_PATH, "utf8") : "";
|
|
323
|
+
const validPolicy = policy === CONTAINERD_CONFIG(false) || policy === CONTAINERD_CONFIG(true);
|
|
324
|
+
return successful(containerd, /containerd/) && successful(nerdctl, new RegExp(NERDCTL_VERSION.replaceAll(".", "\\."))) && successful(buildkit, new RegExp(BUILDKIT_VERSION.replaceAll(".", "\\."))) && active.exitCode === 0 && builder.exitCode === 0 && validPolicy ? { exitCode: 0, output: `${containerd.output}${nerdctl.output}${buildkit.output}` } : { exitCode: 1, output: "containerd, nerdctl, BuildKit, or ForgeZero storage/runtime policy is unavailable" };
|
|
325
|
+
}
|
|
326
|
+
if (software === "kata-containers") {
|
|
327
|
+
const shim = await run(["/opt/kata/bin/containerd-shim-kata-v2", "--version"]);
|
|
328
|
+
const policy = existsSync(CONTAINERD_CONFIG_PATH) ? readFileSync(CONTAINERD_CONFIG_PATH, "utf8") : "";
|
|
329
|
+
const activeConfig = existsSync(KATA_ACTIVE_CONFIG_PATH) ? readFileSync(KATA_ACTIVE_CONFIG_PATH, "utf8") : "";
|
|
330
|
+
const host = ["/dev/kvm", "/dev/sev", KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH].every(existsSync) && ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"].every((parameter) => existsSync(parameter) && /^(1|Y)$/i.test(readFileSync(parameter, "utf8").trim()));
|
|
331
|
+
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]);
|
|
332
|
+
return successful(shim, /kata.*4\.0\.0/i) && policy === CONTAINERD_CONFIG(true) && activeConfig === (existsSync(KATA_SNP_CONFIG_PATH) ? readFileSync(KATA_SNP_CONFIG_PATH, "utf8") : "") && host && active.exitCode === 0 ? { exitCode: 0, output: shim.output } : { exitCode: 1, output: "Kata QEMU SNP runtime, host capability, or containerd policy is unavailable" };
|
|
333
|
+
}
|
|
146
334
|
if (software === "nginx") {
|
|
147
335
|
const binary = await run(["/usr/sbin/nginx", "-v"]);
|
|
148
336
|
return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
|
|
@@ -194,6 +382,17 @@ async function executeSoftwareOperation(operation) {
|
|
|
194
382
|
}
|
|
195
383
|
const directory = mkdtempSync(join(tmpdir(), "forgezero-software-"));
|
|
196
384
|
try {
|
|
385
|
+
if (software === "containerd")
|
|
386
|
+
return installContainerdRuntime(directory, version === "ubuntu-24.04" ? "24.04" : "26.04");
|
|
387
|
+
if (software === "kata-containers") {
|
|
388
|
+
const containerd = await executeSoftwareOperation({ kind: "check", software: "containerd", version: "ubuntu-26.04" });
|
|
389
|
+
if (containerd.exitCode !== 0) {
|
|
390
|
+
const installed2 = await installContainerdRuntime(directory, "26.04");
|
|
391
|
+
if (installed2.exitCode !== 0)
|
|
392
|
+
return installed2;
|
|
393
|
+
}
|
|
394
|
+
return installKataRuntime(directory);
|
|
395
|
+
}
|
|
197
396
|
if (software === "cloudflare-warp") {
|
|
198
397
|
const key = join(directory, "cloudflare-warp-key.gpg");
|
|
199
398
|
await download("https://pkg.cloudflareclient.com/pubkey.gpg", key, "0f37fc298c98e88ee3c0ee68c95b69f1dba9eb477abe3167e13982105911264d");
|
|
@@ -275,7 +474,7 @@ function validateSoftwareRequirements(value, _options = {}) {
|
|
|
275
474
|
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
276
475
|
throw new Error("software requirement contains an unknown field");
|
|
277
476
|
}
|
|
278
|
-
if (!["bun", "docker", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client", "git"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
477
|
+
if (!["bun", "docker", "containerd", "kata-containers", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client", "git"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
279
478
|
throw new Error("software requirement coordinate is invalid");
|
|
280
479
|
}
|
|
281
480
|
const requirement = { id: row.id, version: row.version };
|
|
@@ -747,34 +946,180 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
|
|
|
747
946
|
};
|
|
748
947
|
}
|
|
749
948
|
|
|
949
|
+
// src/deployment-connectivity.ts
|
|
950
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
951
|
+
import { mkdirSync as mkdirSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
952
|
+
import { dirname as dirname2 } from "node:path";
|
|
953
|
+
var TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
|
|
954
|
+
var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
|
|
955
|
+
var HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
956
|
+
var NAME2 = /^[a-z][a-z0-9._-]{0,99}$/;
|
|
957
|
+
var idFor = (key) => createHash2("sha256").update(key).digest("hex").slice(0, 20);
|
|
958
|
+
var checked = async (host, argv2, label) => {
|
|
959
|
+
const result = await host.exec(argv2);
|
|
960
|
+
if (result.exitCode !== 0)
|
|
961
|
+
throw new Error(`${label} failed: ${result.output.slice(0, 512)}`);
|
|
962
|
+
return result.output.trim();
|
|
963
|
+
};
|
|
964
|
+
async function sealWithSystemd(name, path2, value) {
|
|
965
|
+
mkdirSync2(dirname2(path2), { recursive: true, mode: 448 });
|
|
966
|
+
const next = `${path2}.next`;
|
|
967
|
+
const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
|
|
968
|
+
stdin: "pipe",
|
|
969
|
+
stdout: "pipe",
|
|
970
|
+
stderr: "pipe",
|
|
971
|
+
env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" }
|
|
972
|
+
});
|
|
973
|
+
child.stdin.write(value);
|
|
974
|
+
child.stdin.end();
|
|
975
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
976
|
+
new Response(child.stdout).text(),
|
|
977
|
+
new Response(child.stderr).text(),
|
|
978
|
+
child.exited
|
|
979
|
+
]);
|
|
980
|
+
if (exitCode !== 0)
|
|
981
|
+
throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
|
|
982
|
+
renameSync2(next, path2);
|
|
983
|
+
}
|
|
984
|
+
var defaultHost = {
|
|
985
|
+
seal: sealWithSystemd,
|
|
986
|
+
write(path2, content, mode) {
|
|
987
|
+
mkdirSync2(dirname2(path2), { recursive: true, mode: 493 });
|
|
988
|
+
const next = `${path2}.next`;
|
|
989
|
+
writeFileSync2(next, content, { mode });
|
|
990
|
+
renameSync2(next, path2);
|
|
991
|
+
},
|
|
992
|
+
async exec(argv2) {
|
|
993
|
+
const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
|
|
994
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
995
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
996
|
+
},
|
|
997
|
+
sleep: (ms) => Bun.sleep(ms)
|
|
998
|
+
};
|
|
999
|
+
function validate(request) {
|
|
1000
|
+
if (!request.key || request.key.length > 256)
|
|
1001
|
+
throw new Error("deployment connectivity key is invalid");
|
|
1002
|
+
const publicIntent = request.intent.public;
|
|
1003
|
+
const privateIntent = request.intent.private;
|
|
1004
|
+
if (publicIntent?.mode === "cloudflare-tunnel") {
|
|
1005
|
+
if (!NAME2.test(publicIntent.credential) || !HOSTNAME.test(publicIntent.hostname) || !NAME2.test(publicIntent.tunnelKey) || publicIntent.service.protocol !== "http" || !Number.isInteger(publicIntent.service.port) || publicIntent.service.port < 1 || publicIntent.service.port > 65535 || !request.capabilities.public || !UUID.test(request.capabilities.public.tunnelId) || !TOKEN.test(request.capabilities.public.connectorToken)) {
|
|
1006
|
+
throw new Error("public deployment connectivity is malformed");
|
|
1007
|
+
}
|
|
1008
|
+
} else if (request.capabilities.public)
|
|
1009
|
+
throw new Error("unexpected public deployment capability");
|
|
1010
|
+
if (privateIntent?.mode === "cloudflare-warp") {
|
|
1011
|
+
if (!NAME2.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !TOKEN.test(request.capabilities.private.connectorToken)) {
|
|
1012
|
+
throw new Error("private deployment connectivity is malformed");
|
|
1013
|
+
}
|
|
1014
|
+
} else if (request.capabilities.private)
|
|
1015
|
+
throw new Error("unexpected private deployment capability");
|
|
1016
|
+
}
|
|
1017
|
+
async function applyDeploymentConnectivity(request, host = defaultHost) {
|
|
1018
|
+
validate(request);
|
|
1019
|
+
const id = idFor(request.key);
|
|
1020
|
+
const evidence = { key: request.key };
|
|
1021
|
+
if (request.intent.public?.mode === "cloudflare-tunnel") {
|
|
1022
|
+
const capability = request.capabilities.public;
|
|
1023
|
+
const credentialName = `FZ_TUNNEL_${id.toUpperCase()}`;
|
|
1024
|
+
const credentialPath = `/etc/forgezero/creds/deployment-tunnel-${id}.cred`;
|
|
1025
|
+
const unit = `forgezero-cloudflared-${id}.service`;
|
|
1026
|
+
const unitPath = `/etc/systemd/system/${unit}`;
|
|
1027
|
+
const metricsPort = 20000 + Number.parseInt(id.slice(0, 4), 16) % 20000;
|
|
1028
|
+
await host.seal(credentialName, credentialPath, capability.connectorToken);
|
|
1029
|
+
host.write(unitPath, `[Unit]
|
|
1030
|
+
Description=ForgeZero deployment Tunnel ${id}
|
|
1031
|
+
After=network-online.target
|
|
1032
|
+
Wants=network-online.target
|
|
1033
|
+
|
|
1034
|
+
[Service]
|
|
1035
|
+
Type=simple
|
|
1036
|
+
DynamicUser=yes
|
|
1037
|
+
LoadCredentialEncrypted=${credentialName}:${credentialPath}
|
|
1038
|
+
ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --metrics 127.0.0.1:${metricsPort} run --token-file %d/${credentialName}
|
|
1039
|
+
Restart=always
|
|
1040
|
+
RestartSec=5
|
|
1041
|
+
NoNewPrivileges=true
|
|
1042
|
+
PrivateTmp=true
|
|
1043
|
+
ProtectSystem=strict
|
|
1044
|
+
ProtectHome=true
|
|
1045
|
+
|
|
1046
|
+
[Install]
|
|
1047
|
+
WantedBy=multi-user.target
|
|
1048
|
+
`, 420);
|
|
1049
|
+
await checked(host, ["/usr/bin/systemctl", "daemon-reload"], "cloudflared daemon reload");
|
|
1050
|
+
await checked(host, ["/usr/bin/systemctl", "enable", "--now", unit], "cloudflared start");
|
|
1051
|
+
await checked(host, ["/usr/bin/systemctl", "is-active", "--quiet", unit], "cloudflared readiness");
|
|
1052
|
+
evidence.public = { hostname: request.intent.public.hostname, tunnelId: capability.tunnelId, unit, active: true };
|
|
1053
|
+
}
|
|
1054
|
+
if (request.intent.private?.mode === "cloudflare-warp") {
|
|
1055
|
+
const capability = request.capabilities.private;
|
|
1056
|
+
const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
|
|
1057
|
+
const unit = "forgezero-deployment-mesh.service";
|
|
1058
|
+
await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
|
|
1059
|
+
host.write(`/etc/systemd/system/${unit}`, `[Unit]
|
|
1060
|
+
Description=ForgeZero deployment Mesh/WARP connector
|
|
1061
|
+
Requires=warp-svc.service
|
|
1062
|
+
After=network-online.target warp-svc.service
|
|
1063
|
+
Wants=network-online.target
|
|
1064
|
+
|
|
1065
|
+
[Service]
|
|
1066
|
+
Type=oneshot
|
|
1067
|
+
RemainAfterExit=yes
|
|
1068
|
+
LoadCredentialEncrypted=CF_WARP_CONNECTOR_TOKEN:${credentialPath}
|
|
1069
|
+
ExecStart=/usr/local/bin/fz-agent mesh-config
|
|
1070
|
+
NoNewPrivileges=true
|
|
1071
|
+
PrivateTmp=true
|
|
1072
|
+
ProtectSystem=strict
|
|
1073
|
+
ProtectHome=true
|
|
1074
|
+
ReadWritePaths=/var/lib/cloudflare-warp
|
|
1075
|
+
|
|
1076
|
+
[Install]
|
|
1077
|
+
WantedBy=multi-user.target
|
|
1078
|
+
`, 420);
|
|
1079
|
+
await checked(host, ["/usr/bin/systemctl", "daemon-reload"], "WARP daemon reload");
|
|
1080
|
+
await checked(host, ["/usr/bin/systemctl", "enable", "--now", "warp-svc.service", unit], "WARP connector start");
|
|
1081
|
+
for (let attempt = 0;attempt < 10; attempt += 1) {
|
|
1082
|
+
const status = await host.exec(["/usr/bin/warp-cli", "--accept-tos", "status"]);
|
|
1083
|
+
if (status.exitCode === 0 && /\bconnected\b/i.test(status.output) && !/\bdisconnected\b/i.test(status.output)) {
|
|
1084
|
+
evidence.private = { network: request.intent.private.network, connectorId: capability.connectorId, unit, active: true };
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
await host.sleep(1000);
|
|
1088
|
+
}
|
|
1089
|
+
if (!evidence.private)
|
|
1090
|
+
throw new Error("WARP connector readiness failed");
|
|
1091
|
+
}
|
|
1092
|
+
return evidence;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
750
1095
|
// src/software-helper.ts
|
|
751
|
-
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as
|
|
1096
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync5, unlinkSync as unlinkSync2 } from "node:fs";
|
|
752
1097
|
import { connect, createServer } from "node:net";
|
|
753
|
-
import { dirname as
|
|
1098
|
+
import { dirname as dirname5 } from "node:path";
|
|
754
1099
|
|
|
755
1100
|
// src/service-supervisor.ts
|
|
756
|
-
import { createHash as
|
|
757
|
-
import { existsSync as existsSync2, mkdirSync as
|
|
758
|
-
import { dirname, join as join2, resolve, sep } from "node:path";
|
|
1101
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1102
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, readdirSync, realpathSync, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1103
|
+
import { dirname as dirname3, join as join2, resolve, sep } from "node:path";
|
|
759
1104
|
var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
|
|
760
1105
|
var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
|
|
761
1106
|
var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
|
|
762
1107
|
var SERVICE_NGINX_DIRECTORY = "/etc/nginx/conf.d";
|
|
763
|
-
var
|
|
1108
|
+
var idFor2 = (key) => createHash3("sha256").update(key).digest("hex").slice(0, 24);
|
|
764
1109
|
var statePath = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
|
|
765
1110
|
var within = (root, path2) => path2 === root || path2.startsWith(`${root}${sep}`);
|
|
766
|
-
var
|
|
1111
|
+
var defaultHost2 = {
|
|
767
1112
|
write(path2, content, mode) {
|
|
768
|
-
|
|
1113
|
+
mkdirSync3(dirname3(path2), { recursive: true, mode: 493 });
|
|
769
1114
|
const next = `${path2}.next`;
|
|
770
|
-
|
|
771
|
-
|
|
1115
|
+
writeFileSync3(next, content, { mode });
|
|
1116
|
+
renameSync3(next, path2);
|
|
772
1117
|
},
|
|
773
1118
|
read: (path2) => readFileSync2(path2, "utf8"),
|
|
774
1119
|
exists: existsSync2,
|
|
775
1120
|
list: (path2) => existsSync2(path2) ? readdirSync(path2) : [],
|
|
776
1121
|
realpath: realpathSync,
|
|
777
|
-
mkdir: (path2, mode) =>
|
|
1122
|
+
mkdir: (path2, mode) => mkdirSync3(path2, { recursive: true, mode }),
|
|
778
1123
|
remove: (path2) => rmSync2(path2, { force: true }),
|
|
779
1124
|
async exec(argv2) {
|
|
780
1125
|
const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: {
|
|
@@ -833,7 +1178,7 @@ function allocatedPorts(request, id, previous, states) {
|
|
|
833
1178
|
if (previous && previous.applicationPorts.length === required && previous.applicationPorts.every((port) => port >= allocation.from && port <= allocation.to && !occupied.has(port)))
|
|
834
1179
|
return [...previous.applicationPorts];
|
|
835
1180
|
const width = allocation.to - allocation.from + 1;
|
|
836
|
-
const start = Number.parseInt(
|
|
1181
|
+
const start = Number.parseInt(createHash3("sha256").update(request.key).digest("hex").slice(0, 8), 16) % width;
|
|
837
1182
|
for (let offset = 0;offset < width; offset += 1) {
|
|
838
1183
|
const first = allocation.from + (start + offset) % width;
|
|
839
1184
|
const candidate = Array.from({ length: required }, (_, index) => first + index);
|
|
@@ -891,12 +1236,12 @@ ${request.service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
|
|
|
891
1236
|
}
|
|
892
1237
|
}
|
|
893
1238
|
`;
|
|
894
|
-
async function
|
|
1239
|
+
async function checked2(host, argv2, code) {
|
|
895
1240
|
const result = await host.exec(argv2);
|
|
896
1241
|
if (result.exitCode !== 0)
|
|
897
1242
|
throw new Error(`${code}: ${result.output.slice(0, 512)}`);
|
|
898
1243
|
}
|
|
899
|
-
async function activateSupervisedService(request, host =
|
|
1244
|
+
async function activateSupervisedService(request, host = defaultHost2) {
|
|
900
1245
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/.test(request.key) || !/^[a-f0-9]{40}$/.test(request.revision)) {
|
|
901
1246
|
throw new Error("SERVICE_IDENTITY_INVALID");
|
|
902
1247
|
}
|
|
@@ -905,7 +1250,7 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
905
1250
|
if (!within(root, release))
|
|
906
1251
|
throw new Error("SERVICE_RELEASE_OUTSIDE_ROOT");
|
|
907
1252
|
request = { ...request, root, release, service: validateDeploymentService(request.service) };
|
|
908
|
-
const id =
|
|
1253
|
+
const id = idFor2(request.key);
|
|
909
1254
|
host.mkdir(SERVICE_STATE_DIRECTORY, 448);
|
|
910
1255
|
host.mkdir(SERVICE_CONFIG_DIRECTORY, 457);
|
|
911
1256
|
const previous = readState(host, statePath(id));
|
|
@@ -916,11 +1261,11 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
916
1261
|
const unitName = `forgezero-app-${id}-slot${nextSlot}.service`;
|
|
917
1262
|
host.write(configPath, appConfig({ ...request, release }, port), 292);
|
|
918
1263
|
host.write(`${SERVICE_UNIT_DIRECTORY}/${unitName}`, unit(id, nextSlot), 420);
|
|
919
|
-
await
|
|
1264
|
+
await checked2(host, ["/usr/bin/systemctl", "daemon-reload"], "SERVICE_UNIT_RELOAD_FAILED");
|
|
920
1265
|
if (request.service.strategy === "direct" && previous) {
|
|
921
1266
|
await host.exec(["/usr/bin/systemctl", "stop", `forgezero-app-${id}-slot${previous.activeSlot}.service`]);
|
|
922
1267
|
}
|
|
923
|
-
await
|
|
1268
|
+
await checked2(host, ["/usr/bin/systemctl", "restart", unitName], "SERVICE_START_FAILED");
|
|
924
1269
|
let healthy = false;
|
|
925
1270
|
for (let attempt = 0;attempt < 30; attempt += 1) {
|
|
926
1271
|
const active = await host.exec(["/usr/bin/systemctl", "is-active", "--quiet", unitName]);
|
|
@@ -938,8 +1283,8 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
938
1283
|
const oldNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
|
|
939
1284
|
try {
|
|
940
1285
|
host.write(nginxPath, nginx(request, id, port), 420);
|
|
941
|
-
await
|
|
942
|
-
await
|
|
1286
|
+
await checked2(host, ["/usr/sbin/nginx", "-t"], "SERVICE_NGINX_INVALID");
|
|
1287
|
+
await checked2(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "SERVICE_NGINX_RELOAD_FAILED");
|
|
943
1288
|
} catch (cause) {
|
|
944
1289
|
if (oldNginx === null)
|
|
945
1290
|
host.remove(nginxPath);
|
|
@@ -970,22 +1315,22 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
970
1315
|
}
|
|
971
1316
|
|
|
972
1317
|
// src/container-supervisor.ts
|
|
973
|
-
import { createHash as
|
|
974
|
-
import { existsSync as existsSync3, mkdirSync as
|
|
975
|
-
import { dirname as
|
|
976
|
-
var
|
|
1318
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1319
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync4, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1320
|
+
import { dirname as dirname4, resolve as resolve2, sep as sep2 } from "node:path";
|
|
1321
|
+
var defaultHost3 = {
|
|
977
1322
|
realpath: realpathSync2,
|
|
978
1323
|
exists: existsSync3,
|
|
979
1324
|
read: (path2) => readFileSync3(path2, "utf8"),
|
|
980
1325
|
write(path2, content, mode) {
|
|
981
|
-
|
|
1326
|
+
mkdirSync4(dirname4(path2), { recursive: true, mode: 493 });
|
|
982
1327
|
const next = `${path2}.next`;
|
|
983
|
-
|
|
984
|
-
|
|
1328
|
+
writeFileSync4(next, content, { mode });
|
|
1329
|
+
renameSync4(next, path2);
|
|
985
1330
|
},
|
|
986
1331
|
remove: (path2) => rmSync3(path2, { force: true }),
|
|
987
1332
|
list: (path2) => existsSync3(path2) ? readdirSync2(path2) : [],
|
|
988
|
-
mkdir: (path2, mode) =>
|
|
1333
|
+
mkdir: (path2, mode) => mkdirSync4(path2, { recursive: true, mode }),
|
|
989
1334
|
async exec(argv2) {
|
|
990
1335
|
const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
|
|
991
1336
|
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
@@ -1003,9 +1348,41 @@ var defaultHost2 = {
|
|
|
1003
1348
|
now: Date.now
|
|
1004
1349
|
};
|
|
1005
1350
|
var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
|
|
1006
|
-
var
|
|
1351
|
+
var idFor3 = (key) => createHash4("sha256").update(key).digest("hex").slice(0, 24);
|
|
1007
1352
|
var statePath2 = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
|
|
1008
1353
|
var nameFor = (id, slot) => `forgezero-${id}-slot${slot}`;
|
|
1354
|
+
function backendFor(application) {
|
|
1355
|
+
const runtime = application.runtime;
|
|
1356
|
+
if (runtime.kind !== "container")
|
|
1357
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1358
|
+
if (runtime.provider === "forgezero.docker") {
|
|
1359
|
+
if (runtime.runtimeClass && runtime.runtimeClass !== "runc")
|
|
1360
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
1361
|
+
return { engine: "docker", runtimeClass: "runc", prefix: ["/usr/bin/docker"] };
|
|
1362
|
+
}
|
|
1363
|
+
if (runtime.provider === "forgezero.containerd") {
|
|
1364
|
+
if ((runtime.runtimeClass ?? "runc") !== "runc")
|
|
1365
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
1366
|
+
return {
|
|
1367
|
+
engine: "containerd",
|
|
1368
|
+
runtimeClass: "runc",
|
|
1369
|
+
prefix: ["/usr/local/bin/nerdctl", "--namespace", "forgezero"],
|
|
1370
|
+
runtimeName: "io.containerd.runc.v2"
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
if (runtime.provider === "forgezero.kata") {
|
|
1374
|
+
if (runtime.runtimeClass !== "kata-qemu-snp")
|
|
1375
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
1376
|
+
return {
|
|
1377
|
+
engine: "containerd",
|
|
1378
|
+
runtimeClass: "kata-qemu-snp",
|
|
1379
|
+
prefix: ["/usr/local/bin/nerdctl", "--namespace", "forgezero"],
|
|
1380
|
+
runtimeName: "io.containerd.kata.v2"
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_UNSUPPORTED");
|
|
1384
|
+
}
|
|
1385
|
+
var command = (backend, ...argv2) => [...backend.prefix, ...argv2];
|
|
1009
1386
|
function validateRequest(request, host) {
|
|
1010
1387
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/.test(request.key) || !/^[a-f0-9]{40}$/.test(request.revision) || !/^[a-z][A-Za-z0-9-]{0,62}$/.test(request.component))
|
|
1011
1388
|
throw new Error("CONTAINER_IDENTITY_INVALID");
|
|
@@ -1016,6 +1393,7 @@ function validateRequest(request, host) {
|
|
|
1016
1393
|
const application = request.application;
|
|
1017
1394
|
if (application.kind !== "application" || application.runtime.kind !== "container")
|
|
1018
1395
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1396
|
+
backendFor(application);
|
|
1019
1397
|
if (!application.resources.cpu || !application.resources.memory || !application.resources.pids)
|
|
1020
1398
|
throw new Error("CONTAINER_LIMITS_REQUIRED");
|
|
1021
1399
|
if (application.runtime.security.privileged !== false || application.runtime.security.noNewPrivileges !== true || !application.runtime.security.dropCapabilities.includes("ALL"))
|
|
@@ -1026,23 +1404,24 @@ function validateRequest(request, host) {
|
|
|
1026
1404
|
throw new Error("CONTAINER_STABLE_PORT_REQUIRED");
|
|
1027
1405
|
return { ...request, root, release };
|
|
1028
1406
|
}
|
|
1029
|
-
async function
|
|
1407
|
+
async function checked3(host, argv2, code) {
|
|
1030
1408
|
const result = await host.exec(argv2);
|
|
1031
1409
|
if (result.exitCode !== 0)
|
|
1032
1410
|
throw new Error(`${code}: ${result.output.slice(0, 512)}`);
|
|
1033
1411
|
return result.output.trim();
|
|
1034
1412
|
}
|
|
1035
|
-
async function buildContainerImage(requestValue, host =
|
|
1413
|
+
async function buildContainerImage(requestValue, host = defaultHost3) {
|
|
1036
1414
|
const request = validateRequest(requestValue, host);
|
|
1037
1415
|
const runtime = request.application.runtime;
|
|
1038
1416
|
if (runtime.kind !== "container")
|
|
1039
1417
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1418
|
+
const backend = backendFor(request.application);
|
|
1040
1419
|
const source = runtime.image.source;
|
|
1041
1420
|
if (source.kind === "registry") {
|
|
1042
1421
|
if (!source.reference.includes("@sha256:"))
|
|
1043
1422
|
throw new Error("CONTAINER_IMAGE_DIGEST_REQUIRED");
|
|
1044
|
-
await
|
|
1045
|
-
const digest2 = await
|
|
1423
|
+
await checked3(host, command(backend, "pull", "--quiet", source.reference), "CONTAINER_PULL_FAILED");
|
|
1424
|
+
const digest2 = await checked3(host, command(backend, "image", "inspect", "--format={{.Id}}", source.reference), "CONTAINER_INSPECT_FAILED");
|
|
1046
1425
|
if (!/^sha256:[a-f0-9]{64}$/.test(digest2))
|
|
1047
1426
|
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
1048
1427
|
return { image: source.reference, digest: digest2 };
|
|
@@ -1051,23 +1430,9 @@ async function buildContainerImage(requestValue, host = defaultHost2) {
|
|
|
1051
1430
|
const dockerfile = host.realpath(resolve2(request.release, source.dockerfile));
|
|
1052
1431
|
if (!within2(request.release, context) || !within2(context, dockerfile))
|
|
1053
1432
|
throw new Error("CONTAINER_BUILD_PATH_INVALID");
|
|
1054
|
-
const tag = `forgezero/${
|
|
1055
|
-
await
|
|
1056
|
-
|
|
1057
|
-
"build",
|
|
1058
|
-
"--pull",
|
|
1059
|
-
...request.noCache ? ["--no-cache"] : [],
|
|
1060
|
-
"--file",
|
|
1061
|
-
dockerfile,
|
|
1062
|
-
"--tag",
|
|
1063
|
-
tag,
|
|
1064
|
-
"--label",
|
|
1065
|
-
`net.forgezero.revision=${request.revision}`,
|
|
1066
|
-
"--label",
|
|
1067
|
-
`net.forgezero.component=${request.component}`,
|
|
1068
|
-
context
|
|
1069
|
-
], "CONTAINER_BUILD_FAILED");
|
|
1070
|
-
const digest = await checked2(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", tag], "CONTAINER_INSPECT_FAILED");
|
|
1433
|
+
const tag = `forgezero/${idFor3(request.key)}:${request.revision}`;
|
|
1434
|
+
await checked3(host, command(backend, "build", "--pull", ...request.noCache ? ["--no-cache"] : [], "--file", dockerfile, "--tag", tag, "--label", `net.forgezero.revision=${request.revision}`, "--label", `net.forgezero.component=${request.component}`, context), "CONTAINER_BUILD_FAILED");
|
|
1435
|
+
const digest = await checked3(host, command(backend, "image", "inspect", "--format={{.Id}}", tag), "CONTAINER_INSPECT_FAILED");
|
|
1071
1436
|
if (!/^sha256:[a-f0-9]{64}$/.test(digest))
|
|
1072
1437
|
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
1073
1438
|
return { image: tag, digest };
|
|
@@ -1135,7 +1500,7 @@ ${service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
|
|
|
1135
1500
|
}
|
|
1136
1501
|
`;
|
|
1137
1502
|
}
|
|
1138
|
-
function runArgv(request, id, slot, port, networkName) {
|
|
1503
|
+
function runArgv(request, backend, id, slot, port, networkName) {
|
|
1139
1504
|
const { application } = request;
|
|
1140
1505
|
const resources = application.resources;
|
|
1141
1506
|
const runtime = application.runtime;
|
|
@@ -1143,35 +1508,7 @@ function runArgv(request, id, slot, port, networkName) {
|
|
|
1143
1508
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1144
1509
|
const memory = resources.memory;
|
|
1145
1510
|
const network = application.network.container;
|
|
1146
|
-
const argv2 = [
|
|
1147
|
-
"/usr/bin/docker",
|
|
1148
|
-
"run",
|
|
1149
|
-
"--detach",
|
|
1150
|
-
"--name",
|
|
1151
|
-
nameFor(id, slot),
|
|
1152
|
-
"--restart",
|
|
1153
|
-
"unless-stopped",
|
|
1154
|
-
"--label",
|
|
1155
|
-
`net.forgezero.key=${request.key}`,
|
|
1156
|
-
"--label",
|
|
1157
|
-
`net.forgezero.revision=${request.revision}`,
|
|
1158
|
-
"--cpus",
|
|
1159
|
-
String(resources.cpu.limit),
|
|
1160
|
-
"--cpu-shares",
|
|
1161
|
-
String(resources.cpu.weight ?? 100),
|
|
1162
|
-
"--memory",
|
|
1163
|
-
`${memory.limitMiB}m`,
|
|
1164
|
-
...memory.reservationMiB ? ["--memory-reservation", `${memory.reservationMiB}m`] : [],
|
|
1165
|
-
...memory.swap === "disabled" ? ["--memory-swap", `${memory.limitMiB}m`] : [],
|
|
1166
|
-
"--pids-limit",
|
|
1167
|
-
String(resources.pids.limit),
|
|
1168
|
-
"--security-opt",
|
|
1169
|
-
"no-new-privileges=true",
|
|
1170
|
-
"--cap-drop",
|
|
1171
|
-
"ALL",
|
|
1172
|
-
...runtime.security.root === "read-only" ? ["--read-only"] : [],
|
|
1173
|
-
...network.mode === "host" ? ["--network", "host", "--env", `FZ_APP_PORT=${port}`] : ["--network", networkName, "--publish", `127.0.0.1:${port}:${application.service.port}`]
|
|
1174
|
-
];
|
|
1511
|
+
const argv2 = command(backend, "run", "--detach", "--name", nameFor(id, slot), "--restart", "unless-stopped", "--label", `net.forgezero.key=${request.key}`, "--label", `net.forgezero.revision=${request.revision}`, ...backend.runtimeName ? ["--runtime", backend.runtimeName] : [], "--cpus", String(resources.cpu.limit), "--cpu-shares", String(resources.cpu.weight ?? 100), "--memory", `${memory.limitMiB}m`, ...memory.reservationMiB ? ["--memory-reservation", `${memory.reservationMiB}m`] : [], ...memory.swap === "disabled" ? ["--memory-swap", `${memory.limitMiB}m`] : [], "--pids-limit", String(resources.pids.limit), "--security-opt", "no-new-privileges=true", "--cap-drop", "ALL", ...runtime.security.root === "read-only" ? ["--read-only"] : [], ...network.mode === "host" ? ["--network", "host", "--env", `FZ_APP_PORT=${port}`] : ["--network", networkName, "--publish", `127.0.0.1:${port}:${application.service.port}`]);
|
|
1175
1512
|
for (const mount of application.storage ?? []) {
|
|
1176
1513
|
if (mount.class === "ephemeral")
|
|
1177
1514
|
argv2.push("--tmpfs", `${mount.path}:rw,noexec,nosuid,size=${mount.sizeMiB}m`);
|
|
@@ -1181,11 +1518,12 @@ function runArgv(request, id, slot, port, networkName) {
|
|
|
1181
1518
|
argv2.push(request.image, ...runtime.entrypoint ?? []);
|
|
1182
1519
|
return argv2;
|
|
1183
1520
|
}
|
|
1184
|
-
async function activateContainer(requestValue, host =
|
|
1521
|
+
async function activateContainer(requestValue, host = defaultHost3) {
|
|
1185
1522
|
const request = validateRequest(requestValue, host);
|
|
1186
1523
|
if (!/^sha256:[a-f0-9]{64}$/.test(request.image) && !/^forgezero\/[a-f0-9]{24}:[a-f0-9]{40}$/.test(request.image) && !request.image.includes("@sha256:"))
|
|
1187
1524
|
throw new Error("CONTAINER_IMAGE_INVALID");
|
|
1188
|
-
const
|
|
1525
|
+
const backend = backendFor(request.application);
|
|
1526
|
+
const id = idFor3(request.key);
|
|
1189
1527
|
host.mkdir(SERVICE_STATE_DIRECTORY, 448);
|
|
1190
1528
|
host.mkdir(SERVICE_NGINX_DIRECTORY, 493);
|
|
1191
1529
|
const previous = readState2(host, id);
|
|
@@ -1197,9 +1535,9 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1197
1535
|
if (containerNetwork.mode === "bridge") {
|
|
1198
1536
|
networkName = containerNetwork.network ? `forgezero-${id}-${containerNetwork.network}` : "bridge";
|
|
1199
1537
|
if (networkName !== "bridge") {
|
|
1200
|
-
const inspected = await host.exec(
|
|
1538
|
+
const inspected = await host.exec(command(backend, "network", "inspect", '--format={{index .Labels "net.forgezero.owner"}}', networkName));
|
|
1201
1539
|
if (inspected.exitCode !== 0) {
|
|
1202
|
-
await
|
|
1540
|
+
await checked3(host, command(backend, "network", "create", "--driver", "bridge", "--label", `net.forgezero.owner=${id}`, networkName), "CONTAINER_NETWORK_CREATE_FAILED");
|
|
1203
1541
|
} else if (inspected.output.trim() !== id)
|
|
1204
1542
|
throw new Error("CONTAINER_NETWORK_OWNERSHIP_INVALID");
|
|
1205
1543
|
}
|
|
@@ -1208,8 +1546,8 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1208
1546
|
const nextSlot = slots === 2 && previous?.activeSlot === 0 ? 1 : 0;
|
|
1209
1547
|
const port = ports[nextSlot];
|
|
1210
1548
|
const name = nameFor(id, nextSlot);
|
|
1211
|
-
await host.exec(
|
|
1212
|
-
await
|
|
1549
|
+
await host.exec(command(backend, "rm", "--force", name));
|
|
1550
|
+
await checked3(host, runArgv(request, backend, id, nextSlot, port, networkName), "CONTAINER_START_FAILED");
|
|
1213
1551
|
let healthy = false;
|
|
1214
1552
|
const health = request.application.service.health;
|
|
1215
1553
|
for (let attempt = 0;attempt < (health.attempts ?? 30); attempt += 1) {
|
|
@@ -1220,15 +1558,28 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1220
1558
|
await host.sleep(health.intervalMs ?? 1000);
|
|
1221
1559
|
}
|
|
1222
1560
|
if (!healthy) {
|
|
1223
|
-
await host.exec(
|
|
1561
|
+
await host.exec(command(backend, "rm", "--force", name));
|
|
1224
1562
|
throw new Error("CONTAINER_HEALTH_FAILED");
|
|
1225
1563
|
}
|
|
1564
|
+
let runtimeEvidence;
|
|
1565
|
+
if (backend.engine === "containerd") {
|
|
1566
|
+
const raw = await checked3(host, ["/usr/bin/ctr", "--namespace", "forgezero", "containers", "info", name], "CONTAINER_RUNTIME_EVIDENCE_FAILED");
|
|
1567
|
+
let observed = "";
|
|
1568
|
+
try {
|
|
1569
|
+
observed = String(JSON.parse(raw).Runtime?.Name ?? "");
|
|
1570
|
+
} catch {
|
|
1571
|
+
throw new Error("CONTAINER_RUNTIME_EVIDENCE_INVALID");
|
|
1572
|
+
}
|
|
1573
|
+
if (observed !== backend.runtimeName)
|
|
1574
|
+
throw new Error("CONTAINER_RUNTIME_DOWNGRADE");
|
|
1575
|
+
runtimeEvidence = observed;
|
|
1576
|
+
}
|
|
1226
1577
|
const nginxPath = `${SERVICE_NGINX_DIRECTORY}/forgezero-${id}.conf`;
|
|
1227
1578
|
const previousNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
|
|
1228
1579
|
host.write(nginxPath, nginx2(request, id, port), 420);
|
|
1229
1580
|
try {
|
|
1230
|
-
await
|
|
1231
|
-
await
|
|
1581
|
+
await checked3(host, ["/usr/sbin/nginx", "-t"], "CONTAINER_NGINX_INVALID");
|
|
1582
|
+
await checked3(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "CONTAINER_NGINX_RELOAD_FAILED");
|
|
1232
1583
|
} catch (cause) {
|
|
1233
1584
|
if (previousNginx === null)
|
|
1234
1585
|
host.remove(nginxPath);
|
|
@@ -1236,7 +1587,7 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1236
1587
|
host.write(nginxPath, previousNginx, 420);
|
|
1237
1588
|
await host.exec(["/usr/sbin/nginx", "-t"]);
|
|
1238
1589
|
await host.exec(["/usr/bin/systemctl", "reload", "nginx.service"]);
|
|
1239
|
-
await host.exec(
|
|
1590
|
+
await host.exec(command(backend, "rm", "--force", name));
|
|
1240
1591
|
throw cause;
|
|
1241
1592
|
}
|
|
1242
1593
|
const state = {
|
|
@@ -1249,13 +1600,18 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1249
1600
|
applicationPorts: ports,
|
|
1250
1601
|
activeSlot: nextSlot,
|
|
1251
1602
|
healthPath: health.path,
|
|
1603
|
+
containerRuntime: {
|
|
1604
|
+
engine: backend.engine,
|
|
1605
|
+
runtimeClass: backend.runtimeClass,
|
|
1606
|
+
...backend.engine === "containerd" ? { namespace: "forgezero", evidence: runtimeEvidence } : {}
|
|
1607
|
+
},
|
|
1252
1608
|
updatedAtTs: host.now()
|
|
1253
1609
|
};
|
|
1254
1610
|
host.write(statePath2(id), `${JSON.stringify(state, null, 2)}
|
|
1255
1611
|
`, 384);
|
|
1256
1612
|
if (previous && previous.activeSlot !== nextSlot) {
|
|
1257
1613
|
await host.sleep(request.application.rollout.drainMs ?? 30000);
|
|
1258
|
-
await host.exec(
|
|
1614
|
+
await host.exec(command(backend, "rm", "--force", nameFor(id, previous.activeSlot)));
|
|
1259
1615
|
}
|
|
1260
1616
|
return state;
|
|
1261
1617
|
}
|
|
@@ -1270,11 +1626,12 @@ function startSoftwareHelper(options = {}) {
|
|
|
1270
1626
|
const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
|
|
1271
1627
|
if (existsSync4(socketPath))
|
|
1272
1628
|
unlinkSync2(socketPath);
|
|
1273
|
-
|
|
1629
|
+
mkdirSync5(dirname5(socketPath), { recursive: true, mode: 488 });
|
|
1274
1630
|
const ensure = options.ensure ?? ensureSoftwareRequirements;
|
|
1275
1631
|
const activate = options.activate ?? activateSupervisedService;
|
|
1276
1632
|
const buildContainer = options.buildContainer ?? buildContainerImage;
|
|
1277
1633
|
const activateTypedContainer = options.activateContainer ?? activateContainer;
|
|
1634
|
+
const applyConnectivity = options.applyConnectivity ?? applyDeploymentConnectivity;
|
|
1278
1635
|
let tail = Promise.resolve();
|
|
1279
1636
|
let pending = 0;
|
|
1280
1637
|
const server = createServer((socket) => {
|
|
@@ -1331,6 +1688,12 @@ function startSoftwareHelper(options = {}) {
|
|
|
1331
1688
|
throw new Error("container deployment root is not owned by this helper");
|
|
1332
1689
|
const state = await activateTypedContainer(containerRequest);
|
|
1333
1690
|
socket.end(`${JSON.stringify({ ok: true, state })}
|
|
1691
|
+
`);
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
if (request.op === "apply-connectivity" && request.request) {
|
|
1695
|
+
const result = await applyConnectivity(request.request);
|
|
1696
|
+
socket.end(`${JSON.stringify({ ok: true, result })}
|
|
1334
1697
|
`);
|
|
1335
1698
|
return;
|
|
1336
1699
|
}
|
|
@@ -1388,6 +1751,9 @@ function requestContainerBuild(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOC
|
|
|
1388
1751
|
function requestContainerActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
1389
1752
|
return requestContainer("activate-container", request, socketPath, timeoutMs);
|
|
1390
1753
|
}
|
|
1754
|
+
function requestDeploymentConnectivity(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
1755
|
+
return requestContainer("apply-connectivity", request, socketPath, timeoutMs);
|
|
1756
|
+
}
|
|
1391
1757
|
function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
|
|
1392
1758
|
return new Promise((resolve3, reject) => {
|
|
1393
1759
|
const socket = connect(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
|
|
@@ -1449,6 +1815,7 @@ export {
|
|
|
1449
1815
|
startSoftwareHelper,
|
|
1450
1816
|
requestSoftware,
|
|
1451
1817
|
requestServiceActivation,
|
|
1818
|
+
requestDeploymentConnectivity,
|
|
1452
1819
|
requestContainerBuild,
|
|
1453
1820
|
requestContainerActivation,
|
|
1454
1821
|
SOFTWARE_HELPER_UNIT_PATH,
|