@forgezero/agent 0.1.63 → 0.1.64
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/deployment.d.ts +11 -0
- package/dist/fz-agent.js +120 -70
- package/dist/fz.js +377 -96
- 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.64";
|
|
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] },
|
|
@@ -14188,7 +14189,15 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14188
14189
|
if (connectorCapabilities?.meshConnectorToken && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
|
|
14189
14190
|
await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, connectorCapabilities.meshConnectorToken);
|
|
14190
14191
|
}
|
|
14191
|
-
|
|
14192
|
+
if (alreadyEnrolled) {
|
|
14193
|
+
await host.installAgent(config, "bound");
|
|
14194
|
+
} else if (config.kind === "platform") {
|
|
14195
|
+
if (bootstrapManifest)
|
|
14196
|
+
await host.installAgent(config, "bootstrap");
|
|
14197
|
+
} else {
|
|
14198
|
+
await host.installAgent(config, "enrol");
|
|
14199
|
+
await host.installAgent(config, "bound");
|
|
14200
|
+
}
|
|
14192
14201
|
if (config.kind === "platform" && config.firewall.enabled) {
|
|
14193
14202
|
await host.ensureSoftware(plan.software.filter(({ id: id2 }) => id2 === "ufw"));
|
|
14194
14203
|
} else
|
|
@@ -14338,7 +14347,19 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
14338
14347
|
`, 384);
|
|
14339
14348
|
host.remove(config.bootstrapBundle.bundleFile);
|
|
14340
14349
|
host.remove(config.bootstrapBundle.manifestFile);
|
|
14341
|
-
|
|
14350
|
+
}
|
|
14351
|
+
if (!alreadyEnrolled) {
|
|
14352
|
+
await checked3(host, [
|
|
14353
|
+
"curl",
|
|
14354
|
+
"--fail",
|
|
14355
|
+
"--silent",
|
|
14356
|
+
"--show-error",
|
|
14357
|
+
"--max-time",
|
|
14358
|
+
"10",
|
|
14359
|
+
`http://127.0.0.1:3000${runtime.healthPath}`
|
|
14360
|
+
], "release-one API health");
|
|
14361
|
+
await host.installAgent(config, "enrol");
|
|
14362
|
+
await host.installAgent(config, "bound");
|
|
14342
14363
|
}
|
|
14343
14364
|
}
|
|
14344
14365
|
if (config.cloudflareHandoff) {
|
|
@@ -14507,6 +14528,65 @@ function readBootstrapConfig(path) {
|
|
|
14507
14528
|
}
|
|
14508
14529
|
return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync9(path, "utf8"))));
|
|
14509
14530
|
}
|
|
14531
|
+
function planBootstrapAgentInstall(config, phase, context) {
|
|
14532
|
+
const { capabilities, hasBinding, hasEnrolCredential, initialBundle } = context;
|
|
14533
|
+
if (phase === "bootstrap") {
|
|
14534
|
+
if (config.kind !== "platform" || hasBinding || !initialBundle) {
|
|
14535
|
+
throw new Error("release-one Agent install requires an unbound platform host and verified bootstrap bundle");
|
|
14536
|
+
}
|
|
14537
|
+
} else if (phase === "enrol") {
|
|
14538
|
+
if (hasBinding || !hasEnrolCredential) {
|
|
14539
|
+
throw new Error("Agent enrolment requires one unbound host and its sealed one-use credential");
|
|
14540
|
+
}
|
|
14541
|
+
} else if (!hasBinding) {
|
|
14542
|
+
throw new Error("bound Agent convergence requires durable enrolment state");
|
|
14543
|
+
}
|
|
14544
|
+
const bound = phase === "bound";
|
|
14545
|
+
const enrolling = phase === "enrol";
|
|
14546
|
+
const authenticated = enrolling || bound;
|
|
14547
|
+
const bootstrapRunner = platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner);
|
|
14548
|
+
const options = {
|
|
14549
|
+
capabilities,
|
|
14550
|
+
socketPath: DEFAULT_SOCKET,
|
|
14551
|
+
seedPath: "/var/lib/forgezero/node.seed",
|
|
14552
|
+
controlSocketPath: CONTROL_SOCKET,
|
|
14553
|
+
repository: phase === "bootstrap" ? initialBundle.path : bound && config.kind === "enrolled-compute" ? config.repository : undefined,
|
|
14554
|
+
branch: phase === "bootstrap" ? initialBundle.manifest.branch : bound && config.kind === "enrolled-compute" ? config.branch : undefined,
|
|
14555
|
+
bootstrapBundlePath: phase === "bootstrap" ? initialBundle.path : undefined,
|
|
14556
|
+
bootstrapBundleManifestPath: phase === "bootstrap" ? initialBundle.manifestPath : undefined,
|
|
14557
|
+
profile: config.profile,
|
|
14558
|
+
deployRoot: config.deployRoot ?? "/opt/forgezero",
|
|
14559
|
+
deploymentCredentials: config.deploymentCredentials,
|
|
14560
|
+
publicApiUrl: config.apiUrl,
|
|
14561
|
+
gitCredentialPath: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/creds/git-deploy-key.cred" : undefined,
|
|
14562
|
+
gitPublicKeyPath: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/git/deploy.pub" : undefined,
|
|
14563
|
+
generateGitIdentity: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey === true,
|
|
14564
|
+
pullDeployments: bound,
|
|
14565
|
+
pullMigrations: bound && config.kind === "platform",
|
|
14566
|
+
pullBootstrap: bound && bootstrapRunner,
|
|
14567
|
+
bootstrapSshCredentialPath: authenticated && bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
|
|
14568
|
+
bootstrapSshSourcePath: enrolling && config.kind === "enrolled-compute" ? config.bootstrapRunner?.sshPrivateKeyFile : undefined,
|
|
14569
|
+
bootstrapSshPublicKeyPath: authenticated && bootstrapRunner ? BOOTSTRAP_SSH_PUBLIC_KEY : undefined,
|
|
14570
|
+
bootstrapTargetTelemetryEndpoint: bound && bootstrapRunner ? platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined : undefined,
|
|
14571
|
+
lifecycleProfilePath: bound && config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
|
|
14572
|
+
enforceEgress: true,
|
|
14573
|
+
nodeHostname: config.nodeHostname,
|
|
14574
|
+
telemetryEndpoint: config.telemetryEndpoint,
|
|
14575
|
+
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
14576
|
+
sourceBinPath: PACKAGED_AGENT_BIN,
|
|
14577
|
+
...enrolling ? {
|
|
14578
|
+
enrolTokenCredentialPath: ENROL_CREDENTIAL,
|
|
14579
|
+
enrolStatePath: "/var/lib/forgezero/enrolment.json"
|
|
14580
|
+
} : bound ? { enrolStatePath: "/var/lib/forgezero/enrolment.json" } : {},
|
|
14581
|
+
...authenticated ? {
|
|
14582
|
+
apiUrl: config.apiUrl,
|
|
14583
|
+
project: config.kind === "enrolled-compute" ? config.realm : "platform",
|
|
14584
|
+
environment: config.kind === "enrolled-compute" ? undefined : config.environment,
|
|
14585
|
+
nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
|
|
14586
|
+
} : {}
|
|
14587
|
+
};
|
|
14588
|
+
return planInstall(options);
|
|
14589
|
+
}
|
|
14510
14590
|
function localBootstrapHost() {
|
|
14511
14591
|
const execute = async (argv2, options = {}) => {
|
|
14512
14592
|
const child = Bun.spawn([...argv2], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
|
|
@@ -14554,10 +14634,10 @@ function localBootstrapHost() {
|
|
|
14554
14634
|
throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
|
|
14555
14635
|
return result;
|
|
14556
14636
|
},
|
|
14557
|
-
async installAgent(config) {
|
|
14637
|
+
async installAgent(config, phase) {
|
|
14558
14638
|
const capabilities = await readCapabilities(localRunner);
|
|
14559
|
-
const deployRoot = config.deployRoot ?? "/opt/forgezero";
|
|
14560
14639
|
const hasBinding = existsSync9("/var/lib/forgezero/enrolment.json");
|
|
14640
|
+
const hasEnrolCredential = existsSync9(ENROL_CREDENTIAL);
|
|
14561
14641
|
const initialBundle = config.kind === "platform" && !existsSync9(BOOTSTRAP_RELEASE_EVIDENCE) ? {
|
|
14562
14642
|
path: config.bootstrapBundle.bundleFile,
|
|
14563
14643
|
manifestPath: config.bootstrapBundle.manifestFile,
|
|
@@ -14578,45 +14658,11 @@ function localBootstrapHost() {
|
|
|
14578
14658
|
writeFileSync9(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
|
|
14579
14659
|
`, { mode: 256 });
|
|
14580
14660
|
}
|
|
14581
|
-
const plan =
|
|
14661
|
+
const plan = planBootstrapAgentInstall(config, phase, {
|
|
14582
14662
|
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
|
-
} : {}
|
|
14663
|
+
hasBinding,
|
|
14664
|
+
hasEnrolCredential,
|
|
14665
|
+
initialBundle
|
|
14620
14666
|
});
|
|
14621
14667
|
for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
|
|
14622
14668
|
mkdirSync9(dirname9(unit.path), { recursive: true, mode: 493 });
|
|
@@ -15671,10 +15717,10 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15671
15717
|
|
|
15672
15718
|
// src/operator-bootstrap.ts
|
|
15673
15719
|
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";
|
|
15720
|
+
import { chmodSync as chmodSync6, lstatSync as lstatSync7, mkdirSync as mkdirSync12, mkdtempSync, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
15675
15721
|
import { isIP as isIP6 } from "net";
|
|
15676
15722
|
import { tmpdir } from "os";
|
|
15677
|
-
import { basename as basename2, join as join10 } from "path";
|
|
15723
|
+
import { basename as basename2, dirname as dirname13, isAbsolute as isAbsolute5, join as join10, resolve as resolve9 } from "path";
|
|
15678
15724
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
15679
15725
|
|
|
15680
15726
|
// src/platform-genesis.ts
|
|
@@ -15809,6 +15855,25 @@ var exactKeys5 = (value, keys, label) => {
|
|
|
15809
15855
|
throw new Error(`${label} contains unknown fields: ${unknown.join(", ")}`);
|
|
15810
15856
|
return record3;
|
|
15811
15857
|
};
|
|
15858
|
+
var canonicalOutputPath = (path, label) => {
|
|
15859
|
+
if (!isAbsolute5(path) || resolve9(path) !== path || /[\r\n\0]/.test(path)) {
|
|
15860
|
+
throw new Error(`${label} must be a canonical absolute path`);
|
|
15861
|
+
}
|
|
15862
|
+
return path;
|
|
15863
|
+
};
|
|
15864
|
+
var writeOwnerJson2 = (path, value, label) => {
|
|
15865
|
+
canonicalOutputPath(path, label);
|
|
15866
|
+
mkdirSync12(dirname13(path), { recursive: true, mode: 448 });
|
|
15867
|
+
const parent = lstatSync7(dirname13(path));
|
|
15868
|
+
const uid = process.getuid?.() ?? parent.uid;
|
|
15869
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== uid || (parent.mode & 63) !== 0) {
|
|
15870
|
+
throw new Error(`${label} parent must be an owner-only directory`);
|
|
15871
|
+
}
|
|
15872
|
+
writeFileSync12(path, `${JSON.stringify(value, null, 2)}
|
|
15873
|
+
`, { mode: 384, flag: "wx" });
|
|
15874
|
+
chmodSync6(path, 384);
|
|
15875
|
+
return path;
|
|
15876
|
+
};
|
|
15812
15877
|
var ownerFile = (path, limit, label) => {
|
|
15813
15878
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
15814
15879
|
throw new Error(`${label} path must be absolute`);
|
|
@@ -15867,8 +15932,10 @@ var validateHop = (value, label) => {
|
|
|
15867
15932
|
}
|
|
15868
15933
|
return hop;
|
|
15869
15934
|
};
|
|
15870
|
-
function
|
|
15871
|
-
|
|
15935
|
+
function createOperatorSshHop(input) {
|
|
15936
|
+
return validateHop({ ...input, hostKeySha256: fingerprint(input.hostKey) }, "operator SSH hop");
|
|
15937
|
+
}
|
|
15938
|
+
var validatePlatformRequestValue = (value) => {
|
|
15872
15939
|
const root = exactKeys5(value, ["kind", "target", "platformConfigFile"], "operator bootstrap request");
|
|
15873
15940
|
if (root.kind !== "platform-remote")
|
|
15874
15941
|
throw new Error("operator bootstrap kind must be platform-remote");
|
|
@@ -15887,6 +15954,7 @@ function readOperatorPlatformBootstrapRequest(path) {
|
|
|
15887
15954
|
socketPath(targetValue.agentSocket);
|
|
15888
15955
|
if (typeof root.platformConfigFile !== "string")
|
|
15889
15956
|
throw new Error("platformConfigFile must be an absolute path");
|
|
15957
|
+
canonicalOutputPath(root.platformConfigFile, "platformConfigFile");
|
|
15890
15958
|
const config = readBootstrapConfig(root.platformConfigFile);
|
|
15891
15959
|
if (config.kind !== "platform")
|
|
15892
15960
|
throw new Error("operator bootstrap requires a platform config");
|
|
@@ -15900,9 +15968,39 @@ function readOperatorPlatformBootstrapRequest(path) {
|
|
|
15900
15968
|
...targetValue.jump === undefined ? {} : { jump: validateHop(targetValue.jump, "operator bootstrap jump") }
|
|
15901
15969
|
}
|
|
15902
15970
|
};
|
|
15903
|
-
}
|
|
15904
|
-
function
|
|
15905
|
-
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator
|
|
15971
|
+
};
|
|
15972
|
+
function readOperatorPlatformBootstrapRequest(path) {
|
|
15973
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator bootstrap request")));
|
|
15974
|
+
return validatePlatformRequestValue(value);
|
|
15975
|
+
}
|
|
15976
|
+
function writeOperatorPlatformBootstrapRequest(path, request) {
|
|
15977
|
+
validatePlatformRequestValue(request);
|
|
15978
|
+
return writeOwnerJson2(path, request, "operator bootstrap request");
|
|
15979
|
+
}
|
|
15980
|
+
function readOperatorPlatformBootstrapFleetRequest(path) {
|
|
15981
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator fleet request")));
|
|
15982
|
+
const root = exactKeys5(value, ["kind", "requests"], "operator fleet request");
|
|
15983
|
+
if (root.kind !== "platform-fleet-remote")
|
|
15984
|
+
throw new Error("operator fleet kind must be platform-fleet-remote");
|
|
15985
|
+
if (!Array.isArray(root.requests) || root.requests.length !== 3 || root.requests.some((entry) => typeof entry !== "string")) {
|
|
15986
|
+
throw new Error("operator fleet requires exactly three request files");
|
|
15987
|
+
}
|
|
15988
|
+
const requests = root.requests.map((entry) => canonicalOutputPath(entry, "operator fleet request file"));
|
|
15989
|
+
if (new Set(requests).size !== 3)
|
|
15990
|
+
throw new Error("operator fleet request files must be unique");
|
|
15991
|
+
for (const request of requests)
|
|
15992
|
+
readOperatorPlatformBootstrapRequest(request);
|
|
15993
|
+
return { kind: "platform-fleet-remote", requests };
|
|
15994
|
+
}
|
|
15995
|
+
function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles) {
|
|
15996
|
+
const request = { kind: "platform-fleet-remote", requests: requestFiles };
|
|
15997
|
+
if (new Set(requestFiles).size !== 3)
|
|
15998
|
+
throw new Error("operator fleet request files must be unique");
|
|
15999
|
+
for (const file of requestFiles)
|
|
16000
|
+
readOperatorPlatformBootstrapRequest(file);
|
|
16001
|
+
return writeOwnerJson2(path, request, "operator fleet request");
|
|
16002
|
+
}
|
|
16003
|
+
var validateMetalRequestValue = (value) => {
|
|
15906
16004
|
const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
|
|
15907
16005
|
if (root.kind !== "metal-remote")
|
|
15908
16006
|
throw new Error("operator metal bootstrap kind must be metal-remote");
|
|
@@ -15921,6 +16019,7 @@ function readOperatorMetalBootstrapRequest(path) {
|
|
|
15921
16019
|
socketPath(targetValue.agentSocket);
|
|
15922
16020
|
if (typeof root.metalConfigFile !== "string")
|
|
15923
16021
|
throw new Error("metalConfigFile must be an absolute path");
|
|
16022
|
+
canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
|
|
15924
16023
|
readMetalBootstrapConfig(root.metalConfigFile);
|
|
15925
16024
|
const genesis = exactKeys5(root.genesis, ["environment", "sshPublicKeyFiles"], "operator metal genesis");
|
|
15926
16025
|
if (typeof genesis.environment !== "string" || !PLATFORM_GENESIS_ENVIRONMENTS.includes(genesis.environment)) {
|
|
@@ -15944,6 +16043,14 @@ function readOperatorMetalBootstrapRequest(path) {
|
|
|
15944
16043
|
sshPublicKeyFiles: genesis.sshPublicKeyFiles
|
|
15945
16044
|
}
|
|
15946
16045
|
};
|
|
16046
|
+
};
|
|
16047
|
+
function readOperatorMetalBootstrapRequest(path) {
|
|
16048
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal request")));
|
|
16049
|
+
return validateMetalRequestValue(value);
|
|
16050
|
+
}
|
|
16051
|
+
function writeOperatorMetalBootstrapRequest(path, request) {
|
|
16052
|
+
validateMetalRequestValue(request);
|
|
16053
|
+
return writeOwnerJson2(path, request, "operator metal request");
|
|
15947
16054
|
}
|
|
15948
16055
|
function planOperatorMetalBootstrap(request, mode) {
|
|
15949
16056
|
const config = readMetalBootstrapConfig(request.metalConfigFile);
|
|
@@ -16003,6 +16110,87 @@ function planOperatorPlatformBootstrap(request, mode) {
|
|
|
16003
16110
|
secretInputs: mode === "apply" ? [...attendedSecretNames(config), ...secretSources(config).map(([name]) => name)] : []
|
|
16004
16111
|
};
|
|
16005
16112
|
}
|
|
16113
|
+
var fleetConfigIdentity = (config) => {
|
|
16114
|
+
const normalized = structuredClone(config);
|
|
16115
|
+
delete normalized.computeReference;
|
|
16116
|
+
delete normalized.nodeHostname;
|
|
16117
|
+
delete normalized.cloudflareHandoff;
|
|
16118
|
+
delete normalized.database.role;
|
|
16119
|
+
delete normalized.database.address;
|
|
16120
|
+
delete normalized.database.master;
|
|
16121
|
+
const environment = normalized.runtime.environment;
|
|
16122
|
+
delete environment.databaseRole;
|
|
16123
|
+
delete environment.databaseAddress;
|
|
16124
|
+
delete environment.databaseMaster;
|
|
16125
|
+
delete environment.nodeHostname;
|
|
16126
|
+
delete environment.seedSyncPeers;
|
|
16127
|
+
delete environment.custodianEmail;
|
|
16128
|
+
return JSON.stringify(normalized);
|
|
16129
|
+
};
|
|
16130
|
+
function validatedPlatformFleet(fleet) {
|
|
16131
|
+
const requests = fleet.requests.map(readOperatorPlatformBootstrapRequest);
|
|
16132
|
+
const configs = requests.map((request) => {
|
|
16133
|
+
const config = readBootstrapConfig(request.platformConfigFile);
|
|
16134
|
+
if (config.kind !== "platform")
|
|
16135
|
+
throw new Error("operator fleet requires platform configs");
|
|
16136
|
+
return config;
|
|
16137
|
+
});
|
|
16138
|
+
const environment = configs[0].environment;
|
|
16139
|
+
const expected = PLATFORM_GENESIS_FLEETS[environment];
|
|
16140
|
+
if (!expected || expected.length !== 3)
|
|
16141
|
+
throw new Error("operator fleet environment is invalid");
|
|
16142
|
+
const identity = fleetConfigIdentity(configs[0]);
|
|
16143
|
+
const operatorIdentity = `${requests[0].target.identityPublicKeyFile}\x00${requests[0].target.agentSocket}`;
|
|
16144
|
+
const jumpIdentity = JSON.stringify(requests[0].target.jump ?? null);
|
|
16145
|
+
const coordinators = expected.map((guest) => `http://${guest.address}:8529`);
|
|
16146
|
+
const nodeHostnames = new Set;
|
|
16147
|
+
for (let index = 0;index < 3; index += 1) {
|
|
16148
|
+
const config = configs[index];
|
|
16149
|
+
const request = requests[index];
|
|
16150
|
+
const guest = expected[index];
|
|
16151
|
+
const role = index === 0 ? "master" : "joiner";
|
|
16152
|
+
if (config.environment !== environment || fleetConfigIdentity(config) !== identity) {
|
|
16153
|
+
throw new Error("operator fleet platform configs do not share one immutable identity");
|
|
16154
|
+
}
|
|
16155
|
+
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) {
|
|
16156
|
+
throw new Error(`operator fleet ${guest.name} does not match the exact genesis topology`);
|
|
16157
|
+
}
|
|
16158
|
+
if (request.target.address !== guest.address || request.target.port !== 22 || request.target.user !== "forgezero") {
|
|
16159
|
+
throw new Error(`operator fleet ${guest.name} SSH target must be forgezero@${guest.address}:22`);
|
|
16160
|
+
}
|
|
16161
|
+
if (`${request.target.identityPublicKeyFile}\x00${request.target.agentSocket}` !== operatorIdentity) {
|
|
16162
|
+
throw new Error("operator fleet must use one caller-approved SSH identity and agent socket");
|
|
16163
|
+
}
|
|
16164
|
+
if (JSON.stringify(request.target.jump ?? null) !== jumpIdentity) {
|
|
16165
|
+
throw new Error("operator fleet must use one pinned Metal jump or no jump on every node");
|
|
16166
|
+
}
|
|
16167
|
+
if (nodeHostnames.has(config.nodeHostname))
|
|
16168
|
+
throw new Error("operator fleet node hostnames must be unique");
|
|
16169
|
+
nodeHostnames.add(config.nodeHostname);
|
|
16170
|
+
}
|
|
16171
|
+
return { requests, configs, environment };
|
|
16172
|
+
}
|
|
16173
|
+
function planOperatorPlatformFleetBootstrap(fleet, mode) {
|
|
16174
|
+
const { requests, configs, environment } = validatedPlatformFleet(fleet);
|
|
16175
|
+
const secretInputs = mode === "apply" ? attendedSecretNames(configs[0]) : [];
|
|
16176
|
+
for (const config of configs.slice(1)) {
|
|
16177
|
+
if (JSON.stringify(attendedSecretNames(config)) !== JSON.stringify(secretInputs)) {
|
|
16178
|
+
throw new Error("operator fleet configs do not share one attended secret schema");
|
|
16179
|
+
}
|
|
16180
|
+
}
|
|
16181
|
+
return {
|
|
16182
|
+
kind: "platform-fleet-remote",
|
|
16183
|
+
mode,
|
|
16184
|
+
mutation: mode !== "status",
|
|
16185
|
+
environment,
|
|
16186
|
+
nodes: requests.map((request, index) => ({
|
|
16187
|
+
computeReference: configs[index].computeReference,
|
|
16188
|
+
address: request.target.address,
|
|
16189
|
+
...request.target.jump ? { via: request.target.jump.address } : {}
|
|
16190
|
+
})),
|
|
16191
|
+
secretInputs
|
|
16192
|
+
};
|
|
16193
|
+
}
|
|
16006
16194
|
var defaultExec3 = async (argv2, options = {}) => {
|
|
16007
16195
|
const child = Bun.spawn([...argv2], {
|
|
16008
16196
|
stdin: options.stdin === undefined ? "ignore" : "pipe",
|
|
@@ -16245,6 +16433,30 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
16245
16433
|
rmSync7(directory, { recursive: true, force: true });
|
|
16246
16434
|
}
|
|
16247
16435
|
}
|
|
16436
|
+
async function applyOperatorPlatformFleetBootstrap(fleet, mode, options = {}) {
|
|
16437
|
+
const plan = planOperatorPlatformFleetBootstrap(fleet, mode);
|
|
16438
|
+
const { requests, configs } = validatedPlatformFleet(fleet);
|
|
16439
|
+
const secrets = mode === "apply" ? validatePlatformBootstrapSecrets(configs[0], options.secrets) : undefined;
|
|
16440
|
+
if (secrets) {
|
|
16441
|
+
for (const config of configs.slice(1))
|
|
16442
|
+
validatePlatformBootstrapSecrets(config, secrets);
|
|
16443
|
+
}
|
|
16444
|
+
const settled = await Promise.allSettled(requests.map((request) => applyOperatorPlatformBootstrap(request, mode, { ...options, secrets })));
|
|
16445
|
+
const outcomes = settled.map((result, index) => {
|
|
16446
|
+
const config = configs[index];
|
|
16447
|
+
const address = requests[index].target.address;
|
|
16448
|
+
if (result.status === "fulfilled")
|
|
16449
|
+
return {
|
|
16450
|
+
computeReference: config.computeReference,
|
|
16451
|
+
address,
|
|
16452
|
+
ok: true,
|
|
16453
|
+
output: result.value.output.slice(0, 65536)
|
|
16454
|
+
};
|
|
16455
|
+
const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
16456
|
+
return { computeReference: config.computeReference, address, ok: false, error: message.slice(0, 4096) };
|
|
16457
|
+
});
|
|
16458
|
+
return { plan, ok: outcomes.every(({ ok }) => ok), outcomes };
|
|
16459
|
+
}
|
|
16248
16460
|
async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
16249
16461
|
const plan = planOperatorMetalBootstrap(request, mode);
|
|
16250
16462
|
publicIdentity(request.target.identityPublicKeyFile);
|
|
@@ -16478,7 +16690,7 @@ async function applyHostMaintenance(request, runtime = localRuntime()) {
|
|
|
16478
16690
|
|
|
16479
16691
|
// src/cli/maintenance.ts
|
|
16480
16692
|
import { existsSync as existsSync11, lstatSync as lstatSync9, readFileSync as readFileSync14, realpathSync as realpathSync5 } from "fs";
|
|
16481
|
-
import { isAbsolute as
|
|
16693
|
+
import { isAbsolute as isAbsolute6, join as join11, relative as relative2, resolve as resolve10 } from "path";
|
|
16482
16694
|
var API_OPERATION_ENTRYPOINTS = {
|
|
16483
16695
|
"dev-reset": ["src", "server", "maintenance", "dev-reset.ts"],
|
|
16484
16696
|
"db-backup": ["src", "server", "maintenance", "snapshot-backup.ts"],
|
|
@@ -16572,13 +16784,13 @@ function checkedEntrypoint(root, parts) {
|
|
|
16572
16784
|
}
|
|
16573
16785
|
const actual = realpathSync5(candidate);
|
|
16574
16786
|
const within = relative2(root, actual);
|
|
16575
|
-
if (within.startsWith("..") ||
|
|
16787
|
+
if (within.startsWith("..") || isAbsolute6(within)) {
|
|
16576
16788
|
throw new Error(`The reviewed operation entrypoint escapes repository root ${root}.`);
|
|
16577
16789
|
}
|
|
16578
16790
|
return actual;
|
|
16579
16791
|
}
|
|
16580
16792
|
function resolveRepositoryOperation(operation, requestedRoot) {
|
|
16581
|
-
const root = realpathSync5(
|
|
16793
|
+
const root = realpathSync5(resolve10(requestedRoot));
|
|
16582
16794
|
const name = manifestName(root);
|
|
16583
16795
|
if (operation in API_OPERATION_ENTRYPOINTS) {
|
|
16584
16796
|
const parts = API_OPERATION_ENTRYPOINTS[operation];
|
|
@@ -16879,7 +17091,7 @@ async function api(options, path, init) {
|
|
|
16879
17091
|
return { status: response.status, body };
|
|
16880
17092
|
}
|
|
16881
17093
|
function sleep(ms) {
|
|
16882
|
-
return new Promise((
|
|
17094
|
+
return new Promise((resolve12) => setTimeout(resolve12, ms));
|
|
16883
17095
|
}
|
|
16884
17096
|
function browserCommand(url) {
|
|
16885
17097
|
if (process.platform === "darwin")
|
|
@@ -17313,7 +17525,7 @@ async function cmdAgent(options, args) {
|
|
|
17313
17525
|
writeFileSync13(plan.unitPath, plan.unit, { mode: 420 });
|
|
17314
17526
|
out.ok(`Wrote ${plan.unitPath}`);
|
|
17315
17527
|
for (const auxiliary of plan.auxiliaryUnits) {
|
|
17316
|
-
|
|
17528
|
+
mkdirSync13(dirname14(auxiliary.path), { recursive: true, mode: 493 });
|
|
17317
17529
|
writeFileSync13(auxiliary.path, auxiliary.unit, { mode: 420 });
|
|
17318
17530
|
out.ok(`Wrote ${auxiliary.path}`);
|
|
17319
17531
|
}
|
|
@@ -17322,7 +17534,7 @@ async function cmdAgent(options, args) {
|
|
|
17322
17534
|
const token = await bootstrapSecret("ForgeZero one-time enrolment token");
|
|
17323
17535
|
if (!/^fze_[A-Za-z0-9_-]{40,100}$/.test(token))
|
|
17324
17536
|
throw new Error("A valid fze_ enrolment token was not provided.");
|
|
17325
|
-
|
|
17537
|
+
mkdirSync13(dirname14(enrolTokenCredentialPath), { recursive: true, mode: 448 });
|
|
17326
17538
|
const sealing = Bun.spawn(["systemd-creds", "encrypt", "--name=enrol-token", "-", enrolTokenCredentialPath], { stdin: "pipe", stdout: "ignore", stderr: "pipe" });
|
|
17327
17539
|
if (sealing.stdin && typeof sealing.stdin !== "number") {
|
|
17328
17540
|
sealing.stdin.write(`${token}
|
|
@@ -17448,9 +17660,9 @@ function interactiveMetalBootstrap() {
|
|
|
17448
17660
|
});
|
|
17449
17661
|
}
|
|
17450
17662
|
function writeBootstrapConfig(path, config) {
|
|
17451
|
-
if (!
|
|
17663
|
+
if (!isAbsolute7(path) || resolve11(path) !== path)
|
|
17452
17664
|
throw new Error("--output must be a canonical absolute path");
|
|
17453
|
-
|
|
17665
|
+
mkdirSync13(dirname14(path), { recursive: true, mode: 448 });
|
|
17454
17666
|
writeFileSync13(path, `${JSON.stringify(config, null, 2)}
|
|
17455
17667
|
`, { mode: 384, flag: "wx" });
|
|
17456
17668
|
return path;
|
|
@@ -17512,10 +17724,10 @@ async function interactiveCloudflareBootstrap() {
|
|
|
17512
17724
|
});
|
|
17513
17725
|
}
|
|
17514
17726
|
function genesisOutputDirectory(path) {
|
|
17515
|
-
if (!
|
|
17727
|
+
if (!isAbsolute7(path) || resolve11(path) !== path)
|
|
17516
17728
|
throw new Error("--output must be a canonical absolute directory");
|
|
17517
17729
|
if (!existsSync12(path))
|
|
17518
|
-
|
|
17730
|
+
mkdirSync13(path, { recursive: true, mode: 448 });
|
|
17519
17731
|
const metadata = lstatSync10(path);
|
|
17520
17732
|
const uid = process.getuid?.();
|
|
17521
17733
|
if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 63) !== 0 || uid !== undefined && uid !== 0 && metadata.uid !== uid) {
|
|
@@ -17523,6 +17735,37 @@ function genesisOutputDirectory(path) {
|
|
|
17523
17735
|
}
|
|
17524
17736
|
return path;
|
|
17525
17737
|
}
|
|
17738
|
+
function interactiveOperatorHop(label, defaults) {
|
|
17739
|
+
return createOperatorSshHop({
|
|
17740
|
+
address: bootstrapAnswer(`${label} explicit SSH IP`, defaults.address),
|
|
17741
|
+
port: bootstrapNumber(`${label} SSH port`, "22"),
|
|
17742
|
+
user: bootstrapAnswer(`${label} SSH user`, defaults.user),
|
|
17743
|
+
hostKey: bootstrapAnswer(`${label} trusted ssh-ed25519 host public key`)
|
|
17744
|
+
});
|
|
17745
|
+
}
|
|
17746
|
+
function operatorIdentityCoordinates() {
|
|
17747
|
+
return {
|
|
17748
|
+
identityPublicKeyFile: bootstrapAnswer("Operator SSH public-key file (private key remains in Bitwarden SSH agent)"),
|
|
17749
|
+
agentSocket: bootstrapAnswer("Bitwarden SSH agent socket", process.env.SSH_AUTH_SOCK)
|
|
17750
|
+
};
|
|
17751
|
+
}
|
|
17752
|
+
function writeMetalOperatorFiles(directory) {
|
|
17753
|
+
const output = genesisOutputDirectory(directory);
|
|
17754
|
+
const config = interactiveMetalBootstrap();
|
|
17755
|
+
const metalConfig = writeBootstrapConfig(join12(output, "metal.json"), config);
|
|
17756
|
+
const identity = operatorIdentityCoordinates();
|
|
17757
|
+
const target = interactiveOperatorHop("Metal", { user: "root" });
|
|
17758
|
+
const remoteRequest = writeOperatorMetalBootstrapRequest(join12(output, "metal-remote.json"), {
|
|
17759
|
+
kind: "metal-remote",
|
|
17760
|
+
metalConfigFile: metalConfig,
|
|
17761
|
+
target: { ...target, ...identity },
|
|
17762
|
+
genesis: {
|
|
17763
|
+
environment: bootstrapAnswer("Genesis environment (production/development)", "development"),
|
|
17764
|
+
sshPublicKeyFiles: [identity.identityPublicKeyFile]
|
|
17765
|
+
}
|
|
17766
|
+
});
|
|
17767
|
+
return { metalConfig, remoteRequest };
|
|
17768
|
+
}
|
|
17526
17769
|
function writePlatformGenesisFleet(directory) {
|
|
17527
17770
|
const output = genesisOutputDirectory(directory);
|
|
17528
17771
|
const template = interactiveBootstrap("platform", true);
|
|
@@ -17539,7 +17782,20 @@ function writePlatformGenesisFleet(directory) {
|
|
|
17539
17782
|
writeBootstrapConfig(path, config);
|
|
17540
17783
|
return path;
|
|
17541
17784
|
});
|
|
17542
|
-
|
|
17785
|
+
const identity = operatorIdentityCoordinates();
|
|
17786
|
+
const useJump = bootstrapAnswer("Reach genesis computes through the Metal SSH jump? (yes/no)", "yes") === "yes";
|
|
17787
|
+
const jump = useJump ? interactiveOperatorHop("Metal jump", { user: "root" }) : undefined;
|
|
17788
|
+
const requests = configs.map((platformConfigFile, index) => {
|
|
17789
|
+
const guest = fleet[index];
|
|
17790
|
+
const target = interactiveOperatorHop(`${guest.name} compute`, { address: guest.address, user: "forgezero" });
|
|
17791
|
+
return writeOperatorPlatformBootstrapRequest(join12(output, `${guest.name}-remote.json`), {
|
|
17792
|
+
kind: "platform-remote",
|
|
17793
|
+
platformConfigFile,
|
|
17794
|
+
target: { ...target, ...identity, ...jump ? { jump } : {} }
|
|
17795
|
+
});
|
|
17796
|
+
});
|
|
17797
|
+
const fleetRequest = writeOperatorPlatformBootstrapFleetRequest(join12(output, "platform-fleet-remote.json"), requests);
|
|
17798
|
+
return { configs, requests, fleetRequest };
|
|
17543
17799
|
}
|
|
17544
17800
|
function interactiveBootstrap(kind, genesis = false) {
|
|
17545
17801
|
if (!process.stdin.isTTY)
|
|
@@ -17661,7 +17917,7 @@ async function cmdBootstrap(options, args) {
|
|
|
17661
17917
|
if (operation === "config") {
|
|
17662
17918
|
const kind = args[1];
|
|
17663
17919
|
if (kind !== "metal" && kind !== "platform" && kind !== "cloudflare" || args[2] !== undefined || !options.outputPath) {
|
|
17664
|
-
throw new Error("Usage: fz bootstrap config
|
|
17920
|
+
throw new Error("Usage: fz bootstrap config cloudflare --output <absolute-file> | fz bootstrap config metal|platform --output <absolute-owner-only-directory>");
|
|
17665
17921
|
}
|
|
17666
17922
|
if (kind === "platform") {
|
|
17667
17923
|
out.line(JSON.stringify({ kind, ...writePlatformGenesisFleet(options.outputPath), mode: "0600" }, null, 2));
|
|
@@ -17673,10 +17929,32 @@ async function cmdBootstrap(options, args) {
|
|
|
17673
17929
|
out.line(JSON.stringify({ kind, path, mode: "0600" }, null, 2));
|
|
17674
17930
|
return 0;
|
|
17675
17931
|
}
|
|
17676
|
-
|
|
17677
|
-
out.line(JSON.stringify({ kind, path: writeBootstrapConfig(options.outputPath, config2), mode: "0600" }, null, 2));
|
|
17932
|
+
out.line(JSON.stringify({ kind, ...writeMetalOperatorFiles(options.outputPath), mode: "0600" }, null, 2));
|
|
17678
17933
|
return 0;
|
|
17679
17934
|
}
|
|
17935
|
+
if (operation === "platform" && args[1] === "fleet") {
|
|
17936
|
+
const mode = args[2];
|
|
17937
|
+
if (!mode || !["apply", "status"].includes(mode) || args[3] !== undefined) {
|
|
17938
|
+
throw new Error("Usage: fz bootstrap platform fleet <apply|status> --bootstrap-config <owner-only-platform-fleet-remote.json> [--apply]");
|
|
17939
|
+
}
|
|
17940
|
+
if (!options.bootstrapConfigPath)
|
|
17941
|
+
throw new Error("remote platform fleet bootstrap requires --bootstrap-config");
|
|
17942
|
+
const fleet = readOperatorPlatformBootstrapFleetRequest(options.bootstrapConfigPath);
|
|
17943
|
+
const plan2 = planOperatorPlatformFleetBootstrap(fleet, mode);
|
|
17944
|
+
if (!options.apply) {
|
|
17945
|
+
out.line(JSON.stringify(plan2, null, 2));
|
|
17946
|
+
out.step("Review all three pinned targets and shared secret names, then repeat with --apply.");
|
|
17947
|
+
return 0;
|
|
17948
|
+
}
|
|
17949
|
+
const first = readOperatorPlatformBootstrapRequest(fleet.requests[0]);
|
|
17950
|
+
const firstConfig = readBootstrapConfig(first.platformConfigFile);
|
|
17951
|
+
if (firstConfig.kind !== "platform")
|
|
17952
|
+
throw new Error("remote platform fleet requires platform configs");
|
|
17953
|
+
const secrets2 = mode === "apply" ? await promptPlatformBootstrapSecrets(firstConfig) : undefined;
|
|
17954
|
+
const result2 = await applyOperatorPlatformFleetBootstrap(fleet, mode, { secrets: secrets2 });
|
|
17955
|
+
out.line(JSON.stringify(result2, null, 2));
|
|
17956
|
+
return result2.ok ? 0 : 1;
|
|
17957
|
+
}
|
|
17680
17958
|
if (operation === "platform" && args[1] === "remote") {
|
|
17681
17959
|
const mode = args[2];
|
|
17682
17960
|
if (!mode || !["apply", "status"].includes(mode) || args[3] !== undefined) {
|
|
@@ -17812,7 +18090,7 @@ async function cmdBootstrap(options, args) {
|
|
|
17812
18090
|
return 0;
|
|
17813
18091
|
}
|
|
17814
18092
|
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]");
|
|
18093
|
+
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
18094
|
}
|
|
17817
18095
|
const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
|
|
17818
18096
|
throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
|
|
@@ -18188,7 +18466,7 @@ async function cmdDeploy(options, args) {
|
|
|
18188
18466
|
out.ok(`Initialized legacy .fz/deploy.json (${created.summary.digest}).`);
|
|
18189
18467
|
return 0;
|
|
18190
18468
|
}
|
|
18191
|
-
initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(
|
|
18469
|
+
initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(resolve11(options.projectRoot)), force: options.force });
|
|
18192
18470
|
const compiled = await compileDeploymentProject(options.projectRoot);
|
|
18193
18471
|
if (options.json)
|
|
18194
18472
|
out.line(JSON.stringify({ source: compiled.source, output: compiled.output, digest: compiled.digest }, null, 2));
|
|
@@ -18213,7 +18491,7 @@ async function cmdDeploy(options, args) {
|
|
|
18213
18491
|
return problems.length === 0 ? 0 : 1;
|
|
18214
18492
|
}
|
|
18215
18493
|
if (operation === "check" || operation === "sync") {
|
|
18216
|
-
if (existsSync12(
|
|
18494
|
+
if (existsSync12(resolve11(options.projectRoot, DEPLOY_SOURCE_FILE))) {
|
|
18217
18495
|
const expected = await compileDeploymentProject(options.projectRoot, { write: false });
|
|
18218
18496
|
const actual = inspectCompiledDeployment(options.projectRoot);
|
|
18219
18497
|
const current = canonicalJson(expected.plan) === canonicalJson(actual.plan);
|
|
@@ -18390,10 +18668,13 @@ function usage() {
|
|
|
18390
18668
|
fz bootstrap platform remote <apply|status>
|
|
18391
18669
|
Run typed bootstrap from the operator laptop through
|
|
18392
18670
|
a pinned host; apply copies and verifies that bundle
|
|
18671
|
+
fz bootstrap platform fleet <apply|status>
|
|
18672
|
+
Validate the exact three-node genesis identity, prompt
|
|
18673
|
+
shared secrets once and run all pinned nodes concurrently
|
|
18393
18674
|
fz bootstrap platform Install/repair a typed elastic platform compute
|
|
18394
18675
|
fz bootstrap config <metal|platform|cloudflare>
|
|
18395
|
-
Write
|
|
18396
|
-
|
|
18676
|
+
Write validated owner-only metal config+remote request,
|
|
18677
|
+
Cloudflare file, or complete three-node platform request set
|
|
18397
18678
|
fz bootstrap platform cloudflare
|
|
18398
18679
|
Plan/apply attended Tunnel and DNS reconciliation
|
|
18399
18680
|
using hidden management and KV/Worker runtime token prompts
|