@forgezero/agent 0.1.74 → 0.1.76
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 +126 -17
- package/dist/agent-heartbeat.d.ts +25 -1
- package/dist/agent-heartbeat.js +45 -5
- package/dist/bootstrap.js +10 -7
- package/dist/community-rehearsal-host.js +218 -13
- 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 +155 -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 +1122 -313
- package/dist/fz.js +234 -26
- 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 +11 -9
- package/dist/platform-bootstrap-runtime.js +209 -10
- package/dist/platform-fleet-verification.js +525 -155
- package/dist/platform-genesis.js +206 -7
- package/dist/process-input.d.ts +11 -0
- package/dist/provision.js +472 -99
- package/dist/service-supervisor.d.ts +6 -0
- package/dist/software-helper.d.ts +3 -0
- package/dist/software-helper.js +472 -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,187 @@ 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
|
+
|
|
954
|
+
// src/process-input.ts
|
|
955
|
+
async function writeAndCloseProcessInput(input, value) {
|
|
956
|
+
input.write(value);
|
|
957
|
+
await input.end();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/deployment-connectivity.ts
|
|
961
|
+
var TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
|
|
962
|
+
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;
|
|
963
|
+
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])?$/;
|
|
964
|
+
var NAME2 = /^[a-z][a-z0-9._-]{0,99}$/;
|
|
965
|
+
var idFor = (key) => createHash2("sha256").update(key).digest("hex").slice(0, 20);
|
|
966
|
+
var checked = async (host, argv2, label) => {
|
|
967
|
+
const result = await host.exec(argv2);
|
|
968
|
+
if (result.exitCode !== 0)
|
|
969
|
+
throw new Error(`${label} failed: ${result.output.slice(0, 512)}`);
|
|
970
|
+
return result.output.trim();
|
|
971
|
+
};
|
|
972
|
+
async function sealWithSystemd(name, path2, value) {
|
|
973
|
+
mkdirSync2(dirname2(path2), { recursive: true, mode: 448 });
|
|
974
|
+
const next = `${path2}.next`;
|
|
975
|
+
const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
|
|
976
|
+
stdin: "pipe",
|
|
977
|
+
stdout: "pipe",
|
|
978
|
+
stderr: "pipe",
|
|
979
|
+
env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" }
|
|
980
|
+
});
|
|
981
|
+
await writeAndCloseProcessInput(child.stdin, value);
|
|
982
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
983
|
+
new Response(child.stdout).text(),
|
|
984
|
+
new Response(child.stderr).text(),
|
|
985
|
+
child.exited
|
|
986
|
+
]);
|
|
987
|
+
if (exitCode !== 0)
|
|
988
|
+
throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
|
|
989
|
+
renameSync2(next, path2);
|
|
990
|
+
}
|
|
991
|
+
var defaultHost = {
|
|
992
|
+
seal: sealWithSystemd,
|
|
993
|
+
write(path2, content, mode) {
|
|
994
|
+
mkdirSync2(dirname2(path2), { recursive: true, mode: 493 });
|
|
995
|
+
const next = `${path2}.next`;
|
|
996
|
+
writeFileSync2(next, content, { mode });
|
|
997
|
+
renameSync2(next, path2);
|
|
998
|
+
},
|
|
999
|
+
async exec(argv2) {
|
|
1000
|
+
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" } });
|
|
1001
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
1002
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
1003
|
+
},
|
|
1004
|
+
sleep: (ms) => Bun.sleep(ms)
|
|
1005
|
+
};
|
|
1006
|
+
function validate(request) {
|
|
1007
|
+
if (!request.key || request.key.length > 256)
|
|
1008
|
+
throw new Error("deployment connectivity key is invalid");
|
|
1009
|
+
const publicIntent = request.intent.public;
|
|
1010
|
+
const privateIntent = request.intent.private;
|
|
1011
|
+
if (publicIntent?.mode === "cloudflare-tunnel") {
|
|
1012
|
+
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)) {
|
|
1013
|
+
throw new Error("public deployment connectivity is malformed");
|
|
1014
|
+
}
|
|
1015
|
+
} else if (request.capabilities.public)
|
|
1016
|
+
throw new Error("unexpected public deployment capability");
|
|
1017
|
+
if (privateIntent?.mode === "cloudflare-warp") {
|
|
1018
|
+
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)) {
|
|
1019
|
+
throw new Error("private deployment connectivity is malformed");
|
|
1020
|
+
}
|
|
1021
|
+
} else if (request.capabilities.private)
|
|
1022
|
+
throw new Error("unexpected private deployment capability");
|
|
1023
|
+
}
|
|
1024
|
+
async function applyDeploymentConnectivity(request, host = defaultHost) {
|
|
1025
|
+
validate(request);
|
|
1026
|
+
const id = idFor(request.key);
|
|
1027
|
+
const evidence = { key: request.key };
|
|
1028
|
+
if (request.intent.public?.mode === "cloudflare-tunnel") {
|
|
1029
|
+
const capability = request.capabilities.public;
|
|
1030
|
+
const credentialName = `FZ_TUNNEL_${id.toUpperCase()}`;
|
|
1031
|
+
const credentialPath = `/etc/forgezero/creds/deployment-tunnel-${id}.cred`;
|
|
1032
|
+
const unit = `forgezero-cloudflared-${id}.service`;
|
|
1033
|
+
const unitPath = `/etc/systemd/system/${unit}`;
|
|
1034
|
+
const metricsPort = 20000 + Number.parseInt(id.slice(0, 4), 16) % 20000;
|
|
1035
|
+
await host.seal(credentialName, credentialPath, capability.connectorToken);
|
|
1036
|
+
host.write(unitPath, `[Unit]
|
|
1037
|
+
Description=ForgeZero deployment Tunnel ${id}
|
|
1038
|
+
After=network-online.target
|
|
1039
|
+
Wants=network-online.target
|
|
1040
|
+
|
|
1041
|
+
[Service]
|
|
1042
|
+
Type=simple
|
|
1043
|
+
DynamicUser=yes
|
|
1044
|
+
LoadCredentialEncrypted=${credentialName}:${credentialPath}
|
|
1045
|
+
ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --metrics 127.0.0.1:${metricsPort} run --token-file %d/${credentialName}
|
|
1046
|
+
Restart=always
|
|
1047
|
+
RestartSec=5
|
|
1048
|
+
NoNewPrivileges=true
|
|
1049
|
+
PrivateTmp=true
|
|
1050
|
+
ProtectSystem=strict
|
|
1051
|
+
ProtectHome=true
|
|
1052
|
+
|
|
1053
|
+
[Install]
|
|
1054
|
+
WantedBy=multi-user.target
|
|
1055
|
+
`, 420);
|
|
1056
|
+
await checked(host, ["/usr/bin/systemctl", "daemon-reload"], "cloudflared daemon reload");
|
|
1057
|
+
await checked(host, ["/usr/bin/systemctl", "enable", "--now", unit], "cloudflared start");
|
|
1058
|
+
await checked(host, ["/usr/bin/systemctl", "is-active", "--quiet", unit], "cloudflared readiness");
|
|
1059
|
+
evidence.public = { hostname: request.intent.public.hostname, tunnelId: capability.tunnelId, unit, active: true };
|
|
1060
|
+
}
|
|
1061
|
+
if (request.intent.private?.mode === "cloudflare-warp") {
|
|
1062
|
+
const capability = request.capabilities.private;
|
|
1063
|
+
const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
|
|
1064
|
+
const unit = "forgezero-deployment-mesh.service";
|
|
1065
|
+
await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
|
|
1066
|
+
host.write(`/etc/systemd/system/${unit}`, `[Unit]
|
|
1067
|
+
Description=ForgeZero deployment Mesh/WARP connector
|
|
1068
|
+
Requires=warp-svc.service
|
|
1069
|
+
After=network-online.target warp-svc.service
|
|
1070
|
+
Wants=network-online.target
|
|
1071
|
+
|
|
1072
|
+
[Service]
|
|
1073
|
+
Type=oneshot
|
|
1074
|
+
RemainAfterExit=yes
|
|
1075
|
+
LoadCredentialEncrypted=CF_WARP_CONNECTOR_TOKEN:${credentialPath}
|
|
1076
|
+
ExecStart=/usr/local/bin/fz-agent mesh-config
|
|
1077
|
+
NoNewPrivileges=true
|
|
1078
|
+
PrivateTmp=true
|
|
1079
|
+
ProtectSystem=strict
|
|
1080
|
+
ProtectHome=true
|
|
1081
|
+
ReadWritePaths=/var/lib/cloudflare-warp
|
|
1082
|
+
|
|
1083
|
+
[Install]
|
|
1084
|
+
WantedBy=multi-user.target
|
|
1085
|
+
`, 420);
|
|
1086
|
+
await checked(host, ["/usr/bin/systemctl", "daemon-reload"], "WARP daemon reload");
|
|
1087
|
+
await checked(host, ["/usr/bin/systemctl", "enable", "--now", "warp-svc.service", unit], "WARP connector start");
|
|
1088
|
+
for (let attempt = 0;attempt < 10; attempt += 1) {
|
|
1089
|
+
const status = await host.exec(["/usr/bin/warp-cli", "--accept-tos", "status"]);
|
|
1090
|
+
if (status.exitCode === 0 && /\bconnected\b/i.test(status.output) && !/\bdisconnected\b/i.test(status.output)) {
|
|
1091
|
+
evidence.private = { network: request.intent.private.network, connectorId: capability.connectorId, unit, active: true };
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
1094
|
+
await host.sleep(1000);
|
|
1095
|
+
}
|
|
1096
|
+
if (!evidence.private)
|
|
1097
|
+
throw new Error("WARP connector readiness failed");
|
|
1098
|
+
}
|
|
1099
|
+
return evidence;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
750
1102
|
// src/software-helper.ts
|
|
751
|
-
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as
|
|
1103
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync5, unlinkSync as unlinkSync2 } from "node:fs";
|
|
752
1104
|
import { connect, createServer } from "node:net";
|
|
753
|
-
import { dirname as
|
|
1105
|
+
import { dirname as dirname5 } from "node:path";
|
|
754
1106
|
|
|
755
1107
|
// 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";
|
|
1108
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1109
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, readdirSync, realpathSync, renameSync as renameSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1110
|
+
import { dirname as dirname3, join as join2, resolve, sep } from "node:path";
|
|
759
1111
|
var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
|
|
760
1112
|
var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
|
|
761
1113
|
var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
|
|
762
1114
|
var SERVICE_NGINX_DIRECTORY = "/etc/nginx/conf.d";
|
|
763
|
-
var
|
|
1115
|
+
var idFor2 = (key) => createHash3("sha256").update(key).digest("hex").slice(0, 24);
|
|
764
1116
|
var statePath = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
|
|
765
1117
|
var within = (root, path2) => path2 === root || path2.startsWith(`${root}${sep}`);
|
|
766
|
-
var
|
|
1118
|
+
var defaultHost2 = {
|
|
767
1119
|
write(path2, content, mode) {
|
|
768
|
-
|
|
1120
|
+
mkdirSync3(dirname3(path2), { recursive: true, mode: 493 });
|
|
769
1121
|
const next = `${path2}.next`;
|
|
770
|
-
|
|
771
|
-
|
|
1122
|
+
writeFileSync3(next, content, { mode });
|
|
1123
|
+
renameSync3(next, path2);
|
|
772
1124
|
},
|
|
773
1125
|
read: (path2) => readFileSync2(path2, "utf8"),
|
|
774
1126
|
exists: existsSync2,
|
|
775
1127
|
list: (path2) => existsSync2(path2) ? readdirSync(path2) : [],
|
|
776
1128
|
realpath: realpathSync,
|
|
777
|
-
mkdir: (path2, mode) =>
|
|
1129
|
+
mkdir: (path2, mode) => mkdirSync3(path2, { recursive: true, mode }),
|
|
778
1130
|
remove: (path2) => rmSync2(path2, { force: true }),
|
|
779
1131
|
async exec(argv2) {
|
|
780
1132
|
const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: {
|
|
@@ -833,7 +1185,7 @@ function allocatedPorts(request, id, previous, states) {
|
|
|
833
1185
|
if (previous && previous.applicationPorts.length === required && previous.applicationPorts.every((port) => port >= allocation.from && port <= allocation.to && !occupied.has(port)))
|
|
834
1186
|
return [...previous.applicationPorts];
|
|
835
1187
|
const width = allocation.to - allocation.from + 1;
|
|
836
|
-
const start = Number.parseInt(
|
|
1188
|
+
const start = Number.parseInt(createHash3("sha256").update(request.key).digest("hex").slice(0, 8), 16) % width;
|
|
837
1189
|
for (let offset = 0;offset < width; offset += 1) {
|
|
838
1190
|
const first = allocation.from + (start + offset) % width;
|
|
839
1191
|
const candidate = Array.from({ length: required }, (_, index) => first + index);
|
|
@@ -891,12 +1243,12 @@ ${request.service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
|
|
|
891
1243
|
}
|
|
892
1244
|
}
|
|
893
1245
|
`;
|
|
894
|
-
async function
|
|
1246
|
+
async function checked2(host, argv2, code) {
|
|
895
1247
|
const result = await host.exec(argv2);
|
|
896
1248
|
if (result.exitCode !== 0)
|
|
897
1249
|
throw new Error(`${code}: ${result.output.slice(0, 512)}`);
|
|
898
1250
|
}
|
|
899
|
-
async function activateSupervisedService(request, host =
|
|
1251
|
+
async function activateSupervisedService(request, host = defaultHost2) {
|
|
900
1252
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/.test(request.key) || !/^[a-f0-9]{40}$/.test(request.revision)) {
|
|
901
1253
|
throw new Error("SERVICE_IDENTITY_INVALID");
|
|
902
1254
|
}
|
|
@@ -905,7 +1257,7 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
905
1257
|
if (!within(root, release))
|
|
906
1258
|
throw new Error("SERVICE_RELEASE_OUTSIDE_ROOT");
|
|
907
1259
|
request = { ...request, root, release, service: validateDeploymentService(request.service) };
|
|
908
|
-
const id =
|
|
1260
|
+
const id = idFor2(request.key);
|
|
909
1261
|
host.mkdir(SERVICE_STATE_DIRECTORY, 448);
|
|
910
1262
|
host.mkdir(SERVICE_CONFIG_DIRECTORY, 457);
|
|
911
1263
|
const previous = readState(host, statePath(id));
|
|
@@ -916,11 +1268,11 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
916
1268
|
const unitName = `forgezero-app-${id}-slot${nextSlot}.service`;
|
|
917
1269
|
host.write(configPath, appConfig({ ...request, release }, port), 292);
|
|
918
1270
|
host.write(`${SERVICE_UNIT_DIRECTORY}/${unitName}`, unit(id, nextSlot), 420);
|
|
919
|
-
await
|
|
1271
|
+
await checked2(host, ["/usr/bin/systemctl", "daemon-reload"], "SERVICE_UNIT_RELOAD_FAILED");
|
|
920
1272
|
if (request.service.strategy === "direct" && previous) {
|
|
921
1273
|
await host.exec(["/usr/bin/systemctl", "stop", `forgezero-app-${id}-slot${previous.activeSlot}.service`]);
|
|
922
1274
|
}
|
|
923
|
-
await
|
|
1275
|
+
await checked2(host, ["/usr/bin/systemctl", "restart", unitName], "SERVICE_START_FAILED");
|
|
924
1276
|
let healthy = false;
|
|
925
1277
|
for (let attempt = 0;attempt < 30; attempt += 1) {
|
|
926
1278
|
const active = await host.exec(["/usr/bin/systemctl", "is-active", "--quiet", unitName]);
|
|
@@ -938,8 +1290,8 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
938
1290
|
const oldNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
|
|
939
1291
|
try {
|
|
940
1292
|
host.write(nginxPath, nginx(request, id, port), 420);
|
|
941
|
-
await
|
|
942
|
-
await
|
|
1293
|
+
await checked2(host, ["/usr/sbin/nginx", "-t"], "SERVICE_NGINX_INVALID");
|
|
1294
|
+
await checked2(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "SERVICE_NGINX_RELOAD_FAILED");
|
|
943
1295
|
} catch (cause) {
|
|
944
1296
|
if (oldNginx === null)
|
|
945
1297
|
host.remove(nginxPath);
|
|
@@ -970,22 +1322,22 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
970
1322
|
}
|
|
971
1323
|
|
|
972
1324
|
// src/container-supervisor.ts
|
|
973
|
-
import { createHash as
|
|
974
|
-
import { existsSync as existsSync3, mkdirSync as
|
|
975
|
-
import { dirname as
|
|
976
|
-
var
|
|
1325
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1326
|
+
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";
|
|
1327
|
+
import { dirname as dirname4, resolve as resolve2, sep as sep2 } from "node:path";
|
|
1328
|
+
var defaultHost3 = {
|
|
977
1329
|
realpath: realpathSync2,
|
|
978
1330
|
exists: existsSync3,
|
|
979
1331
|
read: (path2) => readFileSync3(path2, "utf8"),
|
|
980
1332
|
write(path2, content, mode) {
|
|
981
|
-
|
|
1333
|
+
mkdirSync4(dirname4(path2), { recursive: true, mode: 493 });
|
|
982
1334
|
const next = `${path2}.next`;
|
|
983
|
-
|
|
984
|
-
|
|
1335
|
+
writeFileSync4(next, content, { mode });
|
|
1336
|
+
renameSync4(next, path2);
|
|
985
1337
|
},
|
|
986
1338
|
remove: (path2) => rmSync3(path2, { force: true }),
|
|
987
1339
|
list: (path2) => existsSync3(path2) ? readdirSync2(path2) : [],
|
|
988
|
-
mkdir: (path2, mode) =>
|
|
1340
|
+
mkdir: (path2, mode) => mkdirSync4(path2, { recursive: true, mode }),
|
|
989
1341
|
async exec(argv2) {
|
|
990
1342
|
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
1343
|
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
@@ -1003,9 +1355,41 @@ var defaultHost2 = {
|
|
|
1003
1355
|
now: Date.now
|
|
1004
1356
|
};
|
|
1005
1357
|
var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
|
|
1006
|
-
var
|
|
1358
|
+
var idFor3 = (key) => createHash4("sha256").update(key).digest("hex").slice(0, 24);
|
|
1007
1359
|
var statePath2 = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
|
|
1008
1360
|
var nameFor = (id, slot) => `forgezero-${id}-slot${slot}`;
|
|
1361
|
+
function backendFor(application) {
|
|
1362
|
+
const runtime = application.runtime;
|
|
1363
|
+
if (runtime.kind !== "container")
|
|
1364
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1365
|
+
if (runtime.provider === "forgezero.docker") {
|
|
1366
|
+
if (runtime.runtimeClass && runtime.runtimeClass !== "runc")
|
|
1367
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
1368
|
+
return { engine: "docker", runtimeClass: "runc", prefix: ["/usr/bin/docker"] };
|
|
1369
|
+
}
|
|
1370
|
+
if (runtime.provider === "forgezero.containerd") {
|
|
1371
|
+
if ((runtime.runtimeClass ?? "runc") !== "runc")
|
|
1372
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
1373
|
+
return {
|
|
1374
|
+
engine: "containerd",
|
|
1375
|
+
runtimeClass: "runc",
|
|
1376
|
+
prefix: ["/usr/local/bin/nerdctl", "--namespace", "forgezero"],
|
|
1377
|
+
runtimeName: "io.containerd.runc.v2"
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
if (runtime.provider === "forgezero.kata") {
|
|
1381
|
+
if (runtime.runtimeClass !== "kata-qemu-snp")
|
|
1382
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
1383
|
+
return {
|
|
1384
|
+
engine: "containerd",
|
|
1385
|
+
runtimeClass: "kata-qemu-snp",
|
|
1386
|
+
prefix: ["/usr/local/bin/nerdctl", "--namespace", "forgezero"],
|
|
1387
|
+
runtimeName: "io.containerd.kata.v2"
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_UNSUPPORTED");
|
|
1391
|
+
}
|
|
1392
|
+
var command = (backend, ...argv2) => [...backend.prefix, ...argv2];
|
|
1009
1393
|
function validateRequest(request, host) {
|
|
1010
1394
|
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
1395
|
throw new Error("CONTAINER_IDENTITY_INVALID");
|
|
@@ -1016,6 +1400,7 @@ function validateRequest(request, host) {
|
|
|
1016
1400
|
const application = request.application;
|
|
1017
1401
|
if (application.kind !== "application" || application.runtime.kind !== "container")
|
|
1018
1402
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1403
|
+
backendFor(application);
|
|
1019
1404
|
if (!application.resources.cpu || !application.resources.memory || !application.resources.pids)
|
|
1020
1405
|
throw new Error("CONTAINER_LIMITS_REQUIRED");
|
|
1021
1406
|
if (application.runtime.security.privileged !== false || application.runtime.security.noNewPrivileges !== true || !application.runtime.security.dropCapabilities.includes("ALL"))
|
|
@@ -1026,23 +1411,24 @@ function validateRequest(request, host) {
|
|
|
1026
1411
|
throw new Error("CONTAINER_STABLE_PORT_REQUIRED");
|
|
1027
1412
|
return { ...request, root, release };
|
|
1028
1413
|
}
|
|
1029
|
-
async function
|
|
1414
|
+
async function checked3(host, argv2, code) {
|
|
1030
1415
|
const result = await host.exec(argv2);
|
|
1031
1416
|
if (result.exitCode !== 0)
|
|
1032
1417
|
throw new Error(`${code}: ${result.output.slice(0, 512)}`);
|
|
1033
1418
|
return result.output.trim();
|
|
1034
1419
|
}
|
|
1035
|
-
async function buildContainerImage(requestValue, host =
|
|
1420
|
+
async function buildContainerImage(requestValue, host = defaultHost3) {
|
|
1036
1421
|
const request = validateRequest(requestValue, host);
|
|
1037
1422
|
const runtime = request.application.runtime;
|
|
1038
1423
|
if (runtime.kind !== "container")
|
|
1039
1424
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1425
|
+
const backend = backendFor(request.application);
|
|
1040
1426
|
const source = runtime.image.source;
|
|
1041
1427
|
if (source.kind === "registry") {
|
|
1042
1428
|
if (!source.reference.includes("@sha256:"))
|
|
1043
1429
|
throw new Error("CONTAINER_IMAGE_DIGEST_REQUIRED");
|
|
1044
|
-
await
|
|
1045
|
-
const digest2 = await
|
|
1430
|
+
await checked3(host, command(backend, "pull", "--quiet", source.reference), "CONTAINER_PULL_FAILED");
|
|
1431
|
+
const digest2 = await checked3(host, command(backend, "image", "inspect", "--format={{.Id}}", source.reference), "CONTAINER_INSPECT_FAILED");
|
|
1046
1432
|
if (!/^sha256:[a-f0-9]{64}$/.test(digest2))
|
|
1047
1433
|
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
1048
1434
|
return { image: source.reference, digest: digest2 };
|
|
@@ -1051,23 +1437,9 @@ async function buildContainerImage(requestValue, host = defaultHost2) {
|
|
|
1051
1437
|
const dockerfile = host.realpath(resolve2(request.release, source.dockerfile));
|
|
1052
1438
|
if (!within2(request.release, context) || !within2(context, dockerfile))
|
|
1053
1439
|
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");
|
|
1440
|
+
const tag = `forgezero/${idFor3(request.key)}:${request.revision}`;
|
|
1441
|
+
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");
|
|
1442
|
+
const digest = await checked3(host, command(backend, "image", "inspect", "--format={{.Id}}", tag), "CONTAINER_INSPECT_FAILED");
|
|
1071
1443
|
if (!/^sha256:[a-f0-9]{64}$/.test(digest))
|
|
1072
1444
|
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
1073
1445
|
return { image: tag, digest };
|
|
@@ -1135,7 +1507,7 @@ ${service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
|
|
|
1135
1507
|
}
|
|
1136
1508
|
`;
|
|
1137
1509
|
}
|
|
1138
|
-
function runArgv(request, id, slot, port, networkName) {
|
|
1510
|
+
function runArgv(request, backend, id, slot, port, networkName) {
|
|
1139
1511
|
const { application } = request;
|
|
1140
1512
|
const resources = application.resources;
|
|
1141
1513
|
const runtime = application.runtime;
|
|
@@ -1143,35 +1515,7 @@ function runArgv(request, id, slot, port, networkName) {
|
|
|
1143
1515
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1144
1516
|
const memory = resources.memory;
|
|
1145
1517
|
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
|
-
];
|
|
1518
|
+
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
1519
|
for (const mount of application.storage ?? []) {
|
|
1176
1520
|
if (mount.class === "ephemeral")
|
|
1177
1521
|
argv2.push("--tmpfs", `${mount.path}:rw,noexec,nosuid,size=${mount.sizeMiB}m`);
|
|
@@ -1181,11 +1525,12 @@ function runArgv(request, id, slot, port, networkName) {
|
|
|
1181
1525
|
argv2.push(request.image, ...runtime.entrypoint ?? []);
|
|
1182
1526
|
return argv2;
|
|
1183
1527
|
}
|
|
1184
|
-
async function activateContainer(requestValue, host =
|
|
1528
|
+
async function activateContainer(requestValue, host = defaultHost3) {
|
|
1185
1529
|
const request = validateRequest(requestValue, host);
|
|
1186
1530
|
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
1531
|
throw new Error("CONTAINER_IMAGE_INVALID");
|
|
1188
|
-
const
|
|
1532
|
+
const backend = backendFor(request.application);
|
|
1533
|
+
const id = idFor3(request.key);
|
|
1189
1534
|
host.mkdir(SERVICE_STATE_DIRECTORY, 448);
|
|
1190
1535
|
host.mkdir(SERVICE_NGINX_DIRECTORY, 493);
|
|
1191
1536
|
const previous = readState2(host, id);
|
|
@@ -1197,9 +1542,9 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1197
1542
|
if (containerNetwork.mode === "bridge") {
|
|
1198
1543
|
networkName = containerNetwork.network ? `forgezero-${id}-${containerNetwork.network}` : "bridge";
|
|
1199
1544
|
if (networkName !== "bridge") {
|
|
1200
|
-
const inspected = await host.exec(
|
|
1545
|
+
const inspected = await host.exec(command(backend, "network", "inspect", '--format={{index .Labels "net.forgezero.owner"}}', networkName));
|
|
1201
1546
|
if (inspected.exitCode !== 0) {
|
|
1202
|
-
await
|
|
1547
|
+
await checked3(host, command(backend, "network", "create", "--driver", "bridge", "--label", `net.forgezero.owner=${id}`, networkName), "CONTAINER_NETWORK_CREATE_FAILED");
|
|
1203
1548
|
} else if (inspected.output.trim() !== id)
|
|
1204
1549
|
throw new Error("CONTAINER_NETWORK_OWNERSHIP_INVALID");
|
|
1205
1550
|
}
|
|
@@ -1208,8 +1553,8 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1208
1553
|
const nextSlot = slots === 2 && previous?.activeSlot === 0 ? 1 : 0;
|
|
1209
1554
|
const port = ports[nextSlot];
|
|
1210
1555
|
const name = nameFor(id, nextSlot);
|
|
1211
|
-
await host.exec(
|
|
1212
|
-
await
|
|
1556
|
+
await host.exec(command(backend, "rm", "--force", name));
|
|
1557
|
+
await checked3(host, runArgv(request, backend, id, nextSlot, port, networkName), "CONTAINER_START_FAILED");
|
|
1213
1558
|
let healthy = false;
|
|
1214
1559
|
const health = request.application.service.health;
|
|
1215
1560
|
for (let attempt = 0;attempt < (health.attempts ?? 30); attempt += 1) {
|
|
@@ -1220,15 +1565,28 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1220
1565
|
await host.sleep(health.intervalMs ?? 1000);
|
|
1221
1566
|
}
|
|
1222
1567
|
if (!healthy) {
|
|
1223
|
-
await host.exec(
|
|
1568
|
+
await host.exec(command(backend, "rm", "--force", name));
|
|
1224
1569
|
throw new Error("CONTAINER_HEALTH_FAILED");
|
|
1225
1570
|
}
|
|
1571
|
+
let runtimeEvidence;
|
|
1572
|
+
if (backend.engine === "containerd") {
|
|
1573
|
+
const raw = await checked3(host, ["/usr/bin/ctr", "--namespace", "forgezero", "containers", "info", name], "CONTAINER_RUNTIME_EVIDENCE_FAILED");
|
|
1574
|
+
let observed = "";
|
|
1575
|
+
try {
|
|
1576
|
+
observed = String(JSON.parse(raw).Runtime?.Name ?? "");
|
|
1577
|
+
} catch {
|
|
1578
|
+
throw new Error("CONTAINER_RUNTIME_EVIDENCE_INVALID");
|
|
1579
|
+
}
|
|
1580
|
+
if (observed !== backend.runtimeName)
|
|
1581
|
+
throw new Error("CONTAINER_RUNTIME_DOWNGRADE");
|
|
1582
|
+
runtimeEvidence = observed;
|
|
1583
|
+
}
|
|
1226
1584
|
const nginxPath = `${SERVICE_NGINX_DIRECTORY}/forgezero-${id}.conf`;
|
|
1227
1585
|
const previousNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
|
|
1228
1586
|
host.write(nginxPath, nginx2(request, id, port), 420);
|
|
1229
1587
|
try {
|
|
1230
|
-
await
|
|
1231
|
-
await
|
|
1588
|
+
await checked3(host, ["/usr/sbin/nginx", "-t"], "CONTAINER_NGINX_INVALID");
|
|
1589
|
+
await checked3(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "CONTAINER_NGINX_RELOAD_FAILED");
|
|
1232
1590
|
} catch (cause) {
|
|
1233
1591
|
if (previousNginx === null)
|
|
1234
1592
|
host.remove(nginxPath);
|
|
@@ -1236,7 +1594,7 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1236
1594
|
host.write(nginxPath, previousNginx, 420);
|
|
1237
1595
|
await host.exec(["/usr/sbin/nginx", "-t"]);
|
|
1238
1596
|
await host.exec(["/usr/bin/systemctl", "reload", "nginx.service"]);
|
|
1239
|
-
await host.exec(
|
|
1597
|
+
await host.exec(command(backend, "rm", "--force", name));
|
|
1240
1598
|
throw cause;
|
|
1241
1599
|
}
|
|
1242
1600
|
const state = {
|
|
@@ -1249,13 +1607,18 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1249
1607
|
applicationPorts: ports,
|
|
1250
1608
|
activeSlot: nextSlot,
|
|
1251
1609
|
healthPath: health.path,
|
|
1610
|
+
containerRuntime: {
|
|
1611
|
+
engine: backend.engine,
|
|
1612
|
+
runtimeClass: backend.runtimeClass,
|
|
1613
|
+
...backend.engine === "containerd" ? { namespace: "forgezero", evidence: runtimeEvidence } : {}
|
|
1614
|
+
},
|
|
1252
1615
|
updatedAtTs: host.now()
|
|
1253
1616
|
};
|
|
1254
1617
|
host.write(statePath2(id), `${JSON.stringify(state, null, 2)}
|
|
1255
1618
|
`, 384);
|
|
1256
1619
|
if (previous && previous.activeSlot !== nextSlot) {
|
|
1257
1620
|
await host.sleep(request.application.rollout.drainMs ?? 30000);
|
|
1258
|
-
await host.exec(
|
|
1621
|
+
await host.exec(command(backend, "rm", "--force", nameFor(id, previous.activeSlot)));
|
|
1259
1622
|
}
|
|
1260
1623
|
return state;
|
|
1261
1624
|
}
|
|
@@ -1270,11 +1633,12 @@ function startSoftwareHelper(options = {}) {
|
|
|
1270
1633
|
const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
|
|
1271
1634
|
if (existsSync4(socketPath))
|
|
1272
1635
|
unlinkSync2(socketPath);
|
|
1273
|
-
|
|
1636
|
+
mkdirSync5(dirname5(socketPath), { recursive: true, mode: 488 });
|
|
1274
1637
|
const ensure = options.ensure ?? ensureSoftwareRequirements;
|
|
1275
1638
|
const activate = options.activate ?? activateSupervisedService;
|
|
1276
1639
|
const buildContainer = options.buildContainer ?? buildContainerImage;
|
|
1277
1640
|
const activateTypedContainer = options.activateContainer ?? activateContainer;
|
|
1641
|
+
const applyConnectivity = options.applyConnectivity ?? applyDeploymentConnectivity;
|
|
1278
1642
|
let tail = Promise.resolve();
|
|
1279
1643
|
let pending = 0;
|
|
1280
1644
|
const server = createServer((socket) => {
|
|
@@ -1331,6 +1695,12 @@ function startSoftwareHelper(options = {}) {
|
|
|
1331
1695
|
throw new Error("container deployment root is not owned by this helper");
|
|
1332
1696
|
const state = await activateTypedContainer(containerRequest);
|
|
1333
1697
|
socket.end(`${JSON.stringify({ ok: true, state })}
|
|
1698
|
+
`);
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
if (request.op === "apply-connectivity" && request.request) {
|
|
1702
|
+
const result = await applyConnectivity(request.request);
|
|
1703
|
+
socket.end(`${JSON.stringify({ ok: true, result })}
|
|
1334
1704
|
`);
|
|
1335
1705
|
return;
|
|
1336
1706
|
}
|
|
@@ -1388,6 +1758,9 @@ function requestContainerBuild(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOC
|
|
|
1388
1758
|
function requestContainerActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
1389
1759
|
return requestContainer("activate-container", request, socketPath, timeoutMs);
|
|
1390
1760
|
}
|
|
1761
|
+
function requestDeploymentConnectivity(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
1762
|
+
return requestContainer("apply-connectivity", request, socketPath, timeoutMs);
|
|
1763
|
+
}
|
|
1391
1764
|
function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
|
|
1392
1765
|
return new Promise((resolve3, reject) => {
|
|
1393
1766
|
const socket = connect(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
|
|
@@ -1449,6 +1822,7 @@ export {
|
|
|
1449
1822
|
startSoftwareHelper,
|
|
1450
1823
|
requestSoftware,
|
|
1451
1824
|
requestServiceActivation,
|
|
1825
|
+
requestDeploymentConnectivity,
|
|
1452
1826
|
requestContainerBuild,
|
|
1453
1827
|
requestContainerActivation,
|
|
1454
1828
|
SOFTWARE_HELPER_UNIT_PATH,
|