@forgezero/agent 0.1.88 → 0.1.90
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 +23 -9
- package/dist/agent-heartbeat.d.ts +3 -0
- package/dist/agent-heartbeat.js +2 -1
- package/dist/bootstrap.d.ts +8 -1
- package/dist/bootstrap.js +241 -62
- package/dist/capacity-calibration.d.ts +35 -5
- package/dist/capacity-calibration.js +66 -22
- package/dist/cli/agent-install.d.ts +3 -0
- package/dist/community-rehearsal-host.js +7 -2
- package/dist/control.d.ts +3 -1
- package/dist/definition.js +83 -28
- package/dist/deploy-file.js +83 -28
- package/dist/deployment.d.ts +5 -0
- package/dist/egress-policy.d.ts +4 -0
- package/dist/fz-agent.js +450 -158
- package/dist/fz.js +429 -138
- package/dist/host-maintenance.js +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/metal-bootstrap.js +1 -1
- package/dist/metal-helper-socket.js +7 -2
- package/dist/metal-provision.js +7 -2
- package/dist/migration-pull.d.ts +1 -0
- package/dist/migration-pull.js +4 -0
- package/dist/operator-bootstrap.d.ts +12 -0
- package/dist/operator-bootstrap.js +385 -72
- package/dist/platform-bootstrap-runtime.d.ts +1 -0
- package/dist/platform-bootstrap-runtime.js +47 -8
- package/dist/platform-fleet-verification.js +273 -95
- package/dist/platform-genesis.js +7 -2
- package/dist/platform-launch-profile.d.ts +1 -0
- package/dist/provision.d.ts +5 -1
- package/dist/provision.js +192 -75
- package/dist/recovery-host.js +1 -1
- package/dist/software-helper.js +144 -48
- package/dist/software.js +7 -2
- package/dist/ssh-bootstrap.d.ts +1 -0
- package/dist/ubuntu.js +7 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/schema/deploy-v3.json +5 -2
|
@@ -1420,7 +1420,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1420
1420
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1421
1421
|
|
|
1422
1422
|
// src/version.ts
|
|
1423
|
-
var VERSION = "0.1.
|
|
1423
|
+
var VERSION = "0.1.90";
|
|
1424
1424
|
|
|
1425
1425
|
// src/software.ts
|
|
1426
1426
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -1698,6 +1698,7 @@ function agentEgressUnit(options) {
|
|
|
1698
1698
|
}
|
|
1699
1699
|
const deploymentEnabled = Boolean(options.repository || bootstrapBundleEnabled || options.pullDeployments);
|
|
1700
1700
|
const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
|
|
1701
|
+
const agentLoopbackPorts = normalizeEgressTcpPorts(options.agentLoopbackPorts ?? []);
|
|
1701
1702
|
const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
|
|
1702
1703
|
if (deploymentEnabled && runnerPublicTcpPorts.length < 1) {
|
|
1703
1704
|
throw new Error("deployed project runner needs at least one vetted public TCP port");
|
|
@@ -1705,8 +1706,10 @@ function agentEgressUnit(options) {
|
|
|
1705
1706
|
if (deploymentEnabled)
|
|
1706
1707
|
systemdAgentEgressDirectives(runnerLoopbackPorts);
|
|
1707
1708
|
const users = [user, ...deploymentEnabled ? [DEPLOYMENT_RUNNER_USER] : []];
|
|
1708
|
-
const
|
|
1709
|
-
const
|
|
1709
|
+
const portGrantEnabled = deploymentEnabled || agentLoopbackPorts.length > 0;
|
|
1710
|
+
const restrictedUser = deploymentEnabled ? DEPLOYMENT_RUNNER_USER : user;
|
|
1711
|
+
const portGrant = portGrantEnabled ? ` --loopback-user=${restrictedUser}` + (deploymentEnabled ? runnerLoopbackPorts.map((port) => ` --loopback-tcp-port=${port}`).join("") : "") + agentLoopbackPorts.map((port) => ` --shared-loopback-tcp-port=${port}`).join("") + (deploymentEnabled ? runnerPublicTcpPorts.map((port) => ` --public-tcp-port=${port}`).join("") : "") : "";
|
|
1712
|
+
const policyProof = `ExecStartPost=${bin} egress-policy-check ${users.map((name) => `--user=${name}`).join(" ")}${portGrant}`;
|
|
1710
1713
|
return `[Unit]
|
|
1711
1714
|
Description=ForgeZero Agent host egress policy
|
|
1712
1715
|
Documentation=https://www.forgezero.net/docs/agent
|
|
@@ -1719,7 +1722,7 @@ Type=notify
|
|
|
1719
1722
|
NotifyAccess=all
|
|
1720
1723
|
User=root
|
|
1721
1724
|
Group=root
|
|
1722
|
-
ExecStart=${bin} egress-policy ${users.map((name) => `--user=${name}`).join(" ")}${
|
|
1725
|
+
ExecStart=${bin} egress-policy ${users.map((name) => `--user=${name}`).join(" ")}${portGrant}
|
|
1723
1726
|
${policyProof}
|
|
1724
1727
|
Restart=on-failure
|
|
1725
1728
|
RestartSec=2
|
|
@@ -2003,7 +2006,7 @@ function agentEnrolmentUnit(options) {
|
|
|
2003
2006
|
Requires=forgezero-agent-egress.service
|
|
2004
2007
|
BindsTo=forgezero-agent-egress.service
|
|
2005
2008
|
` : "";
|
|
2006
|
-
const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
|
|
2009
|
+
const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives(options.agentLoopbackPorts ?? []) : "";
|
|
2007
2010
|
return `[Unit]
|
|
2008
2011
|
Description=Bind this machine to its ForgeZero compute
|
|
2009
2012
|
After=network-online.target
|
|
@@ -2040,7 +2043,7 @@ WantedBy=multi-user.target
|
|
|
2040
2043
|
function deploymentRunnerUnit(options) {
|
|
2041
2044
|
const bin = options.binPath ?? "fz-agent";
|
|
2042
2045
|
const root = options.deployRoot ?? "/opt/forgezero";
|
|
2043
|
-
const agentUser = options.user ?? "forgezero
|
|
2046
|
+
const agentUser = options.user ?? "forgezero";
|
|
2044
2047
|
const egressDependency = options.enforceEgress ? `After=forgezero-agent-egress.service
|
|
2045
2048
|
Requires=forgezero-agent-egress.service
|
|
2046
2049
|
BindsTo=forgezero-agent-egress.service
|
|
@@ -2081,7 +2084,7 @@ RestrictRealtime=true
|
|
|
2081
2084
|
MemoryDenyWriteExecute=true
|
|
2082
2085
|
LockPersonality=true
|
|
2083
2086
|
${egressDirectives}
|
|
2084
|
-
ReadWritePaths=${root}/releases ${root}/runner-home
|
|
2087
|
+
ReadWritePaths=${root}/releases ${root}/runner-home ${root}/slots /etc/nginx/conf.d /var/log/nginx
|
|
2085
2088
|
|
|
2086
2089
|
[Install]
|
|
2087
2090
|
WantedBy=multi-user.target
|
|
@@ -2091,7 +2094,7 @@ function agentUnit(options) {
|
|
|
2091
2094
|
if (!validNodeHostname(options.nodeHostname))
|
|
2092
2095
|
throw new Error("node hostname is invalid");
|
|
2093
2096
|
if (!options.telemetryEndpoint) {
|
|
2094
|
-
throw new Error("compute Agent provisioning requires
|
|
2097
|
+
throw new Error("compute Agent provisioning requires an explicit supervised OTLP collector coordinate");
|
|
2095
2098
|
}
|
|
2096
2099
|
let telemetryEndpoint;
|
|
2097
2100
|
{
|
|
@@ -2099,10 +2102,13 @@ function agentUnit(options) {
|
|
|
2099
2102
|
try {
|
|
2100
2103
|
endpoint2 = new URL(options.telemetryEndpoint);
|
|
2101
2104
|
} catch {
|
|
2102
|
-
throw new Error("compute telemetry endpoint must be an absolute
|
|
2105
|
+
throw new Error("compute telemetry endpoint must be an absolute collector URL");
|
|
2106
|
+
}
|
|
2107
|
+
const localCollector = endpoint2.protocol === "http:" && endpoint2.hostname === "127.0.0.1" && endpoint2.port === "4318" && endpoint2.pathname === "/";
|
|
2108
|
+
const publicCollector = endpoint2.protocol === "https:" && !endpoint2.username && !endpoint2.password && !endpoint2.search && !endpoint2.hash && isIP3(endpoint2.hostname) === 0 && endpoint2.hostname.includes(".") && endpoint2.hostname !== "localhost" && !endpoint2.hostname.endsWith(".local");
|
|
2109
|
+
if (!localCollector && !publicCollector || endpoint2.username || endpoint2.password || endpoint2.search || endpoint2.hash) {
|
|
2110
|
+
throw new Error("compute telemetry endpoint must be the supervised loopback collector or credential-free public HTTPS");
|
|
2103
2111
|
}
|
|
2104
|
-
if (endpoint2.protocol !== "https:" || endpoint2.username || endpoint2.password || endpoint2.search || endpoint2.hash || isIP3(endpoint2.hostname) !== 0 || !endpoint2.hostname.includes(".") || endpoint2.hostname === "localhost" || endpoint2.hostname.endsWith(".local"))
|
|
2105
|
-
throw new Error("compute telemetry endpoint must be an absolute public HTTPS URL without credentials, query or fragment");
|
|
2106
2112
|
telemetryEndpoint = endpoint2.toString().replace(/\/$/, "");
|
|
2107
2113
|
}
|
|
2108
2114
|
const bin = options.binPath ?? "fz-agent";
|
|
@@ -2216,7 +2222,7 @@ function agentUnit(options) {
|
|
|
2216
2222
|
const supplementaryGroups = [
|
|
2217
2223
|
AGENT_UPDATE_GROUP,
|
|
2218
2224
|
deploymentEnabled ? DEPLOYMENT_GROUP : null,
|
|
2219
|
-
|
|
2225
|
+
SOFTWARE_HELPER_GROUP,
|
|
2220
2226
|
lifecycleEnabled ? LIFECYCLE_GROUP : null
|
|
2221
2227
|
].filter((value) => value !== null);
|
|
2222
2228
|
const deploymentGroup = supplementaryGroups.length > 0 ? `SupplementaryGroups=${supplementaryGroups.join(" ")}` : "";
|
|
@@ -2225,7 +2231,7 @@ function agentUnit(options) {
|
|
|
2225
2231
|
"forgezero-agent-update-helper.service",
|
|
2226
2232
|
options.enforceEgress ? "forgezero-agent-egress.service" : null,
|
|
2227
2233
|
deploymentEnabled ? "forgezero-deploy-runner.service" : null,
|
|
2228
|
-
|
|
2234
|
+
"forgezero-software-helper.service",
|
|
2229
2235
|
lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
|
|
2230
2236
|
warpEnabled ? "warp-svc.service" : null,
|
|
2231
2237
|
enrolmentEnabled ? "forgezero-agent-enrol.service" : null
|
|
@@ -2234,7 +2240,7 @@ function agentUnit(options) {
|
|
|
2234
2240
|
"forgezero-agent-update-helper.service",
|
|
2235
2241
|
options.enforceEgress ? "forgezero-agent-egress.service" : null,
|
|
2236
2242
|
deploymentEnabled ? "forgezero-deploy-runner.service" : null,
|
|
2237
|
-
|
|
2243
|
+
"forgezero-software-helper.service",
|
|
2238
2244
|
lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
|
|
2239
2245
|
warpEnabled ? "warp-svc.service" : null,
|
|
2240
2246
|
enrolmentEnabled ? "forgezero-agent-enrol.service" : null
|
|
@@ -2249,9 +2255,9 @@ function agentUnit(options) {
|
|
|
2249
2255
|
const snpDevice = options.mode === "attested" ? `DevicePolicy=closed
|
|
2250
2256
|
DeviceAllow=/dev/sev-guest rw` : "";
|
|
2251
2257
|
const snpPrepare = options.mode === "attested" ? `ExecStartPre=+/bin/chgrp ${VAULT_GROUP} /dev/sev-guest
|
|
2252
|
-
ExecStartPre=+/bin/chmod
|
|
2258
|
+
ExecStartPre=+/bin/chmod 0660 /dev/sev-guest
|
|
2253
2259
|
` : "";
|
|
2254
|
-
const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
|
|
2260
|
+
const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives(options.agentLoopbackPorts ?? []) : "";
|
|
2255
2261
|
return `[Unit]
|
|
2256
2262
|
Description=ForgeZero node agent (${options.mode})
|
|
2257
2263
|
Documentation=https://www.forgezero.net/docs/agent
|
|
@@ -2382,6 +2388,7 @@ function planProvision(options) {
|
|
|
2382
2388
|
const deployRoot = options.deployRoot ?? "/opt/forgezero";
|
|
2383
2389
|
const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
|
|
2384
2390
|
const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
|
|
2391
|
+
const agentLoopbackPorts = normalizeEgressTcpPorts(options.agentLoopbackPorts ?? []);
|
|
2385
2392
|
const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
|
|
2386
2393
|
const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
|
|
2387
2394
|
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
@@ -2430,8 +2437,9 @@ function planProvision(options) {
|
|
|
2430
2437
|
const enabledUnits = [
|
|
2431
2438
|
"forgezero-agent.socket",
|
|
2432
2439
|
"forgezero-agent-update-helper.service",
|
|
2440
|
+
"forgezero-software-helper.service",
|
|
2433
2441
|
...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
|
|
2434
|
-
...deploymentEnabled ? ["forgezero-deploy-runner.service"
|
|
2442
|
+
...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
|
|
2435
2443
|
...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
|
|
2436
2444
|
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
2437
2445
|
...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
|
|
@@ -2439,8 +2447,9 @@ function planProvision(options) {
|
|
|
2439
2447
|
];
|
|
2440
2448
|
const restartedUnits = [
|
|
2441
2449
|
"forgezero-agent-update-helper.service",
|
|
2450
|
+
"forgezero-software-helper.service",
|
|
2442
2451
|
...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
|
|
2443
|
-
...deploymentEnabled ? ["forgezero-deploy-runner.service"
|
|
2452
|
+
...deploymentEnabled ? ["forgezero-deploy-runner.service"] : [],
|
|
2444
2453
|
...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
|
|
2445
2454
|
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
2446
2455
|
...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : []
|
|
@@ -2464,9 +2473,9 @@ function planProvision(options) {
|
|
|
2464
2473
|
...options.enforceEgress ? [
|
|
2465
2474
|
{ path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
|
|
2466
2475
|
] : [],
|
|
2476
|
+
{ path: SOFTWARE_HELPER_UNIT_PATH, unit: softwareHelperUnit(options) },
|
|
2467
2477
|
...deploymentEnabled ? [
|
|
2468
|
-
{ path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) }
|
|
2469
|
-
{ path: SOFTWARE_HELPER_UNIT_PATH, unit: softwareHelperUnit(options) }
|
|
2478
|
+
{ path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) }
|
|
2470
2479
|
] : [],
|
|
2471
2480
|
...enrolmentEnabled ? [
|
|
2472
2481
|
{ path: ENROLMENT_UNIT_PATH, unit: agentEnrolmentUnit(options) }
|
|
@@ -2499,24 +2508,29 @@ function planProvision(options) {
|
|
|
2499
2508
|
step("Agent update helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", AGENT_UPDATE_GROUP], acceptedExitCodes: [0, 9] }] }),
|
|
2500
2509
|
...sourceBinPath && binPath ? [step("root-owned agent runtime", { kind: "install-runtime", source: sourceBinPath, binary: binPath, version: VERSION })] : [],
|
|
2501
2510
|
...warpEnabled ? [step("Cloudflare One client for Ubuntu 26.04", { kind: "install-warp" })] : [],
|
|
2502
|
-
|
|
2503
|
-
{ argv: ["/usr/sbin/groupadd", "--system", DEPLOYMENT_GROUP], acceptedExitCodes: [0, 9] },
|
|
2511
|
+
step("software strategy helper group", { kind: "commands", commands: [
|
|
2504
2512
|
{ argv: ["/usr/sbin/groupadd", "--system", SOFTWARE_HELPER_GROUP], acceptedExitCodes: [0, 9] }
|
|
2513
|
+
] }),
|
|
2514
|
+
...deploymentEnabled ? [step("deployment isolation group", { kind: "commands", commands: [
|
|
2515
|
+
{ argv: ["/usr/sbin/groupadd", "--system", DEPLOYMENT_GROUP], acceptedExitCodes: [0, 9] }
|
|
2505
2516
|
] })] : [],
|
|
2506
2517
|
...lifecycleEnabled ? [step("lifecycle helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", LIFECYCLE_GROUP], acceptedExitCodes: [0, 9] }] })] : [],
|
|
2507
2518
|
step("service account", { kind: "commands", commands: [{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", user], acceptedExitCodes: [0, 9] }] }),
|
|
2508
2519
|
step("bind service account to vault group", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-g", VAULT_GROUP, user] }] }),
|
|
2509
2520
|
step("grant verified Agent update access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", AGENT_UPDATE_GROUP, user] }] }),
|
|
2521
|
+
step("grant software helper socket access", { kind: "commands", commands: [
|
|
2522
|
+
{ argv: ["/usr/sbin/usermod", "-a", "-G", SOFTWARE_HELPER_GROUP, user] }
|
|
2523
|
+
] }),
|
|
2510
2524
|
...lifecycleEnabled ? [step("grant lifecycle helper socket access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", LIFECYCLE_GROUP, user] }] })] : [],
|
|
2511
2525
|
...deploymentEnabled ? [step("credential-free deployment account", { kind: "commands", commands: [
|
|
2512
2526
|
{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", DEPLOYMENT_GROUP, DEPLOYMENT_RUNNER_USER], acceptedExitCodes: [0, 9] },
|
|
2513
2527
|
{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", VAULT_GROUP, APPLICATION_RUNTIME_USER], acceptedExitCodes: [0, 9] },
|
|
2514
2528
|
{ argv: ["/usr/sbin/usermod", "-a", "-G", DEPLOYMENT_GROUP, APPLICATION_RUNTIME_USER] },
|
|
2515
|
-
{ argv: ["/usr/sbin/usermod", "-a", "-G",
|
|
2529
|
+
{ argv: ["/usr/sbin/usermod", "-a", "-G", DEPLOYMENT_GROUP, user] }
|
|
2516
2530
|
] })] : [],
|
|
2517
2531
|
step("credential and state directories", { kind: "directories", directories: [
|
|
2518
2532
|
{ path: credentialDir, mode: 448, owner: "root", group: "root" },
|
|
2519
|
-
{ path: "/var/lib/forgezero", mode: 488, owner: "root", group:
|
|
2533
|
+
{ path: "/var/lib/forgezero", mode: 488, owner: "root", group: VAULT_GROUP }
|
|
2520
2534
|
] }),
|
|
2521
2535
|
step("encrypted node identity", { kind: "ensure-seed", credential: seedCredentialPath }),
|
|
2522
2536
|
...options.generateGitIdentity && gitCredentialPath && gitPublicKeyPath ? [
|
|
@@ -2567,19 +2581,26 @@ function planProvision(options) {
|
|
|
2567
2581
|
{ argv: ["/usr/bin/systemctl", "enable", ...enabledUnits] },
|
|
2568
2582
|
{ argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
|
|
2569
2583
|
...restartedUnits.length ? [{ argv: ["/usr/bin/systemctl", "restart", ...restartedUnits] }] : [],
|
|
2584
|
+
{ argv: ["/usr/bin/systemctl", "stop", "forgezero-agent-proxy.service"], acceptedExitCodes: [0, 1, 5] },
|
|
2570
2585
|
{ argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.socket"] },
|
|
2571
2586
|
{ argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
|
|
2572
2587
|
{ argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.service"] }
|
|
2573
2588
|
] }),
|
|
2574
2589
|
...enrolmentEnabled ? [step("prove the compute binding is durable", { kind: "verify-file", path: enrolStatePath })] : [],
|
|
2575
|
-
...options.enforceEgress ? [step("prove the Agent egress policy is active", {
|
|
2590
|
+
...options.enforceEgress ? [step("prove the Agent egress policy is active", {
|
|
2591
|
+
kind: "verify-egress",
|
|
2592
|
+
deploymentEnabled,
|
|
2593
|
+
runnerPublicTcpPorts,
|
|
2594
|
+
runnerLoopbackPorts,
|
|
2595
|
+
agentLoopbackPorts
|
|
2596
|
+
})] : [],
|
|
2576
2597
|
step("prove it is running", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "is-active", "forgezero-agent.service"] }] }),
|
|
2577
2598
|
step("prove the public Vault socket exists", { kind: "wait-socket", path: options.socketPath, attempts: 100, intervalMs: 100 }),
|
|
2578
2599
|
step("prove the Agent Vault backend exists", { kind: "wait-socket", path: agentBackendSocketPath(options.socketPath), attempts: 100, intervalMs: 100 }),
|
|
2579
2600
|
step("prove the Agent update helper exists", { kind: "wait-socket", path: DEFAULT_AGENT_UPDATE_SOCKET, attempts: 100, intervalMs: 100 }),
|
|
2601
|
+
step("prove the software strategy helper socket exists", { kind: "wait-socket", path: DEFAULT_SOFTWARE_HELPER_SOCKET, attempts: 100, intervalMs: 100 }),
|
|
2580
2602
|
...deploymentEnabled ? [
|
|
2581
|
-
step("prove the deployment runner socket exists", { kind: "wait-socket", path: DEPLOYMENT_RUNNER_SOCKET, attempts: 100, intervalMs: 100 })
|
|
2582
|
-
step("prove the software strategy helper socket exists", { kind: "wait-socket", path: DEFAULT_SOFTWARE_HELPER_SOCKET, attempts: 100, intervalMs: 100 })
|
|
2603
|
+
step("prove the deployment runner socket exists", { kind: "wait-socket", path: DEPLOYMENT_RUNNER_SOCKET, attempts: 100, intervalMs: 100 })
|
|
2583
2604
|
] : [],
|
|
2584
2605
|
...lifecycleEnabled ? [step("prove the lifecycle helper socket exists", { kind: "wait-socket", path: lifecycleHelperSocketPath, attempts: 100, intervalMs: 100 })] : [],
|
|
2585
2606
|
...warpEnabled ? [step("prove Cloudflare WARP is connected", { kind: "verify-warp" })] : [],
|
|
@@ -2604,6 +2625,25 @@ import {
|
|
|
2604
2625
|
writeFileSync as writeFileSync4
|
|
2605
2626
|
} from "fs";
|
|
2606
2627
|
import { dirname as dirname5 } from "path";
|
|
2628
|
+
function normalizeEd25519PublicKey(output) {
|
|
2629
|
+
const line = output.trim();
|
|
2630
|
+
if (!/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}(?: [^\r\n]{1,256})?$/.test(line))
|
|
2631
|
+
return;
|
|
2632
|
+
const [algorithm, encoded] = line.split(" ", 3);
|
|
2633
|
+
if (algorithm !== "ssh-ed25519" || !encoded)
|
|
2634
|
+
return;
|
|
2635
|
+
let blob;
|
|
2636
|
+
try {
|
|
2637
|
+
blob = Buffer.from(encoded, "base64");
|
|
2638
|
+
} catch {
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
if (blob.length !== 51 || blob.readUInt32BE(0) !== 11 || blob.subarray(4, 15).toString("ascii") !== "ssh-ed25519" || blob.readUInt32BE(15) !== 32)
|
|
2642
|
+
return;
|
|
2643
|
+
if (blob.toString("base64") !== encoded)
|
|
2644
|
+
return;
|
|
2645
|
+
return `${algorithm} ${encoded}`;
|
|
2646
|
+
}
|
|
2607
2647
|
async function readCapabilities(run2) {
|
|
2608
2648
|
const answers = {};
|
|
2609
2649
|
const checks = Object.entries(CAPABILITY_CHECKS);
|
|
@@ -2743,7 +2783,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
2743
2783
|
return result;
|
|
2744
2784
|
}
|
|
2745
2785
|
result = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
2746
|
-
if (result.exitCode !== 0 ||
|
|
2786
|
+
if (result.exitCode !== 0 || !normalizeEd25519PublicKey(result.stdout)) {
|
|
2747
2787
|
return { stdout: "bootstrap SSH private key is not a valid Ed25519 OpenSSH key", exitCode: 1 };
|
|
2748
2788
|
}
|
|
2749
2789
|
result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
|
|
@@ -2759,11 +2799,12 @@ var runProvisionOperation = async (operation) => {
|
|
|
2759
2799
|
chmodSync2(key, 384);
|
|
2760
2800
|
}
|
|
2761
2801
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
2762
|
-
|
|
2802
|
+
const publicKey = normalizeEd25519PublicKey(derived.stdout);
|
|
2803
|
+
if (derived.exitCode !== 0 || !publicKey) {
|
|
2763
2804
|
return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
|
|
2764
2805
|
}
|
|
2765
2806
|
mkdirSync4(dirname5(operation.publicKey), { recursive: true, mode: 493 });
|
|
2766
|
-
writeFileSync4(operation.publicKey, `${
|
|
2807
|
+
writeFileSync4(operation.publicKey, `${publicKey} forgezero-bootstrap-runner
|
|
2767
2808
|
`, { mode: 292 });
|
|
2768
2809
|
chmodSync2(operation.publicKey, 292);
|
|
2769
2810
|
}
|
|
@@ -2806,8 +2847,9 @@ var runProvisionOperation = async (operation) => {
|
|
|
2806
2847
|
const policy = await fixed(["/usr/sbin/nft", "--numeric", "list", "table", "inet", "forgezero_agent_egress"]);
|
|
2807
2848
|
const required = [
|
|
2808
2849
|
"forgezero-agent-egress-v1",
|
|
2809
|
-
`
|
|
2810
|
-
...operation.
|
|
2850
|
+
...operation.agentLoopbackPorts.length ? [`shared-loopback=${operation.agentLoopbackPorts.join(",")}`] : [],
|
|
2851
|
+
...operation.deploymentEnabled && operation.runnerPublicTcpPorts.length ? [`public-tcp=${operation.runnerPublicTcpPorts.join(",")}`] : [],
|
|
2852
|
+
...operation.deploymentEnabled && operation.runnerLoopbackPorts.length ? [`:${operation.runnerLoopbackPorts.join(",")}`] : []
|
|
2811
2853
|
];
|
|
2812
2854
|
return policy.exitCode === 0 && required.every((part) => policy.stdout.includes(part)) ? { stdout: policy.stdout, exitCode: 0 } : { stdout: policy.stdout, exitCode: 1 };
|
|
2813
2855
|
}
|
|
@@ -2873,7 +2915,9 @@ async function applyPlan(plan, run2) {
|
|
|
2873
2915
|
const result = await run2(step2.operation);
|
|
2874
2916
|
transcript.push({ label: step2.label, command: step2.command, exitCode: result.exitCode });
|
|
2875
2917
|
if (result.exitCode !== 0 && !step2.optional) {
|
|
2876
|
-
|
|
2918
|
+
const safeDetail = step2.operation.kind === "ensure-bootstrap-ssh-identity" ? result.stdout.trim().slice(0, 1000) : "";
|
|
2919
|
+
throw new Error(`${step2.label} failed (exit ${result.exitCode}): ${step2.command}${safeDetail ? `
|
|
2920
|
+
${safeDetail}` : ""}`);
|
|
2877
2921
|
}
|
|
2878
2922
|
}
|
|
2879
2923
|
return transcript;
|
|
@@ -3371,12 +3415,26 @@ map $limit_conn_status $forgezero_retry_after { default ''; REJECTED 1; REJECTED
|
|
|
3371
3415
|
server {
|
|
3372
3416
|
listen 127.0.0.1:${input.publicPort};
|
|
3373
3417
|
server_name _;
|
|
3418
|
+
server_tokens off;
|
|
3419
|
+
more_clear_headers Server X-Powered-By;
|
|
3374
3420
|
limit_conn forgezero_admission ${concurrencyLimit};
|
|
3375
3421
|
limit_conn_status 503;
|
|
3376
3422
|
add_header Retry-After $forgezero_retry_after always;
|
|
3377
|
-
|
|
3378
|
-
location
|
|
3379
|
-
location / {
|
|
3423
|
+
error_page 400 403 404 405 408 413 414 429 500 502 503 504 = @transport_failure;
|
|
3424
|
+
location @transport_failure { internal; return 444; }
|
|
3425
|
+
location / {
|
|
3426
|
+
proxy_pass http://forgezero;
|
|
3427
|
+
proxy_http_version 1.1;
|
|
3428
|
+
proxy_intercept_errors off;
|
|
3429
|
+
proxy_hide_header Server;
|
|
3430
|
+
proxy_hide_header X-Powered-By;
|
|
3431
|
+
proxy_set_header Host $host;
|
|
3432
|
+
proxy_set_header X-Forwarded-Proto https;
|
|
3433
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
3434
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
3435
|
+
proxy_set_header Connection $forgezero_connection;
|
|
3436
|
+
proxy_read_timeout 3600s;
|
|
3437
|
+
}
|
|
3380
3438
|
}
|
|
3381
3439
|
`
|
|
3382
3440
|
};
|
|
@@ -3988,7 +4046,7 @@ Type=oneshot
|
|
|
3988
4046
|
User=arangodb
|
|
3989
4047
|
Group=arangodb
|
|
3990
4048
|
LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
|
|
3991
|
-
ExecStart=/usr/bin/arangosh --server.endpoint tcp://${unitEscape(address)}:8529 --server.jwt-secret-keyfile %d/arangodb-jwt --javascript.execute-string 'const c=require("@arangodb").db._connection;let last;for(let i=0;i<90;i++){try{const v=c.GET("/_api/version?details=true");const s=c.GET("/_admin/status");const m=c.GET("/_admin/server/mode");if(!v.error&&v.version==="3.11.14"&&v.details&&v.details.license==="community"&&!s.error&&s.serverInfo&&s.serverInfo.role==="COORDINATOR"&&!m.error&&m.mode==="default")
|
|
4049
|
+
ExecStart=/usr/bin/arangosh --server.endpoint tcp://${unitEscape(address)}:8529 --server.jwt-secret-keyfile %d/arangodb-jwt --javascript.execute-string 'const c=require("@arangodb").db._connection;let last;let ok=false;for(let i=0;i<90;i++){try{const v=c.GET("/_api/version?details=true");const s=c.GET("/_admin/status");const m=c.GET("/_admin/server/mode");if(!v.error&&v.version==="3.11.14"&&v.details&&v.details.license==="community"&&!s.error&&s.serverInfo&&s.serverInfo.role==="COORDINATOR"&&!m.error&&m.mode==="default"){ok=true;break;}last={v,s,m};}catch(e){last=String(e);}require("internal").wait(2);}if(!ok)throw new Error("writable Community Coordinator verification failed: "+JSON.stringify(last));'
|
|
3992
4050
|
RemainAfterExit=yes
|
|
3993
4051
|
TimeoutStartSec=200
|
|
3994
4052
|
NoNewPrivileges=true
|
|
@@ -4114,11 +4172,11 @@ async function seal(host, name, destination2, value) {
|
|
|
4114
4172
|
async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
|
|
4115
4173
|
const manifestMetadata = host.inspect?.(config.bootstrapBundle.manifestFile);
|
|
4116
4174
|
const bundleMetadata = host.inspect?.(config.bootstrapBundle.bundleFile);
|
|
4117
|
-
if (manifestMetadata && (!manifestMetadata.regular || manifestMetadata.symbolic || manifestMetadata.uid !== 0 || manifestMetadata.links !== 1 || (manifestMetadata.mode &
|
|
4118
|
-
throw new Error("bootstrap bundle manifest must be a root-owned
|
|
4175
|
+
if (manifestMetadata && (!manifestMetadata.regular || manifestMetadata.symbolic || manifestMetadata.uid !== 0 || manifestMetadata.links !== 1 || (manifestMetadata.mode & 18) !== 0 || manifestMetadata.size > 16 * 1024)) {
|
|
4176
|
+
throw new Error("bootstrap bundle manifest must be a root-owned non-writable regular file");
|
|
4119
4177
|
}
|
|
4120
|
-
if (bundleMetadata && (!bundleMetadata.regular || bundleMetadata.symbolic || bundleMetadata.uid !== 0 || bundleMetadata.links !== 1 || (bundleMetadata.mode &
|
|
4121
|
-
throw new Error("bootstrap bundle must be a root-owned
|
|
4178
|
+
if (bundleMetadata && (!bundleMetadata.regular || bundleMetadata.symbolic || bundleMetadata.uid !== 0 || bundleMetadata.links !== 1 || (bundleMetadata.mode & 18) !== 0 || bundleMetadata.size < 1 || bundleMetadata.size > 512 * 1024 * 1024)) {
|
|
4179
|
+
throw new Error("bootstrap bundle must be a root-owned non-writable regular file within 512 MiB");
|
|
4122
4180
|
}
|
|
4123
4181
|
if (!host.exists(config.bootstrapBundle.bundleFile) || !host.exists(config.bootstrapBundle.manifestFile)) {
|
|
4124
4182
|
throw new Error("attended bootstrap bundle and manifest are required for release generation one");
|
|
@@ -4137,11 +4195,25 @@ async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
|
|
|
4137
4195
|
if (digest !== manifest.sha256)
|
|
4138
4196
|
throw new Error("bootstrap bundle digest does not match its manifest");
|
|
4139
4197
|
if (verifyGit) {
|
|
4140
|
-
|
|
4198
|
+
const verificationRepository = "/run/forgezero-bootstrap-bundle-verify";
|
|
4199
|
+
await checked3(host, ["/usr/bin/rm", "-rf", "--", verificationRepository], "stale bootstrap bundle verifier cleanup");
|
|
4200
|
+
await checked3(host, ["/usr/bin/git", "init", "--bare", "--quiet", verificationRepository], "bootstrap bundle verifier repository");
|
|
4201
|
+
try {
|
|
4202
|
+
await checked3(host, [
|
|
4203
|
+
"/usr/bin/git",
|
|
4204
|
+
"-C",
|
|
4205
|
+
verificationRepository,
|
|
4206
|
+
"bundle",
|
|
4207
|
+
"verify",
|
|
4208
|
+
config.bootstrapBundle.bundleFile
|
|
4209
|
+
], "bootstrap Git bundle verification");
|
|
4210
|
+
} finally {
|
|
4211
|
+
await checked3(host, ["/usr/bin/rm", "-rf", "--", verificationRepository], "bootstrap bundle verifier cleanup");
|
|
4212
|
+
}
|
|
4141
4213
|
}
|
|
4142
4214
|
return manifest;
|
|
4143
4215
|
}
|
|
4144
|
-
function bootstrapIdentity(config) {
|
|
4216
|
+
function bootstrapIdentity(config, includeDeploymentEvidence = false) {
|
|
4145
4217
|
if (config.kind === "enrolled-compute")
|
|
4146
4218
|
return {
|
|
4147
4219
|
kind: config.kind,
|
|
@@ -4160,7 +4232,13 @@ function bootstrapIdentity(config) {
|
|
|
4160
4232
|
bootstrapRunner: config.bootstrapRunner ? { targetTelemetryEndpoint: config.bootstrapRunner.targetTelemetryEndpoint } : null
|
|
4161
4233
|
};
|
|
4162
4234
|
const environment = config.runtime.environment;
|
|
4163
|
-
const { cloudflare: _cloudflare, realtime: _realtime, ...
|
|
4235
|
+
const { cloudflare: _cloudflare, realtime: _realtime, initialInventory, ...stableEnvironmentRest } = environment;
|
|
4236
|
+
const stableEnvironment = {
|
|
4237
|
+
...stableEnvironmentRest,
|
|
4238
|
+
...initialInventory ? {
|
|
4239
|
+
initialInventory: { ...initialInventory, deployment: includeDeploymentEvidence ? initialInventory.deployment : undefined }
|
|
4240
|
+
} : {}
|
|
4241
|
+
};
|
|
4164
4242
|
return {
|
|
4165
4243
|
kind: config.kind,
|
|
4166
4244
|
environment: config.environment,
|
|
@@ -4201,6 +4279,26 @@ function bootstrapIdentity(config) {
|
|
|
4201
4279
|
function bootstrapIdentityDigest(config) {
|
|
4202
4280
|
return createHash2("sha256").update(JSON.stringify(bootstrapIdentity(config))).digest("hex");
|
|
4203
4281
|
}
|
|
4282
|
+
var legacyBootstrapIdentityDigest = (config) => createHash2("sha256").update(JSON.stringify(bootstrapIdentity(config, true))).digest("hex");
|
|
4283
|
+
function interruptedPlatformReleaseIdentityDigests(config, releaseEvidence, bootstrapManifest) {
|
|
4284
|
+
if (!config.runtime.environment.initialInventory)
|
|
4285
|
+
return [];
|
|
4286
|
+
const previous = structuredClone(config);
|
|
4287
|
+
previous.runtime.environment.initialInventory.deployment = {
|
|
4288
|
+
source: "bootstrap-bundle",
|
|
4289
|
+
branch: releaseEvidence.branch,
|
|
4290
|
+
revision: releaseEvidence.revision,
|
|
4291
|
+
bundleSha256: releaseEvidence.sha256
|
|
4292
|
+
};
|
|
4293
|
+
const expectedCurrentEpoch = bootstrapManifest ? `${config.environment}-${bootstrapManifest.revision.slice(0, 16)}` : undefined;
|
|
4294
|
+
if (expectedCurrentEpoch && config.runtime.environment.seedSyncEpoch === expectedCurrentEpoch) {
|
|
4295
|
+
previous.runtime.environment.seedSyncEpoch = `${config.environment}-${releaseEvidence.revision.slice(0, 16)}`;
|
|
4296
|
+
}
|
|
4297
|
+
return [bootstrapIdentityDigest(previous), legacyBootstrapIdentityDigest(previous)];
|
|
4298
|
+
}
|
|
4299
|
+
function storedPlatformCoordinatesMatch(state, config) {
|
|
4300
|
+
return state.kind === "platform" && state.profile === config.profile && state.nodeHostname === config.nodeHostname && state.apiUrl === config.apiUrl && state.environment === config.environment && state.databaseRole === config.database.role && state.databaseAgency === config.database.agency && state.databaseReadPreferred === config.database.readPreferred && state.databaseServerMode === config.database.serverMode && (state.databaseAddress ?? null) === (config.database.address ?? null) && JSON.stringify(state.databaseCoordinators ?? []) === JSON.stringify(config.database.coordinators) && state.collectorUnit === config.runtime.environment.otlpCollectorUnit && state.publicApiPort === config.runtime.environment.publicApiPort && state.healthPath === config.runtime.healthPath;
|
|
4301
|
+
}
|
|
4204
4302
|
function parseStoredState(raw) {
|
|
4205
4303
|
let value;
|
|
4206
4304
|
try {
|
|
@@ -4231,11 +4329,21 @@ function parseStoredIntent(raw) {
|
|
|
4231
4329
|
}
|
|
4232
4330
|
return intent;
|
|
4233
4331
|
}
|
|
4234
|
-
function bindBootstrapIntent(host, config) {
|
|
4332
|
+
function bindBootstrapIntent(host, config, previousReleaseDigests = []) {
|
|
4235
4333
|
const identityDigest = bootstrapIdentityDigest(config);
|
|
4236
4334
|
if (host.exists(INTENT_PATH)) {
|
|
4237
4335
|
const intent = parseStoredIntent(host.read(INTENT_PATH));
|
|
4238
4336
|
if (intent.kind !== config.kind || intent.identityDigest !== identityDigest) {
|
|
4337
|
+
if (previousReleaseDigests.includes(intent.identityDigest) && intent.kind === "platform" && config.kind === "platform") {
|
|
4338
|
+
host.write(INTENT_PATH, `${JSON.stringify({
|
|
4339
|
+
format: 1,
|
|
4340
|
+
kind: config.kind,
|
|
4341
|
+
identityDigest,
|
|
4342
|
+
createdAt: new Date().toISOString()
|
|
4343
|
+
}, null, 2)}
|
|
4344
|
+
`, 384);
|
|
4345
|
+
return identityDigest;
|
|
4346
|
+
}
|
|
4239
4347
|
throw new Error("bootstrap resume coordinates do not match the interrupted host intent");
|
|
4240
4348
|
}
|
|
4241
4349
|
} else if (!host.exists(STATE_PATH)) {
|
|
@@ -4310,7 +4418,7 @@ async function bootstrapStatus(host = localBootstrapHost()) {
|
|
|
4310
4418
|
if (result.exitCode !== 0)
|
|
4311
4419
|
problems.push(`${unit} is not active`);
|
|
4312
4420
|
}
|
|
4313
|
-
for (const socket of [DEFAULT_SOCKET2
|
|
4421
|
+
for (const socket of [DEFAULT_SOCKET2]) {
|
|
4314
4422
|
services[socket] = host.exists(socket);
|
|
4315
4423
|
if (!services[socket])
|
|
4316
4424
|
problems.push(`${socket} is missing`);
|
|
@@ -4403,7 +4511,42 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4403
4511
|
let installed;
|
|
4404
4512
|
if (host.exists(STATE_PATH))
|
|
4405
4513
|
installed = parseStoredState(host.read(STATE_PATH));
|
|
4406
|
-
|
|
4514
|
+
let bootstrapManifest;
|
|
4515
|
+
let releaseEvidence;
|
|
4516
|
+
if (config.kind === "platform") {
|
|
4517
|
+
const staged = host.exists(config.bootstrapBundle.bundleFile) || host.exists(config.bootstrapBundle.manifestFile);
|
|
4518
|
+
if (staged && !(host.exists(config.bootstrapBundle.bundleFile) && host.exists(config.bootstrapBundle.manifestFile))) {
|
|
4519
|
+
throw new Error("bootstrap bundle and manifest must be staged together");
|
|
4520
|
+
}
|
|
4521
|
+
const reviewed = staged ? await verifyBootstrapBundleOnHost(host, config) : undefined;
|
|
4522
|
+
if (!host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
|
|
4523
|
+
if (!reviewed)
|
|
4524
|
+
throw new Error("attended bootstrap bundle and manifest are required for release generation one");
|
|
4525
|
+
bootstrapManifest = reviewed;
|
|
4526
|
+
} else if (reviewed) {
|
|
4527
|
+
let evidence;
|
|
4528
|
+
try {
|
|
4529
|
+
evidence = JSON.parse(host.read(BOOTSTRAP_RELEASE_EVIDENCE));
|
|
4530
|
+
} catch {
|
|
4531
|
+
throw new Error("bootstrap release evidence is malformed");
|
|
4532
|
+
}
|
|
4533
|
+
if (typeof evidence.revision !== "string" || !/^[a-f0-9]{40}$/.test(evidence.revision) || typeof evidence.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(evidence.sha256) || typeof evidence.branch !== "string")
|
|
4534
|
+
throw new Error("bootstrap release evidence is malformed");
|
|
4535
|
+
releaseEvidence = { revision: evidence.revision, sha256: evidence.sha256, branch: evidence.branch };
|
|
4536
|
+
if (evidence.revision !== reviewed.revision || evidence.sha256 !== reviewed.sha256 || evidence.branch !== reviewed.branch)
|
|
4537
|
+
bootstrapManifest = reviewed;
|
|
4538
|
+
} else if (host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
|
|
4539
|
+
let evidence;
|
|
4540
|
+
try {
|
|
4541
|
+
evidence = JSON.parse(host.read(BOOTSTRAP_RELEASE_EVIDENCE));
|
|
4542
|
+
} catch {
|
|
4543
|
+
throw new Error("bootstrap release evidence is malformed");
|
|
4544
|
+
}
|
|
4545
|
+
if (typeof evidence.revision !== "string" || !/^[a-f0-9]{40}$/.test(evidence.revision) || typeof evidence.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(evidence.sha256) || typeof evidence.branch !== "string")
|
|
4546
|
+
throw new Error("bootstrap release evidence is malformed");
|
|
4547
|
+
releaseEvidence = { revision: evidence.revision, sha256: evidence.sha256, branch: evidence.branch };
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4407
4550
|
let cloudflare;
|
|
4408
4551
|
if (config.cloudflareHandoff) {
|
|
4409
4552
|
if (host.exists(config.cloudflareHandoff.handoffFile)) {
|
|
@@ -4453,10 +4596,14 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4453
4596
|
throw new Error("platform realtime coordinates require a realtime-enabled Cloudflare handoff");
|
|
4454
4597
|
}
|
|
4455
4598
|
}
|
|
4599
|
+
let allowInterruptedReleaseRebind = false;
|
|
4456
4600
|
if (installed) {
|
|
4457
|
-
|
|
4601
|
+
const exactIdentity = installed.kind === config.kind && installed.identityDigest === bootstrapIdentityDigest(config);
|
|
4602
|
+
const boundedReleaseResume = config.kind === "platform" && !host.exists("/var/lib/forgezero/enrolment.json") && host.exists(BOOTSTRAP_RELEASE_EVIDENCE) && storedPlatformCoordinatesMatch(installed, config);
|
|
4603
|
+
if (!exactIdentity && !boundedReleaseResume) {
|
|
4458
4604
|
throw new Error("bootstrap repair coordinates do not match the installed host identity");
|
|
4459
4605
|
}
|
|
4606
|
+
allowInterruptedReleaseRebind = !exactIdentity && boundedReleaseResume;
|
|
4460
4607
|
}
|
|
4461
4608
|
const platformPrivate = config.kind === "platform" ? (() => {
|
|
4462
4609
|
if (!secrets)
|
|
@@ -4476,10 +4623,14 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4476
4623
|
if (config.kind === "enrolled-compute" && config.bootstrapRunner?.sshPrivateKeyFile && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
|
|
4477
4624
|
privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key");
|
|
4478
4625
|
}
|
|
4479
|
-
|
|
4626
|
+
let previousReleaseDigests = [];
|
|
4627
|
+
if (!installed && config.kind === "platform" && releaseEvidence && !host.exists("/var/lib/forgezero/enrolment.json")) {
|
|
4628
|
+
previousReleaseDigests = [...interruptedPlatformReleaseIdentityDigests(config, releaseEvidence, bootstrapManifest)];
|
|
4629
|
+
}
|
|
4630
|
+
bindBootstrapIntent(host, config, allowInterruptedReleaseRebind && installed?.identityDigest ? [installed.identityDigest] : previousReleaseDigests);
|
|
4480
4631
|
const plan = planBootstrap(config, host.exists(STATE_PATH));
|
|
4481
4632
|
await checked3(host, ["hostnamectl", "set-hostname", "--static", config.nodeHostname], "compute hostname");
|
|
4482
|
-
|
|
4633
|
+
let alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
|
|
4483
4634
|
host.mkdir(CREDS, 448);
|
|
4484
4635
|
host.mkdir("/var/lib/forgezero", 448);
|
|
4485
4636
|
if (!alreadyEnrolled && !host.exists(ENROL_CREDENTIAL)) {
|
|
@@ -4508,14 +4659,29 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4508
4659
|
if (connectorCapabilities?.meshConnectorToken && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
|
|
4509
4660
|
await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, connectorCapabilities.meshConnectorToken);
|
|
4510
4661
|
}
|
|
4662
|
+
let installedAgentPlan;
|
|
4511
4663
|
if (alreadyEnrolled) {
|
|
4512
|
-
await host.installAgent(config, "bound");
|
|
4664
|
+
installedAgentPlan = await host.installAgent(config, bootstrapManifest ? "bootstrap" : "bound");
|
|
4513
4665
|
} else if (config.kind === "platform") {
|
|
4514
4666
|
if (bootstrapManifest)
|
|
4515
|
-
await host.installAgent(config, "bootstrap");
|
|
4667
|
+
installedAgentPlan = await host.installAgent(config, "bootstrap");
|
|
4668
|
+
else if (host.exists(BOOTSTRAP_RELEASE_EVIDENCE)) {
|
|
4669
|
+
await checked3(host, [
|
|
4670
|
+
"curl",
|
|
4671
|
+
"--fail",
|
|
4672
|
+
"--silent",
|
|
4673
|
+
"--show-error",
|
|
4674
|
+
"--max-time",
|
|
4675
|
+
"10",
|
|
4676
|
+
`http://127.0.0.1:${config.runtime.environment.publicApiPort}${config.runtime.healthPath}`
|
|
4677
|
+
], "persisted release-one API health");
|
|
4678
|
+
await host.installAgent(config, "enrol");
|
|
4679
|
+
installedAgentPlan = await host.installAgent(config, "bound");
|
|
4680
|
+
alreadyEnrolled = true;
|
|
4681
|
+
}
|
|
4516
4682
|
} else {
|
|
4517
4683
|
await host.installAgent(config, "enrol");
|
|
4518
|
-
await host.installAgent(config, "bound");
|
|
4684
|
+
installedAgentPlan = await host.installAgent(config, "bound");
|
|
4519
4685
|
}
|
|
4520
4686
|
if (config.kind === "platform" && config.firewall.enabled) {
|
|
4521
4687
|
await host.ensureSoftware(plan.software.filter(({ id }) => id === "ufw"));
|
|
@@ -4595,6 +4761,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4595
4761
|
await checked3(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
|
|
4596
4762
|
await checked3(host, ["id", runtime.serviceUser], "existing API service account");
|
|
4597
4763
|
});
|
|
4764
|
+
await checked3(host, ["usermod", "-a", "-G", DEPLOYMENT_GROUP, runtime.serviceUser], "API deployment group");
|
|
4598
4765
|
host.mkdir(runtime.environment.sharedDirectory, 488);
|
|
4599
4766
|
host.mkdir(runtime.slotsDirectory, 493);
|
|
4600
4767
|
host.write(envPath, renderPlatformSharedEnvironment(runtime.environment), 416);
|
|
@@ -4613,7 +4780,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4613
4780
|
host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
|
|
4614
4781
|
await checked3(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
|
|
4615
4782
|
const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
|
|
4616
|
-
await checked3(host, telemetry.unitCheck.argv, "OTLP collector supervision");
|
|
4783
|
+
await checked3(host, [telemetry.unitCheck.command, ...telemetry.unitCheck.argv], "OTLP collector supervision");
|
|
4617
4784
|
const otlpStatus = (await checked3(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
|
|
4618
4785
|
if (!/^2\d\d$/.test(otlpStatus))
|
|
4619
4786
|
throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
|
|
@@ -4646,10 +4813,12 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4646
4813
|
`, 384);
|
|
4647
4814
|
}
|
|
4648
4815
|
if (bootstrapManifest) {
|
|
4816
|
+
if (!installedAgentPlan?.user)
|
|
4817
|
+
throw new Error("initial Agent deployment requires the converged Agent user");
|
|
4649
4818
|
await checked3(host, [
|
|
4650
4819
|
"runuser",
|
|
4651
4820
|
"-u",
|
|
4652
|
-
|
|
4821
|
+
installedAgentPlan.user,
|
|
4653
4822
|
"--",
|
|
4654
4823
|
"/usr/local/bin/fz-agent",
|
|
4655
4824
|
"deploy",
|
|
@@ -4667,6 +4836,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4667
4836
|
`, 384);
|
|
4668
4837
|
host.remove(config.bootstrapBundle.bundleFile);
|
|
4669
4838
|
host.remove(config.bootstrapBundle.manifestFile);
|
|
4839
|
+
if (alreadyEnrolled)
|
|
4840
|
+
installedAgentPlan = await host.installAgent(config, "bound");
|
|
4670
4841
|
}
|
|
4671
4842
|
if (!alreadyEnrolled) {
|
|
4672
4843
|
await checked3(host, [
|
|
@@ -4676,7 +4847,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4676
4847
|
"--show-error",
|
|
4677
4848
|
"--max-time",
|
|
4678
4849
|
"10",
|
|
4679
|
-
`http://127.0.0.1
|
|
4850
|
+
`http://127.0.0.1:${runtime.environment.publicApiPort}${runtime.healthPath}`
|
|
4680
4851
|
], "release-one API health");
|
|
4681
4852
|
await host.installAgent(config, "enrol");
|
|
4682
4853
|
await host.installAgent(config, "bound");
|
|
@@ -4878,8 +5049,8 @@ function readBootstrapConfig(path) {
|
|
|
4878
5049
|
function planBootstrapAgentInstall(config, phase, context) {
|
|
4879
5050
|
const { capabilities, hasBinding, hasEnrolCredential, initialBundle } = context;
|
|
4880
5051
|
if (phase === "bootstrap") {
|
|
4881
|
-
if (config.kind !== "platform" ||
|
|
4882
|
-
throw new Error("
|
|
5052
|
+
if (config.kind !== "platform" || !initialBundle) {
|
|
5053
|
+
throw new Error("platform bundle Agent install requires a verified bootstrap bundle");
|
|
4883
5054
|
}
|
|
4884
5055
|
} else if (phase === "enrol") {
|
|
4885
5056
|
if (hasBinding || !hasEnrolCredential) {
|
|
@@ -4917,8 +5088,10 @@ function planBootstrapAgentInstall(config, phase, context) {
|
|
|
4917
5088
|
bootstrapTargetTelemetryEndpoint: bound && bootstrapRunner ? platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined : undefined,
|
|
4918
5089
|
lifecycleProfilePath: bound && config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
|
|
4919
5090
|
enforceEgress: true,
|
|
5091
|
+
runnerLoopbackPorts: config.kind === "platform" ? [config.runtime.environment.publicApiPort, config.runtime.bluePort, config.runtime.greenPort] : [],
|
|
5092
|
+
agentLoopbackPorts: config.kind === "platform" ? [config.runtime.environment.publicApiPort] : [],
|
|
4920
5093
|
nodeHostname: config.nodeHostname,
|
|
4921
|
-
telemetryEndpoint: config.
|
|
5094
|
+
telemetryEndpoint: config.kind === "platform" ? config.runtime.environment.otlpEndpoint : "http://127.0.0.1:4318",
|
|
4922
5095
|
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
4923
5096
|
sourceBinPath: PACKAGED_AGENT_BIN,
|
|
4924
5097
|
...enrolling ? {
|
|
@@ -4926,7 +5099,7 @@ function planBootstrapAgentInstall(config, phase, context) {
|
|
|
4926
5099
|
enrolStatePath: "/var/lib/forgezero/enrolment.json"
|
|
4927
5100
|
} : bound ? { enrolStatePath: "/var/lib/forgezero/enrolment.json" } : {},
|
|
4928
5101
|
...authenticated ? {
|
|
4929
|
-
apiUrl: config.apiUrl,
|
|
5102
|
+
apiUrl: config.kind === "platform" ? `http://127.0.0.1:${config.runtime.environment.publicApiPort}` : config.apiUrl,
|
|
4930
5103
|
project: config.kind === "enrolled-compute" ? config.realm : "platform",
|
|
4931
5104
|
environment: config.kind === "enrolled-compute" ? undefined : config.environment,
|
|
4932
5105
|
nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
|
|
@@ -4935,6 +5108,7 @@ function planBootstrapAgentInstall(config, phase, context) {
|
|
|
4935
5108
|
return planInstall(options);
|
|
4936
5109
|
}
|
|
4937
5110
|
function localBootstrapHost() {
|
|
5111
|
+
let softwareClientUser;
|
|
4938
5112
|
const execute = async (argv, options = {}) => {
|
|
4939
5113
|
const child = Bun.spawn([...argv], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
|
|
4940
5114
|
if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
|
|
@@ -4967,10 +5141,12 @@ function localBootstrapHost() {
|
|
|
4967
5141
|
exec: execute,
|
|
4968
5142
|
sleep: (milliseconds) => Bun.sleep(milliseconds),
|
|
4969
5143
|
async ensureSoftware(requirements) {
|
|
5144
|
+
if (!softwareClientUser)
|
|
5145
|
+
throw new Error("Agent must be converged before software requirements are applied");
|
|
4970
5146
|
const result = await execute([
|
|
4971
5147
|
"runuser",
|
|
4972
5148
|
"-u",
|
|
4973
|
-
|
|
5149
|
+
softwareClientUser,
|
|
4974
5150
|
"--",
|
|
4975
5151
|
"/usr/local/bin/fz-agent",
|
|
4976
5152
|
"software-ensure",
|
|
@@ -4984,7 +5160,7 @@ function localBootstrapHost() {
|
|
|
4984
5160
|
const capabilities = await readCapabilities(localRunner);
|
|
4985
5161
|
const hasBinding = existsSync5("/var/lib/forgezero/enrolment.json");
|
|
4986
5162
|
const hasEnrolCredential = existsSync5(ENROL_CREDENTIAL);
|
|
4987
|
-
const initialBundle = config.kind === "platform" &&
|
|
5163
|
+
const initialBundle = config.kind === "platform" && phase === "bootstrap" && existsSync5(config.bootstrapBundle.bundleFile) && existsSync5(config.bootstrapBundle.manifestFile) ? {
|
|
4988
5164
|
path: config.bootstrapBundle.bundleFile,
|
|
4989
5165
|
manifestPath: config.bootstrapBundle.manifestFile,
|
|
4990
5166
|
manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync5(config.bootstrapBundle.manifestFile, "utf8")))
|
|
@@ -5015,6 +5191,7 @@ function localBootstrapHost() {
|
|
|
5015
5191
|
writeFileSync5(unit.path, unit.unit, { mode: 420 });
|
|
5016
5192
|
}
|
|
5017
5193
|
await applyPlan(plan, localRunner);
|
|
5194
|
+
softwareClientUser = plan.user;
|
|
5018
5195
|
return plan;
|
|
5019
5196
|
}
|
|
5020
5197
|
};
|
|
@@ -5987,11 +6164,26 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
5987
6164
|
|
|
5988
6165
|
// src/operator-bootstrap.ts
|
|
5989
6166
|
import { createHash as createHash4, randomBytes as randomBytes5 } from "crypto";
|
|
5990
|
-
import { chmodSync as chmodSync5, lstatSync as lstatSync5, mkdirSync as mkdirSync8, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
6167
|
+
import { chmodSync as chmodSync5, existsSync as existsSync7, lstatSync as lstatSync5, mkdirSync as mkdirSync8, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
5991
6168
|
import { isIP as isIP6 } from "net";
|
|
5992
6169
|
import { tmpdir } from "os";
|
|
5993
6170
|
import { basename, dirname as dirname9, isAbsolute as isAbsolute4, join as join6, resolve as resolve6 } from "path";
|
|
5994
6171
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6172
|
+
function operatorPackagedBinary(name) {
|
|
6173
|
+
const candidates = [
|
|
6174
|
+
fileURLToPath2(new URL(`./${name}`, import.meta.url)),
|
|
6175
|
+
fileURLToPath2(new URL(`../dist/${name}`, import.meta.url))
|
|
6176
|
+
];
|
|
6177
|
+
const found = candidates.find(existsSync7);
|
|
6178
|
+
if (!found)
|
|
6179
|
+
throw new Error(`packaged ${name} is missing; checked ${candidates.join(" and ")}`);
|
|
6180
|
+
return found;
|
|
6181
|
+
}
|
|
6182
|
+
var operatorPackagedFzCliPath = () => operatorPackagedBinary("fz.js");
|
|
6183
|
+
var operatorPackagedAgentPath = () => operatorPackagedBinary("fz-agent.js");
|
|
6184
|
+
var operatorPackagedGitSshPath = () => operatorPackagedBinary("fz-git-ssh.js");
|
|
6185
|
+
var PLATFORM_CLUSTER_CREDENTIAL_NAME = "forgezero-platform-cluster-bootstrap-code";
|
|
6186
|
+
var PLATFORM_CLUSTER_CREDENTIAL_PATH = `/etc/forgezero/creds/${PLATFORM_CLUSTER_CREDENTIAL_NAME}.cred`;
|
|
5995
6187
|
var REQUEST_LIMIT = 64 * 1024;
|
|
5996
6188
|
var SECRET_LIMIT = 256 * 1024;
|
|
5997
6189
|
var REMOTE_STAGE = "/run/forgezero-operator-bootstrap";
|
|
@@ -6434,9 +6626,11 @@ function operatorPlatformFleetCoordinates(fleet) {
|
|
|
6434
6626
|
function planOperatorPlatformFleetBootstrap(fleet, mode) {
|
|
6435
6627
|
const { requests, configs, environment } = validatedPlatformFleet(fleet);
|
|
6436
6628
|
const secretInputs = mode === "apply" ? attendedSecretNames(configs[0]) : [];
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6629
|
+
if (mode === "apply") {
|
|
6630
|
+
for (const config of configs.slice(1)) {
|
|
6631
|
+
if (JSON.stringify(attendedSecretNames(config)) !== JSON.stringify(secretInputs)) {
|
|
6632
|
+
throw new Error("operator fleet configs do not share one attended secret schema");
|
|
6633
|
+
}
|
|
6440
6634
|
}
|
|
6441
6635
|
}
|
|
6442
6636
|
return {
|
|
@@ -6474,9 +6668,33 @@ var defaultExec3 = async (argv, options = {}) => {
|
|
|
6474
6668
|
]);
|
|
6475
6669
|
return {
|
|
6476
6670
|
exitCode,
|
|
6477
|
-
output: options.secret ?
|
|
6671
|
+
output: options.secret ? exitCode === 0 ? options.safeStdout ? stdout.slice(0, 65536) : "" : redactOperatorSecretDiagnostic(stderr, options.stdin) : `${stdout}${stderr}`.slice(0, 65536)
|
|
6478
6672
|
};
|
|
6479
6673
|
};
|
|
6674
|
+
function redactOperatorSecretDiagnostic(stderr, stdin) {
|
|
6675
|
+
if (!stdin)
|
|
6676
|
+
return "";
|
|
6677
|
+
let parsed;
|
|
6678
|
+
try {
|
|
6679
|
+
parsed = JSON.parse(stdin);
|
|
6680
|
+
} catch {
|
|
6681
|
+
return "";
|
|
6682
|
+
}
|
|
6683
|
+
const values = [];
|
|
6684
|
+
const visit = (value) => {
|
|
6685
|
+
if (typeof value === "string" && value)
|
|
6686
|
+
values.push(value);
|
|
6687
|
+
else if (Array.isArray(value))
|
|
6688
|
+
value.forEach(visit);
|
|
6689
|
+
else if (value && typeof value === "object")
|
|
6690
|
+
Object.values(value).forEach(visit);
|
|
6691
|
+
};
|
|
6692
|
+
visit(parsed);
|
|
6693
|
+
let safe = stderr.slice(0, 16384);
|
|
6694
|
+
for (const value of values.sort((left, right) => right.length - left.length))
|
|
6695
|
+
safe = safe.replaceAll(value, "[REDACTED]");
|
|
6696
|
+
return safe.slice(0, 8192);
|
|
6697
|
+
}
|
|
6480
6698
|
var checked5 = async (exec, argv, label, options) => {
|
|
6481
6699
|
const result = await exec(argv, options);
|
|
6482
6700
|
if (result.exitCode !== 0)
|
|
@@ -6519,7 +6737,7 @@ var sshOptions = (request, knownHosts, scp = false) => [
|
|
|
6519
6737
|
"-o",
|
|
6520
6738
|
"ClearAllForwardings=yes",
|
|
6521
6739
|
"-o",
|
|
6522
|
-
"ConnectTimeout=
|
|
6740
|
+
"ConnectTimeout=30",
|
|
6523
6741
|
"-o",
|
|
6524
6742
|
request.target.jump ? `ProxyJump=${destination2(request.target.jump)}:${request.target.jump.port}` : "ProxyJump=none",
|
|
6525
6743
|
scp ? "-P" : "-p",
|
|
@@ -6554,6 +6772,93 @@ var remoteRegularFileExists = async (exec, request, knownHosts, path) => {
|
|
|
6554
6772
|
return false;
|
|
6555
6773
|
throw new Error(`remote initialized-state probe failed${result.output ? `: ${result.output.trim()}` : ""}`);
|
|
6556
6774
|
};
|
|
6775
|
+
async function operatorMetalClusterBootstrapCode(requestInput, options = {}) {
|
|
6776
|
+
const request = validateMetalRequestValue(requestInput, { validateMetalConfig: false });
|
|
6777
|
+
publicIdentity(request.target.identityPublicKeyFile);
|
|
6778
|
+
socketPath(request.target.agentSocket);
|
|
6779
|
+
const exec = options.exec ?? defaultExec3;
|
|
6780
|
+
const directory = mkdtempSync(join6(tmpdir(), "forgezero-operator-metal-credential-"));
|
|
6781
|
+
try {
|
|
6782
|
+
const knownHosts = writeKnownHosts(request, directory);
|
|
6783
|
+
const probe = await exec([
|
|
6784
|
+
"ssh",
|
|
6785
|
+
...sshOptions(request, knownHosts),
|
|
6786
|
+
destination2(request.target),
|
|
6787
|
+
"--",
|
|
6788
|
+
"/usr/bin/sudo",
|
|
6789
|
+
"-n",
|
|
6790
|
+
"/usr/bin/test",
|
|
6791
|
+
"-f",
|
|
6792
|
+
PLATFORM_CLUSTER_CREDENTIAL_PATH
|
|
6793
|
+
]);
|
|
6794
|
+
if (probe.exitCode !== 0 && probe.exitCode !== 1) {
|
|
6795
|
+
throw new Error("Metal cluster credential probe failed");
|
|
6796
|
+
}
|
|
6797
|
+
if (probe.exitCode === 1) {
|
|
6798
|
+
const value2 = randomBytes5(32).toString("hex");
|
|
6799
|
+
await remote(exec, request, knownHosts, [
|
|
6800
|
+
"/usr/bin/sudo",
|
|
6801
|
+
"-n",
|
|
6802
|
+
"/usr/bin/install",
|
|
6803
|
+
"-d",
|
|
6804
|
+
"-o",
|
|
6805
|
+
"root",
|
|
6806
|
+
"-g",
|
|
6807
|
+
"root",
|
|
6808
|
+
"-m",
|
|
6809
|
+
"0700",
|
|
6810
|
+
dirname9(PLATFORM_CLUSTER_CREDENTIAL_PATH)
|
|
6811
|
+
], "Metal credential directory");
|
|
6812
|
+
await remote(exec, request, knownHosts, [
|
|
6813
|
+
"/usr/bin/sudo",
|
|
6814
|
+
"-n",
|
|
6815
|
+
"/usr/bin/systemd-creds",
|
|
6816
|
+
"encrypt",
|
|
6817
|
+
`--name=${PLATFORM_CLUSTER_CREDENTIAL_NAME}`,
|
|
6818
|
+
"-",
|
|
6819
|
+
PLATFORM_CLUSTER_CREDENTIAL_PATH
|
|
6820
|
+
], "seal Metal cluster credential", true, `${value2}
|
|
6821
|
+
`);
|
|
6822
|
+
await remote(exec, request, knownHosts, [
|
|
6823
|
+
"/usr/bin/sudo",
|
|
6824
|
+
"-n",
|
|
6825
|
+
"/usr/bin/chown",
|
|
6826
|
+
"root:root",
|
|
6827
|
+
PLATFORM_CLUSTER_CREDENTIAL_PATH
|
|
6828
|
+
], "own Metal cluster credential");
|
|
6829
|
+
await remote(exec, request, knownHosts, [
|
|
6830
|
+
"/usr/bin/sudo",
|
|
6831
|
+
"-n",
|
|
6832
|
+
"/usr/bin/chmod",
|
|
6833
|
+
"0600",
|
|
6834
|
+
PLATFORM_CLUSTER_CREDENTIAL_PATH
|
|
6835
|
+
], "protect Metal cluster credential");
|
|
6836
|
+
}
|
|
6837
|
+
const metadata = await remote(exec, request, knownHosts, [
|
|
6838
|
+
"/usr/bin/sudo",
|
|
6839
|
+
"-n",
|
|
6840
|
+
"/usr/bin/stat",
|
|
6841
|
+
"--format=%u:%g:%a:%h",
|
|
6842
|
+
PLATFORM_CLUSTER_CREDENTIAL_PATH
|
|
6843
|
+
], "verify Metal cluster credential");
|
|
6844
|
+
if (metadata !== "0:0:600:1")
|
|
6845
|
+
throw new Error("Metal cluster credential is not one root-owned 0600 file");
|
|
6846
|
+
const value = await remote(exec, request, knownHosts, [
|
|
6847
|
+
"/usr/bin/sudo",
|
|
6848
|
+
"-n",
|
|
6849
|
+
"/usr/bin/systemd-creds",
|
|
6850
|
+
"decrypt",
|
|
6851
|
+
`--name=${PLATFORM_CLUSTER_CREDENTIAL_NAME}`,
|
|
6852
|
+
PLATFORM_CLUSTER_CREDENTIAL_PATH,
|
|
6853
|
+
"-"
|
|
6854
|
+
], "open Metal cluster credential", true, undefined, true);
|
|
6855
|
+
if (!/^[a-f0-9]{64}$/i.test(value))
|
|
6856
|
+
throw new Error("Metal cluster credential is invalid");
|
|
6857
|
+
return value;
|
|
6858
|
+
} finally {
|
|
6859
|
+
rmSync6(directory, { recursive: true, force: true });
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6557
6862
|
var copy = async (exec, request, knownHosts, local, remotePath, secret = false) => {
|
|
6558
6863
|
safeRemoteArg(remotePath);
|
|
6559
6864
|
await checked5(exec, [
|
|
@@ -6733,9 +7038,9 @@ async function verifiedBunArchive(directory, fetcher) {
|
|
|
6733
7038
|
}
|
|
6734
7039
|
async function installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options) {
|
|
6735
7040
|
const artifacts = [
|
|
6736
|
-
[options.fzCliPath ??
|
|
6737
|
-
[options.fzAgentPath ??
|
|
6738
|
-
[options.fzGitSshPath ??
|
|
7041
|
+
[options.fzCliPath ?? operatorPackagedFzCliPath(), "fz.js"],
|
|
7042
|
+
[options.fzAgentPath ?? operatorPackagedAgentPath(), "fz-agent.js"],
|
|
7043
|
+
[options.fzGitSshPath ?? operatorPackagedGitSshPath(), "fz-git-ssh.js"]
|
|
6739
7044
|
];
|
|
6740
7045
|
for (const [artifact] of artifacts) {
|
|
6741
7046
|
if (!readFileSync7(artifact).length)
|
|
@@ -6772,6 +7077,7 @@ async function installPackagedAgent(request, knownHosts, exec, directory, remote
|
|
|
6772
7077
|
for (const [, name] of artifacts)
|
|
6773
7078
|
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/install", "-m", "0755", `${remoteTemp}/${name}`, `/usr/local/lib/forgezero/agent/${name}`], "remote Agent install");
|
|
6774
7079
|
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/ln", "-sfn", "/usr/local/lib/forgezero/agent/fz.js", "/usr/local/bin/fz"], "remote fz link");
|
|
7080
|
+
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/ln", "-sfn", "/usr/local/lib/forgezero/agent/fz-agent", "/usr/local/bin/fz-agent"], "remote fz-agent link");
|
|
6775
7081
|
}
|
|
6776
7082
|
async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
6777
7083
|
const plan = planOperatorPlatformBootstrap(request, mode);
|
|
@@ -6798,8 +7104,10 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
6798
7104
|
await copy(exec, request, knownHosts, join6(directory, name), `${remoteTemp}/${name}`, true);
|
|
6799
7105
|
for (const bundle of staged.bundleFiles)
|
|
6800
7106
|
await copy(exec, request, knownHosts, bundle.source, `${remoteTemp}/${bundle.name}`, true);
|
|
6801
|
-
for (const name of ["platform-config.json", ...staged.files
|
|
7107
|
+
for (const name of ["platform-config.json", ...staged.files])
|
|
6802
7108
|
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/install", "-m", "0600", `${remoteTemp}/${name}`, `${REMOTE_STAGE}/${name}`], "remote bootstrap handoff", true);
|
|
7109
|
+
for (const { name } of staged.bundleFiles)
|
|
7110
|
+
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/install", "-m", "0644", `${remoteTemp}/${name}`, `${REMOTE_STAGE}/${name}`], "remote bootstrap bundle", true);
|
|
6803
7111
|
const command = ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "platform", "credentials-stdin"];
|
|
6804
7112
|
command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
|
|
6805
7113
|
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
|
|
@@ -6939,6 +7247,7 @@ export {
|
|
|
6939
7247
|
writeOperatorGuestHostKeyEvidence,
|
|
6940
7248
|
withOperatorGuestTransport,
|
|
6941
7249
|
verifyOperatorPlatformFleet,
|
|
7250
|
+
redactOperatorSecretDiagnostic,
|
|
6942
7251
|
readOperatorPlatformBootstrapRequest,
|
|
6943
7252
|
readOperatorPlatformBootstrapFleetRequest,
|
|
6944
7253
|
readOperatorMetalBootstrapRequest,
|
|
@@ -6947,6 +7256,10 @@ export {
|
|
|
6947
7256
|
planOperatorPlatformBootstrap,
|
|
6948
7257
|
planOperatorMetalBootstrap,
|
|
6949
7258
|
operatorPlatformFleetCoordinates,
|
|
7259
|
+
operatorPackagedGitSshPath,
|
|
7260
|
+
operatorPackagedFzCliPath,
|
|
7261
|
+
operatorPackagedAgentPath,
|
|
7262
|
+
operatorMetalClusterBootstrapCode,
|
|
6950
7263
|
createOperatorSshHop,
|
|
6951
7264
|
collectOperatorGuestHostKeys,
|
|
6952
7265
|
applyOperatorPlatformFleetBootstrap,
|