@forgezero/agent 0.1.63 → 0.1.65
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 +13 -9
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.d.ts +17 -1
- package/dist/bootstrap.js +115 -68
- package/dist/cli/agent-install.d.ts +9 -0
- package/dist/deployment.d.ts +11 -0
- package/dist/fz-agent.js +120 -70
- package/dist/fz.js +391 -103
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.d.ts +42 -0
- package/dist/operator-bootstrap.js +294 -75
- package/dist/platform-fleet-verification.js +92 -66
- package/dist/provision.js +27 -26
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/fz.js
CHANGED
|
@@ -4811,8 +4811,8 @@ async function spawnWith(command, env, report = () => {}, options = {}) {
|
|
|
4811
4811
|
|
|
4812
4812
|
// src/cli/index.ts
|
|
4813
4813
|
init_dist();
|
|
4814
|
-
import { existsSync as existsSync12, lstatSync as lstatSync10, mkdirSync as
|
|
4815
|
-
import { basename as basename3, dirname as
|
|
4814
|
+
import { existsSync as existsSync12, lstatSync as lstatSync10, mkdirSync as mkdirSync13, readFileSync as readFileSync15, writeFileSync as writeFileSync13 } from "fs";
|
|
4815
|
+
import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute7, join as join12, resolve as resolve11 } from "path";
|
|
4816
4816
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4817
4817
|
import { hostname } from "os";
|
|
4818
4818
|
|
|
@@ -4829,7 +4829,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
4829
4829
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
4830
4830
|
|
|
4831
4831
|
// src/version.ts
|
|
4832
|
-
var VERSION2 = "0.1.
|
|
4832
|
+
var VERSION2 = "0.1.65";
|
|
4833
4833
|
|
|
4834
4834
|
// src/software.ts
|
|
4835
4835
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -5838,17 +5838,16 @@ function agentUnit(options) {
|
|
|
5838
5838
|
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
5839
5839
|
throw new Error("migration pull and lifecycle profile must be supplied together");
|
|
5840
5840
|
}
|
|
5841
|
-
const
|
|
5842
|
-
if (
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
throw new Error("bootstrap pull, SSH credential, public key and target telemetry endpoint must be supplied together");
|
|
5841
|
+
const bootstrapIdentityEnabled = Boolean(options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath);
|
|
5842
|
+
if (Boolean(options.bootstrapSshCredentialPath) !== Boolean(options.bootstrapSshPublicKeyPath)) {
|
|
5843
|
+
throw new Error("bootstrap SSH credential and public key paths must be supplied together");
|
|
5844
|
+
}
|
|
5845
|
+
const bootstrapEnabled = Boolean(options.pullBootstrap);
|
|
5846
|
+
if (bootstrapEnabled && (!bootstrapIdentityEnabled || !options.bootstrapTargetTelemetryEndpoint) || !bootstrapEnabled && options.bootstrapTargetTelemetryEndpoint) {
|
|
5847
|
+
throw new Error("bootstrap pull, SSH identity and target telemetry endpoint must be supplied together");
|
|
5849
5848
|
}
|
|
5850
5849
|
const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
|
|
5851
|
-
const bootstrapSshCredentialPath =
|
|
5850
|
+
const bootstrapSshCredentialPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
|
|
5852
5851
|
const warpValues = [
|
|
5853
5852
|
options.warpOrganization,
|
|
5854
5853
|
options.warpClientIdCredentialPath,
|
|
@@ -6087,15 +6086,13 @@ function planProvision(options) {
|
|
|
6087
6086
|
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
6088
6087
|
throw new Error("migration pull and lifecycle profile must be supplied together");
|
|
6089
6088
|
}
|
|
6090
|
-
const
|
|
6091
|
-
if (
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
].some(Boolean) && !bootstrapEnabled) {
|
|
6098
|
-
throw new Error("bootstrap pull, SSH credential, public key and target telemetry endpoint must be supplied together");
|
|
6089
|
+
const bootstrapIdentityEnabled = Boolean(options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath);
|
|
6090
|
+
if (Boolean(options.bootstrapSshCredentialPath) !== Boolean(options.bootstrapSshPublicKeyPath) || options.bootstrapSshSourcePath && !bootstrapIdentityEnabled) {
|
|
6091
|
+
throw new Error("bootstrap SSH credential and public key paths must be supplied together");
|
|
6092
|
+
}
|
|
6093
|
+
const bootstrapEnabled = Boolean(options.pullBootstrap);
|
|
6094
|
+
if (bootstrapEnabled && (!bootstrapIdentityEnabled || !options.bootstrapTargetTelemetryEndpoint) || !bootstrapEnabled && options.bootstrapTargetTelemetryEndpoint) {
|
|
6095
|
+
throw new Error("bootstrap pull, SSH identity and target telemetry endpoint must be supplied together");
|
|
6099
6096
|
}
|
|
6100
6097
|
const warpValues = [
|
|
6101
6098
|
options.warpOrganization,
|
|
@@ -6105,13 +6102,13 @@ function planProvision(options) {
|
|
|
6105
6102
|
const warpEnabled = warpValues.every(Boolean);
|
|
6106
6103
|
if (warpValues.some(Boolean) && !warpEnabled)
|
|
6107
6104
|
throw new Error("WARP configuration must be supplied together");
|
|
6108
|
-
const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath
|
|
6109
|
-
if (
|
|
6110
|
-
throw new Error("direct enrolment credential
|
|
6105
|
+
const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath);
|
|
6106
|
+
if (options.enrolTokenCredentialPath && !options.enrolStatePath || options.enrolTokenSourcePath && (!options.enrolTokenCredentialPath || !options.enrolStatePath)) {
|
|
6107
|
+
throw new Error("direct enrolment credential requires its durable state path");
|
|
6111
6108
|
}
|
|
6112
6109
|
const enrolTokenSourcePath = options.enrolTokenSourcePath ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
|
|
6113
6110
|
const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
|
|
6114
|
-
const enrolStatePath =
|
|
6111
|
+
const enrolStatePath = options.enrolStatePath ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
|
|
6115
6112
|
const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
|
|
6116
6113
|
const sourceBinPath = options.sourceBinPath ? systemdPath(options.sourceBinPath, "agent source binary") : undefined;
|
|
6117
6114
|
const binPath = options.binPath ? systemdPath(options.binPath, "agent binary") : undefined;
|
|
@@ -6123,8 +6120,8 @@ function planProvision(options) {
|
|
|
6123
6120
|
const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
|
|
6124
6121
|
const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
|
|
6125
6122
|
const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
|
|
6126
|
-
const bootstrapSshCredentialPath =
|
|
6127
|
-
const bootstrapSshPublicKeyPath =
|
|
6123
|
+
const bootstrapSshCredentialPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
|
|
6124
|
+
const bootstrapSshPublicKeyPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshPublicKeyPath, "bootstrap SSH public key") : undefined;
|
|
6128
6125
|
const bootstrapSshSourcePath = options.bootstrapSshSourcePath ? systemdPath(options.bootstrapSshSourcePath, "bootstrap SSH private-key source") : undefined;
|
|
6129
6126
|
const bootstrapSshPublicKeyDir = bootstrapSshPublicKeyPath?.replace(/\/[^/]+$/, "");
|
|
6130
6127
|
const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
|
|
@@ -6224,7 +6221,7 @@ function planProvision(options) {
|
|
|
6224
6221
|
step("Git deploy identity directory", { kind: "directories", directories: [{ path: gitPublicKeyDir, mode: 493, owner: "root", group: "root" }] }),
|
|
6225
6222
|
step("unique encrypted Git deploy identity", { kind: "ensure-git-identity", credential: gitCredentialPath, publicKey: gitPublicKeyPath })
|
|
6226
6223
|
] : [],
|
|
6227
|
-
...
|
|
6224
|
+
...bootstrapIdentityEnabled ? [
|
|
6228
6225
|
step("bootstrap SSH public identity directory", { kind: "directories", directories: [
|
|
6229
6226
|
{ path: bootstrapSshPublicKeyDir, mode: 493, owner: "root", group: "root" }
|
|
6230
6227
|
] }),
|
|
@@ -6260,6 +6257,10 @@ function planProvision(options) {
|
|
|
6260
6257
|
{ argv: ["/usr/bin/rm", "-f", "/etc/systemd/system/forgezero-deploy-runner.socket"] },
|
|
6261
6258
|
{ argv: ["/usr/bin/systemctl", "daemon-reload"] }
|
|
6262
6259
|
] })] : [],
|
|
6260
|
+
...enrolStatePath && !enrolmentEnabled ? [step("retire consumed enrolment unit", { kind: "commands", commands: [
|
|
6261
|
+
{ argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-agent-enrol.service"], acceptedExitCodes: [0, 1, 5] },
|
|
6262
|
+
{ argv: ["/usr/bin/rm", "-f", ENROLMENT_UNIT_PATH] }
|
|
6263
|
+
] })] : [],
|
|
6263
6264
|
step("enable and converge services", { kind: "commands", commands: [
|
|
6264
6265
|
{ argv: ["/usr/bin/systemctl", "enable", ...enabledUnits] },
|
|
6265
6266
|
{ argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
|
|
@@ -6595,6 +6596,16 @@ async function localRunner(operation) {
|
|
|
6595
6596
|
`, exitCode: 1 };
|
|
6596
6597
|
}
|
|
6597
6598
|
}
|
|
6599
|
+
function resolveGitIdentityInstallOptions(environment) {
|
|
6600
|
+
const generateGitIdentity = environment.FZ_GENERATE_GIT_DEPLOY_KEY === "true";
|
|
6601
|
+
const explicitCredential = environment.FZ_GIT_CREDENTIAL_PATH?.trim() || undefined;
|
|
6602
|
+
const explicitPublicKey = environment.FZ_GIT_PUBLIC_KEY_PATH?.trim() || undefined;
|
|
6603
|
+
return {
|
|
6604
|
+
gitCredentialPath: explicitCredential ?? (generateGitIdentity ? "/etc/forgezero/creds/git-deploy-key.cred" : undefined),
|
|
6605
|
+
gitPublicKeyPath: explicitPublicKey ?? (generateGitIdentity ? "/etc/forgezero/git/deploy.pub" : undefined),
|
|
6606
|
+
generateGitIdentity
|
|
6607
|
+
};
|
|
6608
|
+
}
|
|
6598
6609
|
function planInstall(options) {
|
|
6599
6610
|
const { capabilities, ...unit } = options;
|
|
6600
6611
|
return planProvision({ ...unit, mode: modeFor(capabilities) });
|
|
@@ -14188,7 +14199,15 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14188
14199
|
if (connectorCapabilities?.meshConnectorToken && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
|
|
14189
14200
|
await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, connectorCapabilities.meshConnectorToken);
|
|
14190
14201
|
}
|
|
14191
|
-
|
|
14202
|
+
if (alreadyEnrolled) {
|
|
14203
|
+
await host.installAgent(config, "bound");
|
|
14204
|
+
} else if (config.kind === "platform") {
|
|
14205
|
+
if (bootstrapManifest)
|
|
14206
|
+
await host.installAgent(config, "bootstrap");
|
|
14207
|
+
} else {
|
|
14208
|
+
await host.installAgent(config, "enrol");
|
|
14209
|
+
await host.installAgent(config, "bound");
|
|
14210
|
+
}
|
|
14192
14211
|
if (config.kind === "platform" && config.firewall.enabled) {
|
|
14193
14212
|
await host.ensureSoftware(plan.software.filter(({ id: id2 }) => id2 === "ufw"));
|
|
14194
14213
|
} else
|
|
@@ -14338,7 +14357,19 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14338
14357
|
`, 384);
|
|
14339
14358
|
host.remove(config.bootstrapBundle.bundleFile);
|
|
14340
14359
|
host.remove(config.bootstrapBundle.manifestFile);
|
|
14341
|
-
|
|
14360
|
+
}
|
|
14361
|
+
if (!alreadyEnrolled) {
|
|
14362
|
+
await checked3(host, [
|
|
14363
|
+
"curl",
|
|
14364
|
+
"--fail",
|
|
14365
|
+
"--silent",
|
|
14366
|
+
"--show-error",
|
|
14367
|
+
"--max-time",
|
|
14368
|
+
"10",
|
|
14369
|
+
`http://127.0.0.1:3000${runtime.healthPath}`
|
|
14370
|
+
], "release-one API health");
|
|
14371
|
+
await host.installAgent(config, "enrol");
|
|
14372
|
+
await host.installAgent(config, "bound");
|
|
14342
14373
|
}
|
|
14343
14374
|
}
|
|
14344
14375
|
if (config.cloudflareHandoff) {
|
|
@@ -14507,6 +14538,65 @@ function readBootstrapConfig(path) {
|
|
|
14507
14538
|
}
|
|
14508
14539
|
return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync9(path, "utf8"))));
|
|
14509
14540
|
}
|
|
14541
|
+
function planBootstrapAgentInstall(config, phase, context) {
|
|
14542
|
+
const { capabilities, hasBinding, hasEnrolCredential, initialBundle } = context;
|
|
14543
|
+
if (phase === "bootstrap") {
|
|
14544
|
+
if (config.kind !== "platform" || hasBinding || !initialBundle) {
|
|
14545
|
+
throw new Error("release-one Agent install requires an unbound platform host and verified bootstrap bundle");
|
|
14546
|
+
}
|
|
14547
|
+
} else if (phase === "enrol") {
|
|
14548
|
+
if (hasBinding || !hasEnrolCredential) {
|
|
14549
|
+
throw new Error("Agent enrolment requires one unbound host and its sealed one-use credential");
|
|
14550
|
+
}
|
|
14551
|
+
} else if (!hasBinding) {
|
|
14552
|
+
throw new Error("bound Agent convergence requires durable enrolment state");
|
|
14553
|
+
}
|
|
14554
|
+
const bound = phase === "bound";
|
|
14555
|
+
const enrolling = phase === "enrol";
|
|
14556
|
+
const authenticated = enrolling || bound;
|
|
14557
|
+
const bootstrapRunner = platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner);
|
|
14558
|
+
const options = {
|
|
14559
|
+
capabilities,
|
|
14560
|
+
socketPath: DEFAULT_SOCKET,
|
|
14561
|
+
seedPath: "/var/lib/forgezero/node.seed",
|
|
14562
|
+
controlSocketPath: CONTROL_SOCKET,
|
|
14563
|
+
repository: phase === "bootstrap" ? initialBundle.path : bound && config.kind === "enrolled-compute" ? config.repository : undefined,
|
|
14564
|
+
branch: phase === "bootstrap" ? initialBundle.manifest.branch : bound && config.kind === "enrolled-compute" ? config.branch : undefined,
|
|
14565
|
+
bootstrapBundlePath: phase === "bootstrap" ? initialBundle.path : undefined,
|
|
14566
|
+
bootstrapBundleManifestPath: phase === "bootstrap" ? initialBundle.manifestPath : undefined,
|
|
14567
|
+
profile: config.profile,
|
|
14568
|
+
deployRoot: config.deployRoot ?? "/opt/forgezero",
|
|
14569
|
+
deploymentCredentials: config.deploymentCredentials,
|
|
14570
|
+
publicApiUrl: config.apiUrl,
|
|
14571
|
+
gitCredentialPath: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/creds/git-deploy-key.cred" : undefined,
|
|
14572
|
+
gitPublicKeyPath: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/git/deploy.pub" : undefined,
|
|
14573
|
+
generateGitIdentity: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey === true,
|
|
14574
|
+
pullDeployments: bound,
|
|
14575
|
+
pullMigrations: bound && config.kind === "platform",
|
|
14576
|
+
pullBootstrap: bound && bootstrapRunner,
|
|
14577
|
+
bootstrapSshCredentialPath: authenticated && bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
|
|
14578
|
+
bootstrapSshSourcePath: enrolling && config.kind === "enrolled-compute" ? config.bootstrapRunner?.sshPrivateKeyFile : undefined,
|
|
14579
|
+
bootstrapSshPublicKeyPath: authenticated && bootstrapRunner ? BOOTSTRAP_SSH_PUBLIC_KEY : undefined,
|
|
14580
|
+
bootstrapTargetTelemetryEndpoint: bound && bootstrapRunner ? platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined : undefined,
|
|
14581
|
+
lifecycleProfilePath: bound && config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
|
|
14582
|
+
enforceEgress: true,
|
|
14583
|
+
nodeHostname: config.nodeHostname,
|
|
14584
|
+
telemetryEndpoint: config.telemetryEndpoint,
|
|
14585
|
+
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
14586
|
+
sourceBinPath: PACKAGED_AGENT_BIN,
|
|
14587
|
+
...enrolling ? {
|
|
14588
|
+
enrolTokenCredentialPath: ENROL_CREDENTIAL,
|
|
14589
|
+
enrolStatePath: "/var/lib/forgezero/enrolment.json"
|
|
14590
|
+
} : bound ? { enrolStatePath: "/var/lib/forgezero/enrolment.json" } : {},
|
|
14591
|
+
...authenticated ? {
|
|
14592
|
+
apiUrl: config.apiUrl,
|
|
14593
|
+
project: config.kind === "enrolled-compute" ? config.realm : "platform",
|
|
14594
|
+
environment: config.kind === "enrolled-compute" ? undefined : config.environment,
|
|
14595
|
+
nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
|
|
14596
|
+
} : {}
|
|
14597
|
+
};
|
|
14598
|
+
return planInstall(options);
|
|
14599
|
+
}
|
|
14510
14600
|
function localBootstrapHost() {
|
|
14511
14601
|
const execute = async (argv2, options = {}) => {
|
|
14512
14602
|
const child = Bun.spawn([...argv2], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
|
|
@@ -14554,10 +14644,10 @@ function localBootstrapHost() {
|
|
|
14554
14644
|
throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
|
|
14555
14645
|
return result;
|
|
14556
14646
|
},
|
|
14557
|
-
async installAgent(config) {
|
|
14647
|
+
async installAgent(config, phase) {
|
|
14558
14648
|
const capabilities = await readCapabilities(localRunner);
|
|
14559
|
-
const deployRoot = config.deployRoot ?? "/opt/forgezero";
|
|
14560
14649
|
const hasBinding = existsSync9("/var/lib/forgezero/enrolment.json");
|
|
14650
|
+
const hasEnrolCredential = existsSync9(ENROL_CREDENTIAL);
|
|
14561
14651
|
const initialBundle = config.kind === "platform" && !existsSync9(BOOTSTRAP_RELEASE_EVIDENCE) ? {
|
|
14562
14652
|
path: config.bootstrapBundle.bundleFile,
|
|
14563
14653
|
manifestPath: config.bootstrapBundle.manifestFile,
|
|
@@ -14578,45 +14668,11 @@ function localBootstrapHost() {
|
|
|
14578
14668
|
writeFileSync9(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
|
|
14579
14669
|
`, { mode: 256 });
|
|
14580
14670
|
}
|
|
14581
|
-
const plan =
|
|
14671
|
+
const plan = planBootstrapAgentInstall(config, phase, {
|
|
14582
14672
|
capabilities,
|
|
14583
|
-
|
|
14584
|
-
|
|
14585
|
-
|
|
14586
|
-
repository: initialBundle?.path ?? (config.kind === "enrolled-compute" ? config.repository : undefined),
|
|
14587
|
-
branch: initialBundle?.manifest.branch ?? (config.kind === "enrolled-compute" ? config.branch : undefined),
|
|
14588
|
-
bootstrapBundlePath: initialBundle?.path,
|
|
14589
|
-
bootstrapBundleManifestPath: initialBundle?.manifestPath,
|
|
14590
|
-
profile: config.kind === "platform" ? config.profile : config.profile,
|
|
14591
|
-
deployRoot,
|
|
14592
|
-
deploymentCredentials: config.deploymentCredentials,
|
|
14593
|
-
publicApiUrl: config.apiUrl,
|
|
14594
|
-
gitCredentialPath: config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/creds/git-deploy-key.cred" : undefined,
|
|
14595
|
-
gitPublicKeyPath: config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/git/deploy.pub" : undefined,
|
|
14596
|
-
generateGitIdentity: config.kind === "enrolled-compute" && config.gitDeployKey === true,
|
|
14597
|
-
pullDeployments: hasBinding,
|
|
14598
|
-
pullMigrations: config.kind === "platform" && hasBinding,
|
|
14599
|
-
pullBootstrap: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner),
|
|
14600
|
-
bootstrapSshCredentialPath: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && config.bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
|
|
14601
|
-
bootstrapSshSourcePath: config.kind === "enrolled-compute" ? config.bootstrapRunner?.sshPrivateKeyFile : undefined,
|
|
14602
|
-
bootstrapSshPublicKeyPath: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && config.bootstrapRunner ? BOOTSTRAP_SSH_PUBLIC_KEY : undefined,
|
|
14603
|
-
bootstrapTargetTelemetryEndpoint: platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined,
|
|
14604
|
-
lifecycleProfilePath: config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
|
|
14605
|
-
enforceEgress: true,
|
|
14606
|
-
nodeHostname: config.nodeHostname,
|
|
14607
|
-
telemetryEndpoint: config.telemetryEndpoint,
|
|
14608
|
-
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
14609
|
-
sourceBinPath: PACKAGED_AGENT_BIN,
|
|
14610
|
-
...existsSync9(ENROL_CREDENTIAL) || hasBinding ? {
|
|
14611
|
-
enrolTokenCredentialPath: existsSync9(ENROL_CREDENTIAL) ? ENROL_CREDENTIAL : undefined,
|
|
14612
|
-
enrolStatePath: "/var/lib/forgezero/enrolment.json"
|
|
14613
|
-
} : {},
|
|
14614
|
-
...hasBinding ? {
|
|
14615
|
-
apiUrl: config.apiUrl,
|
|
14616
|
-
project: config.kind === "enrolled-compute" ? config.realm : "platform",
|
|
14617
|
-
environment: config.kind === "enrolled-compute" ? undefined : config.environment,
|
|
14618
|
-
nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
|
|
14619
|
-
} : {}
|
|
14673
|
+
hasBinding,
|
|
14674
|
+
hasEnrolCredential,
|
|
14675
|
+
initialBundle
|
|
14620
14676
|
});
|
|
14621
14677
|
for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
|
|
14622
14678
|
mkdirSync9(dirname9(unit.path), { recursive: true, mode: 493 });
|
|
@@ -15671,10 +15727,10 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15671
15727
|
|
|
15672
15728
|
// src/operator-bootstrap.ts
|
|
15673
15729
|
import { createHash as createHash6, randomBytes as randomBytes9 } from "crypto";
|
|
15674
|
-
import { lstatSync as lstatSync7, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
15730
|
+
import { chmodSync as chmodSync6, lstatSync as lstatSync7, mkdirSync as mkdirSync12, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
15675
15731
|
import { isIP as isIP6 } from "net";
|
|
15676
15732
|
import { tmpdir } from "os";
|
|
15677
|
-
import { basename as basename2, join as join10 } from "path";
|
|
15733
|
+
import { basename as basename2, dirname as dirname13, isAbsolute as isAbsolute5, join as join10, resolve as resolve9 } from "path";
|
|
15678
15734
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
15679
15735
|
|
|
15680
15736
|
// src/platform-genesis.ts
|
|
@@ -15809,6 +15865,25 @@ var exactKeys5 = (value, keys, label) => {
|
|
|
15809
15865
|
throw new Error(`${label} contains unknown fields: ${unknown.join(", ")}`);
|
|
15810
15866
|
return record3;
|
|
15811
15867
|
};
|
|
15868
|
+
var canonicalOutputPath = (path, label) => {
|
|
15869
|
+
if (!isAbsolute5(path) || resolve9(path) !== path || /[\r\n\0]/.test(path)) {
|
|
15870
|
+
throw new Error(`${label} must be a canonical absolute path`);
|
|
15871
|
+
}
|
|
15872
|
+
return path;
|
|
15873
|
+
};
|
|
15874
|
+
var writeOwnerJson2 = (path, value, label) => {
|
|
15875
|
+
canonicalOutputPath(path, label);
|
|
15876
|
+
mkdirSync12(dirname13(path), { recursive: true, mode: 448 });
|
|
15877
|
+
const parent = lstatSync7(dirname13(path));
|
|
15878
|
+
const uid = process.getuid?.() ?? parent.uid;
|
|
15879
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== uid || (parent.mode & 63) !== 0) {
|
|
15880
|
+
throw new Error(`${label} parent must be an owner-only directory`);
|
|
15881
|
+
}
|
|
15882
|
+
writeFileSync12(path, `${JSON.stringify(value, null, 2)}
|
|
15883
|
+
`, { mode: 384, flag: "wx" });
|
|
15884
|
+
chmodSync6(path, 384);
|
|
15885
|
+
return path;
|
|
15886
|
+
};
|
|
15812
15887
|
var ownerFile = (path, limit, label) => {
|
|
15813
15888
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
15814
15889
|
throw new Error(`${label} path must be absolute`);
|
|
@@ -15867,8 +15942,10 @@ var validateHop = (value, label) => {
|
|
|
15867
15942
|
}
|
|
15868
15943
|
return hop;
|
|
15869
15944
|
};
|
|
15870
|
-
function
|
|
15871
|
-
|
|
15945
|
+
function createOperatorSshHop(input) {
|
|
15946
|
+
return validateHop({ ...input, hostKeySha256: fingerprint(input.hostKey) }, "operator SSH hop");
|
|
15947
|
+
}
|
|
15948
|
+
var validatePlatformRequestValue = (value) => {
|
|
15872
15949
|
const root = exactKeys5(value, ["kind", "target", "platformConfigFile"], "operator bootstrap request");
|
|
15873
15950
|
if (root.kind !== "platform-remote")
|
|
15874
15951
|
throw new Error("operator bootstrap kind must be platform-remote");
|
|
@@ -15887,6 +15964,7 @@ function readOperatorPlatformBootstrapRequest(path) {
|
|
|
15887
15964
|
socketPath(targetValue.agentSocket);
|
|
15888
15965
|
if (typeof root.platformConfigFile !== "string")
|
|
15889
15966
|
throw new Error("platformConfigFile must be an absolute path");
|
|
15967
|
+
canonicalOutputPath(root.platformConfigFile, "platformConfigFile");
|
|
15890
15968
|
const config = readBootstrapConfig(root.platformConfigFile);
|
|
15891
15969
|
if (config.kind !== "platform")
|
|
15892
15970
|
throw new Error("operator bootstrap requires a platform config");
|
|
@@ -15900,9 +15978,39 @@ function readOperatorPlatformBootstrapRequest(path) {
|
|
|
15900
15978
|
...targetValue.jump === undefined ? {} : { jump: validateHop(targetValue.jump, "operator bootstrap jump") }
|
|
15901
15979
|
}
|
|
15902
15980
|
};
|
|
15903
|
-
}
|
|
15904
|
-
function
|
|
15905
|
-
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator
|
|
15981
|
+
};
|
|
15982
|
+
function readOperatorPlatformBootstrapRequest(path) {
|
|
15983
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator bootstrap request")));
|
|
15984
|
+
return validatePlatformRequestValue(value);
|
|
15985
|
+
}
|
|
15986
|
+
function writeOperatorPlatformBootstrapRequest(path, request) {
|
|
15987
|
+
validatePlatformRequestValue(request);
|
|
15988
|
+
return writeOwnerJson2(path, request, "operator bootstrap request");
|
|
15989
|
+
}
|
|
15990
|
+
function readOperatorPlatformBootstrapFleetRequest(path) {
|
|
15991
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator fleet request")));
|
|
15992
|
+
const root = exactKeys5(value, ["kind", "requests"], "operator fleet request");
|
|
15993
|
+
if (root.kind !== "platform-fleet-remote")
|
|
15994
|
+
throw new Error("operator fleet kind must be platform-fleet-remote");
|
|
15995
|
+
if (!Array.isArray(root.requests) || root.requests.length !== 3 || root.requests.some((entry) => typeof entry !== "string")) {
|
|
15996
|
+
throw new Error("operator fleet requires exactly three request files");
|
|
15997
|
+
}
|
|
15998
|
+
const requests = root.requests.map((entry) => canonicalOutputPath(entry, "operator fleet request file"));
|
|
15999
|
+
if (new Set(requests).size !== 3)
|
|
16000
|
+
throw new Error("operator fleet request files must be unique");
|
|
16001
|
+
for (const request of requests)
|
|
16002
|
+
readOperatorPlatformBootstrapRequest(request);
|
|
16003
|
+
return { kind: "platform-fleet-remote", requests };
|
|
16004
|
+
}
|
|
16005
|
+
function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles) {
|
|
16006
|
+
const request = { kind: "platform-fleet-remote", requests: requestFiles };
|
|
16007
|
+
if (new Set(requestFiles).size !== 3)
|
|
16008
|
+
throw new Error("operator fleet request files must be unique");
|
|
16009
|
+
for (const file of requestFiles)
|
|
16010
|
+
readOperatorPlatformBootstrapRequest(file);
|
|
16011
|
+
return writeOwnerJson2(path, request, "operator fleet request");
|
|
16012
|
+
}
|
|
16013
|
+
var validateMetalRequestValue = (value) => {
|
|
15906
16014
|
const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
|
|
15907
16015
|
if (root.kind !== "metal-remote")
|
|
15908
16016
|
throw new Error("operator metal bootstrap kind must be metal-remote");
|
|
@@ -15921,6 +16029,7 @@ function readOperatorMetalBootstrapRequest(path) {
|
|
|
15921
16029
|
socketPath(targetValue.agentSocket);
|
|
15922
16030
|
if (typeof root.metalConfigFile !== "string")
|
|
15923
16031
|
throw new Error("metalConfigFile must be an absolute path");
|
|
16032
|
+
canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
|
|
15924
16033
|
readMetalBootstrapConfig(root.metalConfigFile);
|
|
15925
16034
|
const genesis = exactKeys5(root.genesis, ["environment", "sshPublicKeyFiles"], "operator metal genesis");
|
|
15926
16035
|
if (typeof genesis.environment !== "string" || !PLATFORM_GENESIS_ENVIRONMENTS.includes(genesis.environment)) {
|
|
@@ -15944,6 +16053,14 @@ function readOperatorMetalBootstrapRequest(path) {
|
|
|
15944
16053
|
sshPublicKeyFiles: genesis.sshPublicKeyFiles
|
|
15945
16054
|
}
|
|
15946
16055
|
};
|
|
16056
|
+
};
|
|
16057
|
+
function readOperatorMetalBootstrapRequest(path) {
|
|
16058
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal request")));
|
|
16059
|
+
return validateMetalRequestValue(value);
|
|
16060
|
+
}
|
|
16061
|
+
function writeOperatorMetalBootstrapRequest(path, request) {
|
|
16062
|
+
validateMetalRequestValue(request);
|
|
16063
|
+
return writeOwnerJson2(path, request, "operator metal request");
|
|
15947
16064
|
}
|
|
15948
16065
|
function planOperatorMetalBootstrap(request, mode) {
|
|
15949
16066
|
const config = readMetalBootstrapConfig(request.metalConfigFile);
|
|
@@ -16003,6 +16120,87 @@ function planOperatorPlatformBootstrap(request, mode) {
|
|
|
16003
16120
|
secretInputs: mode === "apply" ? [...attendedSecretNames(config), ...secretSources(config).map(([name]) => name)] : []
|
|
16004
16121
|
};
|
|
16005
16122
|
}
|
|
16123
|
+
var fleetConfigIdentity = (config) => {
|
|
16124
|
+
const normalized = structuredClone(config);
|
|
16125
|
+
delete normalized.computeReference;
|
|
16126
|
+
delete normalized.nodeHostname;
|
|
16127
|
+
delete normalized.cloudflareHandoff;
|
|
16128
|
+
delete normalized.database.role;
|
|
16129
|
+
delete normalized.database.address;
|
|
16130
|
+
delete normalized.database.master;
|
|
16131
|
+
const environment = normalized.runtime.environment;
|
|
16132
|
+
delete environment.databaseRole;
|
|
16133
|
+
delete environment.databaseAddress;
|
|
16134
|
+
delete environment.databaseMaster;
|
|
16135
|
+
delete environment.nodeHostname;
|
|
16136
|
+
delete environment.seedSyncPeers;
|
|
16137
|
+
delete environment.custodianEmail;
|
|
16138
|
+
return JSON.stringify(normalized);
|
|
16139
|
+
};
|
|
16140
|
+
function validatedPlatformFleet(fleet) {
|
|
16141
|
+
const requests = fleet.requests.map(readOperatorPlatformBootstrapRequest);
|
|
16142
|
+
const configs = requests.map((request) => {
|
|
16143
|
+
const config = readBootstrapConfig(request.platformConfigFile);
|
|
16144
|
+
if (config.kind !== "platform")
|
|
16145
|
+
throw new Error("operator fleet requires platform configs");
|
|
16146
|
+
return config;
|
|
16147
|
+
});
|
|
16148
|
+
const environment = configs[0].environment;
|
|
16149
|
+
const expected = PLATFORM_GENESIS_FLEETS[environment];
|
|
16150
|
+
if (!expected || expected.length !== 3)
|
|
16151
|
+
throw new Error("operator fleet environment is invalid");
|
|
16152
|
+
const identity = fleetConfigIdentity(configs[0]);
|
|
16153
|
+
const operatorIdentity = `${requests[0].target.identityPublicKeyFile}\x00${requests[0].target.agentSocket}`;
|
|
16154
|
+
const jumpIdentity = JSON.stringify(requests[0].target.jump ?? null);
|
|
16155
|
+
const coordinators = expected.map((guest) => `http://${guest.address}:8529`);
|
|
16156
|
+
const nodeHostnames = new Set;
|
|
16157
|
+
for (let index = 0;index < 3; index += 1) {
|
|
16158
|
+
const config = configs[index];
|
|
16159
|
+
const request = requests[index];
|
|
16160
|
+
const guest = expected[index];
|
|
16161
|
+
const role = index === 0 ? "master" : "joiner";
|
|
16162
|
+
if (config.environment !== environment || fleetConfigIdentity(config) !== identity) {
|
|
16163
|
+
throw new Error("operator fleet platform configs do not share one immutable identity");
|
|
16164
|
+
}
|
|
16165
|
+
if (config.computeReference !== guest.name || config.database.address !== guest.address || config.database.role !== role || config.database.agency !== "member" || (role === "master" ? config.database.master !== undefined : config.database.master !== expected[0].address) || config.database.coordinators.join(",") !== coordinators.join(",") || config.enrolment.source !== "genesis-derived" || config.cloudflareHandoff?.nodeName !== guest.name) {
|
|
16166
|
+
throw new Error(`operator fleet ${guest.name} does not match the exact genesis topology`);
|
|
16167
|
+
}
|
|
16168
|
+
if (request.target.address !== guest.address || request.target.port !== 22 || request.target.user !== "forgezero") {
|
|
16169
|
+
throw new Error(`operator fleet ${guest.name} SSH target must be forgezero@${guest.address}:22`);
|
|
16170
|
+
}
|
|
16171
|
+
if (`${request.target.identityPublicKeyFile}\x00${request.target.agentSocket}` !== operatorIdentity) {
|
|
16172
|
+
throw new Error("operator fleet must use one caller-approved SSH identity and agent socket");
|
|
16173
|
+
}
|
|
16174
|
+
if (JSON.stringify(request.target.jump ?? null) !== jumpIdentity) {
|
|
16175
|
+
throw new Error("operator fleet must use one pinned Metal jump or no jump on every node");
|
|
16176
|
+
}
|
|
16177
|
+
if (nodeHostnames.has(config.nodeHostname))
|
|
16178
|
+
throw new Error("operator fleet node hostnames must be unique");
|
|
16179
|
+
nodeHostnames.add(config.nodeHostname);
|
|
16180
|
+
}
|
|
16181
|
+
return { requests, configs, environment };
|
|
16182
|
+
}
|
|
16183
|
+
function planOperatorPlatformFleetBootstrap(fleet, mode) {
|
|
16184
|
+
const { requests, configs, environment } = validatedPlatformFleet(fleet);
|
|
16185
|
+
const secretInputs = mode === "apply" ? attendedSecretNames(configs[0]) : [];
|
|
16186
|
+
for (const config of configs.slice(1)) {
|
|
16187
|
+
if (JSON.stringify(attendedSecretNames(config)) !== JSON.stringify(secretInputs)) {
|
|
16188
|
+
throw new Error("operator fleet configs do not share one attended secret schema");
|
|
16189
|
+
}
|
|
16190
|
+
}
|
|
16191
|
+
return {
|
|
16192
|
+
kind: "platform-fleet-remote",
|
|
16193
|
+
mode,
|
|
16194
|
+
mutation: mode !== "status",
|
|
16195
|
+
environment,
|
|
16196
|
+
nodes: requests.map((request, index) => ({
|
|
16197
|
+
computeReference: configs[index].computeReference,
|
|
16198
|
+
address: request.target.address,
|
|
16199
|
+
...request.target.jump ? { via: request.target.jump.address } : {}
|
|
16200
|
+
})),
|
|
16201
|
+
secretInputs
|
|
16202
|
+
};
|
|
16203
|
+
}
|
|
16006
16204
|
var defaultExec3 = async (argv2, options = {}) => {
|
|
16007
16205
|
const child = Bun.spawn([...argv2], {
|
|
16008
16206
|
stdin: options.stdin === undefined ? "ignore" : "pipe",
|
|
@@ -16245,6 +16443,30 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
16245
16443
|
rmSync7(directory, { recursive: true, force: true });
|
|
16246
16444
|
}
|
|
16247
16445
|
}
|
|
16446
|
+
async function applyOperatorPlatformFleetBootstrap(fleet, mode, options = {}) {
|
|
16447
|
+
const plan = planOperatorPlatformFleetBootstrap(fleet, mode);
|
|
16448
|
+
const { requests, configs } = validatedPlatformFleet(fleet);
|
|
16449
|
+
const secrets = mode === "apply" ? validatePlatformBootstrapSecrets(configs[0], options.secrets) : undefined;
|
|
16450
|
+
if (secrets) {
|
|
16451
|
+
for (const config of configs.slice(1))
|
|
16452
|
+
validatePlatformBootstrapSecrets(config, secrets);
|
|
16453
|
+
}
|
|
16454
|
+
const settled = await Promise.allSettled(requests.map((request) => applyOperatorPlatformBootstrap(request, mode, { ...options, secrets })));
|
|
16455
|
+
const outcomes = settled.map((result, index) => {
|
|
16456
|
+
const config = configs[index];
|
|
16457
|
+
const address = requests[index].target.address;
|
|
16458
|
+
if (result.status === "fulfilled")
|
|
16459
|
+
return {
|
|
16460
|
+
computeReference: config.computeReference,
|
|
16461
|
+
address,
|
|
16462
|
+
ok: true,
|
|
16463
|
+
output: result.value.output.slice(0, 65536)
|
|
16464
|
+
};
|
|
16465
|
+
const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
16466
|
+
return { computeReference: config.computeReference, address, ok: false, error: message.slice(0, 4096) };
|
|
16467
|
+
});
|
|
16468
|
+
return { plan, ok: outcomes.every(({ ok }) => ok), outcomes };
|
|
16469
|
+
}
|
|
16248
16470
|
async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
16249
16471
|
const plan = planOperatorMetalBootstrap(request, mode);
|
|
16250
16472
|
publicIdentity(request.target.identityPublicKeyFile);
|
|
@@ -16478,7 +16700,7 @@ async function applyHostMaintenance(request, runtime = localRuntime()) {
|
|
|
16478
16700
|
|
|
16479
16701
|
// src/cli/maintenance.ts
|
|
16480
16702
|
import { existsSync as existsSync11, lstatSync as lstatSync9, readFileSync as readFileSync14, realpathSync as realpathSync5 } from "fs";
|
|
16481
|
-
import { isAbsolute as
|
|
16703
|
+
import { isAbsolute as isAbsolute6, join as join11, relative as relative2, resolve as resolve10 } from "path";
|
|
16482
16704
|
var API_OPERATION_ENTRYPOINTS = {
|
|
16483
16705
|
"dev-reset": ["src", "server", "maintenance", "dev-reset.ts"],
|
|
16484
16706
|
"db-backup": ["src", "server", "maintenance", "snapshot-backup.ts"],
|
|
@@ -16572,13 +16794,13 @@ function checkedEntrypoint(root, parts) {
|
|
|
16572
16794
|
}
|
|
16573
16795
|
const actual = realpathSync5(candidate);
|
|
16574
16796
|
const within = relative2(root, actual);
|
|
16575
|
-
if (within.startsWith("..") ||
|
|
16797
|
+
if (within.startsWith("..") || isAbsolute6(within)) {
|
|
16576
16798
|
throw new Error(`The reviewed operation entrypoint escapes repository root ${root}.`);
|
|
16577
16799
|
}
|
|
16578
16800
|
return actual;
|
|
16579
16801
|
}
|
|
16580
16802
|
function resolveRepositoryOperation(operation, requestedRoot) {
|
|
16581
|
-
const root = realpathSync5(
|
|
16803
|
+
const root = realpathSync5(resolve10(requestedRoot));
|
|
16582
16804
|
const name = manifestName(root);
|
|
16583
16805
|
if (operation in API_OPERATION_ENTRYPOINTS) {
|
|
16584
16806
|
const parts = API_OPERATION_ENTRYPOINTS[operation];
|
|
@@ -16879,7 +17101,7 @@ async function api(options, path, init) {
|
|
|
16879
17101
|
return { status: response.status, body };
|
|
16880
17102
|
}
|
|
16881
17103
|
function sleep(ms) {
|
|
16882
|
-
return new Promise((
|
|
17104
|
+
return new Promise((resolve12) => setTimeout(resolve12, ms));
|
|
16883
17105
|
}
|
|
16884
17106
|
function browserCommand(url) {
|
|
16885
17107
|
if (process.platform === "darwin")
|
|
@@ -17244,16 +17466,13 @@ async function cmdAgent(options, args) {
|
|
|
17244
17466
|
}
|
|
17245
17467
|
const enrolTokenCredentialPath = "/etc/forgezero/creds/enrol-token.cred";
|
|
17246
17468
|
const enrolStatePath = "/var/lib/forgezero/enrolment.json";
|
|
17247
|
-
const
|
|
17248
|
-
const gitPublicKeyPath = process.env.FZ_GIT_PUBLIC_KEY_PATH ?? "/etc/forgezero/git/deploy.pub";
|
|
17469
|
+
const gitIdentity = resolveGitIdentityInstallOptions(process.env);
|
|
17249
17470
|
const plan = planInstall({
|
|
17250
17471
|
capabilities: await readCapabilities(localRunner),
|
|
17251
17472
|
socketPath: process.env.FZ_SOCKET_PATH ?? DEFAULT_SOCKET,
|
|
17252
17473
|
seedPath: process.env.FZ_SEED_PATH ?? "/var/lib/forgezero/node.seed",
|
|
17253
17474
|
seedCredentialPath: process.env.FZ_SEED_CREDENTIAL_PATH,
|
|
17254
|
-
|
|
17255
|
-
gitPublicKeyPath,
|
|
17256
|
-
generateGitIdentity: process.env.FZ_GENERATE_GIT_DEPLOY_KEY === "true",
|
|
17475
|
+
...gitIdentity,
|
|
17257
17476
|
controlSocketPath: process.env.FZ_CONTROL_SOCKET,
|
|
17258
17477
|
repository: process.env.FZ_DEPLOY_REPO,
|
|
17259
17478
|
branch: process.env.FZ_DEPLOY_BRANCH,
|
|
@@ -17313,7 +17532,7 @@ async function cmdAgent(options, args) {
|
|
|
17313
17532
|
writeFileSync13(plan.unitPath, plan.unit, { mode: 420 });
|
|
17314
17533
|
out.ok(`Wrote ${plan.unitPath}`);
|
|
17315
17534
|
for (const auxiliary of plan.auxiliaryUnits) {
|
|
17316
|
-
|
|
17535
|
+
mkdirSync13(dirname14(auxiliary.path), { recursive: true, mode: 493 });
|
|
17317
17536
|
writeFileSync13(auxiliary.path, auxiliary.unit, { mode: 420 });
|
|
17318
17537
|
out.ok(`Wrote ${auxiliary.path}`);
|
|
17319
17538
|
}
|
|
@@ -17322,7 +17541,7 @@ async function cmdAgent(options, args) {
|
|
|
17322
17541
|
const token = await bootstrapSecret("ForgeZero one-time enrolment token");
|
|
17323
17542
|
if (!/^fze_[A-Za-z0-9_-]{40,100}$/.test(token))
|
|
17324
17543
|
throw new Error("A valid fze_ enrolment token was not provided.");
|
|
17325
|
-
|
|
17544
|
+
mkdirSync13(dirname14(enrolTokenCredentialPath), { recursive: true, mode: 448 });
|
|
17326
17545
|
const sealing = Bun.spawn(["systemd-creds", "encrypt", "--name=enrol-token", "-", enrolTokenCredentialPath], { stdin: "pipe", stdout: "ignore", stderr: "pipe" });
|
|
17327
17546
|
if (sealing.stdin && typeof sealing.stdin !== "number") {
|
|
17328
17547
|
sealing.stdin.write(`${token}
|
|
@@ -17338,11 +17557,11 @@ async function cmdAgent(options, args) {
|
|
|
17338
17557
|
for (const step3 of transcript)
|
|
17339
17558
|
out.ok(step3.label);
|
|
17340
17559
|
out.ok("Agent service and socket verified");
|
|
17341
|
-
if (existsSync12(gitPublicKeyPath)) {
|
|
17560
|
+
if (gitIdentity.gitPublicKeyPath && existsSync12(gitIdentity.gitPublicKeyPath)) {
|
|
17342
17561
|
out.line();
|
|
17343
17562
|
out.line(" Compatibility SSH deploy public key:");
|
|
17344
17563
|
out.line();
|
|
17345
|
-
out.line(` ${readFileSync15(gitPublicKeyPath, "utf8").trim()}`);
|
|
17564
|
+
out.line(` ${readFileSync15(gitIdentity.gitPublicKeyPath, "utf8").trim()}`);
|
|
17346
17565
|
out.line();
|
|
17347
17566
|
}
|
|
17348
17567
|
return 0;
|
|
@@ -17448,9 +17667,9 @@ function interactiveMetalBootstrap() {
|
|
|
17448
17667
|
});
|
|
17449
17668
|
}
|
|
17450
17669
|
function writeBootstrapConfig(path, config) {
|
|
17451
|
-
if (!
|
|
17670
|
+
if (!isAbsolute7(path) || resolve11(path) !== path)
|
|
17452
17671
|
throw new Error("--output must be a canonical absolute path");
|
|
17453
|
-
|
|
17672
|
+
mkdirSync13(dirname14(path), { recursive: true, mode: 448 });
|
|
17454
17673
|
writeFileSync13(path, `${JSON.stringify(config, null, 2)}
|
|
17455
17674
|
`, { mode: 384, flag: "wx" });
|
|
17456
17675
|
return path;
|
|
@@ -17512,10 +17731,10 @@ async function interactiveCloudflareBootstrap() {
|
|
|
17512
17731
|
});
|
|
17513
17732
|
}
|
|
17514
17733
|
function genesisOutputDirectory(path) {
|
|
17515
|
-
if (!
|
|
17734
|
+
if (!isAbsolute7(path) || resolve11(path) !== path)
|
|
17516
17735
|
throw new Error("--output must be a canonical absolute directory");
|
|
17517
17736
|
if (!existsSync12(path))
|
|
17518
|
-
|
|
17737
|
+
mkdirSync13(path, { recursive: true, mode: 448 });
|
|
17519
17738
|
const metadata = lstatSync10(path);
|
|
17520
17739
|
const uid = process.getuid?.();
|
|
17521
17740
|
if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 63) !== 0 || uid !== undefined && uid !== 0 && metadata.uid !== uid) {
|
|
@@ -17523,6 +17742,37 @@ function genesisOutputDirectory(path) {
|
|
|
17523
17742
|
}
|
|
17524
17743
|
return path;
|
|
17525
17744
|
}
|
|
17745
|
+
function interactiveOperatorHop(label, defaults) {
|
|
17746
|
+
return createOperatorSshHop({
|
|
17747
|
+
address: bootstrapAnswer(`${label} explicit SSH IP`, defaults.address),
|
|
17748
|
+
port: bootstrapNumber(`${label} SSH port`, "22"),
|
|
17749
|
+
user: bootstrapAnswer(`${label} SSH user`, defaults.user),
|
|
17750
|
+
hostKey: bootstrapAnswer(`${label} trusted ssh-ed25519 host public key`)
|
|
17751
|
+
});
|
|
17752
|
+
}
|
|
17753
|
+
function operatorIdentityCoordinates() {
|
|
17754
|
+
return {
|
|
17755
|
+
identityPublicKeyFile: bootstrapAnswer("Operator SSH public-key file (private key remains in Bitwarden SSH agent)"),
|
|
17756
|
+
agentSocket: bootstrapAnswer("Bitwarden SSH agent socket", process.env.SSH_AUTH_SOCK)
|
|
17757
|
+
};
|
|
17758
|
+
}
|
|
17759
|
+
function writeMetalOperatorFiles(directory) {
|
|
17760
|
+
const output = genesisOutputDirectory(directory);
|
|
17761
|
+
const config = interactiveMetalBootstrap();
|
|
17762
|
+
const metalConfig = writeBootstrapConfig(join12(output, "metal.json"), config);
|
|
17763
|
+
const identity = operatorIdentityCoordinates();
|
|
17764
|
+
const target = interactiveOperatorHop("Metal", { user: "root" });
|
|
17765
|
+
const remoteRequest = writeOperatorMetalBootstrapRequest(join12(output, "metal-remote.json"), {
|
|
17766
|
+
kind: "metal-remote",
|
|
17767
|
+
metalConfigFile: metalConfig,
|
|
17768
|
+
target: { ...target, ...identity },
|
|
17769
|
+
genesis: {
|
|
17770
|
+
environment: bootstrapAnswer("Genesis environment (production/development)", "development"),
|
|
17771
|
+
sshPublicKeyFiles: [identity.identityPublicKeyFile]
|
|
17772
|
+
}
|
|
17773
|
+
});
|
|
17774
|
+
return { metalConfig, remoteRequest };
|
|
17775
|
+
}
|
|
17526
17776
|
function writePlatformGenesisFleet(directory) {
|
|
17527
17777
|
const output = genesisOutputDirectory(directory);
|
|
17528
17778
|
const template = interactiveBootstrap("platform", true);
|
|
@@ -17539,7 +17789,20 @@ function writePlatformGenesisFleet(directory) {
|
|
|
17539
17789
|
writeBootstrapConfig(path, config);
|
|
17540
17790
|
return path;
|
|
17541
17791
|
});
|
|
17542
|
-
|
|
17792
|
+
const identity = operatorIdentityCoordinates();
|
|
17793
|
+
const useJump = bootstrapAnswer("Reach genesis computes through the Metal SSH jump? (yes/no)", "yes") === "yes";
|
|
17794
|
+
const jump = useJump ? interactiveOperatorHop("Metal jump", { user: "root" }) : undefined;
|
|
17795
|
+
const requests = configs.map((platformConfigFile, index) => {
|
|
17796
|
+
const guest = fleet[index];
|
|
17797
|
+
const target = interactiveOperatorHop(`${guest.name} compute`, { address: guest.address, user: "forgezero" });
|
|
17798
|
+
return writeOperatorPlatformBootstrapRequest(join12(output, `${guest.name}-remote.json`), {
|
|
17799
|
+
kind: "platform-remote",
|
|
17800
|
+
platformConfigFile,
|
|
17801
|
+
target: { ...target, ...identity, ...jump ? { jump } : {} }
|
|
17802
|
+
});
|
|
17803
|
+
});
|
|
17804
|
+
const fleetRequest = writeOperatorPlatformBootstrapFleetRequest(join12(output, "platform-fleet-remote.json"), requests);
|
|
17805
|
+
return { configs, requests, fleetRequest };
|
|
17543
17806
|
}
|
|
17544
17807
|
function interactiveBootstrap(kind, genesis = false) {
|
|
17545
17808
|
if (!process.stdin.isTTY)
|
|
@@ -17661,7 +17924,7 @@ async function cmdBootstrap(options, args) {
|
|
|
17661
17924
|
if (operation === "config") {
|
|
17662
17925
|
const kind = args[1];
|
|
17663
17926
|
if (kind !== "metal" && kind !== "platform" && kind !== "cloudflare" || args[2] !== undefined || !options.outputPath) {
|
|
17664
|
-
throw new Error("Usage: fz bootstrap config
|
|
17927
|
+
throw new Error("Usage: fz bootstrap config cloudflare --output <absolute-file> | fz bootstrap config metal|platform --output <absolute-owner-only-directory>");
|
|
17665
17928
|
}
|
|
17666
17929
|
if (kind === "platform") {
|
|
17667
17930
|
out.line(JSON.stringify({ kind, ...writePlatformGenesisFleet(options.outputPath), mode: "0600" }, null, 2));
|
|
@@ -17673,10 +17936,32 @@ async function cmdBootstrap(options, args) {
|
|
|
17673
17936
|
out.line(JSON.stringify({ kind, path, mode: "0600" }, null, 2));
|
|
17674
17937
|
return 0;
|
|
17675
17938
|
}
|
|
17676
|
-
|
|
17677
|
-
out.line(JSON.stringify({ kind, path: writeBootstrapConfig(options.outputPath, config2), mode: "0600" }, null, 2));
|
|
17939
|
+
out.line(JSON.stringify({ kind, ...writeMetalOperatorFiles(options.outputPath), mode: "0600" }, null, 2));
|
|
17678
17940
|
return 0;
|
|
17679
17941
|
}
|
|
17942
|
+
if (operation === "platform" && args[1] === "fleet") {
|
|
17943
|
+
const mode = args[2];
|
|
17944
|
+
if (!mode || !["apply", "status"].includes(mode) || args[3] !== undefined) {
|
|
17945
|
+
throw new Error("Usage: fz bootstrap platform fleet <apply|status> --bootstrap-config <owner-only-platform-fleet-remote.json> [--apply]");
|
|
17946
|
+
}
|
|
17947
|
+
if (!options.bootstrapConfigPath)
|
|
17948
|
+
throw new Error("remote platform fleet bootstrap requires --bootstrap-config");
|
|
17949
|
+
const fleet = readOperatorPlatformBootstrapFleetRequest(options.bootstrapConfigPath);
|
|
17950
|
+
const plan2 = planOperatorPlatformFleetBootstrap(fleet, mode);
|
|
17951
|
+
if (!options.apply) {
|
|
17952
|
+
out.line(JSON.stringify(plan2, null, 2));
|
|
17953
|
+
out.step("Review all three pinned targets and shared secret names, then repeat with --apply.");
|
|
17954
|
+
return 0;
|
|
17955
|
+
}
|
|
17956
|
+
const first = readOperatorPlatformBootstrapRequest(fleet.requests[0]);
|
|
17957
|
+
const firstConfig = readBootstrapConfig(first.platformConfigFile);
|
|
17958
|
+
if (firstConfig.kind !== "platform")
|
|
17959
|
+
throw new Error("remote platform fleet requires platform configs");
|
|
17960
|
+
const secrets2 = mode === "apply" ? await promptPlatformBootstrapSecrets(firstConfig) : undefined;
|
|
17961
|
+
const result2 = await applyOperatorPlatformFleetBootstrap(fleet, mode, { secrets: secrets2 });
|
|
17962
|
+
out.line(JSON.stringify(result2, null, 2));
|
|
17963
|
+
return result2.ok ? 0 : 1;
|
|
17964
|
+
}
|
|
17680
17965
|
if (operation === "platform" && args[1] === "remote") {
|
|
17681
17966
|
const mode = args[2];
|
|
17682
17967
|
if (!mode || !["apply", "status"].includes(mode) || args[3] !== undefined) {
|
|
@@ -17812,7 +18097,7 @@ async function cmdBootstrap(options, args) {
|
|
|
17812
18097
|
return 0;
|
|
17813
18098
|
}
|
|
17814
18099
|
if (!["platform", "repair"].includes(operation)) {
|
|
17815
|
-
throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [bundle|remote <apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|rehearsal|rehearsal-cleanup|status>]|status|repair [--bootstrap-config <path>] [--apply]");
|
|
18100
|
+
throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [bundle|fleet <apply|status>|remote <apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|rehearsal|rehearsal-cleanup|status>]|status|repair [--bootstrap-config <path>] [--apply]");
|
|
17816
18101
|
}
|
|
17817
18102
|
const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
|
|
17818
18103
|
throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
|
|
@@ -18188,7 +18473,7 @@ async function cmdDeploy(options, args) {
|
|
|
18188
18473
|
out.ok(`Initialized legacy .fz/deploy.json (${created.summary.digest}).`);
|
|
18189
18474
|
return 0;
|
|
18190
18475
|
}
|
|
18191
|
-
initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(
|
|
18476
|
+
initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(resolve11(options.projectRoot)), force: options.force });
|
|
18192
18477
|
const compiled = await compileDeploymentProject(options.projectRoot);
|
|
18193
18478
|
if (options.json)
|
|
18194
18479
|
out.line(JSON.stringify({ source: compiled.source, output: compiled.output, digest: compiled.digest }, null, 2));
|
|
@@ -18213,7 +18498,7 @@ async function cmdDeploy(options, args) {
|
|
|
18213
18498
|
return problems.length === 0 ? 0 : 1;
|
|
18214
18499
|
}
|
|
18215
18500
|
if (operation === "check" || operation === "sync") {
|
|
18216
|
-
if (existsSync12(
|
|
18501
|
+
if (existsSync12(resolve11(options.projectRoot, DEPLOY_SOURCE_FILE))) {
|
|
18217
18502
|
const expected = await compileDeploymentProject(options.projectRoot, { write: false });
|
|
18218
18503
|
const actual = inspectCompiledDeployment(options.projectRoot);
|
|
18219
18504
|
const current = canonicalJson(expected.plan) === canonicalJson(actual.plan);
|
|
@@ -18390,10 +18675,13 @@ function usage() {
|
|
|
18390
18675
|
fz bootstrap platform remote <apply|status>
|
|
18391
18676
|
Run typed bootstrap from the operator laptop through
|
|
18392
18677
|
a pinned host; apply copies and verifies that bundle
|
|
18678
|
+
fz bootstrap platform fleet <apply|status>
|
|
18679
|
+
Validate the exact three-node genesis identity, prompt
|
|
18680
|
+
shared secrets once and run all pinned nodes concurrently
|
|
18393
18681
|
fz bootstrap platform Install/repair a typed elastic platform compute
|
|
18394
18682
|
fz bootstrap config <metal|platform|cloudflare>
|
|
18395
|
-
Write
|
|
18396
|
-
|
|
18683
|
+
Write validated owner-only metal config+remote request,
|
|
18684
|
+
Cloudflare file, or complete three-node platform request set
|
|
18397
18685
|
fz bootstrap platform cloudflare
|
|
18398
18686
|
Plan/apply attended Tunnel and DNS reconciliation
|
|
18399
18687
|
using hidden management and KV/Worker runtime token prompts
|