@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/metal-provision.js
CHANGED
|
@@ -144,22 +144,30 @@ import {
|
|
|
144
144
|
accessSync,
|
|
145
145
|
chmodSync,
|
|
146
146
|
copyFileSync,
|
|
147
|
+
createReadStream,
|
|
147
148
|
existsSync,
|
|
148
149
|
mkdtempSync,
|
|
149
150
|
mkdirSync,
|
|
150
151
|
readFileSync,
|
|
151
152
|
renameSync,
|
|
152
153
|
rmSync,
|
|
154
|
+
statSync,
|
|
153
155
|
symlinkSync,
|
|
154
156
|
unlinkSync,
|
|
155
157
|
writeFileSync
|
|
156
158
|
} from "node:fs";
|
|
157
159
|
import { tmpdir } from "node:os";
|
|
158
|
-
import { join } from "node:path";
|
|
160
|
+
import { dirname, join } from "node:path";
|
|
159
161
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
160
162
|
var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
|
|
161
163
|
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
162
164
|
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
165
|
+
var NERDCTL_VERSION = "2.3.5";
|
|
166
|
+
var BUILDKIT_VERSION = "0.32.2";
|
|
167
|
+
var KATA_CONTAINERS_VERSION = "4.0.0";
|
|
168
|
+
var NERDCTL_SHA256 = "de3206aeb7cbd5f20f5fb1f55c1e3bf2db1be567812a8a3f5e65eba2488347ee";
|
|
169
|
+
var BUILDKIT_SHA256 = "2975d0f651ad96ba8b80b9992ae1f9a964f4408569af5b6dc36544165c3926af";
|
|
170
|
+
var KATA_SHA256 = "2c3b9dfeba355582b40aee462b12916c9740654d0230f696adf719d67b063a8c";
|
|
163
171
|
var OS_CATALOG = [
|
|
164
172
|
{
|
|
165
173
|
id: "ubuntu",
|
|
@@ -189,6 +197,8 @@ var OS_CATALOG = [
|
|
|
189
197
|
var SOFTWARE_CATALOG = [
|
|
190
198
|
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
191
199
|
{ id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
200
|
+
{ id: "containerd", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
201
|
+
{ id: "kata-containers", version: "4.0.0", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
192
202
|
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
193
203
|
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
194
204
|
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -197,6 +207,7 @@ var SOFTWARE_CATALOG = [
|
|
|
197
207
|
{ id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
198
208
|
{ id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
199
209
|
{ id: "docker", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
210
|
+
{ id: "containerd", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
200
211
|
{ id: "nginx", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
201
212
|
{ id: "ufw", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
202
213
|
{ id: "openssh-client", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -205,6 +216,8 @@ var SOFTWARE_CATALOG = [
|
|
|
205
216
|
var UBUNTU_2604_X64 = [
|
|
206
217
|
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
207
218
|
{ requirement: { id: "docker", version: "ubuntu-26.04" } },
|
|
219
|
+
{ requirement: { id: "containerd", version: "ubuntu-26.04" } },
|
|
220
|
+
{ requirement: { id: "kata-containers", version: "4.0.0" } },
|
|
208
221
|
{ requirement: { id: "nginx", version: "ubuntu-26.04" } },
|
|
209
222
|
{ requirement: { id: "arangodb", version: "3.11.14" } },
|
|
210
223
|
{ requirement: { id: "cloudflared", version: "2026.7.3" } },
|
|
@@ -216,6 +229,7 @@ var UBUNTU_2604_X64 = [
|
|
|
216
229
|
var UBUNTU_2404_X64 = [
|
|
217
230
|
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
218
231
|
{ requirement: { id: "docker", version: "ubuntu-24.04" } },
|
|
232
|
+
{ requirement: { id: "containerd", version: "ubuntu-24.04" } },
|
|
219
233
|
{ requirement: { id: "nginx", version: "ubuntu-24.04" } },
|
|
220
234
|
{ requirement: { id: "ufw", version: "ubuntu-24.04" } },
|
|
221
235
|
{ requirement: { id: "openssh-client", version: "ubuntu-24.04" } },
|
|
@@ -232,6 +246,46 @@ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
|
232
246
|
"log-opts": { "max-size": "10m", "max-file": "3" }
|
|
233
247
|
}, null, 2)}
|
|
234
248
|
`;
|
|
249
|
+
var CONTAINERD_CONFIG_PATH = "/etc/containerd/config.toml";
|
|
250
|
+
var KATA_SNP_CONFIG_PATH = "/opt/kata/share/defaults/kata-containers/configuration-qemu-snp.toml";
|
|
251
|
+
var KATA_ACTIVE_CONFIG_PATH = "/etc/kata-containers/configuration.toml";
|
|
252
|
+
var CONTAINERD_CONFIG = (kata) => `version = 2
|
|
253
|
+
root = "/var/lib/forgezero/containerd"
|
|
254
|
+
state = "/run/forgezero/containerd"
|
|
255
|
+
|
|
256
|
+
[grpc]
|
|
257
|
+
address = "/run/containerd/containerd.sock"
|
|
258
|
+
|
|
259
|
+
[plugins."io.containerd.grpc.v1.cri".containerd]
|
|
260
|
+
snapshotter = "overlayfs"
|
|
261
|
+
default_runtime_name = "runc"
|
|
262
|
+
|
|
263
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
|
|
264
|
+
runtime_type = "io.containerd.runc.v2"
|
|
265
|
+
${kata ? `
|
|
266
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-qemu-snp]
|
|
267
|
+
runtime_type = "io.containerd.kata.v2"
|
|
268
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-qemu-snp.options]
|
|
269
|
+
ConfigPath = "${KATA_ACTIVE_CONFIG_PATH}"
|
|
270
|
+
` : ""}`;
|
|
271
|
+
var BUILDKIT_UNIT = `[Unit]
|
|
272
|
+
Description=ForgeZero BuildKit daemon
|
|
273
|
+
After=network-online.target containerd.service
|
|
274
|
+
Wants=network-online.target
|
|
275
|
+
Requires=containerd.service
|
|
276
|
+
|
|
277
|
+
[Service]
|
|
278
|
+
Type=notify
|
|
279
|
+
ExecStart=/usr/local/bin/buildkitd --addr unix:///run/buildkit/buildkitd.sock --root /var/lib/forgezero/buildkit
|
|
280
|
+
Restart=on-failure
|
|
281
|
+
RestartSec=2
|
|
282
|
+
NoNewPrivileges=yes
|
|
283
|
+
ProtectSystem=strict
|
|
284
|
+
ReadWritePaths=/var/lib/forgezero/buildkit /run/buildkit
|
|
285
|
+
|
|
286
|
+
[Install]
|
|
287
|
+
WantedBy=multi-user.target
|
|
288
|
+
`;
|
|
235
289
|
var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
|
|
236
290
|
var runSoftwareCommand = async (argv, env = {}) => {
|
|
237
291
|
let child;
|
|
@@ -251,14 +305,28 @@ var runSoftwareCommand = async (argv, env = {}) => {
|
|
|
251
305
|
return { exitCode, output: `${stdout}${stderr}` };
|
|
252
306
|
};
|
|
253
307
|
var run = runSoftwareCommand;
|
|
254
|
-
var download = async (url, destination, sha256) => {
|
|
255
|
-
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(
|
|
308
|
+
var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
|
|
309
|
+
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30 * 60000) });
|
|
256
310
|
if (!response.ok)
|
|
257
311
|
throw new Error(`download failed with HTTP ${response.status}`);
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
312
|
+
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
313
|
+
if (declared > maximumBytes)
|
|
314
|
+
throw new Error("download exceeds reviewed size bound");
|
|
315
|
+
if (existsSync(destination))
|
|
316
|
+
throw new Error("download destination already exists");
|
|
317
|
+
await Bun.write(destination, response);
|
|
318
|
+
chmodSync(destination, 384);
|
|
319
|
+
if (statSync(destination).size > maximumBytes) {
|
|
320
|
+
rmSync(destination, { force: true });
|
|
321
|
+
throw new Error("download exceeds reviewed size bound");
|
|
322
|
+
}
|
|
323
|
+
const digest = createHash("sha256");
|
|
324
|
+
for await (const chunk of createReadStream(destination))
|
|
325
|
+
digest.update(chunk);
|
|
326
|
+
if (digest.digest("hex") !== sha256) {
|
|
327
|
+
rmSync(destination, { force: true });
|
|
260
328
|
throw new Error("download checksum mismatch");
|
|
261
|
-
|
|
329
|
+
}
|
|
262
330
|
};
|
|
263
331
|
var successful = (result, pattern) => result.exitCode === 0 && (!pattern || pattern.test(result.output));
|
|
264
332
|
var aptInstall = async (name) => {
|
|
@@ -266,6 +334,106 @@ var aptInstall = async (name) => {
|
|
|
266
334
|
const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
|
|
267
335
|
return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", name], environment) : update;
|
|
268
336
|
};
|
|
337
|
+
var aptInstallMany = async (names) => {
|
|
338
|
+
const environment = { DEBIAN_FRONTEND: "noninteractive" };
|
|
339
|
+
const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
|
|
340
|
+
return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", ...names], environment) : update;
|
|
341
|
+
};
|
|
342
|
+
var writeOwnedPolicy = (pathValue, content, mode = 420) => {
|
|
343
|
+
if (existsSync(pathValue)) {
|
|
344
|
+
if (readFileSync(pathValue, "utf8") !== content)
|
|
345
|
+
throw new Error(`refusing to overwrite non-ForgeZero policy at ${pathValue}`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
mkdirSync(dirname(pathValue), { recursive: true, mode: 493 });
|
|
349
|
+
writeFileSync(pathValue, content, { mode, flag: "wx" });
|
|
350
|
+
};
|
|
351
|
+
var installContainerdRuntime = async (directory, osVersion) => {
|
|
352
|
+
const installed = await aptInstallMany(["containerd", "runc", "containernetworking-plugins"]);
|
|
353
|
+
if (installed.exitCode !== 0)
|
|
354
|
+
return installed;
|
|
355
|
+
const nerdctl = join(directory, "nerdctl.tar.gz");
|
|
356
|
+
await download(`https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz`, nerdctl, NERDCTL_SHA256);
|
|
357
|
+
const nerdctlExtract = await run(["/usr/bin/tar", "-xzf", nerdctl, "-C", "/usr/local/bin", "nerdctl"]);
|
|
358
|
+
if (nerdctlExtract.exitCode !== 0)
|
|
359
|
+
return nerdctlExtract;
|
|
360
|
+
chmodSync("/usr/local/bin/nerdctl", 493);
|
|
361
|
+
const buildkit = join(directory, "buildkit.tar.gz");
|
|
362
|
+
await download(`https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-amd64.tar.gz`, buildkit, BUILDKIT_SHA256);
|
|
363
|
+
const unpacked = join(directory, "buildkit");
|
|
364
|
+
mkdirSync(unpacked, { mode: 448 });
|
|
365
|
+
const buildkitExtract = await run(["/usr/bin/tar", "-xzf", buildkit, "-C", unpacked]);
|
|
366
|
+
if (buildkitExtract.exitCode !== 0)
|
|
367
|
+
return buildkitExtract;
|
|
368
|
+
for (const binary of ["buildctl", "buildkitd"]) {
|
|
369
|
+
copyFileSync(join(unpacked, "bin", binary), `/usr/local/bin/${binary}`);
|
|
370
|
+
chmodSync(`/usr/local/bin/${binary}`, 493);
|
|
371
|
+
}
|
|
372
|
+
mkdirSync("/etc/containerd", { recursive: true, mode: 493 });
|
|
373
|
+
const expected = CONTAINERD_CONFIG(false);
|
|
374
|
+
const kataExpected = CONTAINERD_CONFIG(true);
|
|
375
|
+
if (existsSync(CONTAINERD_CONFIG_PATH) && ![expected, kataExpected].includes(readFileSync(CONTAINERD_CONFIG_PATH, "utf8"))) {
|
|
376
|
+
return { exitCode: 2, output: "refusing to overwrite an existing containerd configuration that differs from the reviewed ForgeZero policy" };
|
|
377
|
+
}
|
|
378
|
+
if (!existsSync(CONTAINERD_CONFIG_PATH))
|
|
379
|
+
writeFileSync(CONTAINERD_CONFIG_PATH, expected, { mode: 420, flag: "wx" });
|
|
380
|
+
writeOwnedPolicy("/etc/systemd/system/forgezero-buildkit.service", BUILDKIT_UNIT);
|
|
381
|
+
mkdirSync("/var/lib/forgezero/containerd", { recursive: true, mode: 448 });
|
|
382
|
+
mkdirSync("/var/lib/forgezero/buildkit", { recursive: true, mode: 448 });
|
|
383
|
+
const reload = await run(["/usr/bin/systemctl", "daemon-reload"]);
|
|
384
|
+
if (reload.exitCode !== 0)
|
|
385
|
+
return reload;
|
|
386
|
+
const enabled = await run(["/usr/bin/systemctl", "enable", "--now", "containerd.service", "forgezero-buildkit.service"]);
|
|
387
|
+
return enabled.exitCode === 0 ? { exitCode: 0, output: `containerd ${osVersion} with nerdctl ${NERDCTL_VERSION} and BuildKit ${BUILDKIT_VERSION}` } : enabled;
|
|
388
|
+
};
|
|
389
|
+
var installKataRuntime = async (directory) => {
|
|
390
|
+
for (const pathValue of ["/dev/kvm", "/dev/sev"])
|
|
391
|
+
if (!existsSync(pathValue))
|
|
392
|
+
return { exitCode: 2, output: `${pathValue} is required for Kata SEV-SNP` };
|
|
393
|
+
for (const parameter of ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"]) {
|
|
394
|
+
if (!existsSync(parameter) || !/^(1|Y)$/i.test(readFileSync(parameter, "utf8").trim()))
|
|
395
|
+
return { exitCode: 2, output: `${parameter} does not enable SEV-SNP` };
|
|
396
|
+
}
|
|
397
|
+
const dependencies = await aptInstallMany(["zstd"]);
|
|
398
|
+
if (dependencies.exitCode !== 0)
|
|
399
|
+
return dependencies;
|
|
400
|
+
const archive = join(directory, "kata.tar.zst");
|
|
401
|
+
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);
|
|
402
|
+
const tar = join(directory, "kata.tar");
|
|
403
|
+
const expanded = await run(["/usr/bin/unzstd", "--quiet", archive, "--output", tar]);
|
|
404
|
+
if (expanded.exitCode !== 0)
|
|
405
|
+
return expanded;
|
|
406
|
+
const extracted = await run(["/usr/bin/tar", "-xf", tar, "-C", "/"]);
|
|
407
|
+
if (extracted.exitCode !== 0)
|
|
408
|
+
return extracted;
|
|
409
|
+
if (!existsSync("/opt/kata/bin/containerd-shim-kata-v2") || !existsSync(KATA_SNP_CONFIG_PATH)) {
|
|
410
|
+
return { exitCode: 2, output: "Kata archive does not contain the reviewed QEMU SNP runtime and configuration" };
|
|
411
|
+
}
|
|
412
|
+
const snpConfig = readFileSync(KATA_SNP_CONFIG_PATH, "utf8");
|
|
413
|
+
if (!/^\s*confidential_guest\s*=\s*true\s*$/m.test(snpConfig) || !/^\s*sev_snp_guest\s*=\s*true\s*$/m.test(snpConfig)) {
|
|
414
|
+
return { exitCode: 2, output: "Kata QEMU configuration does not enable confidential SEV-SNP guests" };
|
|
415
|
+
}
|
|
416
|
+
mkdirSync("/etc/kata-containers", { recursive: true, mode: 493 });
|
|
417
|
+
if (existsSync(KATA_ACTIVE_CONFIG_PATH) && readFileSync(KATA_ACTIVE_CONFIG_PATH, "utf8") !== snpConfig) {
|
|
418
|
+
return { exitCode: 2, output: "refusing to overwrite a non-ForgeZero Kata runtime configuration" };
|
|
419
|
+
}
|
|
420
|
+
if (!existsSync(KATA_ACTIVE_CONFIG_PATH))
|
|
421
|
+
copyFileSync(KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH);
|
|
422
|
+
chmodSync(KATA_ACTIVE_CONFIG_PATH, 420);
|
|
423
|
+
try {
|
|
424
|
+
unlinkSync("/usr/local/bin/containerd-shim-kata-v2");
|
|
425
|
+
} catch {}
|
|
426
|
+
symlinkSync("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
|
|
427
|
+
const base = CONTAINERD_CONFIG(false);
|
|
428
|
+
const kata = CONTAINERD_CONFIG(true);
|
|
429
|
+
if (!existsSync(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync(CONTAINERD_CONFIG_PATH, "utf8"))) {
|
|
430
|
+
return { exitCode: 2, output: "containerd must use the reviewed ForgeZero policy before Kata is installed" };
|
|
431
|
+
}
|
|
432
|
+
if (readFileSync(CONTAINERD_CONFIG_PATH, "utf8") !== kata)
|
|
433
|
+
writeFileSync(CONTAINERD_CONFIG_PATH, kata, { mode: 420 });
|
|
434
|
+
const restarted = await run(["/usr/bin/systemctl", "restart", "containerd.service"]);
|
|
435
|
+
return restarted.exitCode === 0 ? { exitCode: 0, output: `Kata Containers ${KATA_CONTAINERS_VERSION} QEMU SNP` } : restarted;
|
|
436
|
+
};
|
|
269
437
|
async function executeSoftwareOperation(operation) {
|
|
270
438
|
const { software, version } = operation;
|
|
271
439
|
if (![...UBUNTU_2604_X64, ...UBUNTU_2404_X64].some(({ requirement }) => requirement.id === software && requirement.version === version)) {
|
|
@@ -283,6 +451,26 @@ async function executeSoftwareOperation(operation) {
|
|
|
283
451
|
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
|
|
284
452
|
return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
|
|
285
453
|
}
|
|
454
|
+
if (software === "containerd") {
|
|
455
|
+
const [containerd, nerdctl, buildkit, active, builder] = await Promise.all([
|
|
456
|
+
run(["/usr/bin/containerd", "--version"]),
|
|
457
|
+
run(["/usr/local/bin/nerdctl", "--version"]),
|
|
458
|
+
run(["/usr/local/bin/buildkitd", "--version"]),
|
|
459
|
+
run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]),
|
|
460
|
+
run(["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-buildkit.service"])
|
|
461
|
+
]);
|
|
462
|
+
const policy = existsSync(CONTAINERD_CONFIG_PATH) ? readFileSync(CONTAINERD_CONFIG_PATH, "utf8") : "";
|
|
463
|
+
const validPolicy = policy === CONTAINERD_CONFIG(false) || policy === CONTAINERD_CONFIG(true);
|
|
464
|
+
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" };
|
|
465
|
+
}
|
|
466
|
+
if (software === "kata-containers") {
|
|
467
|
+
const shim = await run(["/opt/kata/bin/containerd-shim-kata-v2", "--version"]);
|
|
468
|
+
const policy = existsSync(CONTAINERD_CONFIG_PATH) ? readFileSync(CONTAINERD_CONFIG_PATH, "utf8") : "";
|
|
469
|
+
const activeConfig = existsSync(KATA_ACTIVE_CONFIG_PATH) ? readFileSync(KATA_ACTIVE_CONFIG_PATH, "utf8") : "";
|
|
470
|
+
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()));
|
|
471
|
+
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]);
|
|
472
|
+
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" };
|
|
473
|
+
}
|
|
286
474
|
if (software === "nginx") {
|
|
287
475
|
const binary = await run(["/usr/sbin/nginx", "-v"]);
|
|
288
476
|
return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
|
|
@@ -334,6 +522,17 @@ async function executeSoftwareOperation(operation) {
|
|
|
334
522
|
}
|
|
335
523
|
const directory = mkdtempSync(join(tmpdir(), "forgezero-software-"));
|
|
336
524
|
try {
|
|
525
|
+
if (software === "containerd")
|
|
526
|
+
return installContainerdRuntime(directory, version === "ubuntu-24.04" ? "24.04" : "26.04");
|
|
527
|
+
if (software === "kata-containers") {
|
|
528
|
+
const containerd = await executeSoftwareOperation({ kind: "check", software: "containerd", version: "ubuntu-26.04" });
|
|
529
|
+
if (containerd.exitCode !== 0) {
|
|
530
|
+
const installed2 = await installContainerdRuntime(directory, "26.04");
|
|
531
|
+
if (installed2.exitCode !== 0)
|
|
532
|
+
return installed2;
|
|
533
|
+
}
|
|
534
|
+
return installKataRuntime(directory);
|
|
535
|
+
}
|
|
337
536
|
if (software === "cloudflare-warp") {
|
|
338
537
|
const key = join(directory, "cloudflare-warp-key.gpg");
|
|
339
538
|
await download("https://pkg.cloudflareclient.com/pubkey.gpg", key, "0f37fc298c98e88ee3c0ee68c95b69f1dba9eb477abe3167e13982105911264d");
|
|
@@ -415,7 +614,7 @@ function validateSoftwareRequirements(value, _options = {}) {
|
|
|
415
614
|
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
416
615
|
throw new Error("software requirement contains an unknown field");
|
|
417
616
|
}
|
|
418
|
-
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)) {
|
|
617
|
+
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)) {
|
|
419
618
|
throw new Error("software requirement coordinate is invalid");
|
|
420
619
|
}
|
|
421
620
|
const requirement = { id: row.id, version: row.version };
|
|
@@ -495,11 +694,11 @@ import {
|
|
|
495
694
|
readFileSync as readFileSync2,
|
|
496
695
|
readdirSync,
|
|
497
696
|
rmSync as rmSync2,
|
|
498
|
-
statSync,
|
|
697
|
+
statSync as statSync2,
|
|
499
698
|
unlinkSync as unlinkSync2,
|
|
500
699
|
writeFileSync as writeFileSync2
|
|
501
700
|
} from "node:fs";
|
|
502
|
-
import { dirname, isAbsolute, join as join2 } from "node:path";
|
|
701
|
+
import { dirname as dirname2, isAbsolute, join as join2 } from "node:path";
|
|
503
702
|
import { isIP } from "node:net";
|
|
504
703
|
var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
|
|
505
704
|
var DEVICE = /^[a-zA-Z][a-zA-Z0-9_.-]{0,14}$/;
|
|
@@ -794,7 +993,7 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
794
993
|
if (!image || !isAbsolute(image.path) || !SHA256.test(image.sha256)) {
|
|
795
994
|
throw new MetalProvisionError(`image ${claim.spec.imageKey} is not configured locally`);
|
|
796
995
|
}
|
|
797
|
-
if (!
|
|
996
|
+
if (!statSync2(image.path).isFile())
|
|
798
997
|
throw new MetalProvisionError("configured image is not a file");
|
|
799
998
|
const digest = (await checked(exec, ["sha256sum", image.path])).stdout.trim().split(/\s+/)[0];
|
|
800
999
|
if (digest !== image.sha256)
|
|
@@ -879,7 +1078,7 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
879
1078
|
}
|
|
880
1079
|
const service = `forgezero-guest@${name}.service`;
|
|
881
1080
|
const unitPath = join2(profile.unitDir, service);
|
|
882
|
-
mkdirSync2(
|
|
1081
|
+
mkdirSync2(dirname2(unitPath), { recursive: true });
|
|
883
1082
|
writeFileSync2(unitPath, guestUnit(spec), { mode: 420 });
|
|
884
1083
|
await checked(exec, ["systemctl", "daemon-reload"]);
|
|
885
1084
|
if (prior?.phase === "running") {
|
|
@@ -1416,7 +1416,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1416
1416
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1417
1417
|
|
|
1418
1418
|
// src/version.ts
|
|
1419
|
-
var VERSION = "0.1.
|
|
1419
|
+
var VERSION = "0.1.75";
|
|
1420
1420
|
|
|
1421
1421
|
// src/software.ts
|
|
1422
1422
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -3060,6 +3060,10 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
3060
3060
|
const coordinators = input.databaseCoordinators.map(privateCoordinator);
|
|
3061
3061
|
if (new Set(coordinators).size !== coordinators.length)
|
|
3062
3062
|
throw new Error("databaseCoordinators must be unique.");
|
|
3063
|
+
const readPreferred = (input.databaseReadPreferredCoordinators ?? []).map(privateCoordinator);
|
|
3064
|
+
if (new Set(readPreferred).size !== readPreferred.length || readPreferred.some((url) => !coordinators.includes(url))) {
|
|
3065
|
+
throw new Error("databaseReadPreferredCoordinators must be a unique subset of databaseCoordinators.");
|
|
3066
|
+
}
|
|
3063
3067
|
if (input.databaseNetworkMode !== "private-lan") {
|
|
3064
3068
|
throw new Error("Attended platform bootstrap supports only private-lan database networking.");
|
|
3065
3069
|
}
|
|
@@ -3133,6 +3137,7 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
3133
3137
|
return {
|
|
3134
3138
|
...input,
|
|
3135
3139
|
databaseCoordinators: coordinators,
|
|
3140
|
+
databaseReadPreferredCoordinators: readPreferred,
|
|
3136
3141
|
appOrigin: httpsOrigin("appOrigin", input.appOrigin),
|
|
3137
3142
|
apiOrigin: httpsOrigin("apiOrigin", input.apiOrigin),
|
|
3138
3143
|
agentOtlpEndpoint: httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint),
|
|
@@ -3146,6 +3151,8 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
3146
3151
|
const entries = {
|
|
3147
3152
|
ARANGO_URL: value.databaseCoordinators[0],
|
|
3148
3153
|
ARANGO_URLS: value.databaseCoordinators.join(","),
|
|
3154
|
+
ARANGO_READ_PREFERRED_URLS: (value.databaseReadPreferredCoordinators ?? []).join(","),
|
|
3155
|
+
ARANGO_READ_PREFERRED_FALLBACK: "balanced",
|
|
3149
3156
|
ARANGO_DB: "fz",
|
|
3150
3157
|
FZ_DATABASE_MODE: "platform",
|
|
3151
3158
|
ARANGO_USER: value.databaseUser,
|
|
@@ -3769,6 +3776,10 @@ function validateBootstrapConfig(value) {
|
|
|
3769
3776
|
if (!["member", "none"].includes(value.database.agency) || value.database.role === "master" && value.database.agency !== "member" || value.database.role === "none" && value.database.agency !== "none") {
|
|
3770
3777
|
throw new Error("database role and Agency participation disagree");
|
|
3771
3778
|
}
|
|
3779
|
+
value.database.readPreferred ??= false;
|
|
3780
|
+
if (value.database.readPreferred && (value.database.role !== "joiner" || value.database.agency !== "none")) {
|
|
3781
|
+
throw new Error("read-preferred database routing requires an extra non-Agency joiner");
|
|
3782
|
+
}
|
|
3772
3783
|
if (value.database.role !== "none" && !value.database.address)
|
|
3773
3784
|
throw new Error("database nodes require a private address");
|
|
3774
3785
|
if (value.database.role === "joiner" && !value.database.master)
|
|
@@ -3788,6 +3799,10 @@ function validateBootstrapConfig(value) {
|
|
|
3788
3799
|
if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.databaseCoordinators.join(",") !== write.join(",")) {
|
|
3789
3800
|
throw new Error("platform runtime coordinates disagree with immutable bootstrap coordinates");
|
|
3790
3801
|
}
|
|
3802
|
+
const ownCoordinator = value.database.address ? `http://${value.database.address}:8529` : undefined;
|
|
3803
|
+
if (Boolean(ownCoordinator && (runtime.databaseReadPreferredCoordinators ?? []).includes(ownCoordinator)) !== value.database.readPreferred) {
|
|
3804
|
+
throw new Error("runtime read-preferred coordinator set disagrees with this node database routing intent");
|
|
3805
|
+
}
|
|
3791
3806
|
if (runtime.deployProfile !== value.environment)
|
|
3792
3807
|
throw new Error("runtime deployment profile disagrees with bootstrap environment");
|
|
3793
3808
|
if (value.database.address !== runtime.databaseAddress || value.database.master !== runtime.databaseMaster) {
|
|
@@ -4099,6 +4114,7 @@ function bootstrapIdentity(config) {
|
|
|
4099
4114
|
role: config.database.role,
|
|
4100
4115
|
serverMode: config.database.serverMode,
|
|
4101
4116
|
agency: config.database.agency,
|
|
4117
|
+
readPreferred: config.database.readPreferred,
|
|
4102
4118
|
address: config.database.address ?? null,
|
|
4103
4119
|
master: config.database.master ?? null,
|
|
4104
4120
|
coordinators: config.database.coordinators
|
|
@@ -4134,7 +4150,7 @@ function parseStoredState(raw) {
|
|
|
4134
4150
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
4135
4151
|
throw new Error("bootstrap state is malformed");
|
|
4136
4152
|
const state = value;
|
|
4137
|
-
if (state.format !== 2 || !["platform", "enrolled-compute"].includes(state.kind ?? "") || !/^[a-f0-9]{64}$/.test(state.identityDigest ?? "") || typeof state.profile !== "string" || typeof state.nodeHostname !== "string" || typeof state.apiUrl !== "string" || state.kind === "platform" && !["member", "none"].includes(state.databaseAgency ?? "")) {
|
|
4153
|
+
if (state.format !== 2 || !["platform", "enrolled-compute"].includes(state.kind ?? "") || !/^[a-f0-9]{64}$/.test(state.identityDigest ?? "") || typeof state.profile !== "string" || typeof state.nodeHostname !== "string" || typeof state.apiUrl !== "string" || state.kind === "platform" && (!["member", "none"].includes(state.databaseAgency ?? "") || typeof state.databaseReadPreferred !== "boolean")) {
|
|
4138
4154
|
throw new Error("bootstrap state is legacy or incomplete; refusing an unbound repair");
|
|
4139
4155
|
}
|
|
4140
4156
|
return state;
|
|
@@ -4184,6 +4200,7 @@ function stateFor(config, cloudflare, previousCloudflareTunnelId) {
|
|
|
4184
4200
|
environment: config.environment,
|
|
4185
4201
|
databaseRole: config.database.role,
|
|
4186
4202
|
databaseAgency: config.database.agency,
|
|
4203
|
+
databaseReadPreferred: config.database.readPreferred,
|
|
4187
4204
|
databaseServerMode: config.database.serverMode,
|
|
4188
4205
|
databaseAddress: config.database.address,
|
|
4189
4206
|
databaseCoordinators: config.database.coordinators,
|
|
@@ -4688,7 +4705,7 @@ function strictBootstrapDocument(value) {
|
|
|
4688
4705
|
exactKeys(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
|
|
4689
4706
|
if (root.cloudflareHandoff !== undefined)
|
|
4690
4707
|
exactKeys(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
|
|
4691
|
-
exactKeys(root.database, ["role", "agency", "serverMode", "address", "master", "coordinators"], "database config");
|
|
4708
|
+
exactKeys(root.database, ["role", "agency", "readPreferred", "serverMode", "address", "master", "coordinators"], "database config");
|
|
4692
4709
|
exactKeys(root.enrolment, ["source"], "platform enrolment config");
|
|
4693
4710
|
const runtime = exactKeys(root.runtime, [
|
|
4694
4711
|
"environment",
|
|
@@ -4732,7 +4749,8 @@ function strictBootstrapDocument(value) {
|
|
|
4732
4749
|
"backup",
|
|
4733
4750
|
"cloudflare",
|
|
4734
4751
|
"realtime",
|
|
4735
|
-
"initialInventory"
|
|
4752
|
+
"initialInventory",
|
|
4753
|
+
"databaseReadPreferredCoordinators"
|
|
4736
4754
|
], "runtime environment");
|
|
4737
4755
|
const environment = runtime.environment;
|
|
4738
4756
|
if (environment.initialInventory !== undefined) {
|
|
@@ -47,6 +47,7 @@ export interface PlatformSharedEnvironment {
|
|
|
47
47
|
softwareProfile: PlatformSoftwareProfile;
|
|
48
48
|
databaseRole: PlatformDatabaseRole;
|
|
49
49
|
databaseCoordinators: string[];
|
|
50
|
+
databaseReadPreferredCoordinators?: string[];
|
|
50
51
|
databaseAddress?: string;
|
|
51
52
|
databaseMaster?: string;
|
|
52
53
|
databaseNetworkMode: 'private-lan';
|