@forgezero/agent 0.1.85 → 0.1.87
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 -14
- package/dist/agent-handover.d.ts +13 -0
- package/dist/agent-heartbeat.d.ts +3 -3
- package/dist/agent-heartbeat.js +235 -62
- package/dist/agent-update-helper.d.ts +11 -2
- package/dist/agent-update-helper.js +201 -45
- package/dist/agent-update.d.ts +4 -0
- package/dist/agent-update.js +19 -1
- package/dist/bootstrap.js +56 -8
- package/dist/credential-schema.js +16 -4
- package/dist/fz-agent.js +569 -260
- package/dist/fz.js +415 -43
- package/dist/guest-enrolment.js +4 -2
- package/dist/metal-bootstrap.js +40 -1
- package/dist/migration-pull.js +4 -2
- package/dist/node-vault.js +16 -4
- package/dist/operator-bootstrap.js +95 -8
- package/dist/platform-bootstrap-runtime.d.ts +7 -0
- package/dist/platform-bootstrap-runtime.js +5 -2
- package/dist/platform-fleet-verification.js +411 -213
- package/dist/platform-genesis-config.d.ts +6 -1
- package/dist/platform-launch-profile.d.ts +44 -0
- package/dist/provision.d.ts +16 -1
- package/dist/provision.js +323 -124
- package/dist/provisioning-pull.js +4 -2
- package/dist/signed-node-http.d.ts +2 -1
- package/dist/socket.d.ts +6 -0
- package/dist/version.d.ts +1 -1
- package/package.json +4 -4
package/dist/fz.js
CHANGED
|
@@ -4634,7 +4634,7 @@ var VaultError, runtimeEnvironment = () => typeof process !== "undefined" && pro
|
|
|
4634
4634
|
} catch {
|
|
4635
4635
|
return;
|
|
4636
4636
|
}
|
|
4637
|
-
}, VERSION = "0.1.
|
|
4637
|
+
}, VERSION = "0.1.18";
|
|
4638
4638
|
var init_dist = __esm(() => {
|
|
4639
4639
|
init_identity();
|
|
4640
4640
|
VaultError = class VaultError extends Error {
|
|
@@ -4810,8 +4810,9 @@ async function spawnWith(command, env, report = () => {}, options = {}) {
|
|
|
4810
4810
|
}
|
|
4811
4811
|
|
|
4812
4812
|
// src/cli/index.ts
|
|
4813
|
-
import { existsSync as existsSync12, lstatSync as lstatSync10, mkdirSync as mkdirSync13, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
|
|
4813
|
+
import { chmodSync as chmodSync7, existsSync as existsSync12, lstatSync as lstatSync10, mkdirSync as mkdirSync13, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
|
|
4814
4814
|
import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute7, join as join13, resolve as resolve12 } from "path";
|
|
4815
|
+
import { randomBytes as randomBytes10 } from "crypto";
|
|
4815
4816
|
|
|
4816
4817
|
// src/process-input.ts
|
|
4817
4818
|
async function writeAndCloseProcessInput(input, value) {
|
|
@@ -4826,9 +4827,13 @@ import { hostname } from "os";
|
|
|
4826
4827
|
|
|
4827
4828
|
// src/agent-update.ts
|
|
4828
4829
|
var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
|
|
4830
|
+
var DEFAULT_AGENT_CANDIDATE_LINK = `${DEFAULT_AGENT_RELEASE_ROOT}/candidate`;
|
|
4829
4831
|
var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
|
|
4830
4832
|
var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
|
|
4831
4833
|
|
|
4834
|
+
// src/agent-handover.ts
|
|
4835
|
+
var DEFAULT_AGENT_CANDIDATE_READY_SOCKET = "/run/forgezero/candidate-ready.sock";
|
|
4836
|
+
|
|
4832
4837
|
// src/agent-update-helper.ts
|
|
4833
4838
|
var AGENT_UPDATE_GROUP = "forgezero-update";
|
|
4834
4839
|
var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-helper.service";
|
|
@@ -4837,7 +4842,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
4837
4842
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
4838
4843
|
|
|
4839
4844
|
// src/version.ts
|
|
4840
|
-
var VERSION2 = "0.1.
|
|
4845
|
+
var VERSION2 = "0.1.87";
|
|
4841
4846
|
|
|
4842
4847
|
// src/software.ts
|
|
4843
4848
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -5454,6 +5459,7 @@ var LIFECYCLE_GROUP = "forgezero-lifecycle";
|
|
|
5454
5459
|
var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
|
|
5455
5460
|
var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
|
|
5456
5461
|
var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
|
|
5462
|
+
var AGENT_CANDIDATE_UNIT_PATH = "/etc/systemd/system/forgezero-agent-candidate.service";
|
|
5457
5463
|
var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
5458
5464
|
var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
|
|
5459
5465
|
var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
|
|
@@ -5565,6 +5571,7 @@ Type=simple
|
|
|
5565
5571
|
User=root
|
|
5566
5572
|
Group=${AGENT_UPDATE_GROUP}
|
|
5567
5573
|
Environment=FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}
|
|
5574
|
+
Environment=FZ_AGENT_PUBLIC_SOCKET=${systemdPath(options.socketPath, "agent socket")}
|
|
5568
5575
|
ExecStart=${bin} update-helper
|
|
5569
5576
|
Restart=always
|
|
5570
5577
|
RestartSec=2
|
|
@@ -5609,13 +5616,11 @@ WantedBy=sockets.target
|
|
|
5609
5616
|
`;
|
|
5610
5617
|
}
|
|
5611
5618
|
function agentSocketProxyUnit(options) {
|
|
5612
|
-
const backend =
|
|
5619
|
+
const backend = agentRoutingSocketPath(options.socketPath);
|
|
5613
5620
|
const user = options.user ?? "forgezero";
|
|
5614
5621
|
return `[Unit]
|
|
5615
5622
|
Description=ForgeZero application Vault socket proxy
|
|
5616
5623
|
Documentation=https://www.forgezero.net/docs/agent
|
|
5617
|
-
Requires=forgezero-agent.service
|
|
5618
|
-
After=forgezero-agent.service
|
|
5619
5624
|
|
|
5620
5625
|
[Service]
|
|
5621
5626
|
User=${user}
|
|
@@ -5636,12 +5641,26 @@ RestrictAddressFamilies=AF_UNIX
|
|
|
5636
5641
|
`;
|
|
5637
5642
|
}
|
|
5638
5643
|
function agentBackendSocketPath(publicSocketPath) {
|
|
5644
|
+
const socket = systemdPath(publicSocketPath, "agent socket");
|
|
5645
|
+
const backend = `${socket}.backend.active`;
|
|
5646
|
+
if (Buffer.byteLength(backend) > 100)
|
|
5647
|
+
throw new Error("agent socket path is too long for a Unix socket");
|
|
5648
|
+
return backend;
|
|
5649
|
+
}
|
|
5650
|
+
function agentRoutingSocketPath(publicSocketPath) {
|
|
5639
5651
|
const socket = systemdPath(publicSocketPath, "agent socket");
|
|
5640
5652
|
const backend = `${socket}.backend`;
|
|
5641
5653
|
if (Buffer.byteLength(backend) > 100)
|
|
5642
5654
|
throw new Error("agent socket path is too long for a Unix socket");
|
|
5643
5655
|
return backend;
|
|
5644
5656
|
}
|
|
5657
|
+
function agentCandidateSocketPath(publicSocketPath) {
|
|
5658
|
+
const socket = systemdPath(publicSocketPath, "agent socket");
|
|
5659
|
+
const backend = `${socket}.backend.candidate`;
|
|
5660
|
+
if (Buffer.byteLength(backend) > 100)
|
|
5661
|
+
throw new Error("agent socket path is too long for a Unix socket");
|
|
5662
|
+
return backend;
|
|
5663
|
+
}
|
|
5645
5664
|
var systemdPath = (value, label) => {
|
|
5646
5665
|
if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
|
|
5647
5666
|
throw new Error(`invalid ${label} path`);
|
|
@@ -5925,7 +5944,11 @@ function agentUnit(options) {
|
|
|
5925
5944
|
}
|
|
5926
5945
|
const environment = [
|
|
5927
5946
|
"NODE_ENV=production",
|
|
5928
|
-
`FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
|
|
5947
|
+
`FZ_SOCKET_PATH=${options.backendSocketPath ?? agentBackendSocketPath(options.socketPath)}`,
|
|
5948
|
+
`FZ_AGENT_PUBLIC_SOCKET=${options.socketPath}`,
|
|
5949
|
+
`FZ_AGENT_ROUTE_SOCKET=${agentRoutingSocketPath(options.socketPath)}`,
|
|
5950
|
+
options.handoverCandidate ? "FZ_AGENT_HANDOVER_CANDIDATE=true" : null,
|
|
5951
|
+
options.handoverCandidate ? `FZ_AGENT_HANDOVER_READY_SOCKET=${DEFAULT_AGENT_CANDIDATE_READY_SOCKET}` : null,
|
|
5929
5952
|
`FZ_CONTROL_SOCKET=${controlSocketPath}`,
|
|
5930
5953
|
`FZ_SEED_CREDENTIAL=agent-seed`,
|
|
5931
5954
|
`FZ_AGENT_MODE=${options.mode}`,
|
|
@@ -6062,6 +6085,26 @@ ${deploymentWrites}
|
|
|
6062
6085
|
WantedBy=multi-user.target
|
|
6063
6086
|
`;
|
|
6064
6087
|
}
|
|
6088
|
+
function agentCandidateUnit(options) {
|
|
6089
|
+
return agentUnit({
|
|
6090
|
+
...options,
|
|
6091
|
+
binPath: `${DEFAULT_AGENT_RELEASE_ROOT}/candidate/dist/fz-agent.js`,
|
|
6092
|
+
backendSocketPath: agentCandidateSocketPath(options.socketPath),
|
|
6093
|
+
handoverCandidate: true,
|
|
6094
|
+
gitCredentialPath: undefined,
|
|
6095
|
+
deploymentCredentials: {},
|
|
6096
|
+
bootstrapSshCredentialPath: undefined,
|
|
6097
|
+
bootstrapSshPublicKeyPath: undefined,
|
|
6098
|
+
pullBootstrap: false,
|
|
6099
|
+
pullDeployments: false,
|
|
6100
|
+
pullMigrations: false,
|
|
6101
|
+
lifecycleProfilePath: undefined,
|
|
6102
|
+
bootstrapTargetTelemetryEndpoint: undefined,
|
|
6103
|
+
repository: undefined,
|
|
6104
|
+
bootstrapBundlePath: undefined,
|
|
6105
|
+
bootstrapBundleManifestPath: undefined
|
|
6106
|
+
}).replace("Description=ForgeZero node agent", "Description=ForgeZero candidate node agent");
|
|
6107
|
+
}
|
|
6065
6108
|
var renderOperation = (operation) => {
|
|
6066
6109
|
if (operation.kind === "commands")
|
|
6067
6110
|
return operation.commands.map(({ argv: argv2 }) => argv2.join(" ")).join(`
|
|
@@ -6197,6 +6240,7 @@ function planProvision(options) {
|
|
|
6197
6240
|
auxiliaryUnits: [
|
|
6198
6241
|
{ path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
|
|
6199
6242
|
{ path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
|
|
6243
|
+
{ path: AGENT_CANDIDATE_UNIT_PATH, unit: agentCandidateUnit(options) },
|
|
6200
6244
|
{ path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
|
|
6201
6245
|
...options.enforceEgress ? [
|
|
6202
6246
|
{ path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
|
|
@@ -12139,12 +12183,14 @@ var httpsOrigin = (name, raw) => {
|
|
|
12139
12183
|
return value.origin;
|
|
12140
12184
|
};
|
|
12141
12185
|
function validatePlatformInitialInventory(value) {
|
|
12142
|
-
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "region", "computes"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
|
|
12186
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "region", "computes", "deployment"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
|
|
12143
12187
|
throw new Error("initial platform inventory coordinates are invalid");
|
|
12144
12188
|
}
|
|
12145
12189
|
safeAtom("initialInventory.region.label", value.region.label);
|
|
12146
12190
|
if (value.region.city !== undefined)
|
|
12147
12191
|
safeAtom("initialInventory.region.city", value.region.city);
|
|
12192
|
+
if (value.deployment !== undefined && (!value.deployment || typeof value.deployment !== "object" || Array.isArray(value.deployment) || Object.keys(value.deployment).some((key) => !["source", "branch", "revision", "bundleSha256"].includes(key)) || value.deployment.source !== "bootstrap-bundle" || !["dev", "main"].includes(value.deployment.branch) || !/^[a-f0-9]{40}$/.test(value.deployment.revision) || !/^[a-f0-9]{64}$/.test(value.deployment.bundleSha256)))
|
|
12193
|
+
throw new Error("initial platform deployment evidence is invalid");
|
|
12148
12194
|
const computes = validatePlatformGenesisGuests(value.computes.map((compute) => {
|
|
12149
12195
|
if (!compute || typeof compute !== "object" || Array.isArray(compute) || Object.keys(compute).some((key) => ![
|
|
12150
12196
|
"name",
|
|
@@ -12179,7 +12225,8 @@ function validatePlatformInitialInventory(value) {
|
|
|
12179
12225
|
databaseRole: compute.databaseRole,
|
|
12180
12226
|
databaseAgency: compute.databaseAgency,
|
|
12181
12227
|
confidential: true
|
|
12182
|
-
}))
|
|
12228
|
+
})),
|
|
12229
|
+
...value.deployment ? { deployment: { ...value.deployment } } : {}
|
|
12183
12230
|
};
|
|
12184
12231
|
}
|
|
12185
12232
|
var systemdValue = (name, raw) => {
|
|
@@ -15143,8 +15190,10 @@ function strictBootstrapDocument(value) {
|
|
|
15143
15190
|
], "runtime environment");
|
|
15144
15191
|
const environment = runtime.environment;
|
|
15145
15192
|
if (environment.initialInventory !== undefined) {
|
|
15146
|
-
const inventory = exactKeys2(environment.initialInventory, ["metalHostname", "region", "computes"], "initial inventory");
|
|
15193
|
+
const inventory = exactKeys2(environment.initialInventory, ["metalHostname", "region", "computes", "deployment"], "initial inventory");
|
|
15147
15194
|
exactKeys2(inventory.region, ["key", "label", "country", "city", "confidentialCapable"], "initial inventory region");
|
|
15195
|
+
if (inventory.deployment !== undefined)
|
|
15196
|
+
exactKeys2(inventory.deployment, ["source", "branch", "revision", "bundleSha256"], "initial deployment evidence");
|
|
15148
15197
|
if (!Array.isArray(inventory.computes))
|
|
15149
15198
|
throw new Error("initial inventory computes must be an array");
|
|
15150
15199
|
for (const compute of inventory.computes)
|
|
@@ -16035,6 +16084,7 @@ function planMetalBootstrap(config) {
|
|
|
16035
16084
|
"forgezero-metal-helper.service",
|
|
16036
16085
|
"forgezero-agent-update-helper.service",
|
|
16037
16086
|
"forgezero-metal-agent-egress.service",
|
|
16087
|
+
"forgezero-metal-agent-candidate.service",
|
|
16038
16088
|
"forgezero-metal-agent.service"
|
|
16039
16089
|
],
|
|
16040
16090
|
steps: [
|
|
@@ -16176,6 +16226,44 @@ RestrictAddressFamilies=AF_UNIX AF_NETLINK
|
|
|
16176
16226
|
|
|
16177
16227
|
[Install]
|
|
16178
16228
|
WantedBy=multi-user.target
|
|
16229
|
+
`,
|
|
16230
|
+
"forgezero-metal-agent-candidate.service": `[Unit]
|
|
16231
|
+
Description=ForgeZero candidate physical-host Agent
|
|
16232
|
+
After=network-online.target ${config.hostTelemetryUnit} forgezero-agent-update-helper.service forgezero-metal-agent-egress.service
|
|
16233
|
+
Requires=forgezero-agent-update-helper.service forgezero-metal-agent-egress.service
|
|
16234
|
+
|
|
16235
|
+
[Service]
|
|
16236
|
+
Type=simple
|
|
16237
|
+
User=forgezero-metal
|
|
16238
|
+
Group=forgezero-metal
|
|
16239
|
+
SupplementaryGroups=forgezero-update
|
|
16240
|
+
LoadCredentialEncrypted=metal-agent-seed:${SEED_CREDENTIAL_PATH}
|
|
16241
|
+
Environment=FZ_SEED_CREDENTIAL=metal-agent-seed
|
|
16242
|
+
Environment=FZ_AGENT_ROLE=metal
|
|
16243
|
+
Environment=FZ_AGENT_HANDOVER_CANDIDATE=true
|
|
16244
|
+
Environment=FZ_AGENT_HANDOVER_READY_SOCKET=/run/forgezero/candidate-ready.sock
|
|
16245
|
+
Environment=FZ_METAL_HOSTNAME=${config.metalHostname}
|
|
16246
|
+
Environment=FZ_API=${config.profile.apiUrl}
|
|
16247
|
+
Environment=NODE_ENV=production
|
|
16248
|
+
Environment=OTEL_EXPORTER_OTLP_ENDPOINT=${config.hostTelemetryEndpoint}
|
|
16249
|
+
Environment=OTEL_SERVICE_NAME=forgezero-metal-agent-candidate
|
|
16250
|
+
ExecStart=/opt/forgezero/agent/candidate/dist/fz-agent.js
|
|
16251
|
+
Restart=on-failure
|
|
16252
|
+
RestartSec=2
|
|
16253
|
+
RuntimeDirectory=forgezero
|
|
16254
|
+
RuntimeDirectoryMode=0750
|
|
16255
|
+
RuntimeDirectoryPreserve=yes
|
|
16256
|
+
LimitCORE=0
|
|
16257
|
+
NoNewPrivileges=true
|
|
16258
|
+
PrivateTmp=true
|
|
16259
|
+
ProtectSystem=strict
|
|
16260
|
+
ProtectHome=true
|
|
16261
|
+
ProtectKernelTunables=true
|
|
16262
|
+
ProtectKernelModules=true
|
|
16263
|
+
ProtectControlGroups=true
|
|
16264
|
+
RestrictSUIDSGID=true
|
|
16265
|
+
LockPersonality=true
|
|
16266
|
+
${egress}
|
|
16179
16267
|
`,
|
|
16180
16268
|
"forgezero-metal-agent.service": `[Unit]
|
|
16181
16269
|
Description=ForgeZero identity-only physical-host Agent
|
|
@@ -17430,7 +17518,7 @@ async function applyAttendedPlatformBootstrap(fleetRequest, cloudflareRequest, a
|
|
|
17430
17518
|
}
|
|
17431
17519
|
|
|
17432
17520
|
// src/platform-genesis-config.ts
|
|
17433
|
-
function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region) {
|
|
17521
|
+
function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region, deployment) {
|
|
17434
17522
|
const fleet = validatePlatformGenesisGuests(guests);
|
|
17435
17523
|
if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(metalHostname)) {
|
|
17436
17524
|
throw new Error("platform genesis requires the reviewed metal hostname");
|
|
@@ -17458,7 +17546,8 @@ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname,
|
|
|
17458
17546
|
confidential: guest.confidential,
|
|
17459
17547
|
databaseRole: index === 0 ? "master" : "joiner",
|
|
17460
17548
|
databaseAgency: "member"
|
|
17461
|
-
}))
|
|
17549
|
+
})),
|
|
17550
|
+
...deployment ? { deployment } : {}
|
|
17462
17551
|
};
|
|
17463
17552
|
return fleet.map((guest, index) => {
|
|
17464
17553
|
const node = nodes[index];
|
|
@@ -17499,6 +17588,122 @@ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname,
|
|
|
17499
17588
|
});
|
|
17500
17589
|
}
|
|
17501
17590
|
|
|
17591
|
+
// src/platform-launch-profile.ts
|
|
17592
|
+
var PROFILES = {
|
|
17593
|
+
development: {
|
|
17594
|
+
environment: "development",
|
|
17595
|
+
branch: "dev",
|
|
17596
|
+
appOrigin: "https://dev.forgezero.net",
|
|
17597
|
+
apiOrigin: "https://dev-api.forgezero.net",
|
|
17598
|
+
zoneName: "forgezero.net",
|
|
17599
|
+
kvNamespaceTitle: "forgezero-dev-nodes",
|
|
17600
|
+
workerScriptName: "forgezero-api-edge-development",
|
|
17601
|
+
region: { key: "in-south", label: "India South", country: "IN" },
|
|
17602
|
+
agentOtlpEndpoint: "https://otel.forgezero.net",
|
|
17603
|
+
privateCidrs: ["10.42.0.0/24"]
|
|
17604
|
+
},
|
|
17605
|
+
production: {
|
|
17606
|
+
environment: "production",
|
|
17607
|
+
branch: "main",
|
|
17608
|
+
appOrigin: "https://www.forgezero.net",
|
|
17609
|
+
apiOrigin: "https://api.forgezero.net",
|
|
17610
|
+
zoneName: "forgezero.net",
|
|
17611
|
+
kvNamespaceTitle: "forgezero-nodes",
|
|
17612
|
+
workerScriptName: "forgezero-api-edge-production",
|
|
17613
|
+
region: { key: "in-south", label: "India South", country: "IN" },
|
|
17614
|
+
agentOtlpEndpoint: "https://otel.forgezero.net",
|
|
17615
|
+
privateCidrs: ["10.42.0.0/24"]
|
|
17616
|
+
}
|
|
17617
|
+
};
|
|
17618
|
+
function forgeZeroLaunchProfile(environment) {
|
|
17619
|
+
const profile = PROFILES[environment];
|
|
17620
|
+
return {
|
|
17621
|
+
...profile,
|
|
17622
|
+
nodeHostname: (index) => environment === "development" ? `dev-api-n${index + 1}.forgezero.net` : `api-n${index + 1}.forgezero.net`
|
|
17623
|
+
};
|
|
17624
|
+
}
|
|
17625
|
+
var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
17626
|
+
function forgeZeroLaunchTemplate(profile, guests, bundle, owner) {
|
|
17627
|
+
const fleet = validatePlatformGenesisGuests(guests);
|
|
17628
|
+
if (!EMAIL.test(owner.custodianEmail) || !EMAIL.test(owner.email.from)) {
|
|
17629
|
+
throw new Error("bootstrap custodian and From addresses must be valid email addresses");
|
|
17630
|
+
}
|
|
17631
|
+
if (owner.email.provider === "smtp" && (!owner.email.host || !Number.isInteger(owner.email.port) || owner.email.port < 1 || owner.email.port > 65535 || !owner.email.user)) {
|
|
17632
|
+
throw new Error("SMTP host, port and user are required");
|
|
17633
|
+
}
|
|
17634
|
+
if (bundle.manifest.branch !== profile.branch)
|
|
17635
|
+
throw new Error("bootstrap bundle branch does not match the launch profile");
|
|
17636
|
+
const coordinators = fleet.map(({ address }) => `http://${address}:8529`);
|
|
17637
|
+
const nodeHostnames = fleet.map((_, index) => profile.nodeHostname(index));
|
|
17638
|
+
const first = fleet[0];
|
|
17639
|
+
return validateBootstrapConfig({
|
|
17640
|
+
kind: "platform",
|
|
17641
|
+
environment: profile.environment,
|
|
17642
|
+
profile: "platform-db-api",
|
|
17643
|
+
computeReference: first.name,
|
|
17644
|
+
nodeHostname: nodeHostnames[0],
|
|
17645
|
+
apiUrl: profile.apiOrigin,
|
|
17646
|
+
bootstrapBundle: {
|
|
17647
|
+
bundleFile: bundle.bundlePath,
|
|
17648
|
+
manifestFile: bundle.manifestPath,
|
|
17649
|
+
branch: profile.branch
|
|
17650
|
+
},
|
|
17651
|
+
telemetryEndpoint: profile.agentOtlpEndpoint,
|
|
17652
|
+
database: {
|
|
17653
|
+
role: "master",
|
|
17654
|
+
agency: "member",
|
|
17655
|
+
readPreferred: false,
|
|
17656
|
+
serverMode: "default",
|
|
17657
|
+
address: first.address,
|
|
17658
|
+
coordinators
|
|
17659
|
+
},
|
|
17660
|
+
enrolment: { source: "genesis-derived" },
|
|
17661
|
+
runtime: {
|
|
17662
|
+
environment: {
|
|
17663
|
+
softwareProfile: "platform-db-api",
|
|
17664
|
+
databaseRole: "master",
|
|
17665
|
+
databaseCoordinators: coordinators,
|
|
17666
|
+
databaseReadPreferredCoordinators: [],
|
|
17667
|
+
databaseAddress: first.address,
|
|
17668
|
+
databaseNetworkMode: "private-lan",
|
|
17669
|
+
databaseReplicationFactor: 2,
|
|
17670
|
+
databaseWriteConcern: 2,
|
|
17671
|
+
databaseUser: "forgezero-api",
|
|
17672
|
+
nodeHostname: nodeHostnames[0],
|
|
17673
|
+
nodeRegion: profile.region.key,
|
|
17674
|
+
nodeRole: "guest",
|
|
17675
|
+
appOrigin: profile.appOrigin,
|
|
17676
|
+
apiOrigin: profile.apiOrigin,
|
|
17677
|
+
publicApiPort: 3000,
|
|
17678
|
+
sharedDirectory: "/opt/forgezero/shared",
|
|
17679
|
+
seedSyncPeers: fleet.slice(1).flatMap(({ address }) => [3001, 3002].map((port) => `ws://${address}:${port}/_internal/forgezero/seed-mesh/v2`)),
|
|
17680
|
+
seedSyncMembers: nodeHostnames,
|
|
17681
|
+
seedSyncEpoch: `${profile.environment}-${bundle.manifest.revision.slice(0, 16)}`,
|
|
17682
|
+
concurrencyLimit: 128,
|
|
17683
|
+
drainDeadlineMs: 30000,
|
|
17684
|
+
otlpEndpoint: "http://127.0.0.1:4318",
|
|
17685
|
+
otlpCollectorUnit: FORGEZERO_OTEL_COLLECTOR_UNIT,
|
|
17686
|
+
agentOtlpEndpoint: profile.agentOtlpEndpoint,
|
|
17687
|
+
otlpFlushIntervalMs: 1e4,
|
|
17688
|
+
otlpTraceSampleRatio: 0.1,
|
|
17689
|
+
custodianEmail: owner.custodianEmail,
|
|
17690
|
+
email: owner.email,
|
|
17691
|
+
deployProfile: profile.environment
|
|
17692
|
+
},
|
|
17693
|
+
serviceUser: "forgezero-api",
|
|
17694
|
+
slotsDirectory: "/opt/forgezero/slots",
|
|
17695
|
+
bluePort: 3001,
|
|
17696
|
+
greenPort: 3002,
|
|
17697
|
+
healthPath: "/api/health",
|
|
17698
|
+
keepReleases: 5
|
|
17699
|
+
},
|
|
17700
|
+
firewall: { enabled: true, sshPort: 22, privateCidrs: [...profile.privateCidrs] },
|
|
17701
|
+
installCloudflared: true,
|
|
17702
|
+
installWarp: false,
|
|
17703
|
+
cloudflareHandoff: { handoffFile: "/run/forgezero/cloudflare-handoff.json", nodeName: first.name }
|
|
17704
|
+
});
|
|
17705
|
+
}
|
|
17706
|
+
|
|
17502
17707
|
// src/host-maintenance.ts
|
|
17503
17708
|
import { lstatSync as lstatSync8, readFileSync as readFileSync13 } from "fs";
|
|
17504
17709
|
var ROOT = "/opt/forgezero";
|
|
@@ -18052,6 +18257,8 @@ function parseOptions(argv2) {
|
|
|
18052
18257
|
options.dataFile = argv2[++index];
|
|
18053
18258
|
else if (token === "--bootstrap-config")
|
|
18054
18259
|
options.bootstrapConfigPath = argv2[++index];
|
|
18260
|
+
else if (token === "--secrets-env-file")
|
|
18261
|
+
options.bootstrapSecretsEnvFile = argv2[++index];
|
|
18055
18262
|
else if (token === "--output")
|
|
18056
18263
|
options.outputPath = argv2[++index];
|
|
18057
18264
|
else if (token === "--query")
|
|
@@ -18648,9 +18855,100 @@ async function bootstrapSecret(question) {
|
|
|
18648
18855
|
throw new Error(`${question} was not provided`);
|
|
18649
18856
|
return value;
|
|
18650
18857
|
}
|
|
18651
|
-
|
|
18858
|
+
var PLATFORM_CLUSTER_CREDENTIAL = "forgezero-platform-cluster-bootstrap-code";
|
|
18859
|
+
function readPlatformLaunchEnvironment(path) {
|
|
18860
|
+
const absolute2 = resolve12(path);
|
|
18861
|
+
const stat2 = lstatSync10(absolute2);
|
|
18862
|
+
if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1 || typeof process.getuid === "function" && stat2.uid !== process.getuid() || (stat2.mode & 63) !== 0) {
|
|
18863
|
+
throw new Error(`bootstrap secret environment must be one owner-only regular file: ${absolute2}`);
|
|
18864
|
+
}
|
|
18865
|
+
const values = new Map;
|
|
18866
|
+
for (const [index, raw] of readFileSync16(absolute2, "utf8").split(/\r?\n/).entries()) {
|
|
18867
|
+
const line = raw.trim();
|
|
18868
|
+
if (!line || line.startsWith("#"))
|
|
18869
|
+
continue;
|
|
18870
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
18871
|
+
if (!match)
|
|
18872
|
+
throw new Error(`invalid environment assignment at ${absolute2}:${index + 1}`);
|
|
18873
|
+
if (values.has(match[1]))
|
|
18874
|
+
throw new Error(`duplicate environment assignment ${match[1]} at ${absolute2}:${index + 1}`);
|
|
18875
|
+
let value = match[2].trim();
|
|
18876
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
18877
|
+
value = value.slice(1, -1);
|
|
18878
|
+
}
|
|
18879
|
+
values.set(match[1], value);
|
|
18880
|
+
}
|
|
18881
|
+
const requiredValue = (name) => {
|
|
18882
|
+
const value = values.get(name)?.trim();
|
|
18883
|
+
if (!value)
|
|
18884
|
+
throw new Error(`bootstrap secret environment is missing ${name}`);
|
|
18885
|
+
return value;
|
|
18886
|
+
};
|
|
18887
|
+
const provider = requiredValue("FZ_EMAIL_PROVIDER");
|
|
18888
|
+
if (provider !== "smtp" && provider !== "jetemail")
|
|
18889
|
+
throw new Error("FZ_EMAIL_PROVIDER must be smtp or jetemail");
|
|
18890
|
+
const smtpPort = provider === "smtp" ? Number(requiredValue("FZ_SMTP_PORT")) : undefined;
|
|
18891
|
+
if (smtpPort !== undefined && (!Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535)) {
|
|
18892
|
+
throw new Error("FZ_SMTP_PORT must be an integer from 1 through 65535");
|
|
18893
|
+
}
|
|
18894
|
+
const owner = provider === "jetemail" ? {
|
|
18895
|
+
custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
|
|
18896
|
+
email: {
|
|
18897
|
+
provider,
|
|
18898
|
+
from: requiredValue("FZ_EMAIL_FROM"),
|
|
18899
|
+
eu: (values.get("FZ_JETEMAIL_EU") ?? "false").trim().toLowerCase() === "true"
|
|
18900
|
+
}
|
|
18901
|
+
} : {
|
|
18902
|
+
custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
|
|
18903
|
+
email: {
|
|
18904
|
+
provider,
|
|
18905
|
+
from: requiredValue("FZ_EMAIL_FROM"),
|
|
18906
|
+
host: requiredValue("FZ_SMTP_HOST"),
|
|
18907
|
+
port: smtpPort,
|
|
18908
|
+
user: requiredValue("FZ_SMTP_USER")
|
|
18909
|
+
}
|
|
18910
|
+
};
|
|
18911
|
+
return {
|
|
18912
|
+
owner,
|
|
18913
|
+
emailSecret: requiredValue(provider === "jetemail" ? "FZ_JETEMAIL_API_KEY" : "FZ_SMTP_PASSWORD"),
|
|
18914
|
+
cloudflareTunnelToken: requiredValue("CF_TUNNEL_TOKEN"),
|
|
18915
|
+
cloudflareApiToken: requiredValue("CF_API_TOKEN")
|
|
18916
|
+
};
|
|
18917
|
+
}
|
|
18918
|
+
async function platformLaunchClusterBootstrapCode(outputDirectory) {
|
|
18919
|
+
const path = join13(dirname14(outputDirectory), `${PLATFORM_CLUSTER_CREDENTIAL}.cred`);
|
|
18920
|
+
if (existsSync12(path)) {
|
|
18921
|
+
const stat2 = lstatSync10(path);
|
|
18922
|
+
if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1 || typeof process.getuid === "function" && stat2.uid !== process.getuid() || (stat2.mode & 63) !== 0) {
|
|
18923
|
+
throw new Error(`platform cluster credential must be one owner-only regular file: ${path}`);
|
|
18924
|
+
}
|
|
18925
|
+
const child2 = Bun.spawn(["systemd-creds", "decrypt", `--name=${PLATFORM_CLUSTER_CREDENTIAL}`, path, "-"], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
18926
|
+
const [stdout, stderr2, exitCode2] = await Promise.all([
|
|
18927
|
+
new Response(child2.stdout).text(),
|
|
18928
|
+
new Response(child2.stderr).text(),
|
|
18929
|
+
child2.exited
|
|
18930
|
+
]);
|
|
18931
|
+
if (exitCode2 !== 0)
|
|
18932
|
+
throw new Error(`cannot open the retained platform cluster credential: ${stderr2.trim()}`);
|
|
18933
|
+
const value2 = stdout.trim();
|
|
18934
|
+
if (!/^[a-f0-9]{64}$/i.test(value2))
|
|
18935
|
+
throw new Error("retained platform cluster credential is invalid");
|
|
18936
|
+
return value2;
|
|
18937
|
+
}
|
|
18938
|
+
mkdirSync13(dirname14(path), { recursive: true, mode: 448 });
|
|
18939
|
+
const value = randomBytes10(32).toString("hex");
|
|
18940
|
+
const child = Bun.spawn(["systemd-creds", "encrypt", `--name=${PLATFORM_CLUSTER_CREDENTIAL}`, "-", path], { stdin: "pipe", stdout: "pipe", stderr: "pipe" });
|
|
18941
|
+
await writeAndCloseProcessInput(child.stdin, `${value}
|
|
18942
|
+
`);
|
|
18943
|
+
const [stderr, exitCode] = await Promise.all([new Response(child.stderr).text(), child.exited]);
|
|
18944
|
+
if (exitCode !== 0)
|
|
18945
|
+
throw new Error(`cannot retain the platform cluster credential: ${stderr.trim()}`);
|
|
18946
|
+
chmodSync7(path, 384);
|
|
18947
|
+
return value;
|
|
18948
|
+
}
|
|
18949
|
+
async function promptPlatformBootstrapSecrets(config, generatedClusterBootstrapCode) {
|
|
18652
18950
|
return validatePlatformBootstrapSecrets(config, {
|
|
18653
|
-
clusterBootstrapCode: await bootstrapSecret("Shared 64-hex cluster bootstrap code"),
|
|
18951
|
+
clusterBootstrapCode: generatedClusterBootstrapCode ?? await bootstrapSecret("Shared 64-hex cluster bootstrap code"),
|
|
18654
18952
|
emailSecret: await bootstrapSecret(config.runtime.environment.email?.provider === "jetemail" ? "JetEmail API key" : "SMTP password"),
|
|
18655
18953
|
...config.enrolment.source === "api-token" ? { enrolmentToken: await bootstrapSecret("API-issued one-time platform enrolment token") } : {},
|
|
18656
18954
|
...config.runtime.environment.backup ? { backupS3Secret: await bootstrapSecret("Backup S3 secret key") } : {},
|
|
@@ -18843,51 +19141,53 @@ function writeMetalOperatorFiles(directory) {
|
|
|
18843
19141
|
});
|
|
18844
19142
|
return { metalConfig, remoteRequest };
|
|
18845
19143
|
}
|
|
18846
|
-
function writePlatformGenesisFleet(directory) {
|
|
19144
|
+
function writePlatformGenesisFleet(directory, preset) {
|
|
18847
19145
|
const output = genesisOutputDirectory(directory);
|
|
18848
|
-
const metalRequestFile = bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
|
|
19146
|
+
const metalRequestFile = preset?.metalRequestFile ?? bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
|
|
18849
19147
|
const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile, { validateMetalConfig: false });
|
|
18850
|
-
const guestHostKeysFile = bootstrapAnswer("Owner-only guest host-key evidence file");
|
|
19148
|
+
const guestHostKeysFile = preset?.guestHostKeysFile ?? bootstrapAnswer("Owner-only guest host-key evidence file");
|
|
18851
19149
|
const guestHostKeys = readOperatorGuestHostKeyEvidence(guestHostKeysFile, metalRequest);
|
|
18852
19150
|
const fleet = metalRequest.genesis.nodes;
|
|
18853
19151
|
const metalHostname = readMetalBootstrapConfig(metalRequest.metalConfigFile, {
|
|
18854
19152
|
allowHistoricalRelease: true
|
|
18855
19153
|
}).metalHostname;
|
|
18856
19154
|
const checkpointPath = join13(output, "cloudflare-handoff.json");
|
|
18857
|
-
const template = interactiveBootstrap("platform", fleet, checkpointPath);
|
|
18858
|
-
const region = {
|
|
19155
|
+
const template = preset ? forgeZeroLaunchTemplate(preset.profile, fleet, preset.bundle, preset.owner) : interactiveBootstrap("platform", fleet, checkpointPath);
|
|
19156
|
+
const region = preset ? preset.profile.region : {
|
|
18859
19157
|
label: bootstrapAnswer(`Region ${template.runtime.environment.nodeRegion} display label`),
|
|
18860
19158
|
country: bootstrapAnswer("Region ISO 3166-1 alpha-2 country code").toUpperCase(),
|
|
18861
19159
|
city: bootstrapAnswer("Region city")
|
|
18862
19160
|
};
|
|
18863
|
-
const nodes = fleet.map((guest, index) =>
|
|
18864
|
-
nodeHostname: template.nodeHostname,
|
|
19161
|
+
const nodes = fleet.map((guest, index) => ({
|
|
19162
|
+
nodeHostname: preset ? preset.profile.nodeHostname(index) : index === 0 ? template.nodeHostname : bootstrapAnswer(`${guest.name} public node hostname`),
|
|
18865
19163
|
cloudflareHandoffFile: cloudflareHostHandoffPath(checkpointPath, guest.name)
|
|
18866
|
-
}
|
|
18867
|
-
|
|
18868
|
-
cloudflareHandoffFile: cloudflareHostHandoffPath(checkpointPath, guest.name)
|
|
18869
|
-
});
|
|
18870
|
-
const realtimeEnabled = bootstrapBoolean("Configure existing Worker realtime fan-out?", "yes");
|
|
19164
|
+
}));
|
|
19165
|
+
const realtimeEnabled = preset ? true : bootstrapBoolean("Configure existing Worker realtime fan-out?", "yes");
|
|
18871
19166
|
const cloudflareConfig = writeBootstrapConfig(join13(output, "cloudflare.json"), createCloudflareBootstrapDiscoveryCommandConfig({
|
|
18872
19167
|
checkpointPath,
|
|
18873
19168
|
discovery: {
|
|
18874
|
-
zoneName: bootstrapAnswer("Cloudflare DNS zone name", "forgezero.net"),
|
|
18875
|
-
kvNamespaceTitle: bootstrapAnswer("Existing Worker-bound KV namespace title"),
|
|
19169
|
+
zoneName: preset?.profile.zoneName ?? bootstrapAnswer("Cloudflare DNS zone name", "forgezero.net"),
|
|
19170
|
+
kvNamespaceTitle: preset?.profile.kvNamespaceTitle ?? bootstrapAnswer("Existing Worker-bound KV namespace title"),
|
|
18876
19171
|
...realtimeEnabled ? {
|
|
18877
|
-
workerScriptName: bootstrapAnswer("Existing Worker script name"),
|
|
18878
|
-
endpoint: bootstrapAnswer("Stable public Worker HTTPS endpoint"),
|
|
18879
|
-
producer:
|
|
19172
|
+
workerScriptName: preset?.profile.workerScriptName ?? bootstrapAnswer("Existing Worker script name"),
|
|
19173
|
+
endpoint: preset?.profile.apiOrigin ?? bootstrapAnswer("Stable public Worker HTTPS endpoint"),
|
|
19174
|
+
producer: "platform-api"
|
|
18880
19175
|
} : {}
|
|
18881
19176
|
},
|
|
18882
19177
|
nodes: fleet.map((guest, index) => ({
|
|
18883
19178
|
nodeName: guest.name,
|
|
18884
19179
|
hostname: nodes[index].nodeHostname,
|
|
18885
19180
|
service: "http://127.0.0.1:3000",
|
|
18886
|
-
tunnelName: bootstrapAnswer(`${guest.name} Tunnel name`, guest.name)
|
|
19181
|
+
tunnelName: preset ? guest.name : bootstrapAnswer(`${guest.name} Tunnel name`, guest.name)
|
|
18887
19182
|
}))
|
|
18888
19183
|
}));
|
|
18889
19184
|
const cloudflareAcceptance = join13(output, "cloudflare-acceptance.json");
|
|
18890
|
-
const configs = platformGenesisBootstrapConfigs(template, fleet, nodes, metalHostname, region
|
|
19185
|
+
const configs = platformGenesisBootstrapConfigs(template, fleet, nodes, metalHostname, region, preset ? {
|
|
19186
|
+
source: "bootstrap-bundle",
|
|
19187
|
+
branch: preset.profile.branch,
|
|
19188
|
+
revision: preset.bundle.manifest.revision,
|
|
19189
|
+
bundleSha256: preset.bundle.manifest.sha256
|
|
19190
|
+
} : undefined).map((config) => {
|
|
18891
19191
|
const path = `${output}/${config.computeReference}-platform.json`;
|
|
18892
19192
|
writeBootstrapConfig(path, config);
|
|
18893
19193
|
return path;
|
|
@@ -18919,6 +19219,66 @@ function writePlatformGenesisFleet(directory) {
|
|
|
18919
19219
|
const fleetRequest = writeOperatorPlatformBootstrapFleetRequest(join13(output, "platform-fleet-remote.json"), requests, { configFile: cloudflareConfig, acceptanceFile: cloudflareAcceptance });
|
|
18920
19220
|
return { configs, requests, fleetRequest, cloudflareConfig, cloudflareAcceptance };
|
|
18921
19221
|
}
|
|
19222
|
+
function selectedForgeZeroLaunchEnvironment(value) {
|
|
19223
|
+
if (value === "app")
|
|
19224
|
+
return "development";
|
|
19225
|
+
if (value !== "development" && value !== "production") {
|
|
19226
|
+
throw new Error("platform launch --profile must be development or production");
|
|
19227
|
+
}
|
|
19228
|
+
return value;
|
|
19229
|
+
}
|
|
19230
|
+
function forgeZeroLaunchOwnerInput() {
|
|
19231
|
+
const custodianEmail = bootstrapAnswer("First custodian email");
|
|
19232
|
+
const provider = bootstrapAnswer("Bootstrap email provider (smtp/jetemail)", "jetemail");
|
|
19233
|
+
if (provider !== "smtp" && provider !== "jetemail") {
|
|
19234
|
+
throw new Error("Bootstrap email provider must be smtp or jetemail");
|
|
19235
|
+
}
|
|
19236
|
+
const from = bootstrapAnswer("Bootstrap email From address");
|
|
19237
|
+
if (provider === "jetemail") {
|
|
19238
|
+
return {
|
|
19239
|
+
custodianEmail,
|
|
19240
|
+
email: { provider, from, eu: bootstrapBoolean("Use JetEmail EU processing?", "no") }
|
|
19241
|
+
};
|
|
19242
|
+
}
|
|
19243
|
+
return {
|
|
19244
|
+
custodianEmail,
|
|
19245
|
+
email: {
|
|
19246
|
+
provider,
|
|
19247
|
+
from,
|
|
19248
|
+
host: bootstrapAnswer("SMTP host"),
|
|
19249
|
+
port: bootstrapNumber("SMTP port", "587"),
|
|
19250
|
+
user: bootstrapAnswer("SMTP user")
|
|
19251
|
+
}
|
|
19252
|
+
};
|
|
19253
|
+
}
|
|
19254
|
+
async function prepareForgeZeroPlatformLaunch(environment, outputDirectory, projectRoot, owner) {
|
|
19255
|
+
const profile = forgeZeroLaunchProfile(environment);
|
|
19256
|
+
const base = dirname14(outputDirectory);
|
|
19257
|
+
const metalRequestFile = join13(base, "metal", "metal-remote.json");
|
|
19258
|
+
const guestHostKeysFile = join13(base, "genesis-guest-host-keys.json");
|
|
19259
|
+
for (const [path, label] of [
|
|
19260
|
+
[metalRequestFile, "reviewed Metal request"],
|
|
19261
|
+
[guestHostKeysFile, "pinned genesis guest host-key evidence"]
|
|
19262
|
+
]) {
|
|
19263
|
+
if (!existsSync12(path))
|
|
19264
|
+
throw new Error(`${label} is missing at ${path}`);
|
|
19265
|
+
}
|
|
19266
|
+
const root = resolve12(projectRoot);
|
|
19267
|
+
const nestedApi = join13(root, "api");
|
|
19268
|
+
const repositoryRoot = existsSync12(join13(nestedApi, "package.json")) ? nestedApi : root;
|
|
19269
|
+
const bundlePath = join13(base, "api.bundle");
|
|
19270
|
+
const bundle = existsSync12(bundlePath) || existsSync12(`${bundlePath}.json`) ? await readBootstrapBundle(bundlePath) : await buildBootstrapBundle({ repositoryRoot, outputPath: bundlePath, branch: profile.branch });
|
|
19271
|
+
if (bundle.manifest.branch !== profile.branch) {
|
|
19272
|
+
throw new Error(`existing bootstrap bundle is for ${bundle.manifest.branch}, expected ${profile.branch}`);
|
|
19273
|
+
}
|
|
19274
|
+
return writePlatformGenesisFleet(outputDirectory, {
|
|
19275
|
+
profile,
|
|
19276
|
+
metalRequestFile,
|
|
19277
|
+
guestHostKeysFile,
|
|
19278
|
+
bundle,
|
|
19279
|
+
owner: owner ?? forgeZeroLaunchOwnerInput()
|
|
19280
|
+
});
|
|
19281
|
+
}
|
|
18922
19282
|
function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint) {
|
|
18923
19283
|
if (!process.stdin.isTTY)
|
|
18924
19284
|
throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
|
|
@@ -19038,7 +19398,7 @@ function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint)
|
|
|
19038
19398
|
} : {}
|
|
19039
19399
|
};
|
|
19040
19400
|
}
|
|
19041
|
-
async function runAttendedPlatformFleet(fleet, apply) {
|
|
19401
|
+
async function runAttendedPlatformFleet(fleet, apply, generatedClusterBootstrapCode, environmentInput) {
|
|
19042
19402
|
const plan = planOperatorPlatformFleetBootstrap(fleet, "apply");
|
|
19043
19403
|
const cloudflarePlan = readCloudflareBootstrapCommandPlan(fleet.cloudflareConfigFile);
|
|
19044
19404
|
const coordinates = operatorPlatformFleetCoordinates(fleet);
|
|
@@ -19069,7 +19429,12 @@ async function runAttendedPlatformFleet(fleet, apply) {
|
|
|
19069
19429
|
const firstConfig = readBootstrapConfig(first.platformConfigFile);
|
|
19070
19430
|
if (firstConfig.kind !== "platform")
|
|
19071
19431
|
throw new Error("remote platform fleet requires platform configs");
|
|
19072
|
-
const secrets =
|
|
19432
|
+
const secrets = environmentInput ? validatePlatformBootstrapSecrets(firstConfig, {
|
|
19433
|
+
clusterBootstrapCode: generatedClusterBootstrapCode,
|
|
19434
|
+
emailSecret: environmentInput.emailSecret,
|
|
19435
|
+
cloudflareTunnelToken: environmentInput.cloudflareTunnelToken,
|
|
19436
|
+
cloudflareApiToken: environmentInput.cloudflareApiToken
|
|
19437
|
+
}) : await promptPlatformBootstrapSecrets(firstConfig, generatedClusterBootstrapCode);
|
|
19073
19438
|
const cloudflare = await resolveCloudflareBootstrapCommandConfig(fleet.cloudflareConfigFile, {
|
|
19074
19439
|
tunnelToken: secrets.cloudflareTunnelToken,
|
|
19075
19440
|
apiToken: secrets.cloudflareApiToken
|
|
@@ -19104,15 +19469,21 @@ async function cmdBootstrap(options, args) {
|
|
|
19104
19469
|
return 0;
|
|
19105
19470
|
}
|
|
19106
19471
|
if (operation === "platform" && args[1] === "launch") {
|
|
19107
|
-
if (args[2] !== undefined ||
|
|
19108
|
-
throw new Error("Usage: fz bootstrap platform launch --output <absolute-owner-only-directory> [--apply]");
|
|
19472
|
+
if (args[2] !== undefined || options.bootstrapConfigPath) {
|
|
19473
|
+
throw new Error("Usage: fz bootstrap platform launch [--profile development|production] [--output <absolute-owner-only-directory>] [--secrets-env-file <owner-only-env>] [--apply]");
|
|
19474
|
+
}
|
|
19475
|
+
const environment = selectedForgeZeroLaunchEnvironment(options.deployProfile);
|
|
19476
|
+
if (environment === "production" && options.bootstrapSecretsEnvFile) {
|
|
19477
|
+
throw new Error("production platform launch requires interactive hidden secret prompts");
|
|
19109
19478
|
}
|
|
19110
|
-
const
|
|
19479
|
+
const outputDirectory = options.outputPath ?? `/secure/forgezero/${environment}/genesis`;
|
|
19480
|
+
const environmentInput = options.bootstrapSecretsEnvFile ? readPlatformLaunchEnvironment(options.bootstrapSecretsEnvFile) : undefined;
|
|
19481
|
+
const generated = await prepareForgeZeroPlatformLaunch(environment, outputDirectory, options.projectRoot, environmentInput?.owner);
|
|
19111
19482
|
const fleet = readOperatorPlatformBootstrapFleetRequest(generated.fleetRequest);
|
|
19112
|
-
if (options.apply && bootstrapAnswer("Type APPLY to run the reviewed Cloudflare and three-compute plan") !== "APPLY") {
|
|
19483
|
+
if (options.apply && !environmentInput && bootstrapAnswer("Type APPLY to run the reviewed Cloudflare and three-compute plan") !== "APPLY") {
|
|
19113
19484
|
throw new Error("attended platform bootstrap was not confirmed");
|
|
19114
19485
|
}
|
|
19115
|
-
return await runAttendedPlatformFleet(fleet, options.apply);
|
|
19486
|
+
return await runAttendedPlatformFleet(fleet, options.apply, options.apply ? await platformLaunchClusterBootstrapCode(outputDirectory) : undefined, environmentInput);
|
|
19116
19487
|
}
|
|
19117
19488
|
if (operation === "platform" && args[1] === "fleet") {
|
|
19118
19489
|
const mode = args[2];
|
|
@@ -19922,8 +20293,9 @@ function usage() {
|
|
|
19922
20293
|
fz bootstrap platform bundle
|
|
19923
20294
|
Build one clean exact-revision API Git bundle for genesis
|
|
19924
20295
|
fz bootstrap platform launch
|
|
19925
|
-
|
|
19926
|
-
|
|
20296
|
+
One profile-driven attended launch. Public topology,
|
|
20297
|
+
bundle and edge names are derived; the owner supplies
|
|
20298
|
+
only email settings, hidden provider tokens and APPLY
|
|
19927
20299
|
fz bootstrap platform remote <apply|status>
|
|
19928
20300
|
Run typed bootstrap from the operator laptop through
|
|
19929
20301
|
a pinned host; apply copies and verifies that bundle
|
|
@@ -20025,7 +20397,7 @@ function usage() {
|
|
|
20025
20397
|
--root <path> Project root for project/deploy commands
|
|
20026
20398
|
--name <name> Project or deploy name during init
|
|
20027
20399
|
--purpose <text> Product outcome during project init
|
|
20028
|
-
--profile <name>
|
|
20400
|
+
--profile <name> Platform launch environment or initial deploy profile
|
|
20029
20401
|
--software <key@ver> Initial tested software coordinate; repeatable
|
|
20030
20402
|
--channel <name> Catalog view: production or development (shows testing)
|
|
20031
20403
|
--attestation Require hardware attestation for every deploy step
|