@forgezero/agent 0.1.62 → 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 +45 -16
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap-bundle.d.ts +31 -0
- package/dist/bootstrap-bundle.js +165 -0
- package/dist/bootstrap.d.ts +24 -18
- package/dist/bootstrap.js +509 -269
- package/dist/cli/agent-install.d.ts +2 -0
- package/dist/community-rehearsal-host.js +7 -5
- package/dist/definition.js +7 -5
- package/dist/deploy-file.js +7 -5
- package/dist/deployment.d.ts +27 -0
- package/dist/fz-agent.js +594 -421
- package/dist/fz.js +847 -372
- package/dist/metal-bootstrap.js +1 -1
- package/dist/metal-helper-socket.js +0 -2
- package/dist/metal-provision.js +0 -2
- package/dist/operator-bootstrap.d.ts +43 -1
- package/dist/operator-bootstrap.js +788 -367
- package/dist/platform-bootstrap-runtime.d.ts +0 -2
- package/dist/platform-bootstrap-runtime.js +0 -4
- package/dist/platform-fleet-verification.js +190 -98
- package/dist/provision.d.ts +4 -1
- package/dist/provision.js +41 -32
- package/dist/software-helper.js +7 -5
- package/dist/software.d.ts +1 -1
- package/dist/software.js +7 -5
- package/dist/version.d.ts +1 -1
- package/package.json +5 -1
|
@@ -1209,19 +1209,175 @@ async function finalizeCloudflareBootstrapAcceptance(request, fetcher = fetch) {
|
|
|
1209
1209
|
return evidence;
|
|
1210
1210
|
}
|
|
1211
1211
|
|
|
1212
|
+
// src/bootstrap-bundle.ts
|
|
1213
|
+
import { createHash, randomBytes } from "crypto";
|
|
1214
|
+
import {
|
|
1215
|
+
chmodSync,
|
|
1216
|
+
createReadStream,
|
|
1217
|
+
existsSync,
|
|
1218
|
+
lstatSync,
|
|
1219
|
+
mkdirSync,
|
|
1220
|
+
readFileSync,
|
|
1221
|
+
renameSync,
|
|
1222
|
+
rmSync,
|
|
1223
|
+
writeFileSync
|
|
1224
|
+
} from "fs";
|
|
1225
|
+
import { dirname as dirname2, isAbsolute, resolve as resolve2 } from "path";
|
|
1226
|
+
var BOOTSTRAP_BUNDLE_FORMAT = 1;
|
|
1227
|
+
var BOOTSTRAP_BUNDLE_KIND = "forgezero-api-git-bundle";
|
|
1228
|
+
var MAX_BOOTSTRAP_BUNDLE_BYTES = 512 * 1024 * 1024;
|
|
1229
|
+
var run = async (argv) => {
|
|
1230
|
+
const child = Bun.spawn([...argv], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
1231
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
1232
|
+
new Response(child.stdout).text(),
|
|
1233
|
+
new Response(child.stderr).text(),
|
|
1234
|
+
child.exited
|
|
1235
|
+
]);
|
|
1236
|
+
return { exitCode, output: `${stdout}${stderr}`.slice(0, 65536) };
|
|
1237
|
+
};
|
|
1238
|
+
var checked = async (exec, argv, label) => {
|
|
1239
|
+
const result = await exec(argv);
|
|
1240
|
+
if (result.exitCode !== 0)
|
|
1241
|
+
throw new Error(`${label} failed${result.output.trim() ? `: ${result.output.trim()}` : ""}`);
|
|
1242
|
+
return result.output.trim();
|
|
1243
|
+
};
|
|
1244
|
+
async function sha256File(path) {
|
|
1245
|
+
const hash = createHash("sha256");
|
|
1246
|
+
await new Promise((resolveDone, reject) => {
|
|
1247
|
+
const stream = createReadStream(path);
|
|
1248
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
1249
|
+
stream.on("error", reject);
|
|
1250
|
+
stream.on("end", resolveDone);
|
|
1251
|
+
});
|
|
1252
|
+
return hash.digest("hex");
|
|
1253
|
+
}
|
|
1254
|
+
var branchName = (value) => {
|
|
1255
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9](?:[A-Za-z0-9._/-]{0,126}[A-Za-z0-9])?$/.test(value) || value.includes("..") || value.includes("//") || value.startsWith("-")) {
|
|
1256
|
+
throw new Error("bootstrap bundle branch is malformed");
|
|
1257
|
+
}
|
|
1258
|
+
return value;
|
|
1259
|
+
};
|
|
1260
|
+
function parseBootstrapBundleManifest(value) {
|
|
1261
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1262
|
+
throw new Error("bootstrap bundle manifest must be an object");
|
|
1263
|
+
const source = value;
|
|
1264
|
+
const allowed = ["format", "kind", "branch", "revision", "sha256", "bytes", "createdAt"];
|
|
1265
|
+
const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
|
|
1266
|
+
if (unknown.length)
|
|
1267
|
+
throw new Error(`bootstrap bundle manifest contains unknown field ${unknown[0]}`);
|
|
1268
|
+
if (source.format !== BOOTSTRAP_BUNDLE_FORMAT || source.kind !== BOOTSTRAP_BUNDLE_KIND) {
|
|
1269
|
+
throw new Error("bootstrap bundle manifest format is unsupported");
|
|
1270
|
+
}
|
|
1271
|
+
branchName(source.branch);
|
|
1272
|
+
if (typeof source.revision !== "string" || !/^[a-f0-9]{40}$/.test(source.revision)) {
|
|
1273
|
+
throw new Error("bootstrap bundle revision must be an exact lowercase Git commit");
|
|
1274
|
+
}
|
|
1275
|
+
if (typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256)) {
|
|
1276
|
+
throw new Error("bootstrap bundle digest is malformed");
|
|
1277
|
+
}
|
|
1278
|
+
if (!Number.isSafeInteger(source.bytes) || Number(source.bytes) < 1 || Number(source.bytes) > MAX_BOOTSTRAP_BUNDLE_BYTES) {
|
|
1279
|
+
throw new Error("bootstrap bundle size is outside the supported boundary");
|
|
1280
|
+
}
|
|
1281
|
+
if (typeof source.createdAt !== "string" || Number.isNaN(Date.parse(source.createdAt))) {
|
|
1282
|
+
throw new Error("bootstrap bundle creation time is malformed");
|
|
1283
|
+
}
|
|
1284
|
+
return source;
|
|
1285
|
+
}
|
|
1286
|
+
function ownerRegularFile(path, maximum, label) {
|
|
1287
|
+
if (!isAbsolute(path) || resolve2(path) !== path || /[\r\n\0]/.test(path)) {
|
|
1288
|
+
throw new Error(`${label} path must be canonical and absolute`);
|
|
1289
|
+
}
|
|
1290
|
+
const value = lstatSync(path);
|
|
1291
|
+
const uid = process.getuid?.() ?? value.uid;
|
|
1292
|
+
if (!value.isFile() || value.isSymbolicLink() || value.uid !== uid || value.nlink !== 1 || (value.mode & 63) !== 0 || value.size < 1 || value.size > maximum) {
|
|
1293
|
+
throw new Error(`${label} must be an owner-only regular file with one link and at most ${maximum} bytes`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
async function readBootstrapBundle(bundlePath, manifestPath = `${bundlePath}.json`) {
|
|
1297
|
+
ownerRegularFile(bundlePath, MAX_BOOTSTRAP_BUNDLE_BYTES, "bootstrap bundle");
|
|
1298
|
+
ownerRegularFile(manifestPath, 16 * 1024, "bootstrap bundle manifest");
|
|
1299
|
+
let parsed;
|
|
1300
|
+
try {
|
|
1301
|
+
parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
1302
|
+
} catch {
|
|
1303
|
+
throw new Error("bootstrap bundle manifest is not valid JSON");
|
|
1304
|
+
}
|
|
1305
|
+
const manifest = parseBootstrapBundleManifest(parsed);
|
|
1306
|
+
const size = lstatSync(bundlePath).size;
|
|
1307
|
+
if (size !== manifest.bytes || await sha256File(bundlePath) !== manifest.sha256) {
|
|
1308
|
+
throw new Error("bootstrap bundle bytes do not match their manifest");
|
|
1309
|
+
}
|
|
1310
|
+
return { bundlePath, manifestPath, manifest };
|
|
1311
|
+
}
|
|
1312
|
+
async function buildBootstrapBundle(input, exec = run) {
|
|
1313
|
+
const repositoryRoot = resolve2(input.repositoryRoot);
|
|
1314
|
+
const outputPath = resolve2(input.outputPath);
|
|
1315
|
+
const manifestPath = `${outputPath}.json`;
|
|
1316
|
+
const branch = branchName(input.branch);
|
|
1317
|
+
if (!isAbsolute(input.outputPath) || outputPath !== input.outputPath) {
|
|
1318
|
+
throw new Error("bootstrap bundle output must be a canonical absolute path");
|
|
1319
|
+
}
|
|
1320
|
+
if (existsSync(outputPath) || existsSync(manifestPath)) {
|
|
1321
|
+
throw new Error("bootstrap bundle output already exists");
|
|
1322
|
+
}
|
|
1323
|
+
const root = lstatSync(repositoryRoot);
|
|
1324
|
+
if (!root.isDirectory() || root.isSymbolicLink())
|
|
1325
|
+
throw new Error("bootstrap bundle source must be a real directory");
|
|
1326
|
+
mkdirSync(dirname2(outputPath), { recursive: true, mode: 448 });
|
|
1327
|
+
const parent = lstatSync(dirname2(outputPath));
|
|
1328
|
+
const uid = process.getuid?.() ?? parent.uid;
|
|
1329
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== uid || (parent.mode & 63) !== 0) {
|
|
1330
|
+
throw new Error("bootstrap bundle output directory must be caller-owned and owner-only");
|
|
1331
|
+
}
|
|
1332
|
+
const dirty = await checked(exec, ["git", "-C", repositoryRoot, "status", "--porcelain=v1", "--untracked-files=no"], "Git worktree check");
|
|
1333
|
+
if (dirty)
|
|
1334
|
+
throw new Error("bootstrap bundle source has tracked changes; commit the reviewed API release first");
|
|
1335
|
+
const revision = (await checked(exec, ["git", "-C", repositoryRoot, "rev-parse", "--verify", `refs/heads/${branch}^{commit}`], "Git branch resolution")).toLowerCase();
|
|
1336
|
+
if (!/^[a-f0-9]{40}$/.test(revision))
|
|
1337
|
+
throw new Error("bootstrap bundle branch did not resolve to one exact commit");
|
|
1338
|
+
const temporary = `${outputPath}.next.${process.pid}.${randomBytes(6).toString("hex")}`;
|
|
1339
|
+
try {
|
|
1340
|
+
await checked(exec, ["git", "-C", repositoryRoot, "bundle", "create", temporary, `refs/heads/${branch}`], "Git bundle creation");
|
|
1341
|
+
chmodSync(temporary, 384);
|
|
1342
|
+
const file = lstatSync(temporary);
|
|
1343
|
+
if (!file.isFile() || file.isSymbolicLink() || file.size < 1 || file.size > MAX_BOOTSTRAP_BUNDLE_BYTES) {
|
|
1344
|
+
throw new Error("created bootstrap bundle is outside the supported boundary");
|
|
1345
|
+
}
|
|
1346
|
+
await checked(exec, ["git", "bundle", "verify", temporary], "Git bundle verification");
|
|
1347
|
+
const manifest = {
|
|
1348
|
+
format: BOOTSTRAP_BUNDLE_FORMAT,
|
|
1349
|
+
kind: BOOTSTRAP_BUNDLE_KIND,
|
|
1350
|
+
branch,
|
|
1351
|
+
revision,
|
|
1352
|
+
sha256: await sha256File(temporary),
|
|
1353
|
+
bytes: file.size,
|
|
1354
|
+
createdAt: new Date().toISOString()
|
|
1355
|
+
};
|
|
1356
|
+
const manifestTemporary = `${manifestPath}.next.${process.pid}.${randomBytes(6).toString("hex")}`;
|
|
1357
|
+
writeFileSync(manifestTemporary, `${JSON.stringify(manifest, null, 2)}
|
|
1358
|
+
`, { mode: 384, flag: "wx" });
|
|
1359
|
+
renameSync(temporary, outputPath);
|
|
1360
|
+
renameSync(manifestTemporary, manifestPath);
|
|
1361
|
+
return { bundlePath: outputPath, manifestPath, manifest };
|
|
1362
|
+
} catch (cause) {
|
|
1363
|
+
rmSync(temporary, { force: true });
|
|
1364
|
+
throw cause;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1212
1368
|
// src/bootstrap.ts
|
|
1213
|
-
import { createHash, createHmac as createHmac2, randomBytes as
|
|
1369
|
+
import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes3 } from "crypto";
|
|
1214
1370
|
import {
|
|
1215
|
-
chmodSync as
|
|
1216
|
-
existsSync as
|
|
1217
|
-
lstatSync as
|
|
1218
|
-
mkdirSync as
|
|
1219
|
-
readFileSync as
|
|
1220
|
-
renameSync as
|
|
1221
|
-
rmSync as
|
|
1222
|
-
writeFileSync as
|
|
1371
|
+
chmodSync as chmodSync3,
|
|
1372
|
+
existsSync as existsSync5,
|
|
1373
|
+
lstatSync as lstatSync3,
|
|
1374
|
+
mkdirSync as mkdirSync5,
|
|
1375
|
+
readFileSync as readFileSync5,
|
|
1376
|
+
renameSync as renameSync5,
|
|
1377
|
+
rmSync as rmSync5,
|
|
1378
|
+
writeFileSync as writeFileSync5
|
|
1223
1379
|
} from "fs";
|
|
1224
|
-
import { dirname as
|
|
1380
|
+
import { dirname as dirname6 } from "path";
|
|
1225
1381
|
import { fileURLToPath } from "url";
|
|
1226
1382
|
|
|
1227
1383
|
// src/agent-update-helper.ts
|
|
@@ -1240,7 +1396,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1240
1396
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1241
1397
|
|
|
1242
1398
|
// src/version.ts
|
|
1243
|
-
var VERSION = "0.1.
|
|
1399
|
+
var VERSION = "0.1.64";
|
|
1244
1400
|
|
|
1245
1401
|
// src/software.ts
|
|
1246
1402
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -1255,8 +1411,8 @@ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
|
1255
1411
|
`;
|
|
1256
1412
|
|
|
1257
1413
|
// src/service-supervisor.ts
|
|
1258
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
1259
|
-
import { dirname as
|
|
1414
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1415
|
+
import { dirname as dirname3, join as join2, resolve as resolve3, sep } from "path";
|
|
1260
1416
|
|
|
1261
1417
|
// src/definition.ts
|
|
1262
1418
|
var RESERVED_STEP_ENV = new Set([
|
|
@@ -1275,17 +1431,17 @@ var RESERVED_STEP_ENV = new Set([
|
|
|
1275
1431
|
// src/service-supervisor.ts
|
|
1276
1432
|
var defaultHost = {
|
|
1277
1433
|
write(path, content, mode) {
|
|
1278
|
-
|
|
1434
|
+
mkdirSync2(dirname3(path), { recursive: true, mode: 493 });
|
|
1279
1435
|
const next = `${path}.next`;
|
|
1280
|
-
|
|
1281
|
-
|
|
1436
|
+
writeFileSync2(next, content, { mode });
|
|
1437
|
+
renameSync2(next, path);
|
|
1282
1438
|
},
|
|
1283
|
-
read: (path) =>
|
|
1284
|
-
exists:
|
|
1285
|
-
list: (path) =>
|
|
1439
|
+
read: (path) => readFileSync2(path, "utf8"),
|
|
1440
|
+
exists: existsSync2,
|
|
1441
|
+
list: (path) => existsSync2(path) ? readdirSync(path) : [],
|
|
1286
1442
|
realpath: realpathSync,
|
|
1287
|
-
mkdir: (path, mode) =>
|
|
1288
|
-
remove: (path) =>
|
|
1443
|
+
mkdir: (path, mode) => mkdirSync2(path, { recursive: true, mode }),
|
|
1444
|
+
remove: (path) => rmSync2(path, { force: true }),
|
|
1289
1445
|
async exec(argv) {
|
|
1290
1446
|
const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe", env: {
|
|
1291
1447
|
PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
@@ -1315,21 +1471,21 @@ var defaultHost = {
|
|
|
1315
1471
|
};
|
|
1316
1472
|
|
|
1317
1473
|
// src/container-supervisor.ts
|
|
1318
|
-
import { existsSync as
|
|
1319
|
-
import { dirname as
|
|
1474
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1475
|
+
import { dirname as dirname4, resolve as resolve4, sep as sep2 } from "path";
|
|
1320
1476
|
var defaultHost2 = {
|
|
1321
1477
|
realpath: realpathSync2,
|
|
1322
|
-
exists:
|
|
1323
|
-
read: (path) =>
|
|
1478
|
+
exists: existsSync3,
|
|
1479
|
+
read: (path) => readFileSync3(path, "utf8"),
|
|
1324
1480
|
write(path, content, mode) {
|
|
1325
|
-
|
|
1481
|
+
mkdirSync3(dirname4(path), { recursive: true, mode: 493 });
|
|
1326
1482
|
const next = `${path}.next`;
|
|
1327
|
-
|
|
1328
|
-
|
|
1483
|
+
writeFileSync3(next, content, { mode });
|
|
1484
|
+
renameSync3(next, path);
|
|
1329
1485
|
},
|
|
1330
|
-
remove: (path) =>
|
|
1331
|
-
list: (path) =>
|
|
1332
|
-
mkdir: (path, mode) =>
|
|
1486
|
+
remove: (path) => rmSync3(path, { force: true }),
|
|
1487
|
+
list: (path) => existsSync3(path) ? readdirSync2(path) : [],
|
|
1488
|
+
mkdir: (path, mode) => mkdirSync3(path, { recursive: true, mode }),
|
|
1333
1489
|
async exec(argv) {
|
|
1334
1490
|
const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
|
|
1335
1491
|
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
@@ -1479,7 +1635,11 @@ function agentEgressUnit(options) {
|
|
|
1479
1635
|
const user = options.user ?? "forgezero";
|
|
1480
1636
|
if (!/^[a-z_][a-z0-9_-]{0,30}$/.test(user))
|
|
1481
1637
|
throw new Error("invalid Agent service user");
|
|
1482
|
-
const
|
|
1638
|
+
const bootstrapBundleEnabled = Boolean(options.bootstrapBundlePath && options.bootstrapBundleManifestPath);
|
|
1639
|
+
if (Boolean(options.bootstrapBundlePath) !== Boolean(options.bootstrapBundleManifestPath) || bootstrapBundleEnabled && ![options.bootstrapBundlePath, options.bootstrapBundleManifestPath].every((path) => path.startsWith("/") && !/[\r\n\0:]/.test(path))) {
|
|
1640
|
+
throw new Error("bootstrap bundle and manifest must be supplied together as absolute paths");
|
|
1641
|
+
}
|
|
1642
|
+
const deploymentEnabled = Boolean(options.repository || bootstrapBundleEnabled || options.pullDeployments);
|
|
1483
1643
|
const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
|
|
1484
1644
|
const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
|
|
1485
1645
|
if (deploymentEnabled && runnerPublicTcpPorts.length < 1) {
|
|
@@ -1885,17 +2045,16 @@ function agentUnit(options) {
|
|
|
1885
2045
|
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
1886
2046
|
throw new Error("migration pull and lifecycle profile must be supplied together");
|
|
1887
2047
|
}
|
|
1888
|
-
const
|
|
1889
|
-
if (
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
throw new Error("bootstrap pull, SSH credential, public key and target telemetry endpoint must be supplied together");
|
|
2048
|
+
const bootstrapIdentityEnabled = Boolean(options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath);
|
|
2049
|
+
if (Boolean(options.bootstrapSshCredentialPath) !== Boolean(options.bootstrapSshPublicKeyPath)) {
|
|
2050
|
+
throw new Error("bootstrap SSH credential and public key paths must be supplied together");
|
|
2051
|
+
}
|
|
2052
|
+
const bootstrapEnabled = Boolean(options.pullBootstrap);
|
|
2053
|
+
if (bootstrapEnabled && (!bootstrapIdentityEnabled || !options.bootstrapTargetTelemetryEndpoint) || !bootstrapEnabled && options.bootstrapTargetTelemetryEndpoint) {
|
|
2054
|
+
throw new Error("bootstrap pull, SSH identity and target telemetry endpoint must be supplied together");
|
|
1896
2055
|
}
|
|
1897
2056
|
const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
|
|
1898
|
-
const bootstrapSshCredentialPath =
|
|
2057
|
+
const bootstrapSshCredentialPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
|
|
1899
2058
|
const warpValues = [
|
|
1900
2059
|
options.warpOrganization,
|
|
1901
2060
|
options.warpClientIdCredentialPath,
|
|
@@ -1949,6 +2108,8 @@ function agentUnit(options) {
|
|
|
1949
2108
|
options.gitPublicKeyPath ? `FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}` : null,
|
|
1950
2109
|
options.repository ? `FZ_DEPLOY_REPO=${options.repository}` : null,
|
|
1951
2110
|
options.branch ? `FZ_DEPLOY_BRANCH=${options.branch}` : null,
|
|
2111
|
+
options.bootstrapBundlePath ? `FZ_BOOTSTRAP_BUNDLE=${options.bootstrapBundlePath}` : null,
|
|
2112
|
+
options.bootstrapBundleManifestPath ? `FZ_BOOTSTRAP_BUNDLE_MANIFEST=${options.bootstrapBundleManifestPath}` : null,
|
|
1952
2113
|
options.profile ? `FZ_DEPLOY_PROFILE=${options.profile}` : null,
|
|
1953
2114
|
options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
|
|
1954
2115
|
deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
|
|
@@ -2132,15 +2293,13 @@ function planProvision(options) {
|
|
|
2132
2293
|
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
2133
2294
|
throw new Error("migration pull and lifecycle profile must be supplied together");
|
|
2134
2295
|
}
|
|
2135
|
-
const
|
|
2136
|
-
if (
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
].some(Boolean) && !bootstrapEnabled) {
|
|
2143
|
-
throw new Error("bootstrap pull, SSH credential, public key and target telemetry endpoint must be supplied together");
|
|
2296
|
+
const bootstrapIdentityEnabled = Boolean(options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath);
|
|
2297
|
+
if (Boolean(options.bootstrapSshCredentialPath) !== Boolean(options.bootstrapSshPublicKeyPath) || options.bootstrapSshSourcePath && !bootstrapIdentityEnabled) {
|
|
2298
|
+
throw new Error("bootstrap SSH credential and public key paths must be supplied together");
|
|
2299
|
+
}
|
|
2300
|
+
const bootstrapEnabled = Boolean(options.pullBootstrap);
|
|
2301
|
+
if (bootstrapEnabled && (!bootstrapIdentityEnabled || !options.bootstrapTargetTelemetryEndpoint) || !bootstrapEnabled && options.bootstrapTargetTelemetryEndpoint) {
|
|
2302
|
+
throw new Error("bootstrap pull, SSH identity and target telemetry endpoint must be supplied together");
|
|
2144
2303
|
}
|
|
2145
2304
|
const warpValues = [
|
|
2146
2305
|
options.warpOrganization,
|
|
@@ -2150,13 +2309,13 @@ function planProvision(options) {
|
|
|
2150
2309
|
const warpEnabled = warpValues.every(Boolean);
|
|
2151
2310
|
if (warpValues.some(Boolean) && !warpEnabled)
|
|
2152
2311
|
throw new Error("WARP configuration must be supplied together");
|
|
2153
|
-
const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath
|
|
2154
|
-
if (
|
|
2155
|
-
throw new Error("direct enrolment credential
|
|
2312
|
+
const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath);
|
|
2313
|
+
if (options.enrolTokenCredentialPath && !options.enrolStatePath || options.enrolTokenSourcePath && (!options.enrolTokenCredentialPath || !options.enrolStatePath)) {
|
|
2314
|
+
throw new Error("direct enrolment credential requires its durable state path");
|
|
2156
2315
|
}
|
|
2157
2316
|
const enrolTokenSourcePath = options.enrolTokenSourcePath ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
|
|
2158
2317
|
const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
|
|
2159
|
-
const enrolStatePath =
|
|
2318
|
+
const enrolStatePath = options.enrolStatePath ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
|
|
2160
2319
|
const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
|
|
2161
2320
|
const sourceBinPath = options.sourceBinPath ? systemdPath(options.sourceBinPath, "agent source binary") : undefined;
|
|
2162
2321
|
const binPath = options.binPath ? systemdPath(options.binPath, "agent binary") : undefined;
|
|
@@ -2168,8 +2327,8 @@ function planProvision(options) {
|
|
|
2168
2327
|
const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
|
|
2169
2328
|
const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
|
|
2170
2329
|
const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
|
|
2171
|
-
const bootstrapSshCredentialPath =
|
|
2172
|
-
const bootstrapSshPublicKeyPath =
|
|
2330
|
+
const bootstrapSshCredentialPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
|
|
2331
|
+
const bootstrapSshPublicKeyPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshPublicKeyPath, "bootstrap SSH public key") : undefined;
|
|
2173
2332
|
const bootstrapSshSourcePath = options.bootstrapSshSourcePath ? systemdPath(options.bootstrapSshSourcePath, "bootstrap SSH private-key source") : undefined;
|
|
2174
2333
|
const bootstrapSshPublicKeyDir = bootstrapSshPublicKeyPath?.replace(/\/[^/]+$/, "");
|
|
2175
2334
|
const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
|
|
@@ -2269,7 +2428,7 @@ function planProvision(options) {
|
|
|
2269
2428
|
step("Git deploy identity directory", { kind: "directories", directories: [{ path: gitPublicKeyDir, mode: 493, owner: "root", group: "root" }] }),
|
|
2270
2429
|
step("unique encrypted Git deploy identity", { kind: "ensure-git-identity", credential: gitCredentialPath, publicKey: gitPublicKeyPath })
|
|
2271
2430
|
] : [],
|
|
2272
|
-
...
|
|
2431
|
+
...bootstrapIdentityEnabled ? [
|
|
2273
2432
|
step("bootstrap SSH public identity directory", { kind: "directories", directories: [
|
|
2274
2433
|
{ path: bootstrapSshPublicKeyDir, mode: 493, owner: "root", group: "root" }
|
|
2275
2434
|
] }),
|
|
@@ -2305,6 +2464,10 @@ function planProvision(options) {
|
|
|
2305
2464
|
{ argv: ["/usr/bin/rm", "-f", "/etc/systemd/system/forgezero-deploy-runner.socket"] },
|
|
2306
2465
|
{ argv: ["/usr/bin/systemctl", "daemon-reload"] }
|
|
2307
2466
|
] })] : [],
|
|
2467
|
+
...enrolStatePath && !enrolmentEnabled ? [step("retire consumed enrolment unit", { kind: "commands", commands: [
|
|
2468
|
+
{ argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-agent-enrol.service"], acceptedExitCodes: [0, 1, 5] },
|
|
2469
|
+
{ argv: ["/usr/bin/rm", "-f", ENROLMENT_UNIT_PATH] }
|
|
2470
|
+
] })] : [],
|
|
2308
2471
|
step("enable and converge services", { kind: "commands", commands: [
|
|
2309
2472
|
{ argv: ["/usr/bin/systemctl", "enable", ...enabledUnits] },
|
|
2310
2473
|
{ argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
|
|
@@ -2331,27 +2494,27 @@ function planProvision(options) {
|
|
|
2331
2494
|
}
|
|
2332
2495
|
|
|
2333
2496
|
// src/cli/agent-install.ts
|
|
2334
|
-
import { randomBytes } from "crypto";
|
|
2497
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
2335
2498
|
import {
|
|
2336
|
-
chmodSync,
|
|
2499
|
+
chmodSync as chmodSync2,
|
|
2337
2500
|
copyFileSync,
|
|
2338
|
-
existsSync as
|
|
2339
|
-
lstatSync,
|
|
2340
|
-
mkdirSync as
|
|
2341
|
-
readFileSync as
|
|
2501
|
+
existsSync as existsSync4,
|
|
2502
|
+
lstatSync as lstatSync2,
|
|
2503
|
+
mkdirSync as mkdirSync4,
|
|
2504
|
+
readFileSync as readFileSync4,
|
|
2342
2505
|
realpathSync as realpathSync3,
|
|
2343
|
-
renameSync as
|
|
2344
|
-
rmSync as
|
|
2506
|
+
renameSync as renameSync4,
|
|
2507
|
+
rmSync as rmSync4,
|
|
2345
2508
|
symlinkSync,
|
|
2346
|
-
writeFileSync as
|
|
2509
|
+
writeFileSync as writeFileSync4
|
|
2347
2510
|
} from "fs";
|
|
2348
|
-
import { dirname as
|
|
2349
|
-
async function readCapabilities(
|
|
2511
|
+
import { dirname as dirname5 } from "path";
|
|
2512
|
+
async function readCapabilities(run2) {
|
|
2350
2513
|
const answers = {};
|
|
2351
2514
|
const checks = Object.entries(CAPABILITY_CHECKS);
|
|
2352
2515
|
for (const [id, check] of checks) {
|
|
2353
2516
|
try {
|
|
2354
|
-
const result = await
|
|
2517
|
+
const result = await run2(check.operation);
|
|
2355
2518
|
answers[id] = check.satisfied(result.stdout, result.exitCode);
|
|
2356
2519
|
} catch {
|
|
2357
2520
|
answers[id] = false;
|
|
@@ -2404,52 +2567,52 @@ var runProvisionOperation = async (operation) => {
|
|
|
2404
2567
|
}
|
|
2405
2568
|
if (operation.kind === "install-runtime") {
|
|
2406
2569
|
const release = `/opt/forgezero/agent/versions/${operation.version}`;
|
|
2407
|
-
|
|
2408
|
-
|
|
2570
|
+
mkdirSync4(`${release}/dist`, { recursive: true, mode: 493 });
|
|
2571
|
+
mkdirSync4(dirname5(operation.binary), { recursive: true, mode: 493 });
|
|
2409
2572
|
copyFileSync(operation.source, `${release}/dist/fz-agent.js`);
|
|
2410
|
-
|
|
2411
|
-
const gitSshSource = `${
|
|
2412
|
-
if (!
|
|
2573
|
+
chmodSync2(`${release}/dist/fz-agent.js`, 493);
|
|
2574
|
+
const gitSshSource = `${dirname5(operation.source)}/fz-git-ssh.js`;
|
|
2575
|
+
if (!existsSync4(gitSshSource))
|
|
2413
2576
|
return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
|
|
2414
2577
|
copyFileSync(gitSshSource, `${release}/dist/fz-git-ssh.js`);
|
|
2415
|
-
|
|
2578
|
+
chmodSync2(`${release}/dist/fz-git-ssh.js`, 493);
|
|
2416
2579
|
const pending = "/opt/forgezero/agent/current.next";
|
|
2417
|
-
|
|
2580
|
+
rmSync4(pending, { force: true });
|
|
2418
2581
|
symlinkSync(`versions/${operation.version}`, pending);
|
|
2419
|
-
|
|
2420
|
-
|
|
2582
|
+
renameSync4(pending, "/opt/forgezero/agent/current");
|
|
2583
|
+
rmSync4(operation.binary, { force: true });
|
|
2421
2584
|
symlinkSync("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
|
|
2422
2585
|
const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
|
|
2423
|
-
|
|
2586
|
+
rmSync4(gitSshBinary, { force: true });
|
|
2424
2587
|
symlinkSync("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
|
|
2425
2588
|
return { stdout: "", exitCode: 0 };
|
|
2426
2589
|
}
|
|
2427
2590
|
if (operation.kind === "ensure-seed") {
|
|
2428
|
-
if (
|
|
2591
|
+
if (existsSync4(operation.credential) && lstatSync2(operation.credential).size > 0)
|
|
2429
2592
|
return { stdout: "", exitCode: 0 };
|
|
2430
|
-
const seed =
|
|
2593
|
+
const seed = randomBytes2(32).toString("base64url");
|
|
2431
2594
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
|
|
2432
2595
|
if (result.exitCode === 0)
|
|
2433
|
-
|
|
2596
|
+
chmodSync2(operation.credential, 256);
|
|
2434
2597
|
return result;
|
|
2435
2598
|
}
|
|
2436
2599
|
if (operation.kind === "ensure-git-identity") {
|
|
2437
2600
|
const key = "/run/forgezero-git-deploy-key";
|
|
2438
2601
|
const publicKey = `${key}.pub`;
|
|
2439
2602
|
try {
|
|
2440
|
-
if (!
|
|
2441
|
-
|
|
2442
|
-
|
|
2603
|
+
if (!existsSync4(operation.credential) || lstatSync2(operation.credential).size < 1) {
|
|
2604
|
+
rmSync4(key, { force: true });
|
|
2605
|
+
rmSync4(publicKey, { force: true });
|
|
2443
2606
|
let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
|
|
2444
2607
|
if (result.exitCode !== 0)
|
|
2445
2608
|
return result;
|
|
2446
2609
|
result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=git-deploy-key", key, operation.credential]);
|
|
2447
2610
|
if (result.exitCode !== 0)
|
|
2448
2611
|
return result;
|
|
2449
|
-
|
|
2612
|
+
chmodSync2(operation.credential, 256);
|
|
2450
2613
|
}
|
|
2451
|
-
if (!
|
|
2452
|
-
if (!
|
|
2614
|
+
if (!existsSync4(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
|
|
2615
|
+
if (!existsSync4(key)) {
|
|
2453
2616
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
|
|
2454
2617
|
if (decrypted.exitCode !== 0)
|
|
2455
2618
|
return decrypted;
|
|
@@ -2457,30 +2620,30 @@ var runProvisionOperation = async (operation) => {
|
|
|
2457
2620
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
2458
2621
|
if (derived.exitCode !== 0)
|
|
2459
2622
|
return derived;
|
|
2460
|
-
|
|
2623
|
+
writeFileSync4(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
|
|
2461
2624
|
`, { mode: 292 });
|
|
2462
2625
|
}
|
|
2463
2626
|
return { stdout: "", exitCode: 0 };
|
|
2464
2627
|
} finally {
|
|
2465
|
-
|
|
2466
|
-
|
|
2628
|
+
rmSync4(key, { force: true });
|
|
2629
|
+
rmSync4(publicKey, { force: true });
|
|
2467
2630
|
}
|
|
2468
2631
|
}
|
|
2469
2632
|
if (operation.kind === "ensure-bootstrap-ssh-identity") {
|
|
2470
2633
|
const key = "/run/forgezero-bootstrap-ssh-key";
|
|
2471
2634
|
const generatedPublicKey = `${key}.pub`;
|
|
2472
2635
|
try {
|
|
2473
|
-
if (!
|
|
2474
|
-
|
|
2475
|
-
|
|
2636
|
+
if (!existsSync4(operation.credential) || lstatSync2(operation.credential).size < 1) {
|
|
2637
|
+
rmSync4(key, { force: true });
|
|
2638
|
+
rmSync4(generatedPublicKey, { force: true });
|
|
2476
2639
|
let result;
|
|
2477
2640
|
if (operation.source) {
|
|
2478
|
-
const source =
|
|
2641
|
+
const source = existsSync4(operation.source) ? lstatSync2(operation.source) : undefined;
|
|
2479
2642
|
if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
|
|
2480
2643
|
return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
|
|
2481
2644
|
}
|
|
2482
2645
|
copyFileSync(operation.source, key);
|
|
2483
|
-
|
|
2646
|
+
chmodSync2(key, 384);
|
|
2484
2647
|
} else {
|
|
2485
2648
|
result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-bootstrap-runner", "-f", key]);
|
|
2486
2649
|
if (result.exitCode !== 0)
|
|
@@ -2493,48 +2656,48 @@ var runProvisionOperation = async (operation) => {
|
|
|
2493
2656
|
result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
|
|
2494
2657
|
if (result.exitCode !== 0)
|
|
2495
2658
|
return result;
|
|
2496
|
-
|
|
2659
|
+
chmodSync2(operation.credential, 256);
|
|
2497
2660
|
}
|
|
2498
|
-
if (!
|
|
2499
|
-
if (!
|
|
2661
|
+
if (!existsSync4(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
|
|
2662
|
+
if (!existsSync4(key)) {
|
|
2500
2663
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
|
|
2501
2664
|
if (decrypted.exitCode !== 0)
|
|
2502
2665
|
return decrypted;
|
|
2503
|
-
|
|
2666
|
+
chmodSync2(key, 384);
|
|
2504
2667
|
}
|
|
2505
2668
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
2506
2669
|
if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
|
|
2507
2670
|
return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
|
|
2508
2671
|
}
|
|
2509
|
-
|
|
2510
|
-
|
|
2672
|
+
mkdirSync4(dirname5(operation.publicKey), { recursive: true, mode: 493 });
|
|
2673
|
+
writeFileSync4(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
|
|
2511
2674
|
`, { mode: 292 });
|
|
2512
|
-
|
|
2675
|
+
chmodSync2(operation.publicKey, 292);
|
|
2513
2676
|
}
|
|
2514
2677
|
if (operation.source)
|
|
2515
|
-
|
|
2678
|
+
rmSync4(operation.source, { force: true });
|
|
2516
2679
|
return { stdout: "", exitCode: 0 };
|
|
2517
2680
|
} finally {
|
|
2518
|
-
|
|
2519
|
-
|
|
2681
|
+
rmSync4(key, { force: true });
|
|
2682
|
+
rmSync4(generatedPublicKey, { force: true });
|
|
2520
2683
|
}
|
|
2521
2684
|
}
|
|
2522
2685
|
if (operation.kind === "ensure-enrolment") {
|
|
2523
|
-
if (
|
|
2686
|
+
if (existsSync4(operation.state) && lstatSync2(operation.state).size > 0 || existsSync4(operation.credential) && lstatSync2(operation.credential).size > 0)
|
|
2524
2687
|
return { stdout: "", exitCode: 0 };
|
|
2525
|
-
if (!
|
|
2688
|
+
if (!existsSync4(operation.source))
|
|
2526
2689
|
return { stdout: "enrolment source is missing", exitCode: 1 };
|
|
2527
2690
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
|
|
2528
2691
|
if (result.exitCode === 0) {
|
|
2529
|
-
|
|
2530
|
-
|
|
2692
|
+
chmodSync2(operation.credential, 256);
|
|
2693
|
+
rmSync4(operation.source, { force: true });
|
|
2531
2694
|
}
|
|
2532
2695
|
return result;
|
|
2533
2696
|
}
|
|
2534
2697
|
if (operation.kind === "wait-socket") {
|
|
2535
2698
|
for (let attempt = 0;attempt < operation.attempts; attempt += 1) {
|
|
2536
2699
|
try {
|
|
2537
|
-
if (
|
|
2700
|
+
if (lstatSync2(operation.path).isSocket())
|
|
2538
2701
|
return { stdout: "", exitCode: 0 };
|
|
2539
2702
|
} catch {}
|
|
2540
2703
|
await Bun.sleep(operation.intervalMs);
|
|
@@ -2542,7 +2705,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
2542
2705
|
return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
|
|
2543
2706
|
}
|
|
2544
2707
|
if (operation.kind === "verify-file")
|
|
2545
|
-
return
|
|
2708
|
+
return existsSync4(operation.path) && lstatSync2(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
|
|
2546
2709
|
if (operation.kind === "verify-egress") {
|
|
2547
2710
|
const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
|
|
2548
2711
|
if (active.exitCode !== 0)
|
|
@@ -2564,25 +2727,25 @@ var runProvisionOperation = async (operation) => {
|
|
|
2564
2727
|
}
|
|
2565
2728
|
}
|
|
2566
2729
|
if (operation.kind === "install-warp") {
|
|
2567
|
-
const os =
|
|
2730
|
+
const os = readFileSync4("/etc/os-release", "utf8");
|
|
2568
2731
|
if (!/^ID=ubuntu$/m.test(os) || !/^VERSION_ID="?26\.04"?$/m.test(os))
|
|
2569
2732
|
return { stdout: "unsupported WARP host OS", exitCode: 1 };
|
|
2570
2733
|
const response = await fetch("https://pkg.cloudflareclient.com/pubkey.gpg", { signal: AbortSignal.timeout(30000) });
|
|
2571
2734
|
if (!response.ok)
|
|
2572
2735
|
return { stdout: `WARP key HTTP ${response.status}`, exitCode: 1 };
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2736
|
+
mkdirSync4("/usr/share/keyrings", { recursive: true, mode: 493 });
|
|
2737
|
+
mkdirSync4("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
|
|
2738
|
+
mkdirSync4("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
|
|
2576
2739
|
const key = "/run/cloudflare-warp-key.gpg";
|
|
2577
|
-
|
|
2740
|
+
writeFileSync4(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
|
|
2578
2741
|
let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
|
|
2579
|
-
|
|
2742
|
+
rmSync4(key, { force: true });
|
|
2580
2743
|
if (result.exitCode !== 0)
|
|
2581
2744
|
return result;
|
|
2582
2745
|
const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
|
|
2583
2746
|
if (!codename)
|
|
2584
2747
|
return { stdout: "Ubuntu codename missing", exitCode: 1 };
|
|
2585
|
-
|
|
2748
|
+
writeFileSync4("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ ${codename} main
|
|
2586
2749
|
`, { mode: 420 });
|
|
2587
2750
|
result = await fixed(["/usr/bin/apt-get", "update", "-qq"]);
|
|
2588
2751
|
return result.exitCode === 0 ? fixed(["/usr/bin/apt-get", "install", "-y", "cloudflare-warp"]) : result;
|
|
@@ -2597,7 +2760,7 @@ async function localRunner(operation) {
|
|
|
2597
2760
|
if (capability.kind === "version")
|
|
2598
2761
|
return fixed(capability.argv);
|
|
2599
2762
|
try {
|
|
2600
|
-
const metadata =
|
|
2763
|
+
const metadata = lstatSync2(capability.path);
|
|
2601
2764
|
const present = capability.nodeType === "directory" ? metadata.isDirectory() : true;
|
|
2602
2765
|
return { stdout: present ? `yes
|
|
2603
2766
|
` : `no
|
|
@@ -2611,10 +2774,10 @@ function planInstall(options) {
|
|
|
2611
2774
|
const { capabilities, ...unit } = options;
|
|
2612
2775
|
return planProvision({ ...unit, mode: modeFor(capabilities) });
|
|
2613
2776
|
}
|
|
2614
|
-
async function applyPlan(plan,
|
|
2777
|
+
async function applyPlan(plan, run2) {
|
|
2615
2778
|
const transcript = [];
|
|
2616
2779
|
for (const step2 of plan.steps) {
|
|
2617
|
-
const result = await
|
|
2780
|
+
const result = await run2(step2.operation);
|
|
2618
2781
|
transcript.push({ label: step2.label, command: step2.command, exitCode: result.exitCode });
|
|
2619
2782
|
if (result.exitCode !== 0 && !step2.optional) {
|
|
2620
2783
|
throw new Error(`${step2.label} failed (exit ${result.exitCode}): ${step2.command}`);
|
|
@@ -2708,8 +2871,6 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
2708
2871
|
databaseUser: input.databaseUser,
|
|
2709
2872
|
sharedDirectory: input.sharedDirectory,
|
|
2710
2873
|
seedSyncEpoch: input.seedSyncEpoch,
|
|
2711
|
-
repository: input.repository,
|
|
2712
|
-
branch: input.branch,
|
|
2713
2874
|
deployProfile: input.deployProfile
|
|
2714
2875
|
}))
|
|
2715
2876
|
safeAtom(name, value);
|
|
@@ -2792,8 +2953,6 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
2792
2953
|
FZ_AGENT_OTLP_ENDPOINT: value.agentOtlpEndpoint,
|
|
2793
2954
|
FZ_CUSTODIAN_EMAIL: value.custodianEmail ?? "",
|
|
2794
2955
|
FZ_PROFILE: value.deployProfile,
|
|
2795
|
-
FZ_REPO: value.repository,
|
|
2796
|
-
FZ_BRANCH: value.branch,
|
|
2797
2956
|
FZ_EMAIL_PROVIDER: value.email?.provider ?? "",
|
|
2798
2957
|
FZ_SMTP_HOST: value.email?.provider === "smtp" ? value.email.host : "",
|
|
2799
2958
|
FZ_SMTP_PORT: value.email?.provider === "smtp" ? String(value.email.port) : "",
|
|
@@ -3004,7 +3163,7 @@ var EXTRACT = "/run/forgezero-otelcol-release";
|
|
|
3004
3163
|
var BINARY = "/usr/local/lib/forgezero/otelcol/otelcol";
|
|
3005
3164
|
var CONFIG = "/etc/forgezero/otelcol.yaml";
|
|
3006
3165
|
var UNIT = `/etc/systemd/system/${FORGEZERO_OTEL_COLLECTOR_UNIT}`;
|
|
3007
|
-
var
|
|
3166
|
+
var checked2 = async (host, argv) => {
|
|
3008
3167
|
const result = await host.exec(argv);
|
|
3009
3168
|
const output = `${result.output ?? result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
3010
3169
|
if (result.exitCode !== 0)
|
|
@@ -3086,7 +3245,7 @@ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
|
|
|
3086
3245
|
});
|
|
3087
3246
|
const versionOutput = `${observed.output ?? observed.stdout ?? ""}${observed.stderr ?? ""}`;
|
|
3088
3247
|
if (observed.exitCode !== 0 || !versionOutput.includes(`otelcol version ${FORGEZERO_OTEL_COLLECTOR_VERSION}`)) {
|
|
3089
|
-
await
|
|
3248
|
+
await checked2(host, [
|
|
3090
3249
|
"/usr/bin/curl",
|
|
3091
3250
|
"--fail",
|
|
3092
3251
|
"--silent",
|
|
@@ -3098,19 +3257,19 @@ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
|
|
|
3098
3257
|
ARCHIVE,
|
|
3099
3258
|
`https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${FORGEZERO_OTEL_COLLECTOR_VERSION}/otelcol_${FORGEZERO_OTEL_COLLECTOR_VERSION}_linux_amd64.tar.gz`
|
|
3100
3259
|
]);
|
|
3101
|
-
const digest = (await
|
|
3260
|
+
const digest = (await checked2(host, ["/usr/bin/sha256sum", ARCHIVE])).split(/\s+/)[0];
|
|
3102
3261
|
if (digest !== FORGEZERO_OTEL_COLLECTOR_SHA256)
|
|
3103
3262
|
throw new Error("OTLP collector archive checksum mismatch");
|
|
3104
|
-
await
|
|
3105
|
-
await
|
|
3106
|
-
await
|
|
3107
|
-
await
|
|
3263
|
+
await checked2(host, ["/usr/bin/install", "-d", "-m", "0755", "/usr/local/lib/forgezero/otelcol", EXTRACT]);
|
|
3264
|
+
await checked2(host, ["/usr/bin/tar", "-xzf", ARCHIVE, "-C", EXTRACT, "otelcol"]);
|
|
3265
|
+
await checked2(host, ["/usr/bin/install", "-m", "0755", `${EXTRACT}/otelcol`, BINARY]);
|
|
3266
|
+
await checked2(host, ["/usr/bin/rm", "-f", ARCHIVE, `${EXTRACT}/otelcol`]);
|
|
3108
3267
|
}
|
|
3109
3268
|
if ((await host.exec(["/usr/bin/getent", "group", "forgezero-otel"])).exitCode !== 0) {
|
|
3110
|
-
await
|
|
3269
|
+
await checked2(host, ["/usr/sbin/groupadd", "--system", "forgezero-otel"]);
|
|
3111
3270
|
}
|
|
3112
3271
|
if ((await host.exec(["/usr/bin/id", "forgezero-otel"])).exitCode !== 0) {
|
|
3113
|
-
await
|
|
3272
|
+
await checked2(host, [
|
|
3114
3273
|
"/usr/sbin/useradd",
|
|
3115
3274
|
"--system",
|
|
3116
3275
|
"--no-create-home",
|
|
@@ -3123,8 +3282,8 @@ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
|
|
|
3123
3282
|
}
|
|
3124
3283
|
host.write(CONFIG, rendered.config, 420);
|
|
3125
3284
|
host.write(UNIT, rendered.unit, 420);
|
|
3126
|
-
await
|
|
3127
|
-
await
|
|
3285
|
+
await checked2(host, ["/usr/bin/systemctl", "daemon-reload"]);
|
|
3286
|
+
await checked2(host, ["/usr/bin/systemctl", "enable", "--now", FORGEZERO_OTEL_COLLECTOR_UNIT]);
|
|
3128
3287
|
let ready = false;
|
|
3129
3288
|
for (let attempt = 0;attempt < 100; attempt += 1) {
|
|
3130
3289
|
const probe = await host.exec([
|
|
@@ -3253,7 +3412,7 @@ var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
|
|
|
3253
3412
|
var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
|
|
3254
3413
|
var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
|
|
3255
3414
|
var BOOTSTRAP_SSH_PUBLIC_KEY = "/etc/forgezero/bootstrap/runner.pub";
|
|
3256
|
-
var
|
|
3415
|
+
var BOOTSTRAP_RELEASE_EVIDENCE = "/var/lib/forgezero/bootstrap-release.json";
|
|
3257
3416
|
var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
|
|
3258
3417
|
var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
|
|
3259
3418
|
var CONTROL_SOCKET = "/run/forgezero/control.sock";
|
|
@@ -3326,6 +3485,9 @@ function validateBootstrapConfig(value) {
|
|
|
3326
3485
|
throw new Error("Cloudflare Mesh/WARP requires a node-specific Cloudflare handoff");
|
|
3327
3486
|
}
|
|
3328
3487
|
if (value.kind === "enrolled-compute") {
|
|
3488
|
+
if (value.gitDeployKey !== undefined && typeof value.gitDeployKey !== "boolean") {
|
|
3489
|
+
throw new Error("gitDeployKey must be boolean");
|
|
3490
|
+
}
|
|
3329
3491
|
if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(value.realm))
|
|
3330
3492
|
throw new Error("tenant realm is malformed");
|
|
3331
3493
|
if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
|
|
@@ -3351,6 +3513,9 @@ function validateBootstrapConfig(value) {
|
|
|
3351
3513
|
throw new Error("unsupported platform software profile");
|
|
3352
3514
|
if (!["production", "development"].includes(value.environment))
|
|
3353
3515
|
throw new Error("platform environment must be production or development");
|
|
3516
|
+
if (!value.bootstrapBundle || ![value.bootstrapBundle.bundleFile, value.bootstrapBundle.manifestFile].every((path) => typeof path === "string" && path.startsWith("/") && !/[\r\n\0:]/.test(path))) {
|
|
3517
|
+
throw new Error("platform bootstrap requires absolute bundle and manifest paths");
|
|
3518
|
+
}
|
|
3354
3519
|
let api;
|
|
3355
3520
|
try {
|
|
3356
3521
|
api = new URL(value.apiUrl);
|
|
@@ -3383,7 +3548,7 @@ function validateBootstrapConfig(value) {
|
|
|
3383
3548
|
if (runtime.otlpCollectorUnit !== FORGEZERO_OTEL_COLLECTOR_UNIT) {
|
|
3384
3549
|
throw new Error(`platform bootstrap requires ${FORGEZERO_OTEL_COLLECTOR_UNIT}`);
|
|
3385
3550
|
}
|
|
3386
|
-
if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.
|
|
3551
|
+
if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.databaseCoordinators.join(",") !== write.join(",")) {
|
|
3387
3552
|
throw new Error("platform runtime coordinates disagree with immutable bootstrap coordinates");
|
|
3388
3553
|
}
|
|
3389
3554
|
if (runtime.deployProfile !== value.environment)
|
|
@@ -3413,6 +3578,7 @@ function planBootstrap(input, initialized = false) {
|
|
|
3413
3578
|
] : [
|
|
3414
3579
|
...config.firewall.enabled ? [{ id: "ufw", version: "ubuntu-26.04" }] : [],
|
|
3415
3580
|
{ id: "bun", version: "1.3.14" },
|
|
3581
|
+
{ id: "git", version: "ubuntu-26.04" },
|
|
3416
3582
|
{ id: "nginx", version: "ubuntu-26.04" },
|
|
3417
3583
|
...platformBootstrapRunner(config) ? [{ id: "openssh-client", version: "ubuntu-26.04" }] : [],
|
|
3418
3584
|
...config.profile === "platform-api" ? [] : [{ id: "arangodb", version: "3.11.14" }],
|
|
@@ -3441,7 +3607,7 @@ function planBootstrap(input, initialized = false) {
|
|
|
3441
3607
|
]
|
|
3442
3608
|
};
|
|
3443
3609
|
}
|
|
3444
|
-
var
|
|
3610
|
+
var checked3 = async (host, argv, label, options) => {
|
|
3445
3611
|
const result = await host.exec(argv, options);
|
|
3446
3612
|
if (result.exitCode !== 0)
|
|
3447
3613
|
throw new Error(`${label} failed: ${result.output.trim()}`);
|
|
@@ -3622,6 +3788,37 @@ async function seal(host, name, destination2, value) {
|
|
|
3622
3788
|
if (result.exitCode !== 0)
|
|
3623
3789
|
throw new Error(`could not seal ${name}: ${result.output.trim()}`);
|
|
3624
3790
|
}
|
|
3791
|
+
async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
|
|
3792
|
+
const manifestMetadata = host.inspect?.(config.bootstrapBundle.manifestFile);
|
|
3793
|
+
const bundleMetadata = host.inspect?.(config.bootstrapBundle.bundleFile);
|
|
3794
|
+
if (manifestMetadata && (!manifestMetadata.regular || manifestMetadata.symbolic || manifestMetadata.uid !== 0 || manifestMetadata.links !== 1 || (manifestMetadata.mode & 63) !== 0 || manifestMetadata.size > 16 * 1024)) {
|
|
3795
|
+
throw new Error("bootstrap bundle manifest must be a root-owned owner-only regular file");
|
|
3796
|
+
}
|
|
3797
|
+
if (bundleMetadata && (!bundleMetadata.regular || bundleMetadata.symbolic || bundleMetadata.uid !== 0 || bundleMetadata.links !== 1 || (bundleMetadata.mode & 63) !== 0 || bundleMetadata.size < 1 || bundleMetadata.size > 512 * 1024 * 1024)) {
|
|
3798
|
+
throw new Error("bootstrap bundle must be a root-owned owner-only regular file within 512 MiB");
|
|
3799
|
+
}
|
|
3800
|
+
if (!host.exists(config.bootstrapBundle.bundleFile) || !host.exists(config.bootstrapBundle.manifestFile)) {
|
|
3801
|
+
throw new Error("attended bootstrap bundle and manifest are required for release generation one");
|
|
3802
|
+
}
|
|
3803
|
+
let parsed;
|
|
3804
|
+
try {
|
|
3805
|
+
parsed = JSON.parse(host.read(config.bootstrapBundle.manifestFile));
|
|
3806
|
+
} catch {
|
|
3807
|
+
throw new Error("bootstrap bundle manifest is not valid JSON");
|
|
3808
|
+
}
|
|
3809
|
+
const manifest = parseBootstrapBundleManifest(parsed);
|
|
3810
|
+
const expectedBranch = config.environment === "production" ? "main" : "dev";
|
|
3811
|
+
if (manifest.branch !== expectedBranch || bundleMetadata && bundleMetadata.size !== manifest.bytes) {
|
|
3812
|
+
throw new Error("bootstrap bundle manifest disagrees with the selected platform environment");
|
|
3813
|
+
}
|
|
3814
|
+
const digest = (await checked3(host, ["/usr/bin/sha256sum", config.bootstrapBundle.bundleFile], "bootstrap bundle digest")).trim().split(/\s+/)[0];
|
|
3815
|
+
if (digest !== manifest.sha256)
|
|
3816
|
+
throw new Error("bootstrap bundle digest does not match its manifest");
|
|
3817
|
+
if (verifyGit) {
|
|
3818
|
+
await checked3(host, ["/usr/bin/git", "bundle", "verify", config.bootstrapBundle.bundleFile], "bootstrap Git bundle verification");
|
|
3819
|
+
}
|
|
3820
|
+
return manifest;
|
|
3821
|
+
}
|
|
3625
3822
|
function bootstrapIdentity(config) {
|
|
3626
3823
|
if (config.kind === "enrolled-compute")
|
|
3627
3824
|
return {
|
|
@@ -3648,8 +3845,7 @@ function bootstrapIdentity(config) {
|
|
|
3648
3845
|
computeReference: config.computeReference,
|
|
3649
3846
|
nodeHostname: config.nodeHostname,
|
|
3650
3847
|
apiUrl: config.apiUrl,
|
|
3651
|
-
|
|
3652
|
-
branch: config.branch,
|
|
3848
|
+
bootstrapBundle: config.bootstrapBundle,
|
|
3653
3849
|
deployRoot: config.deployRoot ?? "/opt/forgezero",
|
|
3654
3850
|
telemetryEndpoint: config.telemetryEndpoint,
|
|
3655
3851
|
database: {
|
|
@@ -3679,7 +3875,7 @@ function bootstrapIdentity(config) {
|
|
|
3679
3875
|
};
|
|
3680
3876
|
}
|
|
3681
3877
|
function bootstrapIdentityDigest(config) {
|
|
3682
|
-
return
|
|
3878
|
+
return createHash2("sha256").update(JSON.stringify(bootstrapIdentity(config))).digest("hex");
|
|
3683
3879
|
}
|
|
3684
3880
|
function parseStoredState(raw) {
|
|
3685
3881
|
let value;
|
|
@@ -3729,34 +3925,6 @@ function bindBootstrapIntent(host, config) {
|
|
|
3729
3925
|
}
|
|
3730
3926
|
return identityDigest;
|
|
3731
3927
|
}
|
|
3732
|
-
async function preparePlatformBootstrap(input, host = localBootstrapHost()) {
|
|
3733
|
-
const config = validateBootstrapConfig(structuredClone(input));
|
|
3734
|
-
if (config.kind !== "platform")
|
|
3735
|
-
throw new Error("platform preparation requires a platform bootstrap config");
|
|
3736
|
-
if (host.uid() !== 0)
|
|
3737
|
-
throw new Error("fz bootstrap platform prepare --apply must run as root");
|
|
3738
|
-
if (host.exists(STATE_PATH)) {
|
|
3739
|
-
const installed = parseStoredState(host.read(STATE_PATH));
|
|
3740
|
-
if (installed.kind !== "platform" || installed.identityDigest !== bootstrapIdentityDigest(config)) {
|
|
3741
|
-
throw new Error("platform preparation coordinates do not match the installed host identity");
|
|
3742
|
-
}
|
|
3743
|
-
}
|
|
3744
|
-
const identityDigest = bindBootstrapIntent(host, config);
|
|
3745
|
-
await host.installAgent(config);
|
|
3746
|
-
if (!host.exists(GIT_PUBLIC_KEY))
|
|
3747
|
-
throw new Error("Agent installation did not produce its public Git deploy key");
|
|
3748
|
-
const gitPublicKey = host.read(GIT_PUBLIC_KEY).trim();
|
|
3749
|
-
if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(gitPublicKey)) {
|
|
3750
|
-
throw new Error("Agent public Git deploy key is malformed");
|
|
3751
|
-
}
|
|
3752
|
-
return {
|
|
3753
|
-
kind: "platform",
|
|
3754
|
-
prepared: true,
|
|
3755
|
-
identityDigest,
|
|
3756
|
-
gitPublicKey,
|
|
3757
|
-
next: "register this read-only deploy key, then run --apply concurrently on all three genesis Agency members"
|
|
3758
|
-
};
|
|
3759
|
-
}
|
|
3760
3928
|
function stateFor(config, cloudflare, previousCloudflareTunnelId) {
|
|
3761
3929
|
return `${JSON.stringify({
|
|
3762
3930
|
format: 2,
|
|
@@ -3910,6 +4078,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
3910
4078
|
let installed;
|
|
3911
4079
|
if (host.exists(STATE_PATH))
|
|
3912
4080
|
installed = parseStoredState(host.read(STATE_PATH));
|
|
4081
|
+
const bootstrapManifest = config.kind === "platform" && !host.exists(BOOTSTRAP_RELEASE_EVIDENCE) ? await verifyBootstrapBundleOnHost(host, config) : undefined;
|
|
3913
4082
|
let cloudflare;
|
|
3914
4083
|
if (config.cloudflareHandoff) {
|
|
3915
4084
|
if (host.exists(config.cloudflareHandoff.handoffFile)) {
|
|
@@ -3967,14 +4136,14 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
3967
4136
|
const platformPrivate = config.kind === "platform" ? (() => {
|
|
3968
4137
|
if (!secrets)
|
|
3969
4138
|
throw new Error("platform apply requires attended credentials on stdin");
|
|
3970
|
-
const
|
|
4139
|
+
const checked4 = validatePlatformBootstrapSecrets(config, secrets);
|
|
3971
4140
|
return {
|
|
3972
|
-
root:
|
|
3973
|
-
email:
|
|
3974
|
-
enrolmentToken:
|
|
3975
|
-
backup:
|
|
3976
|
-
cloudflareTunnelToken:
|
|
3977
|
-
cloudflareApiToken:
|
|
4141
|
+
root: checked4.clusterBootstrapCode,
|
|
4142
|
+
email: checked4.emailSecret,
|
|
4143
|
+
enrolmentToken: checked4.enrolmentToken,
|
|
4144
|
+
backup: checked4.backupS3Secret,
|
|
4145
|
+
cloudflareTunnelToken: checked4.cloudflareTunnelToken,
|
|
4146
|
+
cloudflareApiToken: checked4.cloudflareApiToken
|
|
3978
4147
|
};
|
|
3979
4148
|
})() : undefined;
|
|
3980
4149
|
const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
|
|
@@ -4013,24 +4182,35 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4013
4182
|
if (connectorCapabilities?.meshConnectorToken && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
|
|
4014
4183
|
await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, connectorCapabilities.meshConnectorToken);
|
|
4015
4184
|
}
|
|
4016
|
-
|
|
4185
|
+
if (alreadyEnrolled) {
|
|
4186
|
+
await host.installAgent(config, "bound");
|
|
4187
|
+
} else if (config.kind === "platform") {
|
|
4188
|
+
if (bootstrapManifest)
|
|
4189
|
+
await host.installAgent(config, "bootstrap");
|
|
4190
|
+
} else {
|
|
4191
|
+
await host.installAgent(config, "enrol");
|
|
4192
|
+
await host.installAgent(config, "bound");
|
|
4193
|
+
}
|
|
4017
4194
|
if (config.kind === "platform" && config.firewall.enabled) {
|
|
4018
4195
|
await host.ensureSoftware(plan.software.filter(({ id }) => id === "ufw"));
|
|
4019
4196
|
} else
|
|
4020
4197
|
await host.ensureSoftware(plan.software);
|
|
4021
4198
|
if (config.kind === "platform" && config.firewall.enabled) {
|
|
4022
|
-
await
|
|
4023
|
-
await
|
|
4024
|
-
await
|
|
4199
|
+
await checked3(host, ["ufw", "--force", "default", "deny", "incoming"], "firewall inbound policy");
|
|
4200
|
+
await checked3(host, ["ufw", "--force", "default", "allow", "outgoing"], "firewall outbound policy");
|
|
4201
|
+
await checked3(host, ["ufw", "allow", `${config.firewall.sshPort}/tcp`], "firewall SSH rule");
|
|
4025
4202
|
for (const cidr of config.firewall.privateCidrs) {
|
|
4026
4203
|
if (config.database.role !== "none")
|
|
4027
|
-
await
|
|
4204
|
+
await checked3(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
|
|
4028
4205
|
for (const port of [config.runtime.bluePort, config.runtime.greenPort])
|
|
4029
|
-
await
|
|
4206
|
+
await checked3(host, ["ufw", "allow", "from", cidr, "to", "any", "port", String(port), "proto", "tcp"], "seed-mesh firewall rule");
|
|
4030
4207
|
}
|
|
4031
|
-
await
|
|
4208
|
+
await checked3(host, ["ufw", "--force", "enable"], "firewall activation");
|
|
4032
4209
|
await host.ensureSoftware(plan.software.filter(({ id }) => id !== "ufw"));
|
|
4033
4210
|
}
|
|
4211
|
+
if (config.kind === "platform" && bootstrapManifest) {
|
|
4212
|
+
await verifyBootstrapBundleOnHost(host, config, true);
|
|
4213
|
+
}
|
|
4034
4214
|
if (config.kind === "platform") {
|
|
4035
4215
|
const root = platformPrivate.root;
|
|
4036
4216
|
if (!host.exists(JWT_CREDENTIAL)) {
|
|
@@ -4085,8 +4265,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4085
4265
|
keepReleases: runtime.keepReleases,
|
|
4086
4266
|
drainDeadlineMs: runtime.environment.drainDeadlineMs
|
|
4087
4267
|
});
|
|
4088
|
-
await
|
|
4089
|
-
await
|
|
4268
|
+
await checked3(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
|
|
4269
|
+
await checked3(host, ["id", runtime.serviceUser], "existing API service account");
|
|
4090
4270
|
});
|
|
4091
4271
|
host.mkdir(runtime.environment.sharedDirectory, 488);
|
|
4092
4272
|
host.mkdir(runtime.slotsDirectory, 493);
|
|
@@ -4095,7 +4275,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4095
4275
|
host.write("/etc/forgezero/capacity.env", `FZ_CONCURRENCY_LIMIT=${runtime.environment.concurrencyLimit}
|
|
4096
4276
|
`, 420);
|
|
4097
4277
|
}
|
|
4098
|
-
await
|
|
4278
|
+
await checked3(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
|
|
4099
4279
|
host.write("/etc/systemd/system/forgezero@.service", units.template, 420);
|
|
4100
4280
|
host.write("/etc/systemd/system/forgezero@blue.service.d/port.conf", units.dropIns.blue, 420);
|
|
4101
4281
|
host.write("/etc/systemd/system/forgezero@green.service.d/port.conf", units.dropIns.green, 420);
|
|
@@ -4104,30 +4284,30 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4104
4284
|
host.write("/etc/forgezero/deploy.env", activation.environment, 420);
|
|
4105
4285
|
host.write("/etc/forgezero/deploy-activation.json", activation.helper, 384);
|
|
4106
4286
|
host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
|
|
4107
|
-
await
|
|
4287
|
+
await checked3(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
|
|
4108
4288
|
const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
|
|
4109
|
-
await
|
|
4110
|
-
const otlpStatus = (await
|
|
4289
|
+
await checked3(host, telemetry.unitCheck.argv, "OTLP collector supervision");
|
|
4290
|
+
const otlpStatus = (await checked3(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
|
|
4111
4291
|
if (!/^2\d\d$/.test(otlpStatus))
|
|
4112
4292
|
throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
|
|
4113
|
-
await
|
|
4114
|
-
await
|
|
4115
|
-
await
|
|
4293
|
+
await checked3(host, ["nginx", "-t"], "nginx configuration");
|
|
4294
|
+
await checked3(host, ["systemctl", "daemon-reload"], "systemd reload");
|
|
4295
|
+
await checked3(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
|
|
4116
4296
|
if (config.database.role === "master") {
|
|
4117
4297
|
const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
4118
4298
|
if (!host.exists(invite)) {
|
|
4119
|
-
host.write(invite, `plt_${
|
|
4299
|
+
host.write(invite, `plt_${randomBytes3(24).toString("hex")}
|
|
4120
4300
|
`, 384);
|
|
4121
|
-
await
|
|
4301
|
+
await checked3(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
|
|
4122
4302
|
}
|
|
4123
4303
|
}
|
|
4124
4304
|
if (config.database.role !== "none") {
|
|
4125
4305
|
host.mkdir("/var/lib/forgezero-cluster", 448);
|
|
4126
|
-
await
|
|
4306
|
+
await checked3(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
|
|
4127
4307
|
host.write("/etc/systemd/system/forgezero-db.service", databaseUnit(config), 420);
|
|
4128
4308
|
host.write("/etc/systemd/system/forgezero-db-verify.service", databaseVerifyUnit(config), 420);
|
|
4129
|
-
await
|
|
4130
|
-
await
|
|
4309
|
+
await checked3(host, ["systemctl", "daemon-reload"], "database unit reload");
|
|
4310
|
+
await checked3(host, ["systemctl", "enable", "--now", "forgezero-db.service", "forgezero-db-verify.service"], "database supervision");
|
|
4131
4311
|
const evidence = {
|
|
4132
4312
|
expectedMode: "default",
|
|
4133
4313
|
role: "COORDINATOR",
|
|
@@ -4138,15 +4318,42 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4138
4318
|
host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
|
|
4139
4319
|
`, 384);
|
|
4140
4320
|
}
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4321
|
+
if (bootstrapManifest) {
|
|
4322
|
+
await checked3(host, [
|
|
4323
|
+
"runuser",
|
|
4324
|
+
"-u",
|
|
4325
|
+
"forgezero-agent",
|
|
4326
|
+
"--",
|
|
4327
|
+
"/usr/local/bin/fz-agent",
|
|
4328
|
+
"deploy",
|
|
4329
|
+
`--revision=${bootstrapManifest.revision}`,
|
|
4330
|
+
...config.database.role === "master" ? ["--release-executor"] : []
|
|
4331
|
+
], "initial Agent deployment");
|
|
4332
|
+
host.write(BOOTSTRAP_RELEASE_EVIDENCE, `${JSON.stringify({
|
|
4333
|
+
format: 1,
|
|
4334
|
+
kind: "forgezero-bootstrap-release",
|
|
4335
|
+
revision: bootstrapManifest.revision,
|
|
4336
|
+
sha256: bootstrapManifest.sha256,
|
|
4337
|
+
branch: bootstrapManifest.branch,
|
|
4338
|
+
deployedAt: new Date().toISOString()
|
|
4339
|
+
}, null, 2)}
|
|
4340
|
+
`, 384);
|
|
4341
|
+
host.remove(config.bootstrapBundle.bundleFile);
|
|
4342
|
+
host.remove(config.bootstrapBundle.manifestFile);
|
|
4343
|
+
}
|
|
4344
|
+
if (!alreadyEnrolled) {
|
|
4345
|
+
await checked3(host, [
|
|
4346
|
+
"curl",
|
|
4347
|
+
"--fail",
|
|
4348
|
+
"--silent",
|
|
4349
|
+
"--show-error",
|
|
4350
|
+
"--max-time",
|
|
4351
|
+
"10",
|
|
4352
|
+
`http://127.0.0.1:3000${runtime.healthPath}`
|
|
4353
|
+
], "release-one API health");
|
|
4354
|
+
await host.installAgent(config, "enrol");
|
|
4355
|
+
await host.installAgent(config, "bound");
|
|
4356
|
+
}
|
|
4150
4357
|
}
|
|
4151
4358
|
if (config.cloudflareHandoff) {
|
|
4152
4359
|
if (!host.exists(TUNNEL_CREDENTIAL) && connectorCapabilities) {
|
|
@@ -4155,8 +4362,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4155
4362
|
if (!host.exists(TUNNEL_CREDENTIAL))
|
|
4156
4363
|
throw new Error("sealed cloudflared connector credential is missing");
|
|
4157
4364
|
host.write("/etc/systemd/system/cloudflared.service", tunnelUnit(), 420);
|
|
4158
|
-
await
|
|
4159
|
-
await
|
|
4365
|
+
await checked3(host, ["systemctl", "daemon-reload"], "cloudflared unit reload");
|
|
4366
|
+
await checked3(host, ["systemctl", "enable", "--now", "cloudflared.service"], "cloudflared connector supervision");
|
|
4160
4367
|
const tunnelId = cloudflare?.tunnelId ?? installed?.cloudflareTunnelId ?? (config.kind === "platform" ? config.runtime.environment.cloudflare?.tunnelId : undefined);
|
|
4161
4368
|
if (!tunnelId)
|
|
4162
4369
|
throw new Error("Cloudflare tunnel identity is missing after handoff validation");
|
|
@@ -4166,8 +4373,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4166
4373
|
if (!host.exists(WARP_CONNECTOR_CREDENTIAL))
|
|
4167
4374
|
throw new Error("sealed Cloudflare Mesh connector credential is missing");
|
|
4168
4375
|
host.write("/etc/systemd/system/forgezero-mesh-config.service", meshConnectorUnit(), 420);
|
|
4169
|
-
await
|
|
4170
|
-
await
|
|
4376
|
+
await checked3(host, ["systemctl", "daemon-reload"], "Cloudflare Mesh unit reload");
|
|
4377
|
+
await checked3(host, ["systemctl", "enable", "--now", "warp-svc.service", "forgezero-mesh-config.service"], "Cloudflare Mesh connector supervision");
|
|
4171
4378
|
}
|
|
4172
4379
|
host.write(STATE_PATH, stateFor(config, cloudflare, installed?.cloudflareTunnelId), 384);
|
|
4173
4380
|
const status = await bootstrapStatus(host);
|
|
@@ -4212,8 +4419,7 @@ function strictBootstrapDocument(value) {
|
|
|
4212
4419
|
"computeReference",
|
|
4213
4420
|
"nodeHostname",
|
|
4214
4421
|
"apiUrl",
|
|
4215
|
-
"
|
|
4216
|
-
"branch",
|
|
4422
|
+
"bootstrapBundle",
|
|
4217
4423
|
"deployRoot",
|
|
4218
4424
|
"telemetryEndpoint",
|
|
4219
4425
|
"database",
|
|
@@ -4226,9 +4432,11 @@ function strictBootstrapDocument(value) {
|
|
|
4226
4432
|
"realm",
|
|
4227
4433
|
"software",
|
|
4228
4434
|
"deploymentCredentials",
|
|
4435
|
+
"gitDeployKey",
|
|
4229
4436
|
"bootstrapRunner"
|
|
4230
4437
|
], "bootstrap config");
|
|
4231
4438
|
if (root.kind === "platform") {
|
|
4439
|
+
exactKeys(root.bootstrapBundle, ["bundleFile", "manifestFile"], "bootstrap bundle config");
|
|
4232
4440
|
exactKeys(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
|
|
4233
4441
|
if (root.cloudflareHandoff !== undefined)
|
|
4234
4442
|
exactKeys(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
|
|
@@ -4270,8 +4478,6 @@ function strictBootstrapDocument(value) {
|
|
|
4270
4478
|
"agentOtlpEndpoint",
|
|
4271
4479
|
"custodianEmail",
|
|
4272
4480
|
"email",
|
|
4273
|
-
"repository",
|
|
4274
|
-
"branch",
|
|
4275
4481
|
"deployProfile",
|
|
4276
4482
|
"otlpFlushIntervalMs",
|
|
4277
4483
|
"otlpTraceSampleRatio",
|
|
@@ -4309,11 +4515,70 @@ function strictBootstrapDocument(value) {
|
|
|
4309
4515
|
return value;
|
|
4310
4516
|
}
|
|
4311
4517
|
function readBootstrapConfig(path) {
|
|
4312
|
-
const metadata =
|
|
4518
|
+
const metadata = lstatSync3(path);
|
|
4313
4519
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== (process.getuid?.() ?? metadata.uid) || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 64 * 1024) {
|
|
4314
4520
|
throw new Error("bootstrap config must be an owner-only regular file with one link and at most 64 KiB");
|
|
4315
4521
|
}
|
|
4316
|
-
return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(
|
|
4522
|
+
return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync5(path, "utf8"))));
|
|
4523
|
+
}
|
|
4524
|
+
function planBootstrapAgentInstall(config, phase, context) {
|
|
4525
|
+
const { capabilities, hasBinding, hasEnrolCredential, initialBundle } = context;
|
|
4526
|
+
if (phase === "bootstrap") {
|
|
4527
|
+
if (config.kind !== "platform" || hasBinding || !initialBundle) {
|
|
4528
|
+
throw new Error("release-one Agent install requires an unbound platform host and verified bootstrap bundle");
|
|
4529
|
+
}
|
|
4530
|
+
} else if (phase === "enrol") {
|
|
4531
|
+
if (hasBinding || !hasEnrolCredential) {
|
|
4532
|
+
throw new Error("Agent enrolment requires one unbound host and its sealed one-use credential");
|
|
4533
|
+
}
|
|
4534
|
+
} else if (!hasBinding) {
|
|
4535
|
+
throw new Error("bound Agent convergence requires durable enrolment state");
|
|
4536
|
+
}
|
|
4537
|
+
const bound = phase === "bound";
|
|
4538
|
+
const enrolling = phase === "enrol";
|
|
4539
|
+
const authenticated = enrolling || bound;
|
|
4540
|
+
const bootstrapRunner = platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner);
|
|
4541
|
+
const options = {
|
|
4542
|
+
capabilities,
|
|
4543
|
+
socketPath: DEFAULT_SOCKET2,
|
|
4544
|
+
seedPath: "/var/lib/forgezero/node.seed",
|
|
4545
|
+
controlSocketPath: CONTROL_SOCKET,
|
|
4546
|
+
repository: phase === "bootstrap" ? initialBundle.path : bound && config.kind === "enrolled-compute" ? config.repository : undefined,
|
|
4547
|
+
branch: phase === "bootstrap" ? initialBundle.manifest.branch : bound && config.kind === "enrolled-compute" ? config.branch : undefined,
|
|
4548
|
+
bootstrapBundlePath: phase === "bootstrap" ? initialBundle.path : undefined,
|
|
4549
|
+
bootstrapBundleManifestPath: phase === "bootstrap" ? initialBundle.manifestPath : undefined,
|
|
4550
|
+
profile: config.profile,
|
|
4551
|
+
deployRoot: config.deployRoot ?? "/opt/forgezero",
|
|
4552
|
+
deploymentCredentials: config.deploymentCredentials,
|
|
4553
|
+
publicApiUrl: config.apiUrl,
|
|
4554
|
+
gitCredentialPath: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/creds/git-deploy-key.cred" : undefined,
|
|
4555
|
+
gitPublicKeyPath: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/git/deploy.pub" : undefined,
|
|
4556
|
+
generateGitIdentity: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey === true,
|
|
4557
|
+
pullDeployments: bound,
|
|
4558
|
+
pullMigrations: bound && config.kind === "platform",
|
|
4559
|
+
pullBootstrap: bound && bootstrapRunner,
|
|
4560
|
+
bootstrapSshCredentialPath: authenticated && bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
|
|
4561
|
+
bootstrapSshSourcePath: enrolling && config.kind === "enrolled-compute" ? config.bootstrapRunner?.sshPrivateKeyFile : undefined,
|
|
4562
|
+
bootstrapSshPublicKeyPath: authenticated && bootstrapRunner ? BOOTSTRAP_SSH_PUBLIC_KEY : undefined,
|
|
4563
|
+
bootstrapTargetTelemetryEndpoint: bound && bootstrapRunner ? platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined : undefined,
|
|
4564
|
+
lifecycleProfilePath: bound && config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
|
|
4565
|
+
enforceEgress: true,
|
|
4566
|
+
nodeHostname: config.nodeHostname,
|
|
4567
|
+
telemetryEndpoint: config.telemetryEndpoint,
|
|
4568
|
+
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
4569
|
+
sourceBinPath: PACKAGED_AGENT_BIN,
|
|
4570
|
+
...enrolling ? {
|
|
4571
|
+
enrolTokenCredentialPath: ENROL_CREDENTIAL,
|
|
4572
|
+
enrolStatePath: "/var/lib/forgezero/enrolment.json"
|
|
4573
|
+
} : bound ? { enrolStatePath: "/var/lib/forgezero/enrolment.json" } : {},
|
|
4574
|
+
...authenticated ? {
|
|
4575
|
+
apiUrl: config.apiUrl,
|
|
4576
|
+
project: config.kind === "enrolled-compute" ? config.realm : "platform",
|
|
4577
|
+
environment: config.kind === "enrolled-compute" ? undefined : config.environment,
|
|
4578
|
+
nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
|
|
4579
|
+
} : {}
|
|
4580
|
+
};
|
|
4581
|
+
return planInstall(options);
|
|
4317
4582
|
}
|
|
4318
4583
|
function localBootstrapHost() {
|
|
4319
4584
|
const execute = async (argv, options = {}) => {
|
|
@@ -4331,19 +4596,19 @@ function localBootstrapHost() {
|
|
|
4331
4596
|
};
|
|
4332
4597
|
return {
|
|
4333
4598
|
uid: () => process.getuid?.() ?? -1,
|
|
4334
|
-
exists:
|
|
4335
|
-
read: (path) =>
|
|
4599
|
+
exists: existsSync5,
|
|
4600
|
+
read: (path) => readFileSync5(path, "utf8"),
|
|
4336
4601
|
write(path, content, mode) {
|
|
4337
|
-
|
|
4602
|
+
mkdirSync5(dirname6(path), { recursive: true, mode: 493 });
|
|
4338
4603
|
const temporary = `${path}.next.${process.pid}`;
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4604
|
+
writeFileSync5(temporary, content, { mode });
|
|
4605
|
+
chmodSync3(temporary, mode);
|
|
4606
|
+
renameSync5(temporary, path);
|
|
4342
4607
|
},
|
|
4343
|
-
mkdir: (path, mode) =>
|
|
4344
|
-
remove: (path) =>
|
|
4608
|
+
mkdir: (path, mode) => mkdirSync5(path, { recursive: true, mode }),
|
|
4609
|
+
remove: (path) => rmSync5(path, { force: true }),
|
|
4345
4610
|
inspect(path) {
|
|
4346
|
-
const value =
|
|
4611
|
+
const value = lstatSync3(path);
|
|
4347
4612
|
return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
|
|
4348
4613
|
},
|
|
4349
4614
|
exec: execute,
|
|
@@ -4362,10 +4627,15 @@ function localBootstrapHost() {
|
|
|
4362
4627
|
throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
|
|
4363
4628
|
return result;
|
|
4364
4629
|
},
|
|
4365
|
-
async installAgent(config) {
|
|
4630
|
+
async installAgent(config, phase) {
|
|
4366
4631
|
const capabilities = await readCapabilities(localRunner);
|
|
4367
|
-
const
|
|
4368
|
-
const
|
|
4632
|
+
const hasBinding = existsSync5("/var/lib/forgezero/enrolment.json");
|
|
4633
|
+
const hasEnrolCredential = existsSync5(ENROL_CREDENTIAL);
|
|
4634
|
+
const initialBundle = config.kind === "platform" && !existsSync5(BOOTSTRAP_RELEASE_EVIDENCE) ? {
|
|
4635
|
+
path: config.bootstrapBundle.bundleFile,
|
|
4636
|
+
manifestPath: config.bootstrapBundle.manifestFile,
|
|
4637
|
+
manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync5(config.bootstrapBundle.manifestFile, "utf8")))
|
|
4638
|
+
} : undefined;
|
|
4369
4639
|
if (config.kind === "platform") {
|
|
4370
4640
|
const lifecycle = config.database.role === "none" ? {
|
|
4371
4641
|
apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
|
|
@@ -4377,49 +4647,19 @@ function localBootstrapHost() {
|
|
|
4377
4647
|
databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
|
|
4378
4648
|
databasePorts: [8529]
|
|
4379
4649
|
};
|
|
4380
|
-
|
|
4381
|
-
|
|
4650
|
+
mkdirSync5(dirname6(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
|
|
4651
|
+
writeFileSync5(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
|
|
4382
4652
|
`, { mode: 256 });
|
|
4383
4653
|
}
|
|
4384
|
-
const plan =
|
|
4654
|
+
const plan = planBootstrapAgentInstall(config, phase, {
|
|
4385
4655
|
capabilities,
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
repository: config.repository,
|
|
4390
|
-
branch: config.branch,
|
|
4391
|
-
profile: config.kind === "platform" ? config.profile : config.profile,
|
|
4392
|
-
deployRoot,
|
|
4393
|
-
deploymentCredentials: config.deploymentCredentials,
|
|
4394
|
-
publicApiUrl: config.apiUrl,
|
|
4395
|
-
gitCredentialPath: "/etc/forgezero/creds/git-deploy-key.cred",
|
|
4396
|
-
gitPublicKeyPath: "/etc/forgezero/git/deploy.pub",
|
|
4397
|
-
generateGitIdentity: true,
|
|
4398
|
-
pullDeployments: hasBinding,
|
|
4399
|
-
pullMigrations: config.kind === "platform" && hasBinding,
|
|
4400
|
-
pullBootstrap: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner),
|
|
4401
|
-
bootstrapSshCredentialPath: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && config.bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
|
|
4402
|
-
bootstrapSshSourcePath: config.kind === "enrolled-compute" ? config.bootstrapRunner?.sshPrivateKeyFile : undefined,
|
|
4403
|
-
bootstrapSshPublicKeyPath: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && config.bootstrapRunner ? BOOTSTRAP_SSH_PUBLIC_KEY : undefined,
|
|
4404
|
-
bootstrapTargetTelemetryEndpoint: platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined,
|
|
4405
|
-
lifecycleProfilePath: config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
|
|
4406
|
-
enforceEgress: true,
|
|
4407
|
-
nodeHostname: config.nodeHostname,
|
|
4408
|
-
telemetryEndpoint: config.telemetryEndpoint,
|
|
4409
|
-
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
4410
|
-
sourceBinPath: PACKAGED_AGENT_BIN,
|
|
4411
|
-
...hasBinding ? {
|
|
4412
|
-
enrolTokenCredentialPath: ENROL_CREDENTIAL,
|
|
4413
|
-
enrolStatePath: "/var/lib/forgezero/enrolment.json",
|
|
4414
|
-
apiUrl: config.apiUrl,
|
|
4415
|
-
project: config.kind === "enrolled-compute" ? config.realm : "platform",
|
|
4416
|
-
environment: config.kind === "enrolled-compute" ? undefined : config.environment,
|
|
4417
|
-
nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
|
|
4418
|
-
} : {}
|
|
4656
|
+
hasBinding,
|
|
4657
|
+
hasEnrolCredential,
|
|
4658
|
+
initialBundle
|
|
4419
4659
|
});
|
|
4420
4660
|
for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
|
|
4421
|
-
|
|
4422
|
-
|
|
4661
|
+
mkdirSync5(dirname6(unit.path), { recursive: true, mode: 493 });
|
|
4662
|
+
writeFileSync5(unit.path, unit.unit, { mode: 420 });
|
|
4423
4663
|
}
|
|
4424
4664
|
await applyPlan(plan, localRunner);
|
|
4425
4665
|
return plan;
|
|
@@ -4428,31 +4668,31 @@ function localBootstrapHost() {
|
|
|
4428
4668
|
}
|
|
4429
4669
|
|
|
4430
4670
|
// src/metal-bootstrap.ts
|
|
4431
|
-
import { createHash as
|
|
4671
|
+
import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
|
|
4432
4672
|
import {
|
|
4433
|
-
chmodSync as
|
|
4673
|
+
chmodSync as chmodSync4,
|
|
4434
4674
|
chownSync,
|
|
4435
4675
|
copyFileSync as copyFileSync2,
|
|
4436
|
-
existsSync as
|
|
4437
|
-
lstatSync as
|
|
4438
|
-
mkdirSync as
|
|
4439
|
-
readFileSync as
|
|
4676
|
+
existsSync as existsSync6,
|
|
4677
|
+
lstatSync as lstatSync4,
|
|
4678
|
+
mkdirSync as mkdirSync7,
|
|
4679
|
+
readFileSync as readFileSync6,
|
|
4440
4680
|
realpathSync as realpathSync4,
|
|
4441
|
-
renameSync as
|
|
4681
|
+
renameSync as renameSync6,
|
|
4442
4682
|
statSync,
|
|
4443
4683
|
symlinkSync as symlinkSync2,
|
|
4444
4684
|
unlinkSync,
|
|
4445
|
-
writeFileSync as
|
|
4685
|
+
writeFileSync as writeFileSync7
|
|
4446
4686
|
} from "fs";
|
|
4447
|
-
import { dirname as
|
|
4687
|
+
import { dirname as dirname8, isAbsolute as isAbsolute3, join as join5, resolve as resolve5 } from "path";
|
|
4448
4688
|
import { isIP as isIP5 } from "net";
|
|
4449
4689
|
|
|
4450
4690
|
// src/metal-isolation.ts
|
|
4451
|
-
import { mkdirSync as
|
|
4691
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
4452
4692
|
import { join as join4 } from "path";
|
|
4453
4693
|
|
|
4454
4694
|
// src/metal-provision.ts
|
|
4455
|
-
import { dirname as
|
|
4695
|
+
import { dirname as dirname7, isAbsolute as isAbsolute2, join as join3 } from "path";
|
|
4456
4696
|
import { isIP as isIP4 } from "net";
|
|
4457
4697
|
|
|
4458
4698
|
// src/ubuntu.ts
|
|
@@ -4501,7 +4741,7 @@ function validateMetalProfile(profile) {
|
|
|
4501
4741
|
if (!Number.isInteger(profile.addressStart) || !Number.isInteger(profile.addressEnd) || profile.addressStart < 2 || profile.addressEnd > 254 || profile.addressStart > profile.addressEnd)
|
|
4502
4742
|
throw new MetalProvisionError("invalid guest address range");
|
|
4503
4743
|
for (const path of [profile.stateDir, profile.seedDir, profile.unitDir]) {
|
|
4504
|
-
if (!
|
|
4744
|
+
if (!isAbsolute2(path))
|
|
4505
4745
|
throw new MetalProvisionError("metal paths must be absolute");
|
|
4506
4746
|
}
|
|
4507
4747
|
new URL(profile.apiUrl);
|
|
@@ -4609,14 +4849,14 @@ var defaultExec = async (argv) => {
|
|
|
4609
4849
|
]);
|
|
4610
4850
|
return { exitCode, stdout, stderr };
|
|
4611
4851
|
};
|
|
4612
|
-
var
|
|
4852
|
+
var checked4 = async (exec, argv) => {
|
|
4613
4853
|
const result = await exec(argv);
|
|
4614
4854
|
if (result.exitCode !== 0)
|
|
4615
4855
|
throw new Error(`${argv[0]} failed: ${(result.stderr || result.stdout).trim()}`);
|
|
4616
4856
|
return result;
|
|
4617
4857
|
};
|
|
4618
4858
|
var requireGuestsInSlice = async (exec) => {
|
|
4619
|
-
const active = await
|
|
4859
|
+
const active = await checked4(exec, [
|
|
4620
4860
|
"systemctl",
|
|
4621
4861
|
"list-units",
|
|
4622
4862
|
"--type=service",
|
|
@@ -4630,7 +4870,7 @@ var requireGuestsInSlice = async (exec) => {
|
|
|
4630
4870
|
const service = line.trim().split(/\s+/)[0];
|
|
4631
4871
|
if (!service)
|
|
4632
4872
|
continue;
|
|
4633
|
-
const cgroup = await
|
|
4873
|
+
const cgroup = await checked4(exec, ["systemctl", "show", "-p", "ControlGroup", "--value", service]);
|
|
4634
4874
|
if (!cgroup.stdout.trim().includes("/forgezero-guests.slice/")) {
|
|
4635
4875
|
throw new Error(`${service} must be drained and restarted into forgezero-guests.slice`);
|
|
4636
4876
|
}
|
|
@@ -4640,23 +4880,23 @@ async function applyMetalIsolation(profile, exec = defaultExec) {
|
|
|
4640
4880
|
validateMetalProfile(profile);
|
|
4641
4881
|
await requireGuestsInSlice(exec);
|
|
4642
4882
|
const unitDir = profile.unitDir;
|
|
4643
|
-
|
|
4644
|
-
|
|
4883
|
+
mkdirSync6(unitDir, { recursive: true });
|
|
4884
|
+
writeFileSync6(join4(unitDir, "forgezero-guests.slice"), metalGuestSliceUnit(profile), { mode: 420 });
|
|
4645
4885
|
for (const unit of ["system.slice", "user.slice"]) {
|
|
4646
4886
|
const directory = join4(unitDir, `${unit}.d`);
|
|
4647
|
-
|
|
4648
|
-
|
|
4887
|
+
mkdirSync6(directory, { recursive: true });
|
|
4888
|
+
writeFileSync6(join4(directory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "slice"), { mode: 420 });
|
|
4649
4889
|
}
|
|
4650
4890
|
const initDirectory = join4(unitDir, "init.scope.d");
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
await
|
|
4891
|
+
mkdirSync6(initDirectory, { recursive: true });
|
|
4892
|
+
writeFileSync6(join4(initDirectory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "scope"), { mode: 420 });
|
|
4893
|
+
await checked4(exec, ["systemctl", "daemon-reload"]);
|
|
4654
4894
|
await requireGuestsInSlice(exec);
|
|
4655
4895
|
const properties = [`AllowedCPUs=${profile.housekeepingCpus}`];
|
|
4656
4896
|
if (profile.housekeepingMemoryNodes)
|
|
4657
4897
|
properties.push(`AllowedMemoryNodes=${profile.housekeepingMemoryNodes}`);
|
|
4658
4898
|
for (const unit of ["system.slice", "user.slice", "init.scope"]) {
|
|
4659
|
-
await
|
|
4899
|
+
await checked4(exec, ["systemctl", "set-property", "--runtime", unit, ...properties]);
|
|
4660
4900
|
}
|
|
4661
4901
|
}
|
|
4662
4902
|
|
|
@@ -4768,7 +5008,7 @@ function validateMetalBootstrapConfig(config) {
|
|
|
4768
5008
|
throw new MetalBootstrapError("metal bootstrap uses fixed state, seed, and systemd unit directories");
|
|
4769
5009
|
}
|
|
4770
5010
|
const image = config.profile.images[Object.keys(config.profile.images)[0]];
|
|
4771
|
-
if (!image.path.startsWith("/var/lib/forgezero/images/") ||
|
|
5011
|
+
if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve5(image.path) !== image.path || /[\0\r\n]/.test(image.path)) {
|
|
4772
5012
|
throw new MetalBootstrapError("the pinned guest image must use the fixed image directory");
|
|
4773
5013
|
}
|
|
4774
5014
|
if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION) {
|
|
@@ -4803,10 +5043,10 @@ function validateMetalBootstrapConfig(config) {
|
|
|
4803
5043
|
return config;
|
|
4804
5044
|
}
|
|
4805
5045
|
function validateOwnerOnlyPath(path, requireRootOwner) {
|
|
4806
|
-
if (!
|
|
5046
|
+
if (!isAbsolute3(path) || resolve5(path) !== path || path.includes("/../")) {
|
|
4807
5047
|
throw new MetalBootstrapError("private bootstrap paths must be canonical absolute paths");
|
|
4808
5048
|
}
|
|
4809
|
-
const metadata =
|
|
5049
|
+
const metadata = lstatSync4(path);
|
|
4810
5050
|
if (!metadata.isFile() || metadata.isSymbolicLink() || realpathSync4(path) !== path) {
|
|
4811
5051
|
throw new MetalBootstrapError("private bootstrap path must be a regular non-symlink file");
|
|
4812
5052
|
}
|
|
@@ -4824,7 +5064,7 @@ function readMetalBootstrapConfig(path) {
|
|
|
4824
5064
|
throw new MetalBootstrapError("metal bootstrap config size is invalid");
|
|
4825
5065
|
let parsed;
|
|
4826
5066
|
try {
|
|
4827
|
-
parsed = JSON.parse(
|
|
5067
|
+
parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
4828
5068
|
} catch {
|
|
4829
5069
|
throw new MetalBootstrapError("metal bootstrap config is not valid JSON");
|
|
4830
5070
|
}
|
|
@@ -4857,15 +5097,15 @@ function planMetalBootstrap(config) {
|
|
|
4857
5097
|
};
|
|
4858
5098
|
}
|
|
4859
5099
|
var atomicWrite = (path, body, mode) => {
|
|
4860
|
-
|
|
5100
|
+
mkdirSync7(dirname8(path), { recursive: true, mode: 493 });
|
|
4861
5101
|
const temporary = `${path}.next-${process.pid}`;
|
|
4862
|
-
|
|
4863
|
-
|
|
5102
|
+
writeFileSync7(temporary, body, { mode, flag: "wx" });
|
|
5103
|
+
chmodSync4(temporary, mode);
|
|
4864
5104
|
chownSync(temporary, 0, 0);
|
|
4865
|
-
|
|
5105
|
+
renameSync6(temporary, path);
|
|
4866
5106
|
};
|
|
4867
5107
|
var validateAgentSourcePath = (source) => {
|
|
4868
|
-
if (!
|
|
5108
|
+
if (!isAbsolute3(source) || !lstatSync4(source).isFile() || lstatSync4(source).isSymbolicLink()) {
|
|
4869
5109
|
throw new MetalBootstrapError("published Agent source path must be an absolute regular non-symlink file");
|
|
4870
5110
|
}
|
|
4871
5111
|
};
|
|
@@ -5029,11 +5269,11 @@ WantedBy=multi-user.target
|
|
|
5029
5269
|
var installAgentBinary = (source, version) => {
|
|
5030
5270
|
validateAgentSourcePath(source);
|
|
5031
5271
|
const release = `/opt/forgezero/agent/versions/${version}/dist`;
|
|
5032
|
-
|
|
5272
|
+
mkdirSync7(release, { recursive: true, mode: 493 });
|
|
5033
5273
|
copyFileSync2(source, join5(release, "fz-agent.js"));
|
|
5034
|
-
|
|
5274
|
+
chmodSync4(join5(release, "fz-agent.js"), 493);
|
|
5035
5275
|
chownSync(join5(release, "fz-agent.js"), 0, 0);
|
|
5036
|
-
|
|
5276
|
+
mkdirSync7("/opt/forgezero/agent", { recursive: true, mode: 493 });
|
|
5037
5277
|
for (const [link, target] of [
|
|
5038
5278
|
["/opt/forgezero/agent/current.next", `versions/${version}`],
|
|
5039
5279
|
[AGENT_PATH, "/opt/forgezero/agent/current/dist/fz-agent.js"]
|
|
@@ -5043,7 +5283,7 @@ var installAgentBinary = (source, version) => {
|
|
|
5043
5283
|
} catch {}
|
|
5044
5284
|
symlinkSync2(target, link);
|
|
5045
5285
|
if (link.endsWith("current.next"))
|
|
5046
|
-
|
|
5286
|
+
renameSync6(link, "/opt/forgezero/agent/current");
|
|
5047
5287
|
}
|
|
5048
5288
|
};
|
|
5049
5289
|
var preflight = async (config, exec) => {
|
|
@@ -5065,10 +5305,10 @@ var preflight = async (config, exec) => {
|
|
|
5065
5305
|
]);
|
|
5066
5306
|
await runChecked(exec, ["/usr/sbin/vgs", config.profile.volumeGroup]);
|
|
5067
5307
|
await runChecked(exec, ["/usr/sbin/ip", "link", "show", config.profile.bridge]);
|
|
5068
|
-
if (!
|
|
5308
|
+
if (!existsSync6("/dev/kvm"))
|
|
5069
5309
|
throw new MetalBootstrapError("/dev/kvm is required");
|
|
5070
5310
|
if (config.profile.confidential) {
|
|
5071
|
-
if (!
|
|
5311
|
+
if (!existsSync6("/dev/sev"))
|
|
5072
5312
|
throw new MetalBootstrapError("/dev/sev is required by the confidential profile");
|
|
5073
5313
|
await runChecked(exec, ["/usr/bin/qemu-system-x86_64", "-object", "sev-snp-guest,help"]);
|
|
5074
5314
|
}
|
|
@@ -5080,16 +5320,16 @@ var assertSupportedMetalHost = () => {
|
|
|
5080
5320
|
if (process.platform !== "linux" || process.arch !== "x64") {
|
|
5081
5321
|
throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
|
|
5082
5322
|
}
|
|
5083
|
-
const release =
|
|
5323
|
+
const release = readFileSync6("/etc/os-release", "utf8");
|
|
5084
5324
|
if (!/^ID=ubuntu$/m.test(release) || !/^VERSION_ID="?26\.04"?$/m.test(release)) {
|
|
5085
5325
|
throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
|
|
5086
5326
|
}
|
|
5087
5327
|
};
|
|
5088
5328
|
var ensurePinnedGuestImage = async (config, exec) => {
|
|
5089
5329
|
const image = config.profile.images[SUPPORTED_GUEST_IMAGE.key];
|
|
5090
|
-
if (
|
|
5330
|
+
if (existsSync6(image.path))
|
|
5091
5331
|
return;
|
|
5092
|
-
|
|
5332
|
+
mkdirSync7(dirname8(image.path), { recursive: true, mode: 493 });
|
|
5093
5333
|
const temporary = `${image.path}.next-${process.pid}`;
|
|
5094
5334
|
try {
|
|
5095
5335
|
await runChecked(exec, [
|
|
@@ -5106,9 +5346,9 @@ var ensurePinnedGuestImage = async (config, exec) => {
|
|
|
5106
5346
|
const digest = (await runChecked(exec, ["/usr/bin/sha256sum", temporary])).stdout.split(/\s+/)[0];
|
|
5107
5347
|
if (digest !== image.sha256)
|
|
5108
5348
|
throw new MetalBootstrapError("downloaded guest image digest mismatch");
|
|
5109
|
-
|
|
5349
|
+
chmodSync4(temporary, 292);
|
|
5110
5350
|
chownSync(temporary, 0, 0);
|
|
5111
|
-
|
|
5351
|
+
renameSync6(temporary, image.path);
|
|
5112
5352
|
} catch (cause) {
|
|
5113
5353
|
try {
|
|
5114
5354
|
unlinkSync(temporary);
|
|
@@ -5121,11 +5361,11 @@ async function applyMetalBootstrap(config, options) {
|
|
|
5121
5361
|
if ((options.getuid ?? process.getuid)?.() !== 0)
|
|
5122
5362
|
throw new MetalBootstrapError("fz bootstrap metal --apply must run as root");
|
|
5123
5363
|
assertSupportedMetalHost();
|
|
5124
|
-
if (
|
|
5364
|
+
if (existsSync6(STATE_PATH2) && !options.repair)
|
|
5125
5365
|
throw new MetalBootstrapError("metal host is already initialized; use explicit repair");
|
|
5126
5366
|
if (config.agentSeedFile)
|
|
5127
5367
|
validateOwnerOnlyPath(config.agentSeedFile, true);
|
|
5128
|
-
if (config.agentSeedFile &&
|
|
5368
|
+
if (config.agentSeedFile && existsSync6(SEED_CREDENTIAL_PATH)) {
|
|
5129
5369
|
throw new MetalBootstrapError("repair refuses replacement seed material while the sealed metal identity exists");
|
|
5130
5370
|
}
|
|
5131
5371
|
validateAgentSourcePath(options.agentSourcePath);
|
|
@@ -5152,9 +5392,9 @@ async function applyMetalBootstrap(config, options) {
|
|
|
5152
5392
|
await preflight(config, exec);
|
|
5153
5393
|
await ensureAccount(exec);
|
|
5154
5394
|
installAgentBinary(options.agentSourcePath, config.profile.agentVersion);
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5395
|
+
mkdirSync7("/etc/forgezero/creds", { recursive: true, mode: 448 });
|
|
5396
|
+
mkdirSync7(config.profile.stateDir, { recursive: true, mode: 448 });
|
|
5397
|
+
mkdirSync7(config.profile.seedDir, { recursive: true, mode: 448 });
|
|
5158
5398
|
const persistedProfile = {
|
|
5159
5399
|
...config.profile,
|
|
5160
5400
|
metalHostname: config.metalHostname,
|
|
@@ -5163,13 +5403,13 @@ async function applyMetalBootstrap(config, options) {
|
|
|
5163
5403
|
};
|
|
5164
5404
|
atomicWrite(PROFILE_PATH, `${JSON.stringify(persistedProfile, null, 2)}
|
|
5165
5405
|
`, 384);
|
|
5166
|
-
if (!
|
|
5167
|
-
const seed = config.agentSeedFile ?
|
|
5406
|
+
if (!existsSync6(SEED_CREDENTIAL_PATH)) {
|
|
5407
|
+
const seed = config.agentSeedFile ? readFileSync6(config.agentSeedFile, "utf8").trim() : randomBytes4(32).toString("base64url");
|
|
5168
5408
|
if (seed.length < 32 || /[\0\r\n]/.test(seed))
|
|
5169
5409
|
throw new MetalBootstrapError("metal Agent seed is invalid");
|
|
5170
5410
|
await runChecked(exec, ["/usr/bin/systemd-creds", "encrypt", "--name=metal-agent-seed", "-", SEED_CREDENTIAL_PATH], `${seed}
|
|
5171
5411
|
`);
|
|
5172
|
-
|
|
5412
|
+
chmodSync4(SEED_CREDENTIAL_PATH, 256);
|
|
5173
5413
|
chownSync(SEED_CREDENTIAL_PATH, 0, 0);
|
|
5174
5414
|
if (config.agentSeedFile)
|
|
5175
5415
|
unlinkSync(config.agentSeedFile);
|
|
@@ -5218,7 +5458,7 @@ async function applyMetalBootstrap(config, options) {
|
|
|
5218
5458
|
initializedAt: new Date().toISOString(),
|
|
5219
5459
|
role: "metal",
|
|
5220
5460
|
metalHostname: config.metalHostname,
|
|
5221
|
-
profileSha256:
|
|
5461
|
+
profileSha256: createHash3("sha256").update(JSON.stringify(config.profile)).digest("hex")
|
|
5222
5462
|
};
|
|
5223
5463
|
atomicWrite(STATE_PATH2, `${JSON.stringify(state, null, 2)}
|
|
5224
5464
|
`, 384);
|
|
@@ -5226,7 +5466,7 @@ async function applyMetalBootstrap(config, options) {
|
|
|
5226
5466
|
}
|
|
5227
5467
|
var socketReady = (path) => {
|
|
5228
5468
|
try {
|
|
5229
|
-
return
|
|
5469
|
+
return lstatSync4(path).isSocket();
|
|
5230
5470
|
} catch {
|
|
5231
5471
|
return false;
|
|
5232
5472
|
}
|
|
@@ -5235,17 +5475,17 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
5235
5475
|
const problems = [];
|
|
5236
5476
|
let profileValid = false, imageVerified = false, metalHostname, profileSha256;
|
|
5237
5477
|
let profileMode = null;
|
|
5238
|
-
if (
|
|
5478
|
+
if (existsSync6(PROFILE_PATH)) {
|
|
5239
5479
|
try {
|
|
5240
5480
|
const metadata = statSync(PROFILE_PATH);
|
|
5241
5481
|
profileMode = metadata.mode & 511;
|
|
5242
5482
|
if (profileMode !== 384 || metadata.uid !== 0)
|
|
5243
5483
|
problems.push("metal profile is not root-owned mode 0600");
|
|
5244
|
-
const persisted = JSON.parse(
|
|
5484
|
+
const persisted = JSON.parse(readFileSync6(PROFILE_PATH, "utf8"));
|
|
5245
5485
|
const { metalHostname: profileHostname, hostTelemetryEndpoint, hostTelemetryUnit, ...profile } = persisted;
|
|
5246
5486
|
validateMetalProfile(profile);
|
|
5247
5487
|
profileValid = true;
|
|
5248
|
-
profileSha256 =
|
|
5488
|
+
profileSha256 = createHash3("sha256").update(JSON.stringify(profile)).digest("hex");
|
|
5249
5489
|
if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(profileHostname) || hostTelemetryEndpoint !== "http://127.0.0.1:4318") {
|
|
5250
5490
|
problems.push("persisted metal host coordinates are invalid");
|
|
5251
5491
|
}
|
|
@@ -5274,7 +5514,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
5274
5514
|
problems.push("local OTLP metrics receiver did not accept a proof request");
|
|
5275
5515
|
}
|
|
5276
5516
|
const image = profile.images[Object.keys(profile.images)[0]];
|
|
5277
|
-
if (
|
|
5517
|
+
if (existsSync6(image.path)) {
|
|
5278
5518
|
const digest = (await exec(["/usr/bin/sha256sum", image.path])).stdout.split(/\s+/)[0];
|
|
5279
5519
|
imageVerified = digest === image.sha256;
|
|
5280
5520
|
}
|
|
@@ -5285,9 +5525,9 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
5285
5525
|
}
|
|
5286
5526
|
} else
|
|
5287
5527
|
problems.push("metal profile is missing");
|
|
5288
|
-
if (
|
|
5528
|
+
if (existsSync6(STATE_PATH2)) {
|
|
5289
5529
|
try {
|
|
5290
|
-
const state = JSON.parse(
|
|
5530
|
+
const state = JSON.parse(readFileSync6(STATE_PATH2, "utf8"));
|
|
5291
5531
|
metalHostname = state.metalHostname;
|
|
5292
5532
|
if (state.role !== "metal" || !metalHostname || state.profileSha256 !== profileSha256) {
|
|
5293
5533
|
problems.push("metal initialized state does not bind the current profile");
|
|
@@ -5303,7 +5543,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
5303
5543
|
"forgezero-metal-agent-egress.service",
|
|
5304
5544
|
"forgezero-metal-agent.service"
|
|
5305
5545
|
]) {
|
|
5306
|
-
if (!
|
|
5546
|
+
if (!existsSync6(join5(UNIT_DIRECTORY, unit)))
|
|
5307
5547
|
units[unit] = "missing";
|
|
5308
5548
|
else
|
|
5309
5549
|
units[unit] = (await exec(["/usr/bin/systemctl", "is-active", "--quiet", unit])).exitCode === 0 ? "active" : "inactive";
|
|
@@ -5317,7 +5557,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
5317
5557
|
if (!updateSocketReady)
|
|
5318
5558
|
problems.push("Agent update helper socket is not ready");
|
|
5319
5559
|
return {
|
|
5320
|
-
initialized:
|
|
5560
|
+
initialized: existsSync6(STATE_PATH2),
|
|
5321
5561
|
profileValid,
|
|
5322
5562
|
profileMode,
|
|
5323
5563
|
imageVerified,
|
|
@@ -5330,11 +5570,11 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
5330
5570
|
}
|
|
5331
5571
|
|
|
5332
5572
|
// src/operator-bootstrap.ts
|
|
5333
|
-
import { createHash as
|
|
5334
|
-
import { lstatSync as
|
|
5573
|
+
import { createHash as createHash4, randomBytes as randomBytes5 } from "crypto";
|
|
5574
|
+
import { chmodSync as chmodSync5, lstatSync as lstatSync5, mkdirSync as mkdirSync8, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
5335
5575
|
import { isIP as isIP6 } from "net";
|
|
5336
5576
|
import { tmpdir } from "os";
|
|
5337
|
-
import { basename, join as join6 } from "path";
|
|
5577
|
+
import { basename, dirname as dirname9, isAbsolute as isAbsolute4, join as join6, resolve as resolve6 } from "path";
|
|
5338
5578
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5339
5579
|
|
|
5340
5580
|
// src/platform-genesis.ts
|
|
@@ -5469,25 +5709,44 @@ var exactKeys3 = (value, keys, label) => {
|
|
|
5469
5709
|
throw new Error(`${label} contains unknown fields: ${unknown.join(", ")}`);
|
|
5470
5710
|
return record;
|
|
5471
5711
|
};
|
|
5712
|
+
var canonicalOutputPath = (path, label) => {
|
|
5713
|
+
if (!isAbsolute4(path) || resolve6(path) !== path || /[\r\n\0]/.test(path)) {
|
|
5714
|
+
throw new Error(`${label} must be a canonical absolute path`);
|
|
5715
|
+
}
|
|
5716
|
+
return path;
|
|
5717
|
+
};
|
|
5718
|
+
var writeOwnerJson2 = (path, value, label) => {
|
|
5719
|
+
canonicalOutputPath(path, label);
|
|
5720
|
+
mkdirSync8(dirname9(path), { recursive: true, mode: 448 });
|
|
5721
|
+
const parent = lstatSync5(dirname9(path));
|
|
5722
|
+
const uid = process.getuid?.() ?? parent.uid;
|
|
5723
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== uid || (parent.mode & 63) !== 0) {
|
|
5724
|
+
throw new Error(`${label} parent must be an owner-only directory`);
|
|
5725
|
+
}
|
|
5726
|
+
writeFileSync8(path, `${JSON.stringify(value, null, 2)}
|
|
5727
|
+
`, { mode: 384, flag: "wx" });
|
|
5728
|
+
chmodSync5(path, 384);
|
|
5729
|
+
return path;
|
|
5730
|
+
};
|
|
5472
5731
|
var ownerFile = (path, limit, label) => {
|
|
5473
5732
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
5474
5733
|
throw new Error(`${label} path must be absolute`);
|
|
5475
|
-
const metadata =
|
|
5734
|
+
const metadata = lstatSync5(path);
|
|
5476
5735
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
5477
5736
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== uid || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size < 1 || metadata.size > limit) {
|
|
5478
5737
|
throw new Error(`${label} must be an owner-only regular file with one link and at most ${limit} bytes`);
|
|
5479
5738
|
}
|
|
5480
|
-
return
|
|
5739
|
+
return readFileSync7(path);
|
|
5481
5740
|
};
|
|
5482
5741
|
var publicIdentity = (path) => {
|
|
5483
5742
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
5484
5743
|
throw new Error("SSH public-key path must be absolute");
|
|
5485
|
-
const metadata =
|
|
5744
|
+
const metadata = lstatSync5(path);
|
|
5486
5745
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
5487
5746
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== uid || metadata.nlink !== 1 || metadata.size > 16384) {
|
|
5488
5747
|
throw new Error("SSH public key must be a caller-owned regular file with one link");
|
|
5489
5748
|
}
|
|
5490
|
-
const value =
|
|
5749
|
+
const value = readFileSync7(path, "utf8").trim();
|
|
5491
5750
|
if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(value)) {
|
|
5492
5751
|
throw new Error("SSH public key is malformed");
|
|
5493
5752
|
}
|
|
@@ -5496,7 +5755,7 @@ var publicIdentity = (path) => {
|
|
|
5496
5755
|
var socketPath = (path) => {
|
|
5497
5756
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
5498
5757
|
throw new Error("SSH agent socket path must be absolute");
|
|
5499
|
-
const metadata =
|
|
5758
|
+
const metadata = lstatSync5(path);
|
|
5500
5759
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
5501
5760
|
if (!metadata.isSocket() || metadata.isSymbolicLink() || metadata.uid !== uid) {
|
|
5502
5761
|
throw new Error("SSH agent socket must be a caller-owned Unix socket");
|
|
@@ -5510,7 +5769,7 @@ var fingerprint = (key) => {
|
|
|
5510
5769
|
if (!bytes.length || bytes.toString("base64").replace(/=+$/, "") !== encoded.replace(/=+$/, "")) {
|
|
5511
5770
|
throw new Error("SSH host key is malformed");
|
|
5512
5771
|
}
|
|
5513
|
-
return `SHA256:${
|
|
5772
|
+
return `SHA256:${createHash4("sha256").update(bytes).digest("base64").replace(/=+$/, "")}`;
|
|
5514
5773
|
};
|
|
5515
5774
|
var validateHop = (value, label) => {
|
|
5516
5775
|
const hop = exactKeys3(value, ["address", "port", "user", "hostKey", "hostKeySha256"], label);
|
|
@@ -5527,8 +5786,10 @@ var validateHop = (value, label) => {
|
|
|
5527
5786
|
}
|
|
5528
5787
|
return hop;
|
|
5529
5788
|
};
|
|
5530
|
-
function
|
|
5531
|
-
|
|
5789
|
+
function createOperatorSshHop(input) {
|
|
5790
|
+
return validateHop({ ...input, hostKeySha256: fingerprint(input.hostKey) }, "operator SSH hop");
|
|
5791
|
+
}
|
|
5792
|
+
var validatePlatformRequestValue = (value) => {
|
|
5532
5793
|
const root = exactKeys3(value, ["kind", "target", "platformConfigFile"], "operator bootstrap request");
|
|
5533
5794
|
if (root.kind !== "platform-remote")
|
|
5534
5795
|
throw new Error("operator bootstrap kind must be platform-remote");
|
|
@@ -5547,6 +5808,7 @@ function readOperatorPlatformBootstrapRequest(path) {
|
|
|
5547
5808
|
socketPath(targetValue.agentSocket);
|
|
5548
5809
|
if (typeof root.platformConfigFile !== "string")
|
|
5549
5810
|
throw new Error("platformConfigFile must be an absolute path");
|
|
5811
|
+
canonicalOutputPath(root.platformConfigFile, "platformConfigFile");
|
|
5550
5812
|
const config = readBootstrapConfig(root.platformConfigFile);
|
|
5551
5813
|
if (config.kind !== "platform")
|
|
5552
5814
|
throw new Error("operator bootstrap requires a platform config");
|
|
@@ -5560,9 +5822,39 @@ function readOperatorPlatformBootstrapRequest(path) {
|
|
|
5560
5822
|
...targetValue.jump === undefined ? {} : { jump: validateHop(targetValue.jump, "operator bootstrap jump") }
|
|
5561
5823
|
}
|
|
5562
5824
|
};
|
|
5825
|
+
};
|
|
5826
|
+
function readOperatorPlatformBootstrapRequest(path) {
|
|
5827
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator bootstrap request")));
|
|
5828
|
+
return validatePlatformRequestValue(value);
|
|
5563
5829
|
}
|
|
5564
|
-
function
|
|
5565
|
-
|
|
5830
|
+
function writeOperatorPlatformBootstrapRequest(path, request) {
|
|
5831
|
+
validatePlatformRequestValue(request);
|
|
5832
|
+
return writeOwnerJson2(path, request, "operator bootstrap request");
|
|
5833
|
+
}
|
|
5834
|
+
function readOperatorPlatformBootstrapFleetRequest(path) {
|
|
5835
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator fleet request")));
|
|
5836
|
+
const root = exactKeys3(value, ["kind", "requests"], "operator fleet request");
|
|
5837
|
+
if (root.kind !== "platform-fleet-remote")
|
|
5838
|
+
throw new Error("operator fleet kind must be platform-fleet-remote");
|
|
5839
|
+
if (!Array.isArray(root.requests) || root.requests.length !== 3 || root.requests.some((entry) => typeof entry !== "string")) {
|
|
5840
|
+
throw new Error("operator fleet requires exactly three request files");
|
|
5841
|
+
}
|
|
5842
|
+
const requests = root.requests.map((entry) => canonicalOutputPath(entry, "operator fleet request file"));
|
|
5843
|
+
if (new Set(requests).size !== 3)
|
|
5844
|
+
throw new Error("operator fleet request files must be unique");
|
|
5845
|
+
for (const request of requests)
|
|
5846
|
+
readOperatorPlatformBootstrapRequest(request);
|
|
5847
|
+
return { kind: "platform-fleet-remote", requests };
|
|
5848
|
+
}
|
|
5849
|
+
function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles) {
|
|
5850
|
+
const request = { kind: "platform-fleet-remote", requests: requestFiles };
|
|
5851
|
+
if (new Set(requestFiles).size !== 3)
|
|
5852
|
+
throw new Error("operator fleet request files must be unique");
|
|
5853
|
+
for (const file of requestFiles)
|
|
5854
|
+
readOperatorPlatformBootstrapRequest(file);
|
|
5855
|
+
return writeOwnerJson2(path, request, "operator fleet request");
|
|
5856
|
+
}
|
|
5857
|
+
var validateMetalRequestValue = (value) => {
|
|
5566
5858
|
const root = exactKeys3(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
|
|
5567
5859
|
if (root.kind !== "metal-remote")
|
|
5568
5860
|
throw new Error("operator metal bootstrap kind must be metal-remote");
|
|
@@ -5581,6 +5873,7 @@ function readOperatorMetalBootstrapRequest(path) {
|
|
|
5581
5873
|
socketPath(targetValue.agentSocket);
|
|
5582
5874
|
if (typeof root.metalConfigFile !== "string")
|
|
5583
5875
|
throw new Error("metalConfigFile must be an absolute path");
|
|
5876
|
+
canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
|
|
5584
5877
|
readMetalBootstrapConfig(root.metalConfigFile);
|
|
5585
5878
|
const genesis = exactKeys3(root.genesis, ["environment", "sshPublicKeyFiles"], "operator metal genesis");
|
|
5586
5879
|
if (typeof genesis.environment !== "string" || !PLATFORM_GENESIS_ENVIRONMENTS.includes(genesis.environment)) {
|
|
@@ -5604,6 +5897,14 @@ function readOperatorMetalBootstrapRequest(path) {
|
|
|
5604
5897
|
sshPublicKeyFiles: genesis.sshPublicKeyFiles
|
|
5605
5898
|
}
|
|
5606
5899
|
};
|
|
5900
|
+
};
|
|
5901
|
+
function readOperatorMetalBootstrapRequest(path) {
|
|
5902
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal request")));
|
|
5903
|
+
return validateMetalRequestValue(value);
|
|
5904
|
+
}
|
|
5905
|
+
function writeOperatorMetalBootstrapRequest(path, request) {
|
|
5906
|
+
validateMetalRequestValue(request);
|
|
5907
|
+
return writeOwnerJson2(path, request, "operator metal request");
|
|
5607
5908
|
}
|
|
5608
5909
|
function planOperatorMetalBootstrap(request, mode) {
|
|
5609
5910
|
const config = readMetalBootstrapConfig(request.metalConfigFile);
|
|
@@ -5655,13 +5956,95 @@ function planOperatorPlatformBootstrap(request, mode) {
|
|
|
5655
5956
|
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : [
|
|
5656
5957
|
"verify pinned SSH transport and caller-approved SSH agent",
|
|
5657
5958
|
"copy pinned Bun and packaged fz artifacts to a private staging directory",
|
|
5959
|
+
"verify and copy the one immutable API Git bundle plus its manifest",
|
|
5658
5960
|
"stage the rewritten owner-only platform config and its named credential handoffs",
|
|
5659
|
-
|
|
5961
|
+
"run typed fz bootstrap platform --apply from the local bundle",
|
|
5660
5962
|
"remove transient local and remote staging data"
|
|
5661
5963
|
],
|
|
5662
5964
|
secretInputs: mode === "apply" ? [...attendedSecretNames(config), ...secretSources(config).map(([name]) => name)] : []
|
|
5663
5965
|
};
|
|
5664
5966
|
}
|
|
5967
|
+
var fleetConfigIdentity = (config) => {
|
|
5968
|
+
const normalized = structuredClone(config);
|
|
5969
|
+
delete normalized.computeReference;
|
|
5970
|
+
delete normalized.nodeHostname;
|
|
5971
|
+
delete normalized.cloudflareHandoff;
|
|
5972
|
+
delete normalized.database.role;
|
|
5973
|
+
delete normalized.database.address;
|
|
5974
|
+
delete normalized.database.master;
|
|
5975
|
+
const environment = normalized.runtime.environment;
|
|
5976
|
+
delete environment.databaseRole;
|
|
5977
|
+
delete environment.databaseAddress;
|
|
5978
|
+
delete environment.databaseMaster;
|
|
5979
|
+
delete environment.nodeHostname;
|
|
5980
|
+
delete environment.seedSyncPeers;
|
|
5981
|
+
delete environment.custodianEmail;
|
|
5982
|
+
return JSON.stringify(normalized);
|
|
5983
|
+
};
|
|
5984
|
+
function validatedPlatformFleet(fleet) {
|
|
5985
|
+
const requests = fleet.requests.map(readOperatorPlatformBootstrapRequest);
|
|
5986
|
+
const configs = requests.map((request) => {
|
|
5987
|
+
const config = readBootstrapConfig(request.platformConfigFile);
|
|
5988
|
+
if (config.kind !== "platform")
|
|
5989
|
+
throw new Error("operator fleet requires platform configs");
|
|
5990
|
+
return config;
|
|
5991
|
+
});
|
|
5992
|
+
const environment = configs[0].environment;
|
|
5993
|
+
const expected = PLATFORM_GENESIS_FLEETS[environment];
|
|
5994
|
+
if (!expected || expected.length !== 3)
|
|
5995
|
+
throw new Error("operator fleet environment is invalid");
|
|
5996
|
+
const identity = fleetConfigIdentity(configs[0]);
|
|
5997
|
+
const operatorIdentity = `${requests[0].target.identityPublicKeyFile}\x00${requests[0].target.agentSocket}`;
|
|
5998
|
+
const jumpIdentity = JSON.stringify(requests[0].target.jump ?? null);
|
|
5999
|
+
const coordinators = expected.map((guest) => `http://${guest.address}:8529`);
|
|
6000
|
+
const nodeHostnames = new Set;
|
|
6001
|
+
for (let index = 0;index < 3; index += 1) {
|
|
6002
|
+
const config = configs[index];
|
|
6003
|
+
const request = requests[index];
|
|
6004
|
+
const guest = expected[index];
|
|
6005
|
+
const role = index === 0 ? "master" : "joiner";
|
|
6006
|
+
if (config.environment !== environment || fleetConfigIdentity(config) !== identity) {
|
|
6007
|
+
throw new Error("operator fleet platform configs do not share one immutable identity");
|
|
6008
|
+
}
|
|
6009
|
+
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) {
|
|
6010
|
+
throw new Error(`operator fleet ${guest.name} does not match the exact genesis topology`);
|
|
6011
|
+
}
|
|
6012
|
+
if (request.target.address !== guest.address || request.target.port !== 22 || request.target.user !== "forgezero") {
|
|
6013
|
+
throw new Error(`operator fleet ${guest.name} SSH target must be forgezero@${guest.address}:22`);
|
|
6014
|
+
}
|
|
6015
|
+
if (`${request.target.identityPublicKeyFile}\x00${request.target.agentSocket}` !== operatorIdentity) {
|
|
6016
|
+
throw new Error("operator fleet must use one caller-approved SSH identity and agent socket");
|
|
6017
|
+
}
|
|
6018
|
+
if (JSON.stringify(request.target.jump ?? null) !== jumpIdentity) {
|
|
6019
|
+
throw new Error("operator fleet must use one pinned Metal jump or no jump on every node");
|
|
6020
|
+
}
|
|
6021
|
+
if (nodeHostnames.has(config.nodeHostname))
|
|
6022
|
+
throw new Error("operator fleet node hostnames must be unique");
|
|
6023
|
+
nodeHostnames.add(config.nodeHostname);
|
|
6024
|
+
}
|
|
6025
|
+
return { requests, configs, environment };
|
|
6026
|
+
}
|
|
6027
|
+
function planOperatorPlatformFleetBootstrap(fleet, mode) {
|
|
6028
|
+
const { requests, configs, environment } = validatedPlatformFleet(fleet);
|
|
6029
|
+
const secretInputs = mode === "apply" ? attendedSecretNames(configs[0]) : [];
|
|
6030
|
+
for (const config of configs.slice(1)) {
|
|
6031
|
+
if (JSON.stringify(attendedSecretNames(config)) !== JSON.stringify(secretInputs)) {
|
|
6032
|
+
throw new Error("operator fleet configs do not share one attended secret schema");
|
|
6033
|
+
}
|
|
6034
|
+
}
|
|
6035
|
+
return {
|
|
6036
|
+
kind: "platform-fleet-remote",
|
|
6037
|
+
mode,
|
|
6038
|
+
mutation: mode !== "status",
|
|
6039
|
+
environment,
|
|
6040
|
+
nodes: requests.map((request, index) => ({
|
|
6041
|
+
computeReference: configs[index].computeReference,
|
|
6042
|
+
address: request.target.address,
|
|
6043
|
+
...request.target.jump ? { via: request.target.jump.address } : {}
|
|
6044
|
+
})),
|
|
6045
|
+
secretInputs
|
|
6046
|
+
};
|
|
6047
|
+
}
|
|
5665
6048
|
var defaultExec3 = async (argv, options = {}) => {
|
|
5666
6049
|
const child = Bun.spawn([...argv], {
|
|
5667
6050
|
stdin: options.stdin === undefined ? "ignore" : "pipe",
|
|
@@ -5685,7 +6068,7 @@ var defaultExec3 = async (argv, options = {}) => {
|
|
|
5685
6068
|
]);
|
|
5686
6069
|
return { exitCode, output: options.secret ? "" : `${stdout}${stderr}`.slice(0, 65536) };
|
|
5687
6070
|
};
|
|
5688
|
-
var
|
|
6071
|
+
var checked5 = async (exec, argv, label, options) => {
|
|
5689
6072
|
const result = await exec(argv, options);
|
|
5690
6073
|
if (result.exitCode !== 0)
|
|
5691
6074
|
throw new Error(`${label} failed${result.output ? `: ${result.output.trim()}` : ""}`);
|
|
@@ -5698,7 +6081,7 @@ function writeKnownHosts(request, directory) {
|
|
|
5698
6081
|
if (request.target.jump)
|
|
5699
6082
|
lines.push(`${hostLabel(request.target.jump.address, request.target.jump.port)} ${request.target.jump.hostKey}`);
|
|
5700
6083
|
const path = join6(directory, "known_hosts");
|
|
5701
|
-
|
|
6084
|
+
writeFileSync8(path, `${lines.join(`
|
|
5702
6085
|
`)}
|
|
5703
6086
|
`, { mode: 384, flag: "wx" });
|
|
5704
6087
|
return path;
|
|
@@ -5739,7 +6122,7 @@ var safeRemoteArg = (value) => {
|
|
|
5739
6122
|
};
|
|
5740
6123
|
var remote = async (exec, request, knownHosts, argv, label, secret = false, stdin) => {
|
|
5741
6124
|
argv.forEach(safeRemoteArg);
|
|
5742
|
-
return
|
|
6125
|
+
return checked5(exec, ["ssh", ...sshOptions(request, knownHosts), destination2(request.target), "--", ...argv], label, { secret, stdin });
|
|
5743
6126
|
};
|
|
5744
6127
|
var remoteRegularFileExists = async (exec, request, knownHosts, path) => {
|
|
5745
6128
|
safeRemoteArg(path);
|
|
@@ -5760,36 +6143,45 @@ var remoteRegularFileExists = async (exec, request, knownHosts, path) => {
|
|
|
5760
6143
|
};
|
|
5761
6144
|
var copy = async (exec, request, knownHosts, local, remotePath, secret = false) => {
|
|
5762
6145
|
safeRemoteArg(remotePath);
|
|
5763
|
-
await
|
|
6146
|
+
await checked5(exec, [
|
|
5764
6147
|
"scp",
|
|
5765
6148
|
...sshOptions(request, knownHosts, true),
|
|
5766
6149
|
local,
|
|
5767
6150
|
`${destination2(request.target)}:${remotePath}`
|
|
5768
6151
|
], "secure copy", { secret });
|
|
5769
6152
|
};
|
|
5770
|
-
function stageConfig(config, directory) {
|
|
6153
|
+
async function stageConfig(config, directory) {
|
|
5771
6154
|
const rewritten = structuredClone(config);
|
|
5772
6155
|
const staged = [];
|
|
5773
6156
|
for (const [name, source] of secretSources(config)) {
|
|
5774
6157
|
const bytes = ownerFile(source, SECRET_LIMIT, name);
|
|
5775
6158
|
const local = join6(directory, name);
|
|
5776
|
-
|
|
6159
|
+
writeFileSync8(local, bytes, { mode: 384, flag: "wx" });
|
|
5777
6160
|
staged.push(name);
|
|
5778
6161
|
const remotePath = `${REMOTE_STAGE}/${name}`;
|
|
5779
6162
|
if (name === "cloudflare-handoff")
|
|
5780
6163
|
rewritten.cloudflareHandoff.handoffFile = remotePath;
|
|
5781
6164
|
}
|
|
6165
|
+
const bundle = await readBootstrapBundle(config.bootstrapBundle.bundleFile, config.bootstrapBundle.manifestFile);
|
|
6166
|
+
const bundleFiles = [
|
|
6167
|
+
{ source: bundle.bundlePath, name: "bootstrap-api.bundle" },
|
|
6168
|
+
{ source: bundle.manifestPath, name: "bootstrap-api.bundle.json" }
|
|
6169
|
+
];
|
|
6170
|
+
rewritten.bootstrapBundle = {
|
|
6171
|
+
bundleFile: `${REMOTE_STAGE}/bootstrap-api.bundle`,
|
|
6172
|
+
manifestFile: `${REMOTE_STAGE}/bootstrap-api.bundle.json`
|
|
6173
|
+
};
|
|
5782
6174
|
const path = join6(directory, "platform-config.json");
|
|
5783
|
-
|
|
6175
|
+
writeFileSync8(path, `${JSON.stringify(rewritten, null, 2)}
|
|
5784
6176
|
`, { mode: 384, flag: "wx" });
|
|
5785
|
-
return { path, files: staged };
|
|
6177
|
+
return { path, files: staged, bundleFiles };
|
|
5786
6178
|
}
|
|
5787
6179
|
function stageMetalConfig(config, directory) {
|
|
5788
6180
|
const rewritten = structuredClone(config);
|
|
5789
6181
|
const files = [];
|
|
5790
6182
|
if (config.agentSeedFile) {
|
|
5791
6183
|
const name = "metal-agent-seed";
|
|
5792
|
-
|
|
6184
|
+
writeFileSync8(join6(directory, name), ownerFile(config.agentSeedFile, SECRET_LIMIT, name), {
|
|
5793
6185
|
mode: 384,
|
|
5794
6186
|
flag: "wx"
|
|
5795
6187
|
});
|
|
@@ -5797,7 +6189,7 @@ function stageMetalConfig(config, directory) {
|
|
|
5797
6189
|
files.push(name);
|
|
5798
6190
|
}
|
|
5799
6191
|
const path = join6(directory, "metal-config.json");
|
|
5800
|
-
|
|
6192
|
+
writeFileSync8(path, `${JSON.stringify(rewritten, null, 2)}
|
|
5801
6193
|
`, { mode: 384, flag: "wx" });
|
|
5802
6194
|
return { path, files };
|
|
5803
6195
|
}
|
|
@@ -5806,10 +6198,10 @@ async function verifiedBunArchive(directory, fetcher) {
|
|
|
5806
6198
|
if (!response.ok)
|
|
5807
6199
|
throw new Error("pinned Bun download failed");
|
|
5808
6200
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
5809
|
-
if (
|
|
6201
|
+
if (createHash4("sha256").update(bytes).digest("hex") !== BUN_RELEASE_SHA256)
|
|
5810
6202
|
throw new Error("pinned Bun checksum mismatch");
|
|
5811
6203
|
const path = join6(directory, "bun.zip");
|
|
5812
|
-
|
|
6204
|
+
writeFileSync8(path, bytes, { mode: 384, flag: "wx" });
|
|
5813
6205
|
return path;
|
|
5814
6206
|
}
|
|
5815
6207
|
async function installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options) {
|
|
@@ -5819,7 +6211,7 @@ async function installPackagedAgent(request, knownHosts, exec, directory, remote
|
|
|
5819
6211
|
[options.fzGitSshPath ?? fileURLToPath2(new URL("./fz-git-ssh.js", import.meta.url)), "fz-git-ssh.js"]
|
|
5820
6212
|
];
|
|
5821
6213
|
for (const [artifact] of artifacts) {
|
|
5822
|
-
if (!
|
|
6214
|
+
if (!readFileSync7(artifact).length)
|
|
5823
6215
|
throw new Error(`packaged artifact is empty: ${basename(artifact)}`);
|
|
5824
6216
|
}
|
|
5825
6217
|
await remote(exec, request, knownHosts, ["/usr/bin/mkdir", "-m", "0700", remoteTemp], "remote staging");
|
|
@@ -5860,7 +6252,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
5860
6252
|
socketPath(request.target.agentSocket);
|
|
5861
6253
|
const exec = options.exec ?? defaultExec3;
|
|
5862
6254
|
const directory = mkdtempSync(join6(tmpdir(), "forgezero-operator-bootstrap-"));
|
|
5863
|
-
const remoteTemp = `/tmp/forgezero-operator-${
|
|
6255
|
+
const remoteTemp = `/tmp/forgezero-operator-${randomBytes5(12).toString("hex")}`;
|
|
5864
6256
|
let knownHosts = "";
|
|
5865
6257
|
try {
|
|
5866
6258
|
knownHosts = writeKnownHosts(request, directory);
|
|
@@ -5872,18 +6264,16 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
5872
6264
|
if (config.kind !== "platform")
|
|
5873
6265
|
throw new Error("operator bootstrap requires a platform config");
|
|
5874
6266
|
const secrets = mode === "apply" ? validatePlatformBootstrapSecrets(config, options.secrets) : undefined;
|
|
5875
|
-
const staged = stageConfig(config, directory);
|
|
6267
|
+
const staged = await stageConfig(config, directory);
|
|
5876
6268
|
await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
|
|
5877
6269
|
await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
|
|
5878
6270
|
for (const name of staged.files)
|
|
5879
6271
|
await copy(exec, request, knownHosts, join6(directory, name), `${remoteTemp}/${name}`, true);
|
|
5880
|
-
for (const
|
|
6272
|
+
for (const bundle of staged.bundleFiles)
|
|
6273
|
+
await copy(exec, request, knownHosts, bundle.source, `${remoteTemp}/${bundle.name}`, true);
|
|
6274
|
+
for (const name of ["platform-config.json", ...staged.files, ...staged.bundleFiles.map(({ name: name2 }) => name2)])
|
|
5881
6275
|
await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/bin/install", "-m", "0600", `${remoteTemp}/${name}`, `${REMOTE_STAGE}/${name}`], "remote bootstrap handoff", true);
|
|
5882
|
-
const command = ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "platform"];
|
|
5883
|
-
if (mode === "prepare")
|
|
5884
|
-
command.push("prepare");
|
|
5885
|
-
else
|
|
5886
|
-
command.push("credentials-stdin");
|
|
6276
|
+
const command = ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "platform", "credentials-stdin"];
|
|
5887
6277
|
command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
|
|
5888
6278
|
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
|
|
5889
6279
|
` : undefined);
|
|
@@ -5894,16 +6284,40 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
5894
6284
|
return;
|
|
5895
6285
|
});
|
|
5896
6286
|
}
|
|
5897
|
-
|
|
6287
|
+
rmSync6(directory, { recursive: true, force: true });
|
|
5898
6288
|
}
|
|
5899
6289
|
}
|
|
6290
|
+
async function applyOperatorPlatformFleetBootstrap(fleet, mode, options = {}) {
|
|
6291
|
+
const plan = planOperatorPlatformFleetBootstrap(fleet, mode);
|
|
6292
|
+
const { requests, configs } = validatedPlatformFleet(fleet);
|
|
6293
|
+
const secrets = mode === "apply" ? validatePlatformBootstrapSecrets(configs[0], options.secrets) : undefined;
|
|
6294
|
+
if (secrets) {
|
|
6295
|
+
for (const config of configs.slice(1))
|
|
6296
|
+
validatePlatformBootstrapSecrets(config, secrets);
|
|
6297
|
+
}
|
|
6298
|
+
const settled = await Promise.allSettled(requests.map((request) => applyOperatorPlatformBootstrap(request, mode, { ...options, secrets })));
|
|
6299
|
+
const outcomes = settled.map((result, index) => {
|
|
6300
|
+
const config = configs[index];
|
|
6301
|
+
const address = requests[index].target.address;
|
|
6302
|
+
if (result.status === "fulfilled")
|
|
6303
|
+
return {
|
|
6304
|
+
computeReference: config.computeReference,
|
|
6305
|
+
address,
|
|
6306
|
+
ok: true,
|
|
6307
|
+
output: result.value.output.slice(0, 65536)
|
|
6308
|
+
};
|
|
6309
|
+
const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
6310
|
+
return { computeReference: config.computeReference, address, ok: false, error: message.slice(0, 4096) };
|
|
6311
|
+
});
|
|
6312
|
+
return { plan, ok: outcomes.every(({ ok }) => ok), outcomes };
|
|
6313
|
+
}
|
|
5900
6314
|
async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
5901
6315
|
const plan = planOperatorMetalBootstrap(request, mode);
|
|
5902
6316
|
publicIdentity(request.target.identityPublicKeyFile);
|
|
5903
6317
|
socketPath(request.target.agentSocket);
|
|
5904
6318
|
const exec = options.exec ?? defaultExec3;
|
|
5905
6319
|
const directory = mkdtempSync(join6(tmpdir(), "forgezero-operator-metal-"));
|
|
5906
|
-
const remoteTemp = `/tmp/forgezero-operator-${
|
|
6320
|
+
const remoteTemp = `/tmp/forgezero-operator-${randomBytes5(12).toString("hex")}`;
|
|
5907
6321
|
let knownHosts = "";
|
|
5908
6322
|
try {
|
|
5909
6323
|
knownHosts = writeKnownHosts(request, directory);
|
|
@@ -5947,14 +6361,21 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
5947
6361
|
return;
|
|
5948
6362
|
});
|
|
5949
6363
|
}
|
|
5950
|
-
|
|
6364
|
+
rmSync6(directory, { recursive: true, force: true });
|
|
5951
6365
|
}
|
|
5952
6366
|
}
|
|
5953
6367
|
export {
|
|
6368
|
+
writeOperatorPlatformBootstrapRequest,
|
|
6369
|
+
writeOperatorPlatformBootstrapFleetRequest,
|
|
6370
|
+
writeOperatorMetalBootstrapRequest,
|
|
5954
6371
|
readOperatorPlatformBootstrapRequest,
|
|
6372
|
+
readOperatorPlatformBootstrapFleetRequest,
|
|
5955
6373
|
readOperatorMetalBootstrapRequest,
|
|
6374
|
+
planOperatorPlatformFleetBootstrap,
|
|
5956
6375
|
planOperatorPlatformBootstrap,
|
|
5957
6376
|
planOperatorMetalBootstrap,
|
|
6377
|
+
createOperatorSshHop,
|
|
6378
|
+
applyOperatorPlatformFleetBootstrap,
|
|
5958
6379
|
applyOperatorPlatformBootstrap,
|
|
5959
6380
|
applyOperatorMetalBootstrap
|
|
5960
6381
|
};
|