@forgezero/agent 0.1.62 → 0.1.63
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 +36 -11
- 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 +7 -17
- package/dist/bootstrap.js +407 -214
- 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 +16 -0
- package/dist/fz-agent.js +486 -363
- package/dist/fz.js +499 -305
- 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 +1 -1
- package/dist/operator-bootstrap.js +509 -307
- package/dist/platform-bootstrap-runtime.d.ts +0 -2
- package/dist/platform-bootstrap-runtime.js +0 -4
- package/dist/platform-fleet-verification.js +110 -44
- package/dist/provision.d.ts +4 -1
- package/dist/provision.js +15 -7
- 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.63";
|
|
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) {
|
|
@@ -1949,6 +2109,8 @@ function agentUnit(options) {
|
|
|
1949
2109
|
options.gitPublicKeyPath ? `FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}` : null,
|
|
1950
2110
|
options.repository ? `FZ_DEPLOY_REPO=${options.repository}` : null,
|
|
1951
2111
|
options.branch ? `FZ_DEPLOY_BRANCH=${options.branch}` : null,
|
|
2112
|
+
options.bootstrapBundlePath ? `FZ_BOOTSTRAP_BUNDLE=${options.bootstrapBundlePath}` : null,
|
|
2113
|
+
options.bootstrapBundleManifestPath ? `FZ_BOOTSTRAP_BUNDLE_MANIFEST=${options.bootstrapBundleManifestPath}` : null,
|
|
1952
2114
|
options.profile ? `FZ_DEPLOY_PROFILE=${options.profile}` : null,
|
|
1953
2115
|
options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
|
|
1954
2116
|
deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
|
|
@@ -2331,27 +2493,27 @@ function planProvision(options) {
|
|
|
2331
2493
|
}
|
|
2332
2494
|
|
|
2333
2495
|
// src/cli/agent-install.ts
|
|
2334
|
-
import { randomBytes } from "crypto";
|
|
2496
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
2335
2497
|
import {
|
|
2336
|
-
chmodSync,
|
|
2498
|
+
chmodSync as chmodSync2,
|
|
2337
2499
|
copyFileSync,
|
|
2338
|
-
existsSync as
|
|
2339
|
-
lstatSync,
|
|
2340
|
-
mkdirSync as
|
|
2341
|
-
readFileSync as
|
|
2500
|
+
existsSync as existsSync4,
|
|
2501
|
+
lstatSync as lstatSync2,
|
|
2502
|
+
mkdirSync as mkdirSync4,
|
|
2503
|
+
readFileSync as readFileSync4,
|
|
2342
2504
|
realpathSync as realpathSync3,
|
|
2343
|
-
renameSync as
|
|
2344
|
-
rmSync as
|
|
2505
|
+
renameSync as renameSync4,
|
|
2506
|
+
rmSync as rmSync4,
|
|
2345
2507
|
symlinkSync,
|
|
2346
|
-
writeFileSync as
|
|
2508
|
+
writeFileSync as writeFileSync4
|
|
2347
2509
|
} from "fs";
|
|
2348
|
-
import { dirname as
|
|
2349
|
-
async function readCapabilities(
|
|
2510
|
+
import { dirname as dirname5 } from "path";
|
|
2511
|
+
async function readCapabilities(run2) {
|
|
2350
2512
|
const answers = {};
|
|
2351
2513
|
const checks = Object.entries(CAPABILITY_CHECKS);
|
|
2352
2514
|
for (const [id, check] of checks) {
|
|
2353
2515
|
try {
|
|
2354
|
-
const result = await
|
|
2516
|
+
const result = await run2(check.operation);
|
|
2355
2517
|
answers[id] = check.satisfied(result.stdout, result.exitCode);
|
|
2356
2518
|
} catch {
|
|
2357
2519
|
answers[id] = false;
|
|
@@ -2404,52 +2566,52 @@ var runProvisionOperation = async (operation) => {
|
|
|
2404
2566
|
}
|
|
2405
2567
|
if (operation.kind === "install-runtime") {
|
|
2406
2568
|
const release = `/opt/forgezero/agent/versions/${operation.version}`;
|
|
2407
|
-
|
|
2408
|
-
|
|
2569
|
+
mkdirSync4(`${release}/dist`, { recursive: true, mode: 493 });
|
|
2570
|
+
mkdirSync4(dirname5(operation.binary), { recursive: true, mode: 493 });
|
|
2409
2571
|
copyFileSync(operation.source, `${release}/dist/fz-agent.js`);
|
|
2410
|
-
|
|
2411
|
-
const gitSshSource = `${
|
|
2412
|
-
if (!
|
|
2572
|
+
chmodSync2(`${release}/dist/fz-agent.js`, 493);
|
|
2573
|
+
const gitSshSource = `${dirname5(operation.source)}/fz-git-ssh.js`;
|
|
2574
|
+
if (!existsSync4(gitSshSource))
|
|
2413
2575
|
return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
|
|
2414
2576
|
copyFileSync(gitSshSource, `${release}/dist/fz-git-ssh.js`);
|
|
2415
|
-
|
|
2577
|
+
chmodSync2(`${release}/dist/fz-git-ssh.js`, 493);
|
|
2416
2578
|
const pending = "/opt/forgezero/agent/current.next";
|
|
2417
|
-
|
|
2579
|
+
rmSync4(pending, { force: true });
|
|
2418
2580
|
symlinkSync(`versions/${operation.version}`, pending);
|
|
2419
|
-
|
|
2420
|
-
|
|
2581
|
+
renameSync4(pending, "/opt/forgezero/agent/current");
|
|
2582
|
+
rmSync4(operation.binary, { force: true });
|
|
2421
2583
|
symlinkSync("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
|
|
2422
2584
|
const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
|
|
2423
|
-
|
|
2585
|
+
rmSync4(gitSshBinary, { force: true });
|
|
2424
2586
|
symlinkSync("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
|
|
2425
2587
|
return { stdout: "", exitCode: 0 };
|
|
2426
2588
|
}
|
|
2427
2589
|
if (operation.kind === "ensure-seed") {
|
|
2428
|
-
if (
|
|
2590
|
+
if (existsSync4(operation.credential) && lstatSync2(operation.credential).size > 0)
|
|
2429
2591
|
return { stdout: "", exitCode: 0 };
|
|
2430
|
-
const seed =
|
|
2592
|
+
const seed = randomBytes2(32).toString("base64url");
|
|
2431
2593
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
|
|
2432
2594
|
if (result.exitCode === 0)
|
|
2433
|
-
|
|
2595
|
+
chmodSync2(operation.credential, 256);
|
|
2434
2596
|
return result;
|
|
2435
2597
|
}
|
|
2436
2598
|
if (operation.kind === "ensure-git-identity") {
|
|
2437
2599
|
const key = "/run/forgezero-git-deploy-key";
|
|
2438
2600
|
const publicKey = `${key}.pub`;
|
|
2439
2601
|
try {
|
|
2440
|
-
if (!
|
|
2441
|
-
|
|
2442
|
-
|
|
2602
|
+
if (!existsSync4(operation.credential) || lstatSync2(operation.credential).size < 1) {
|
|
2603
|
+
rmSync4(key, { force: true });
|
|
2604
|
+
rmSync4(publicKey, { force: true });
|
|
2443
2605
|
let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
|
|
2444
2606
|
if (result.exitCode !== 0)
|
|
2445
2607
|
return result;
|
|
2446
2608
|
result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=git-deploy-key", key, operation.credential]);
|
|
2447
2609
|
if (result.exitCode !== 0)
|
|
2448
2610
|
return result;
|
|
2449
|
-
|
|
2611
|
+
chmodSync2(operation.credential, 256);
|
|
2450
2612
|
}
|
|
2451
|
-
if (!
|
|
2452
|
-
if (!
|
|
2613
|
+
if (!existsSync4(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
|
|
2614
|
+
if (!existsSync4(key)) {
|
|
2453
2615
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
|
|
2454
2616
|
if (decrypted.exitCode !== 0)
|
|
2455
2617
|
return decrypted;
|
|
@@ -2457,30 +2619,30 @@ var runProvisionOperation = async (operation) => {
|
|
|
2457
2619
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
2458
2620
|
if (derived.exitCode !== 0)
|
|
2459
2621
|
return derived;
|
|
2460
|
-
|
|
2622
|
+
writeFileSync4(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
|
|
2461
2623
|
`, { mode: 292 });
|
|
2462
2624
|
}
|
|
2463
2625
|
return { stdout: "", exitCode: 0 };
|
|
2464
2626
|
} finally {
|
|
2465
|
-
|
|
2466
|
-
|
|
2627
|
+
rmSync4(key, { force: true });
|
|
2628
|
+
rmSync4(publicKey, { force: true });
|
|
2467
2629
|
}
|
|
2468
2630
|
}
|
|
2469
2631
|
if (operation.kind === "ensure-bootstrap-ssh-identity") {
|
|
2470
2632
|
const key = "/run/forgezero-bootstrap-ssh-key";
|
|
2471
2633
|
const generatedPublicKey = `${key}.pub`;
|
|
2472
2634
|
try {
|
|
2473
|
-
if (!
|
|
2474
|
-
|
|
2475
|
-
|
|
2635
|
+
if (!existsSync4(operation.credential) || lstatSync2(operation.credential).size < 1) {
|
|
2636
|
+
rmSync4(key, { force: true });
|
|
2637
|
+
rmSync4(generatedPublicKey, { force: true });
|
|
2476
2638
|
let result;
|
|
2477
2639
|
if (operation.source) {
|
|
2478
|
-
const source =
|
|
2640
|
+
const source = existsSync4(operation.source) ? lstatSync2(operation.source) : undefined;
|
|
2479
2641
|
if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
|
|
2480
2642
|
return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
|
|
2481
2643
|
}
|
|
2482
2644
|
copyFileSync(operation.source, key);
|
|
2483
|
-
|
|
2645
|
+
chmodSync2(key, 384);
|
|
2484
2646
|
} else {
|
|
2485
2647
|
result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-bootstrap-runner", "-f", key]);
|
|
2486
2648
|
if (result.exitCode !== 0)
|
|
@@ -2493,48 +2655,48 @@ var runProvisionOperation = async (operation) => {
|
|
|
2493
2655
|
result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
|
|
2494
2656
|
if (result.exitCode !== 0)
|
|
2495
2657
|
return result;
|
|
2496
|
-
|
|
2658
|
+
chmodSync2(operation.credential, 256);
|
|
2497
2659
|
}
|
|
2498
|
-
if (!
|
|
2499
|
-
if (!
|
|
2660
|
+
if (!existsSync4(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
|
|
2661
|
+
if (!existsSync4(key)) {
|
|
2500
2662
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
|
|
2501
2663
|
if (decrypted.exitCode !== 0)
|
|
2502
2664
|
return decrypted;
|
|
2503
|
-
|
|
2665
|
+
chmodSync2(key, 384);
|
|
2504
2666
|
}
|
|
2505
2667
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
2506
2668
|
if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
|
|
2507
2669
|
return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
|
|
2508
2670
|
}
|
|
2509
|
-
|
|
2510
|
-
|
|
2671
|
+
mkdirSync4(dirname5(operation.publicKey), { recursive: true, mode: 493 });
|
|
2672
|
+
writeFileSync4(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
|
|
2511
2673
|
`, { mode: 292 });
|
|
2512
|
-
|
|
2674
|
+
chmodSync2(operation.publicKey, 292);
|
|
2513
2675
|
}
|
|
2514
2676
|
if (operation.source)
|
|
2515
|
-
|
|
2677
|
+
rmSync4(operation.source, { force: true });
|
|
2516
2678
|
return { stdout: "", exitCode: 0 };
|
|
2517
2679
|
} finally {
|
|
2518
|
-
|
|
2519
|
-
|
|
2680
|
+
rmSync4(key, { force: true });
|
|
2681
|
+
rmSync4(generatedPublicKey, { force: true });
|
|
2520
2682
|
}
|
|
2521
2683
|
}
|
|
2522
2684
|
if (operation.kind === "ensure-enrolment") {
|
|
2523
|
-
if (
|
|
2685
|
+
if (existsSync4(operation.state) && lstatSync2(operation.state).size > 0 || existsSync4(operation.credential) && lstatSync2(operation.credential).size > 0)
|
|
2524
2686
|
return { stdout: "", exitCode: 0 };
|
|
2525
|
-
if (!
|
|
2687
|
+
if (!existsSync4(operation.source))
|
|
2526
2688
|
return { stdout: "enrolment source is missing", exitCode: 1 };
|
|
2527
2689
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
|
|
2528
2690
|
if (result.exitCode === 0) {
|
|
2529
|
-
|
|
2530
|
-
|
|
2691
|
+
chmodSync2(operation.credential, 256);
|
|
2692
|
+
rmSync4(operation.source, { force: true });
|
|
2531
2693
|
}
|
|
2532
2694
|
return result;
|
|
2533
2695
|
}
|
|
2534
2696
|
if (operation.kind === "wait-socket") {
|
|
2535
2697
|
for (let attempt = 0;attempt < operation.attempts; attempt += 1) {
|
|
2536
2698
|
try {
|
|
2537
|
-
if (
|
|
2699
|
+
if (lstatSync2(operation.path).isSocket())
|
|
2538
2700
|
return { stdout: "", exitCode: 0 };
|
|
2539
2701
|
} catch {}
|
|
2540
2702
|
await Bun.sleep(operation.intervalMs);
|
|
@@ -2542,7 +2704,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
2542
2704
|
return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
|
|
2543
2705
|
}
|
|
2544
2706
|
if (operation.kind === "verify-file")
|
|
2545
|
-
return
|
|
2707
|
+
return existsSync4(operation.path) && lstatSync2(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
|
|
2546
2708
|
if (operation.kind === "verify-egress") {
|
|
2547
2709
|
const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
|
|
2548
2710
|
if (active.exitCode !== 0)
|
|
@@ -2564,25 +2726,25 @@ var runProvisionOperation = async (operation) => {
|
|
|
2564
2726
|
}
|
|
2565
2727
|
}
|
|
2566
2728
|
if (operation.kind === "install-warp") {
|
|
2567
|
-
const os =
|
|
2729
|
+
const os = readFileSync4("/etc/os-release", "utf8");
|
|
2568
2730
|
if (!/^ID=ubuntu$/m.test(os) || !/^VERSION_ID="?26\.04"?$/m.test(os))
|
|
2569
2731
|
return { stdout: "unsupported WARP host OS", exitCode: 1 };
|
|
2570
2732
|
const response = await fetch("https://pkg.cloudflareclient.com/pubkey.gpg", { signal: AbortSignal.timeout(30000) });
|
|
2571
2733
|
if (!response.ok)
|
|
2572
2734
|
return { stdout: `WARP key HTTP ${response.status}`, exitCode: 1 };
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2735
|
+
mkdirSync4("/usr/share/keyrings", { recursive: true, mode: 493 });
|
|
2736
|
+
mkdirSync4("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
|
|
2737
|
+
mkdirSync4("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
|
|
2576
2738
|
const key = "/run/cloudflare-warp-key.gpg";
|
|
2577
|
-
|
|
2739
|
+
writeFileSync4(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
|
|
2578
2740
|
let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
|
|
2579
|
-
|
|
2741
|
+
rmSync4(key, { force: true });
|
|
2580
2742
|
if (result.exitCode !== 0)
|
|
2581
2743
|
return result;
|
|
2582
2744
|
const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
|
|
2583
2745
|
if (!codename)
|
|
2584
2746
|
return { stdout: "Ubuntu codename missing", exitCode: 1 };
|
|
2585
|
-
|
|
2747
|
+
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
2748
|
`, { mode: 420 });
|
|
2587
2749
|
result = await fixed(["/usr/bin/apt-get", "update", "-qq"]);
|
|
2588
2750
|
return result.exitCode === 0 ? fixed(["/usr/bin/apt-get", "install", "-y", "cloudflare-warp"]) : result;
|
|
@@ -2597,7 +2759,7 @@ async function localRunner(operation) {
|
|
|
2597
2759
|
if (capability.kind === "version")
|
|
2598
2760
|
return fixed(capability.argv);
|
|
2599
2761
|
try {
|
|
2600
|
-
const metadata =
|
|
2762
|
+
const metadata = lstatSync2(capability.path);
|
|
2601
2763
|
const present = capability.nodeType === "directory" ? metadata.isDirectory() : true;
|
|
2602
2764
|
return { stdout: present ? `yes
|
|
2603
2765
|
` : `no
|
|
@@ -2611,10 +2773,10 @@ function planInstall(options) {
|
|
|
2611
2773
|
const { capabilities, ...unit } = options;
|
|
2612
2774
|
return planProvision({ ...unit, mode: modeFor(capabilities) });
|
|
2613
2775
|
}
|
|
2614
|
-
async function applyPlan(plan,
|
|
2776
|
+
async function applyPlan(plan, run2) {
|
|
2615
2777
|
const transcript = [];
|
|
2616
2778
|
for (const step2 of plan.steps) {
|
|
2617
|
-
const result = await
|
|
2779
|
+
const result = await run2(step2.operation);
|
|
2618
2780
|
transcript.push({ label: step2.label, command: step2.command, exitCode: result.exitCode });
|
|
2619
2781
|
if (result.exitCode !== 0 && !step2.optional) {
|
|
2620
2782
|
throw new Error(`${step2.label} failed (exit ${result.exitCode}): ${step2.command}`);
|
|
@@ -2708,8 +2870,6 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
2708
2870
|
databaseUser: input.databaseUser,
|
|
2709
2871
|
sharedDirectory: input.sharedDirectory,
|
|
2710
2872
|
seedSyncEpoch: input.seedSyncEpoch,
|
|
2711
|
-
repository: input.repository,
|
|
2712
|
-
branch: input.branch,
|
|
2713
2873
|
deployProfile: input.deployProfile
|
|
2714
2874
|
}))
|
|
2715
2875
|
safeAtom(name, value);
|
|
@@ -2792,8 +2952,6 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
2792
2952
|
FZ_AGENT_OTLP_ENDPOINT: value.agentOtlpEndpoint,
|
|
2793
2953
|
FZ_CUSTODIAN_EMAIL: value.custodianEmail ?? "",
|
|
2794
2954
|
FZ_PROFILE: value.deployProfile,
|
|
2795
|
-
FZ_REPO: value.repository,
|
|
2796
|
-
FZ_BRANCH: value.branch,
|
|
2797
2955
|
FZ_EMAIL_PROVIDER: value.email?.provider ?? "",
|
|
2798
2956
|
FZ_SMTP_HOST: value.email?.provider === "smtp" ? value.email.host : "",
|
|
2799
2957
|
FZ_SMTP_PORT: value.email?.provider === "smtp" ? String(value.email.port) : "",
|
|
@@ -3004,7 +3162,7 @@ var EXTRACT = "/run/forgezero-otelcol-release";
|
|
|
3004
3162
|
var BINARY = "/usr/local/lib/forgezero/otelcol/otelcol";
|
|
3005
3163
|
var CONFIG = "/etc/forgezero/otelcol.yaml";
|
|
3006
3164
|
var UNIT = `/etc/systemd/system/${FORGEZERO_OTEL_COLLECTOR_UNIT}`;
|
|
3007
|
-
var
|
|
3165
|
+
var checked2 = async (host, argv) => {
|
|
3008
3166
|
const result = await host.exec(argv);
|
|
3009
3167
|
const output = `${result.output ?? result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
3010
3168
|
if (result.exitCode !== 0)
|
|
@@ -3086,7 +3244,7 @@ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
|
|
|
3086
3244
|
});
|
|
3087
3245
|
const versionOutput = `${observed.output ?? observed.stdout ?? ""}${observed.stderr ?? ""}`;
|
|
3088
3246
|
if (observed.exitCode !== 0 || !versionOutput.includes(`otelcol version ${FORGEZERO_OTEL_COLLECTOR_VERSION}`)) {
|
|
3089
|
-
await
|
|
3247
|
+
await checked2(host, [
|
|
3090
3248
|
"/usr/bin/curl",
|
|
3091
3249
|
"--fail",
|
|
3092
3250
|
"--silent",
|
|
@@ -3098,19 +3256,19 @@ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
|
|
|
3098
3256
|
ARCHIVE,
|
|
3099
3257
|
`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
3258
|
]);
|
|
3101
|
-
const digest = (await
|
|
3259
|
+
const digest = (await checked2(host, ["/usr/bin/sha256sum", ARCHIVE])).split(/\s+/)[0];
|
|
3102
3260
|
if (digest !== FORGEZERO_OTEL_COLLECTOR_SHA256)
|
|
3103
3261
|
throw new Error("OTLP collector archive checksum mismatch");
|
|
3104
|
-
await
|
|
3105
|
-
await
|
|
3106
|
-
await
|
|
3107
|
-
await
|
|
3262
|
+
await checked2(host, ["/usr/bin/install", "-d", "-m", "0755", "/usr/local/lib/forgezero/otelcol", EXTRACT]);
|
|
3263
|
+
await checked2(host, ["/usr/bin/tar", "-xzf", ARCHIVE, "-C", EXTRACT, "otelcol"]);
|
|
3264
|
+
await checked2(host, ["/usr/bin/install", "-m", "0755", `${EXTRACT}/otelcol`, BINARY]);
|
|
3265
|
+
await checked2(host, ["/usr/bin/rm", "-f", ARCHIVE, `${EXTRACT}/otelcol`]);
|
|
3108
3266
|
}
|
|
3109
3267
|
if ((await host.exec(["/usr/bin/getent", "group", "forgezero-otel"])).exitCode !== 0) {
|
|
3110
|
-
await
|
|
3268
|
+
await checked2(host, ["/usr/sbin/groupadd", "--system", "forgezero-otel"]);
|
|
3111
3269
|
}
|
|
3112
3270
|
if ((await host.exec(["/usr/bin/id", "forgezero-otel"])).exitCode !== 0) {
|
|
3113
|
-
await
|
|
3271
|
+
await checked2(host, [
|
|
3114
3272
|
"/usr/sbin/useradd",
|
|
3115
3273
|
"--system",
|
|
3116
3274
|
"--no-create-home",
|
|
@@ -3123,8 +3281,8 @@ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
|
|
|
3123
3281
|
}
|
|
3124
3282
|
host.write(CONFIG, rendered.config, 420);
|
|
3125
3283
|
host.write(UNIT, rendered.unit, 420);
|
|
3126
|
-
await
|
|
3127
|
-
await
|
|
3284
|
+
await checked2(host, ["/usr/bin/systemctl", "daemon-reload"]);
|
|
3285
|
+
await checked2(host, ["/usr/bin/systemctl", "enable", "--now", FORGEZERO_OTEL_COLLECTOR_UNIT]);
|
|
3128
3286
|
let ready = false;
|
|
3129
3287
|
for (let attempt = 0;attempt < 100; attempt += 1) {
|
|
3130
3288
|
const probe = await host.exec([
|
|
@@ -3253,7 +3411,7 @@ var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
|
|
|
3253
3411
|
var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
|
|
3254
3412
|
var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
|
|
3255
3413
|
var BOOTSTRAP_SSH_PUBLIC_KEY = "/etc/forgezero/bootstrap/runner.pub";
|
|
3256
|
-
var
|
|
3414
|
+
var BOOTSTRAP_RELEASE_EVIDENCE = "/var/lib/forgezero/bootstrap-release.json";
|
|
3257
3415
|
var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
|
|
3258
3416
|
var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
|
|
3259
3417
|
var CONTROL_SOCKET = "/run/forgezero/control.sock";
|
|
@@ -3326,6 +3484,9 @@ function validateBootstrapConfig(value) {
|
|
|
3326
3484
|
throw new Error("Cloudflare Mesh/WARP requires a node-specific Cloudflare handoff");
|
|
3327
3485
|
}
|
|
3328
3486
|
if (value.kind === "enrolled-compute") {
|
|
3487
|
+
if (value.gitDeployKey !== undefined && typeof value.gitDeployKey !== "boolean") {
|
|
3488
|
+
throw new Error("gitDeployKey must be boolean");
|
|
3489
|
+
}
|
|
3329
3490
|
if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(value.realm))
|
|
3330
3491
|
throw new Error("tenant realm is malformed");
|
|
3331
3492
|
if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
|
|
@@ -3351,6 +3512,9 @@ function validateBootstrapConfig(value) {
|
|
|
3351
3512
|
throw new Error("unsupported platform software profile");
|
|
3352
3513
|
if (!["production", "development"].includes(value.environment))
|
|
3353
3514
|
throw new Error("platform environment must be production or development");
|
|
3515
|
+
if (!value.bootstrapBundle || ![value.bootstrapBundle.bundleFile, value.bootstrapBundle.manifestFile].every((path) => typeof path === "string" && path.startsWith("/") && !/[\r\n\0:]/.test(path))) {
|
|
3516
|
+
throw new Error("platform bootstrap requires absolute bundle and manifest paths");
|
|
3517
|
+
}
|
|
3354
3518
|
let api;
|
|
3355
3519
|
try {
|
|
3356
3520
|
api = new URL(value.apiUrl);
|
|
@@ -3383,7 +3547,7 @@ function validateBootstrapConfig(value) {
|
|
|
3383
3547
|
if (runtime.otlpCollectorUnit !== FORGEZERO_OTEL_COLLECTOR_UNIT) {
|
|
3384
3548
|
throw new Error(`platform bootstrap requires ${FORGEZERO_OTEL_COLLECTOR_UNIT}`);
|
|
3385
3549
|
}
|
|
3386
|
-
if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.
|
|
3550
|
+
if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.databaseCoordinators.join(",") !== write.join(",")) {
|
|
3387
3551
|
throw new Error("platform runtime coordinates disagree with immutable bootstrap coordinates");
|
|
3388
3552
|
}
|
|
3389
3553
|
if (runtime.deployProfile !== value.environment)
|
|
@@ -3413,6 +3577,7 @@ function planBootstrap(input, initialized = false) {
|
|
|
3413
3577
|
] : [
|
|
3414
3578
|
...config.firewall.enabled ? [{ id: "ufw", version: "ubuntu-26.04" }] : [],
|
|
3415
3579
|
{ id: "bun", version: "1.3.14" },
|
|
3580
|
+
{ id: "git", version: "ubuntu-26.04" },
|
|
3416
3581
|
{ id: "nginx", version: "ubuntu-26.04" },
|
|
3417
3582
|
...platformBootstrapRunner(config) ? [{ id: "openssh-client", version: "ubuntu-26.04" }] : [],
|
|
3418
3583
|
...config.profile === "platform-api" ? [] : [{ id: "arangodb", version: "3.11.14" }],
|
|
@@ -3441,7 +3606,7 @@ function planBootstrap(input, initialized = false) {
|
|
|
3441
3606
|
]
|
|
3442
3607
|
};
|
|
3443
3608
|
}
|
|
3444
|
-
var
|
|
3609
|
+
var checked3 = async (host, argv, label, options) => {
|
|
3445
3610
|
const result = await host.exec(argv, options);
|
|
3446
3611
|
if (result.exitCode !== 0)
|
|
3447
3612
|
throw new Error(`${label} failed: ${result.output.trim()}`);
|
|
@@ -3622,6 +3787,37 @@ async function seal(host, name, destination2, value) {
|
|
|
3622
3787
|
if (result.exitCode !== 0)
|
|
3623
3788
|
throw new Error(`could not seal ${name}: ${result.output.trim()}`);
|
|
3624
3789
|
}
|
|
3790
|
+
async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
|
|
3791
|
+
const manifestMetadata = host.inspect?.(config.bootstrapBundle.manifestFile);
|
|
3792
|
+
const bundleMetadata = host.inspect?.(config.bootstrapBundle.bundleFile);
|
|
3793
|
+
if (manifestMetadata && (!manifestMetadata.regular || manifestMetadata.symbolic || manifestMetadata.uid !== 0 || manifestMetadata.links !== 1 || (manifestMetadata.mode & 63) !== 0 || manifestMetadata.size > 16 * 1024)) {
|
|
3794
|
+
throw new Error("bootstrap bundle manifest must be a root-owned owner-only regular file");
|
|
3795
|
+
}
|
|
3796
|
+
if (bundleMetadata && (!bundleMetadata.regular || bundleMetadata.symbolic || bundleMetadata.uid !== 0 || bundleMetadata.links !== 1 || (bundleMetadata.mode & 63) !== 0 || bundleMetadata.size < 1 || bundleMetadata.size > 512 * 1024 * 1024)) {
|
|
3797
|
+
throw new Error("bootstrap bundle must be a root-owned owner-only regular file within 512 MiB");
|
|
3798
|
+
}
|
|
3799
|
+
if (!host.exists(config.bootstrapBundle.bundleFile) || !host.exists(config.bootstrapBundle.manifestFile)) {
|
|
3800
|
+
throw new Error("attended bootstrap bundle and manifest are required for release generation one");
|
|
3801
|
+
}
|
|
3802
|
+
let parsed;
|
|
3803
|
+
try {
|
|
3804
|
+
parsed = JSON.parse(host.read(config.bootstrapBundle.manifestFile));
|
|
3805
|
+
} catch {
|
|
3806
|
+
throw new Error("bootstrap bundle manifest is not valid JSON");
|
|
3807
|
+
}
|
|
3808
|
+
const manifest = parseBootstrapBundleManifest(parsed);
|
|
3809
|
+
const expectedBranch = config.environment === "production" ? "main" : "dev";
|
|
3810
|
+
if (manifest.branch !== expectedBranch || bundleMetadata && bundleMetadata.size !== manifest.bytes) {
|
|
3811
|
+
throw new Error("bootstrap bundle manifest disagrees with the selected platform environment");
|
|
3812
|
+
}
|
|
3813
|
+
const digest = (await checked3(host, ["/usr/bin/sha256sum", config.bootstrapBundle.bundleFile], "bootstrap bundle digest")).trim().split(/\s+/)[0];
|
|
3814
|
+
if (digest !== manifest.sha256)
|
|
3815
|
+
throw new Error("bootstrap bundle digest does not match its manifest");
|
|
3816
|
+
if (verifyGit) {
|
|
3817
|
+
await checked3(host, ["/usr/bin/git", "bundle", "verify", config.bootstrapBundle.bundleFile], "bootstrap Git bundle verification");
|
|
3818
|
+
}
|
|
3819
|
+
return manifest;
|
|
3820
|
+
}
|
|
3625
3821
|
function bootstrapIdentity(config) {
|
|
3626
3822
|
if (config.kind === "enrolled-compute")
|
|
3627
3823
|
return {
|
|
@@ -3648,8 +3844,7 @@ function bootstrapIdentity(config) {
|
|
|
3648
3844
|
computeReference: config.computeReference,
|
|
3649
3845
|
nodeHostname: config.nodeHostname,
|
|
3650
3846
|
apiUrl: config.apiUrl,
|
|
3651
|
-
|
|
3652
|
-
branch: config.branch,
|
|
3847
|
+
bootstrapBundle: config.bootstrapBundle,
|
|
3653
3848
|
deployRoot: config.deployRoot ?? "/opt/forgezero",
|
|
3654
3849
|
telemetryEndpoint: config.telemetryEndpoint,
|
|
3655
3850
|
database: {
|
|
@@ -3679,7 +3874,7 @@ function bootstrapIdentity(config) {
|
|
|
3679
3874
|
};
|
|
3680
3875
|
}
|
|
3681
3876
|
function bootstrapIdentityDigest(config) {
|
|
3682
|
-
return
|
|
3877
|
+
return createHash2("sha256").update(JSON.stringify(bootstrapIdentity(config))).digest("hex");
|
|
3683
3878
|
}
|
|
3684
3879
|
function parseStoredState(raw) {
|
|
3685
3880
|
let value;
|
|
@@ -3729,34 +3924,6 @@ function bindBootstrapIntent(host, config) {
|
|
|
3729
3924
|
}
|
|
3730
3925
|
return identityDigest;
|
|
3731
3926
|
}
|
|
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
3927
|
function stateFor(config, cloudflare, previousCloudflareTunnelId) {
|
|
3761
3928
|
return `${JSON.stringify({
|
|
3762
3929
|
format: 2,
|
|
@@ -3910,6 +4077,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
3910
4077
|
let installed;
|
|
3911
4078
|
if (host.exists(STATE_PATH))
|
|
3912
4079
|
installed = parseStoredState(host.read(STATE_PATH));
|
|
4080
|
+
const bootstrapManifest = config.kind === "platform" && !host.exists(BOOTSTRAP_RELEASE_EVIDENCE) ? await verifyBootstrapBundleOnHost(host, config) : undefined;
|
|
3913
4081
|
let cloudflare;
|
|
3914
4082
|
if (config.cloudflareHandoff) {
|
|
3915
4083
|
if (host.exists(config.cloudflareHandoff.handoffFile)) {
|
|
@@ -3967,14 +4135,14 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
3967
4135
|
const platformPrivate = config.kind === "platform" ? (() => {
|
|
3968
4136
|
if (!secrets)
|
|
3969
4137
|
throw new Error("platform apply requires attended credentials on stdin");
|
|
3970
|
-
const
|
|
4138
|
+
const checked4 = validatePlatformBootstrapSecrets(config, secrets);
|
|
3971
4139
|
return {
|
|
3972
|
-
root:
|
|
3973
|
-
email:
|
|
3974
|
-
enrolmentToken:
|
|
3975
|
-
backup:
|
|
3976
|
-
cloudflareTunnelToken:
|
|
3977
|
-
cloudflareApiToken:
|
|
4140
|
+
root: checked4.clusterBootstrapCode,
|
|
4141
|
+
email: checked4.emailSecret,
|
|
4142
|
+
enrolmentToken: checked4.enrolmentToken,
|
|
4143
|
+
backup: checked4.backupS3Secret,
|
|
4144
|
+
cloudflareTunnelToken: checked4.cloudflareTunnelToken,
|
|
4145
|
+
cloudflareApiToken: checked4.cloudflareApiToken
|
|
3978
4146
|
};
|
|
3979
4147
|
})() : undefined;
|
|
3980
4148
|
const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
|
|
@@ -4019,18 +4187,21 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4019
4187
|
} else
|
|
4020
4188
|
await host.ensureSoftware(plan.software);
|
|
4021
4189
|
if (config.kind === "platform" && config.firewall.enabled) {
|
|
4022
|
-
await
|
|
4023
|
-
await
|
|
4024
|
-
await
|
|
4190
|
+
await checked3(host, ["ufw", "--force", "default", "deny", "incoming"], "firewall inbound policy");
|
|
4191
|
+
await checked3(host, ["ufw", "--force", "default", "allow", "outgoing"], "firewall outbound policy");
|
|
4192
|
+
await checked3(host, ["ufw", "allow", `${config.firewall.sshPort}/tcp`], "firewall SSH rule");
|
|
4025
4193
|
for (const cidr of config.firewall.privateCidrs) {
|
|
4026
4194
|
if (config.database.role !== "none")
|
|
4027
|
-
await
|
|
4195
|
+
await checked3(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
|
|
4028
4196
|
for (const port of [config.runtime.bluePort, config.runtime.greenPort])
|
|
4029
|
-
await
|
|
4197
|
+
await checked3(host, ["ufw", "allow", "from", cidr, "to", "any", "port", String(port), "proto", "tcp"], "seed-mesh firewall rule");
|
|
4030
4198
|
}
|
|
4031
|
-
await
|
|
4199
|
+
await checked3(host, ["ufw", "--force", "enable"], "firewall activation");
|
|
4032
4200
|
await host.ensureSoftware(plan.software.filter(({ id }) => id !== "ufw"));
|
|
4033
4201
|
}
|
|
4202
|
+
if (config.kind === "platform" && bootstrapManifest) {
|
|
4203
|
+
await verifyBootstrapBundleOnHost(host, config, true);
|
|
4204
|
+
}
|
|
4034
4205
|
if (config.kind === "platform") {
|
|
4035
4206
|
const root = platformPrivate.root;
|
|
4036
4207
|
if (!host.exists(JWT_CREDENTIAL)) {
|
|
@@ -4085,8 +4256,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4085
4256
|
keepReleases: runtime.keepReleases,
|
|
4086
4257
|
drainDeadlineMs: runtime.environment.drainDeadlineMs
|
|
4087
4258
|
});
|
|
4088
|
-
await
|
|
4089
|
-
await
|
|
4259
|
+
await checked3(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
|
|
4260
|
+
await checked3(host, ["id", runtime.serviceUser], "existing API service account");
|
|
4090
4261
|
});
|
|
4091
4262
|
host.mkdir(runtime.environment.sharedDirectory, 488);
|
|
4092
4263
|
host.mkdir(runtime.slotsDirectory, 493);
|
|
@@ -4095,7 +4266,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4095
4266
|
host.write("/etc/forgezero/capacity.env", `FZ_CONCURRENCY_LIMIT=${runtime.environment.concurrencyLimit}
|
|
4096
4267
|
`, 420);
|
|
4097
4268
|
}
|
|
4098
|
-
await
|
|
4269
|
+
await checked3(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
|
|
4099
4270
|
host.write("/etc/systemd/system/forgezero@.service", units.template, 420);
|
|
4100
4271
|
host.write("/etc/systemd/system/forgezero@blue.service.d/port.conf", units.dropIns.blue, 420);
|
|
4101
4272
|
host.write("/etc/systemd/system/forgezero@green.service.d/port.conf", units.dropIns.green, 420);
|
|
@@ -4104,30 +4275,30 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4104
4275
|
host.write("/etc/forgezero/deploy.env", activation.environment, 420);
|
|
4105
4276
|
host.write("/etc/forgezero/deploy-activation.json", activation.helper, 384);
|
|
4106
4277
|
host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
|
|
4107
|
-
await
|
|
4278
|
+
await checked3(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
|
|
4108
4279
|
const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
|
|
4109
|
-
await
|
|
4110
|
-
const otlpStatus = (await
|
|
4280
|
+
await checked3(host, telemetry.unitCheck.argv, "OTLP collector supervision");
|
|
4281
|
+
const otlpStatus = (await checked3(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
|
|
4111
4282
|
if (!/^2\d\d$/.test(otlpStatus))
|
|
4112
4283
|
throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
|
|
4113
|
-
await
|
|
4114
|
-
await
|
|
4115
|
-
await
|
|
4284
|
+
await checked3(host, ["nginx", "-t"], "nginx configuration");
|
|
4285
|
+
await checked3(host, ["systemctl", "daemon-reload"], "systemd reload");
|
|
4286
|
+
await checked3(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
|
|
4116
4287
|
if (config.database.role === "master") {
|
|
4117
4288
|
const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
4118
4289
|
if (!host.exists(invite)) {
|
|
4119
|
-
host.write(invite, `plt_${
|
|
4290
|
+
host.write(invite, `plt_${randomBytes3(24).toString("hex")}
|
|
4120
4291
|
`, 384);
|
|
4121
|
-
await
|
|
4292
|
+
await checked3(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
|
|
4122
4293
|
}
|
|
4123
4294
|
}
|
|
4124
4295
|
if (config.database.role !== "none") {
|
|
4125
4296
|
host.mkdir("/var/lib/forgezero-cluster", 448);
|
|
4126
|
-
await
|
|
4297
|
+
await checked3(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
|
|
4127
4298
|
host.write("/etc/systemd/system/forgezero-db.service", databaseUnit(config), 420);
|
|
4128
4299
|
host.write("/etc/systemd/system/forgezero-db-verify.service", databaseVerifyUnit(config), 420);
|
|
4129
|
-
await
|
|
4130
|
-
await
|
|
4300
|
+
await checked3(host, ["systemctl", "daemon-reload"], "database unit reload");
|
|
4301
|
+
await checked3(host, ["systemctl", "enable", "--now", "forgezero-db.service", "forgezero-db-verify.service"], "database supervision");
|
|
4131
4302
|
const evidence = {
|
|
4132
4303
|
expectedMode: "default",
|
|
4133
4304
|
role: "COORDINATOR",
|
|
@@ -4138,15 +4309,30 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4138
4309
|
host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
|
|
4139
4310
|
`, 384);
|
|
4140
4311
|
}
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4312
|
+
if (bootstrapManifest) {
|
|
4313
|
+
await checked3(host, [
|
|
4314
|
+
"runuser",
|
|
4315
|
+
"-u",
|
|
4316
|
+
"forgezero-agent",
|
|
4317
|
+
"--",
|
|
4318
|
+
"/usr/local/bin/fz-agent",
|
|
4319
|
+
"deploy",
|
|
4320
|
+
`--revision=${bootstrapManifest.revision}`,
|
|
4321
|
+
...config.database.role === "master" ? ["--release-executor"] : []
|
|
4322
|
+
], "initial Agent deployment");
|
|
4323
|
+
host.write(BOOTSTRAP_RELEASE_EVIDENCE, `${JSON.stringify({
|
|
4324
|
+
format: 1,
|
|
4325
|
+
kind: "forgezero-bootstrap-release",
|
|
4326
|
+
revision: bootstrapManifest.revision,
|
|
4327
|
+
sha256: bootstrapManifest.sha256,
|
|
4328
|
+
branch: bootstrapManifest.branch,
|
|
4329
|
+
deployedAt: new Date().toISOString()
|
|
4330
|
+
}, null, 2)}
|
|
4331
|
+
`, 384);
|
|
4332
|
+
host.remove(config.bootstrapBundle.bundleFile);
|
|
4333
|
+
host.remove(config.bootstrapBundle.manifestFile);
|
|
4334
|
+
await host.installAgent(config);
|
|
4335
|
+
}
|
|
4150
4336
|
}
|
|
4151
4337
|
if (config.cloudflareHandoff) {
|
|
4152
4338
|
if (!host.exists(TUNNEL_CREDENTIAL) && connectorCapabilities) {
|
|
@@ -4155,8 +4341,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4155
4341
|
if (!host.exists(TUNNEL_CREDENTIAL))
|
|
4156
4342
|
throw new Error("sealed cloudflared connector credential is missing");
|
|
4157
4343
|
host.write("/etc/systemd/system/cloudflared.service", tunnelUnit(), 420);
|
|
4158
|
-
await
|
|
4159
|
-
await
|
|
4344
|
+
await checked3(host, ["systemctl", "daemon-reload"], "cloudflared unit reload");
|
|
4345
|
+
await checked3(host, ["systemctl", "enable", "--now", "cloudflared.service"], "cloudflared connector supervision");
|
|
4160
4346
|
const tunnelId = cloudflare?.tunnelId ?? installed?.cloudflareTunnelId ?? (config.kind === "platform" ? config.runtime.environment.cloudflare?.tunnelId : undefined);
|
|
4161
4347
|
if (!tunnelId)
|
|
4162
4348
|
throw new Error("Cloudflare tunnel identity is missing after handoff validation");
|
|
@@ -4166,8 +4352,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4166
4352
|
if (!host.exists(WARP_CONNECTOR_CREDENTIAL))
|
|
4167
4353
|
throw new Error("sealed Cloudflare Mesh connector credential is missing");
|
|
4168
4354
|
host.write("/etc/systemd/system/forgezero-mesh-config.service", meshConnectorUnit(), 420);
|
|
4169
|
-
await
|
|
4170
|
-
await
|
|
4355
|
+
await checked3(host, ["systemctl", "daemon-reload"], "Cloudflare Mesh unit reload");
|
|
4356
|
+
await checked3(host, ["systemctl", "enable", "--now", "warp-svc.service", "forgezero-mesh-config.service"], "Cloudflare Mesh connector supervision");
|
|
4171
4357
|
}
|
|
4172
4358
|
host.write(STATE_PATH, stateFor(config, cloudflare, installed?.cloudflareTunnelId), 384);
|
|
4173
4359
|
const status = await bootstrapStatus(host);
|
|
@@ -4212,8 +4398,7 @@ function strictBootstrapDocument(value) {
|
|
|
4212
4398
|
"computeReference",
|
|
4213
4399
|
"nodeHostname",
|
|
4214
4400
|
"apiUrl",
|
|
4215
|
-
"
|
|
4216
|
-
"branch",
|
|
4401
|
+
"bootstrapBundle",
|
|
4217
4402
|
"deployRoot",
|
|
4218
4403
|
"telemetryEndpoint",
|
|
4219
4404
|
"database",
|
|
@@ -4226,9 +4411,11 @@ function strictBootstrapDocument(value) {
|
|
|
4226
4411
|
"realm",
|
|
4227
4412
|
"software",
|
|
4228
4413
|
"deploymentCredentials",
|
|
4414
|
+
"gitDeployKey",
|
|
4229
4415
|
"bootstrapRunner"
|
|
4230
4416
|
], "bootstrap config");
|
|
4231
4417
|
if (root.kind === "platform") {
|
|
4418
|
+
exactKeys(root.bootstrapBundle, ["bundleFile", "manifestFile"], "bootstrap bundle config");
|
|
4232
4419
|
exactKeys(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
|
|
4233
4420
|
if (root.cloudflareHandoff !== undefined)
|
|
4234
4421
|
exactKeys(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
|
|
@@ -4270,8 +4457,6 @@ function strictBootstrapDocument(value) {
|
|
|
4270
4457
|
"agentOtlpEndpoint",
|
|
4271
4458
|
"custodianEmail",
|
|
4272
4459
|
"email",
|
|
4273
|
-
"repository",
|
|
4274
|
-
"branch",
|
|
4275
4460
|
"deployProfile",
|
|
4276
4461
|
"otlpFlushIntervalMs",
|
|
4277
4462
|
"otlpTraceSampleRatio",
|
|
@@ -4309,11 +4494,11 @@ function strictBootstrapDocument(value) {
|
|
|
4309
4494
|
return value;
|
|
4310
4495
|
}
|
|
4311
4496
|
function readBootstrapConfig(path) {
|
|
4312
|
-
const metadata =
|
|
4497
|
+
const metadata = lstatSync3(path);
|
|
4313
4498
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== (process.getuid?.() ?? metadata.uid) || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 64 * 1024) {
|
|
4314
4499
|
throw new Error("bootstrap config must be an owner-only regular file with one link and at most 64 KiB");
|
|
4315
4500
|
}
|
|
4316
|
-
return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(
|
|
4501
|
+
return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync5(path, "utf8"))));
|
|
4317
4502
|
}
|
|
4318
4503
|
function localBootstrapHost() {
|
|
4319
4504
|
const execute = async (argv, options = {}) => {
|
|
@@ -4331,19 +4516,19 @@ function localBootstrapHost() {
|
|
|
4331
4516
|
};
|
|
4332
4517
|
return {
|
|
4333
4518
|
uid: () => process.getuid?.() ?? -1,
|
|
4334
|
-
exists:
|
|
4335
|
-
read: (path) =>
|
|
4519
|
+
exists: existsSync5,
|
|
4520
|
+
read: (path) => readFileSync5(path, "utf8"),
|
|
4336
4521
|
write(path, content, mode) {
|
|
4337
|
-
|
|
4522
|
+
mkdirSync5(dirname6(path), { recursive: true, mode: 493 });
|
|
4338
4523
|
const temporary = `${path}.next.${process.pid}`;
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4524
|
+
writeFileSync5(temporary, content, { mode });
|
|
4525
|
+
chmodSync3(temporary, mode);
|
|
4526
|
+
renameSync5(temporary, path);
|
|
4342
4527
|
},
|
|
4343
|
-
mkdir: (path, mode) =>
|
|
4344
|
-
remove: (path) =>
|
|
4528
|
+
mkdir: (path, mode) => mkdirSync5(path, { recursive: true, mode }),
|
|
4529
|
+
remove: (path) => rmSync5(path, { force: true }),
|
|
4345
4530
|
inspect(path) {
|
|
4346
|
-
const value =
|
|
4531
|
+
const value = lstatSync3(path);
|
|
4347
4532
|
return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
|
|
4348
4533
|
},
|
|
4349
4534
|
exec: execute,
|
|
@@ -4365,7 +4550,12 @@ function localBootstrapHost() {
|
|
|
4365
4550
|
async installAgent(config) {
|
|
4366
4551
|
const capabilities = await readCapabilities(localRunner);
|
|
4367
4552
|
const deployRoot = config.deployRoot ?? "/opt/forgezero";
|
|
4368
|
-
const hasBinding =
|
|
4553
|
+
const hasBinding = existsSync5("/var/lib/forgezero/enrolment.json");
|
|
4554
|
+
const initialBundle = config.kind === "platform" && !existsSync5(BOOTSTRAP_RELEASE_EVIDENCE) ? {
|
|
4555
|
+
path: config.bootstrapBundle.bundleFile,
|
|
4556
|
+
manifestPath: config.bootstrapBundle.manifestFile,
|
|
4557
|
+
manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync5(config.bootstrapBundle.manifestFile, "utf8")))
|
|
4558
|
+
} : undefined;
|
|
4369
4559
|
if (config.kind === "platform") {
|
|
4370
4560
|
const lifecycle = config.database.role === "none" ? {
|
|
4371
4561
|
apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
|
|
@@ -4377,8 +4567,8 @@ function localBootstrapHost() {
|
|
|
4377
4567
|
databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
|
|
4378
4568
|
databasePorts: [8529]
|
|
4379
4569
|
};
|
|
4380
|
-
|
|
4381
|
-
|
|
4570
|
+
mkdirSync5(dirname6(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
|
|
4571
|
+
writeFileSync5(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
|
|
4382
4572
|
`, { mode: 256 });
|
|
4383
4573
|
}
|
|
4384
4574
|
const plan = planInstall({
|
|
@@ -4386,15 +4576,17 @@ function localBootstrapHost() {
|
|
|
4386
4576
|
socketPath: DEFAULT_SOCKET2,
|
|
4387
4577
|
seedPath: "/var/lib/forgezero/node.seed",
|
|
4388
4578
|
controlSocketPath: "/run/forgezero/control.sock",
|
|
4389
|
-
repository: config.repository,
|
|
4390
|
-
branch: config.branch,
|
|
4579
|
+
repository: initialBundle?.path ?? (config.kind === "enrolled-compute" ? config.repository : undefined),
|
|
4580
|
+
branch: initialBundle?.manifest.branch ?? (config.kind === "enrolled-compute" ? config.branch : undefined),
|
|
4581
|
+
bootstrapBundlePath: initialBundle?.path,
|
|
4582
|
+
bootstrapBundleManifestPath: initialBundle?.manifestPath,
|
|
4391
4583
|
profile: config.kind === "platform" ? config.profile : config.profile,
|
|
4392
4584
|
deployRoot,
|
|
4393
4585
|
deploymentCredentials: config.deploymentCredentials,
|
|
4394
4586
|
publicApiUrl: config.apiUrl,
|
|
4395
|
-
gitCredentialPath: "/etc/forgezero/creds/git-deploy-key.cred",
|
|
4396
|
-
gitPublicKeyPath: "/etc/forgezero/git/deploy.pub",
|
|
4397
|
-
generateGitIdentity: true,
|
|
4587
|
+
gitCredentialPath: config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/creds/git-deploy-key.cred" : undefined,
|
|
4588
|
+
gitPublicKeyPath: config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/git/deploy.pub" : undefined,
|
|
4589
|
+
generateGitIdentity: config.kind === "enrolled-compute" && config.gitDeployKey === true,
|
|
4398
4590
|
pullDeployments: hasBinding,
|
|
4399
4591
|
pullMigrations: config.kind === "platform" && hasBinding,
|
|
4400
4592
|
pullBootstrap: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner),
|
|
@@ -4408,9 +4600,11 @@ function localBootstrapHost() {
|
|
|
4408
4600
|
telemetryEndpoint: config.telemetryEndpoint,
|
|
4409
4601
|
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
4410
4602
|
sourceBinPath: PACKAGED_AGENT_BIN,
|
|
4603
|
+
...existsSync5(ENROL_CREDENTIAL) || hasBinding ? {
|
|
4604
|
+
enrolTokenCredentialPath: existsSync5(ENROL_CREDENTIAL) ? ENROL_CREDENTIAL : undefined,
|
|
4605
|
+
enrolStatePath: "/var/lib/forgezero/enrolment.json"
|
|
4606
|
+
} : {},
|
|
4411
4607
|
...hasBinding ? {
|
|
4412
|
-
enrolTokenCredentialPath: ENROL_CREDENTIAL,
|
|
4413
|
-
enrolStatePath: "/var/lib/forgezero/enrolment.json",
|
|
4414
4608
|
apiUrl: config.apiUrl,
|
|
4415
4609
|
project: config.kind === "enrolled-compute" ? config.realm : "platform",
|
|
4416
4610
|
environment: config.kind === "enrolled-compute" ? undefined : config.environment,
|
|
@@ -4418,8 +4612,8 @@ function localBootstrapHost() {
|
|
|
4418
4612
|
} : {}
|
|
4419
4613
|
});
|
|
4420
4614
|
for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
|
|
4421
|
-
|
|
4422
|
-
|
|
4615
|
+
mkdirSync5(dirname6(unit.path), { recursive: true, mode: 493 });
|
|
4616
|
+
writeFileSync5(unit.path, unit.unit, { mode: 420 });
|
|
4423
4617
|
}
|
|
4424
4618
|
await applyPlan(plan, localRunner);
|
|
4425
4619
|
return plan;
|
|
@@ -4428,31 +4622,31 @@ function localBootstrapHost() {
|
|
|
4428
4622
|
}
|
|
4429
4623
|
|
|
4430
4624
|
// src/metal-bootstrap.ts
|
|
4431
|
-
import { createHash as
|
|
4625
|
+
import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
|
|
4432
4626
|
import {
|
|
4433
|
-
chmodSync as
|
|
4627
|
+
chmodSync as chmodSync4,
|
|
4434
4628
|
chownSync,
|
|
4435
4629
|
copyFileSync as copyFileSync2,
|
|
4436
|
-
existsSync as
|
|
4437
|
-
lstatSync as
|
|
4438
|
-
mkdirSync as
|
|
4439
|
-
readFileSync as
|
|
4630
|
+
existsSync as existsSync6,
|
|
4631
|
+
lstatSync as lstatSync4,
|
|
4632
|
+
mkdirSync as mkdirSync7,
|
|
4633
|
+
readFileSync as readFileSync6,
|
|
4440
4634
|
realpathSync as realpathSync4,
|
|
4441
|
-
renameSync as
|
|
4635
|
+
renameSync as renameSync6,
|
|
4442
4636
|
statSync,
|
|
4443
4637
|
symlinkSync as symlinkSync2,
|
|
4444
4638
|
unlinkSync,
|
|
4445
|
-
writeFileSync as
|
|
4639
|
+
writeFileSync as writeFileSync7
|
|
4446
4640
|
} from "fs";
|
|
4447
|
-
import { dirname as
|
|
4641
|
+
import { dirname as dirname8, isAbsolute as isAbsolute3, join as join5, resolve as resolve5 } from "path";
|
|
4448
4642
|
import { isIP as isIP5 } from "net";
|
|
4449
4643
|
|
|
4450
4644
|
// src/metal-isolation.ts
|
|
4451
|
-
import { mkdirSync as
|
|
4645
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
4452
4646
|
import { join as join4 } from "path";
|
|
4453
4647
|
|
|
4454
4648
|
// src/metal-provision.ts
|
|
4455
|
-
import { dirname as
|
|
4649
|
+
import { dirname as dirname7, isAbsolute as isAbsolute2, join as join3 } from "path";
|
|
4456
4650
|
import { isIP as isIP4 } from "net";
|
|
4457
4651
|
|
|
4458
4652
|
// src/ubuntu.ts
|
|
@@ -4501,7 +4695,7 @@ function validateMetalProfile(profile) {
|
|
|
4501
4695
|
if (!Number.isInteger(profile.addressStart) || !Number.isInteger(profile.addressEnd) || profile.addressStart < 2 || profile.addressEnd > 254 || profile.addressStart > profile.addressEnd)
|
|
4502
4696
|
throw new MetalProvisionError("invalid guest address range");
|
|
4503
4697
|
for (const path of [profile.stateDir, profile.seedDir, profile.unitDir]) {
|
|
4504
|
-
if (!
|
|
4698
|
+
if (!isAbsolute2(path))
|
|
4505
4699
|
throw new MetalProvisionError("metal paths must be absolute");
|
|
4506
4700
|
}
|
|
4507
4701
|
new URL(profile.apiUrl);
|
|
@@ -4609,14 +4803,14 @@ var defaultExec = async (argv) => {
|
|
|
4609
4803
|
]);
|
|
4610
4804
|
return { exitCode, stdout, stderr };
|
|
4611
4805
|
};
|
|
4612
|
-
var
|
|
4806
|
+
var checked4 = async (exec, argv) => {
|
|
4613
4807
|
const result = await exec(argv);
|
|
4614
4808
|
if (result.exitCode !== 0)
|
|
4615
4809
|
throw new Error(`${argv[0]} failed: ${(result.stderr || result.stdout).trim()}`);
|
|
4616
4810
|
return result;
|
|
4617
4811
|
};
|
|
4618
4812
|
var requireGuestsInSlice = async (exec) => {
|
|
4619
|
-
const active = await
|
|
4813
|
+
const active = await checked4(exec, [
|
|
4620
4814
|
"systemctl",
|
|
4621
4815
|
"list-units",
|
|
4622
4816
|
"--type=service",
|
|
@@ -4630,7 +4824,7 @@ var requireGuestsInSlice = async (exec) => {
|
|
|
4630
4824
|
const service = line.trim().split(/\s+/)[0];
|
|
4631
4825
|
if (!service)
|
|
4632
4826
|
continue;
|
|
4633
|
-
const cgroup = await
|
|
4827
|
+
const cgroup = await checked4(exec, ["systemctl", "show", "-p", "ControlGroup", "--value", service]);
|
|
4634
4828
|
if (!cgroup.stdout.trim().includes("/forgezero-guests.slice/")) {
|
|
4635
4829
|
throw new Error(`${service} must be drained and restarted into forgezero-guests.slice`);
|
|
4636
4830
|
}
|
|
@@ -4640,23 +4834,23 @@ async function applyMetalIsolation(profile, exec = defaultExec) {
|
|
|
4640
4834
|
validateMetalProfile(profile);
|
|
4641
4835
|
await requireGuestsInSlice(exec);
|
|
4642
4836
|
const unitDir = profile.unitDir;
|
|
4643
|
-
|
|
4644
|
-
|
|
4837
|
+
mkdirSync6(unitDir, { recursive: true });
|
|
4838
|
+
writeFileSync6(join4(unitDir, "forgezero-guests.slice"), metalGuestSliceUnit(profile), { mode: 420 });
|
|
4645
4839
|
for (const unit of ["system.slice", "user.slice"]) {
|
|
4646
4840
|
const directory = join4(unitDir, `${unit}.d`);
|
|
4647
|
-
|
|
4648
|
-
|
|
4841
|
+
mkdirSync6(directory, { recursive: true });
|
|
4842
|
+
writeFileSync6(join4(directory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "slice"), { mode: 420 });
|
|
4649
4843
|
}
|
|
4650
4844
|
const initDirectory = join4(unitDir, "init.scope.d");
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
await
|
|
4845
|
+
mkdirSync6(initDirectory, { recursive: true });
|
|
4846
|
+
writeFileSync6(join4(initDirectory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "scope"), { mode: 420 });
|
|
4847
|
+
await checked4(exec, ["systemctl", "daemon-reload"]);
|
|
4654
4848
|
await requireGuestsInSlice(exec);
|
|
4655
4849
|
const properties = [`AllowedCPUs=${profile.housekeepingCpus}`];
|
|
4656
4850
|
if (profile.housekeepingMemoryNodes)
|
|
4657
4851
|
properties.push(`AllowedMemoryNodes=${profile.housekeepingMemoryNodes}`);
|
|
4658
4852
|
for (const unit of ["system.slice", "user.slice", "init.scope"]) {
|
|
4659
|
-
await
|
|
4853
|
+
await checked4(exec, ["systemctl", "set-property", "--runtime", unit, ...properties]);
|
|
4660
4854
|
}
|
|
4661
4855
|
}
|
|
4662
4856
|
|
|
@@ -4768,7 +4962,7 @@ function validateMetalBootstrapConfig(config) {
|
|
|
4768
4962
|
throw new MetalBootstrapError("metal bootstrap uses fixed state, seed, and systemd unit directories");
|
|
4769
4963
|
}
|
|
4770
4964
|
const image = config.profile.images[Object.keys(config.profile.images)[0]];
|
|
4771
|
-
if (!image.path.startsWith("/var/lib/forgezero/images/") ||
|
|
4965
|
+
if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve5(image.path) !== image.path || /[\0\r\n]/.test(image.path)) {
|
|
4772
4966
|
throw new MetalBootstrapError("the pinned guest image must use the fixed image directory");
|
|
4773
4967
|
}
|
|
4774
4968
|
if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION) {
|
|
@@ -4803,10 +4997,10 @@ function validateMetalBootstrapConfig(config) {
|
|
|
4803
4997
|
return config;
|
|
4804
4998
|
}
|
|
4805
4999
|
function validateOwnerOnlyPath(path, requireRootOwner) {
|
|
4806
|
-
if (!
|
|
5000
|
+
if (!isAbsolute3(path) || resolve5(path) !== path || path.includes("/../")) {
|
|
4807
5001
|
throw new MetalBootstrapError("private bootstrap paths must be canonical absolute paths");
|
|
4808
5002
|
}
|
|
4809
|
-
const metadata =
|
|
5003
|
+
const metadata = lstatSync4(path);
|
|
4810
5004
|
if (!metadata.isFile() || metadata.isSymbolicLink() || realpathSync4(path) !== path) {
|
|
4811
5005
|
throw new MetalBootstrapError("private bootstrap path must be a regular non-symlink file");
|
|
4812
5006
|
}
|
|
@@ -4824,7 +5018,7 @@ function readMetalBootstrapConfig(path) {
|
|
|
4824
5018
|
throw new MetalBootstrapError("metal bootstrap config size is invalid");
|
|
4825
5019
|
let parsed;
|
|
4826
5020
|
try {
|
|
4827
|
-
parsed = JSON.parse(
|
|
5021
|
+
parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
4828
5022
|
} catch {
|
|
4829
5023
|
throw new MetalBootstrapError("metal bootstrap config is not valid JSON");
|
|
4830
5024
|
}
|
|
@@ -4857,15 +5051,15 @@ function planMetalBootstrap(config) {
|
|
|
4857
5051
|
};
|
|
4858
5052
|
}
|
|
4859
5053
|
var atomicWrite = (path, body, mode) => {
|
|
4860
|
-
|
|
5054
|
+
mkdirSync7(dirname8(path), { recursive: true, mode: 493 });
|
|
4861
5055
|
const temporary = `${path}.next-${process.pid}`;
|
|
4862
|
-
|
|
4863
|
-
|
|
5056
|
+
writeFileSync7(temporary, body, { mode, flag: "wx" });
|
|
5057
|
+
chmodSync4(temporary, mode);
|
|
4864
5058
|
chownSync(temporary, 0, 0);
|
|
4865
|
-
|
|
5059
|
+
renameSync6(temporary, path);
|
|
4866
5060
|
};
|
|
4867
5061
|
var validateAgentSourcePath = (source) => {
|
|
4868
|
-
if (!
|
|
5062
|
+
if (!isAbsolute3(source) || !lstatSync4(source).isFile() || lstatSync4(source).isSymbolicLink()) {
|
|
4869
5063
|
throw new MetalBootstrapError("published Agent source path must be an absolute regular non-symlink file");
|
|
4870
5064
|
}
|
|
4871
5065
|
};
|
|
@@ -5029,11 +5223,11 @@ WantedBy=multi-user.target
|
|
|
5029
5223
|
var installAgentBinary = (source, version) => {
|
|
5030
5224
|
validateAgentSourcePath(source);
|
|
5031
5225
|
const release = `/opt/forgezero/agent/versions/${version}/dist`;
|
|
5032
|
-
|
|
5226
|
+
mkdirSync7(release, { recursive: true, mode: 493 });
|
|
5033
5227
|
copyFileSync2(source, join5(release, "fz-agent.js"));
|
|
5034
|
-
|
|
5228
|
+
chmodSync4(join5(release, "fz-agent.js"), 493);
|
|
5035
5229
|
chownSync(join5(release, "fz-agent.js"), 0, 0);
|
|
5036
|
-
|
|
5230
|
+
mkdirSync7("/opt/forgezero/agent", { recursive: true, mode: 493 });
|
|
5037
5231
|
for (const [link, target] of [
|
|
5038
5232
|
["/opt/forgezero/agent/current.next", `versions/${version}`],
|
|
5039
5233
|
[AGENT_PATH, "/opt/forgezero/agent/current/dist/fz-agent.js"]
|
|
@@ -5043,7 +5237,7 @@ var installAgentBinary = (source, version) => {
|
|
|
5043
5237
|
} catch {}
|
|
5044
5238
|
symlinkSync2(target, link);
|
|
5045
5239
|
if (link.endsWith("current.next"))
|
|
5046
|
-
|
|
5240
|
+
renameSync6(link, "/opt/forgezero/agent/current");
|
|
5047
5241
|
}
|
|
5048
5242
|
};
|
|
5049
5243
|
var preflight = async (config, exec) => {
|
|
@@ -5065,10 +5259,10 @@ var preflight = async (config, exec) => {
|
|
|
5065
5259
|
]);
|
|
5066
5260
|
await runChecked(exec, ["/usr/sbin/vgs", config.profile.volumeGroup]);
|
|
5067
5261
|
await runChecked(exec, ["/usr/sbin/ip", "link", "show", config.profile.bridge]);
|
|
5068
|
-
if (!
|
|
5262
|
+
if (!existsSync6("/dev/kvm"))
|
|
5069
5263
|
throw new MetalBootstrapError("/dev/kvm is required");
|
|
5070
5264
|
if (config.profile.confidential) {
|
|
5071
|
-
if (!
|
|
5265
|
+
if (!existsSync6("/dev/sev"))
|
|
5072
5266
|
throw new MetalBootstrapError("/dev/sev is required by the confidential profile");
|
|
5073
5267
|
await runChecked(exec, ["/usr/bin/qemu-system-x86_64", "-object", "sev-snp-guest,help"]);
|
|
5074
5268
|
}
|
|
@@ -5080,16 +5274,16 @@ var assertSupportedMetalHost = () => {
|
|
|
5080
5274
|
if (process.platform !== "linux" || process.arch !== "x64") {
|
|
5081
5275
|
throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
|
|
5082
5276
|
}
|
|
5083
|
-
const release =
|
|
5277
|
+
const release = readFileSync6("/etc/os-release", "utf8");
|
|
5084
5278
|
if (!/^ID=ubuntu$/m.test(release) || !/^VERSION_ID="?26\.04"?$/m.test(release)) {
|
|
5085
5279
|
throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
|
|
5086
5280
|
}
|
|
5087
5281
|
};
|
|
5088
5282
|
var ensurePinnedGuestImage = async (config, exec) => {
|
|
5089
5283
|
const image = config.profile.images[SUPPORTED_GUEST_IMAGE.key];
|
|
5090
|
-
if (
|
|
5284
|
+
if (existsSync6(image.path))
|
|
5091
5285
|
return;
|
|
5092
|
-
|
|
5286
|
+
mkdirSync7(dirname8(image.path), { recursive: true, mode: 493 });
|
|
5093
5287
|
const temporary = `${image.path}.next-${process.pid}`;
|
|
5094
5288
|
try {
|
|
5095
5289
|
await runChecked(exec, [
|
|
@@ -5106,9 +5300,9 @@ var ensurePinnedGuestImage = async (config, exec) => {
|
|
|
5106
5300
|
const digest = (await runChecked(exec, ["/usr/bin/sha256sum", temporary])).stdout.split(/\s+/)[0];
|
|
5107
5301
|
if (digest !== image.sha256)
|
|
5108
5302
|
throw new MetalBootstrapError("downloaded guest image digest mismatch");
|
|
5109
|
-
|
|
5303
|
+
chmodSync4(temporary, 292);
|
|
5110
5304
|
chownSync(temporary, 0, 0);
|
|
5111
|
-
|
|
5305
|
+
renameSync6(temporary, image.path);
|
|
5112
5306
|
} catch (cause) {
|
|
5113
5307
|
try {
|
|
5114
5308
|
unlinkSync(temporary);
|
|
@@ -5121,11 +5315,11 @@ async function applyMetalBootstrap(config, options) {
|
|
|
5121
5315
|
if ((options.getuid ?? process.getuid)?.() !== 0)
|
|
5122
5316
|
throw new MetalBootstrapError("fz bootstrap metal --apply must run as root");
|
|
5123
5317
|
assertSupportedMetalHost();
|
|
5124
|
-
if (
|
|
5318
|
+
if (existsSync6(STATE_PATH2) && !options.repair)
|
|
5125
5319
|
throw new MetalBootstrapError("metal host is already initialized; use explicit repair");
|
|
5126
5320
|
if (config.agentSeedFile)
|
|
5127
5321
|
validateOwnerOnlyPath(config.agentSeedFile, true);
|
|
5128
|
-
if (config.agentSeedFile &&
|
|
5322
|
+
if (config.agentSeedFile && existsSync6(SEED_CREDENTIAL_PATH)) {
|
|
5129
5323
|
throw new MetalBootstrapError("repair refuses replacement seed material while the sealed metal identity exists");
|
|
5130
5324
|
}
|
|
5131
5325
|
validateAgentSourcePath(options.agentSourcePath);
|
|
@@ -5152,9 +5346,9 @@ async function applyMetalBootstrap(config, options) {
|
|
|
5152
5346
|
await preflight(config, exec);
|
|
5153
5347
|
await ensureAccount(exec);
|
|
5154
5348
|
installAgentBinary(options.agentSourcePath, config.profile.agentVersion);
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5349
|
+
mkdirSync7("/etc/forgezero/creds", { recursive: true, mode: 448 });
|
|
5350
|
+
mkdirSync7(config.profile.stateDir, { recursive: true, mode: 448 });
|
|
5351
|
+
mkdirSync7(config.profile.seedDir, { recursive: true, mode: 448 });
|
|
5158
5352
|
const persistedProfile = {
|
|
5159
5353
|
...config.profile,
|
|
5160
5354
|
metalHostname: config.metalHostname,
|
|
@@ -5163,13 +5357,13 @@ async function applyMetalBootstrap(config, options) {
|
|
|
5163
5357
|
};
|
|
5164
5358
|
atomicWrite(PROFILE_PATH, `${JSON.stringify(persistedProfile, null, 2)}
|
|
5165
5359
|
`, 384);
|
|
5166
|
-
if (!
|
|
5167
|
-
const seed = config.agentSeedFile ?
|
|
5360
|
+
if (!existsSync6(SEED_CREDENTIAL_PATH)) {
|
|
5361
|
+
const seed = config.agentSeedFile ? readFileSync6(config.agentSeedFile, "utf8").trim() : randomBytes4(32).toString("base64url");
|
|
5168
5362
|
if (seed.length < 32 || /[\0\r\n]/.test(seed))
|
|
5169
5363
|
throw new MetalBootstrapError("metal Agent seed is invalid");
|
|
5170
5364
|
await runChecked(exec, ["/usr/bin/systemd-creds", "encrypt", "--name=metal-agent-seed", "-", SEED_CREDENTIAL_PATH], `${seed}
|
|
5171
5365
|
`);
|
|
5172
|
-
|
|
5366
|
+
chmodSync4(SEED_CREDENTIAL_PATH, 256);
|
|
5173
5367
|
chownSync(SEED_CREDENTIAL_PATH, 0, 0);
|
|
5174
5368
|
if (config.agentSeedFile)
|
|
5175
5369
|
unlinkSync(config.agentSeedFile);
|
|
@@ -5218,7 +5412,7 @@ async function applyMetalBootstrap(config, options) {
|
|
|
5218
5412
|
initializedAt: new Date().toISOString(),
|
|
5219
5413
|
role: "metal",
|
|
5220
5414
|
metalHostname: config.metalHostname,
|
|
5221
|
-
profileSha256:
|
|
5415
|
+
profileSha256: createHash3("sha256").update(JSON.stringify(config.profile)).digest("hex")
|
|
5222
5416
|
};
|
|
5223
5417
|
atomicWrite(STATE_PATH2, `${JSON.stringify(state, null, 2)}
|
|
5224
5418
|
`, 384);
|
|
@@ -5226,7 +5420,7 @@ async function applyMetalBootstrap(config, options) {
|
|
|
5226
5420
|
}
|
|
5227
5421
|
var socketReady = (path) => {
|
|
5228
5422
|
try {
|
|
5229
|
-
return
|
|
5423
|
+
return lstatSync4(path).isSocket();
|
|
5230
5424
|
} catch {
|
|
5231
5425
|
return false;
|
|
5232
5426
|
}
|
|
@@ -5235,17 +5429,17 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
5235
5429
|
const problems = [];
|
|
5236
5430
|
let profileValid = false, imageVerified = false, metalHostname, profileSha256;
|
|
5237
5431
|
let profileMode = null;
|
|
5238
|
-
if (
|
|
5432
|
+
if (existsSync6(PROFILE_PATH)) {
|
|
5239
5433
|
try {
|
|
5240
5434
|
const metadata = statSync(PROFILE_PATH);
|
|
5241
5435
|
profileMode = metadata.mode & 511;
|
|
5242
5436
|
if (profileMode !== 384 || metadata.uid !== 0)
|
|
5243
5437
|
problems.push("metal profile is not root-owned mode 0600");
|
|
5244
|
-
const persisted = JSON.parse(
|
|
5438
|
+
const persisted = JSON.parse(readFileSync6(PROFILE_PATH, "utf8"));
|
|
5245
5439
|
const { metalHostname: profileHostname, hostTelemetryEndpoint, hostTelemetryUnit, ...profile } = persisted;
|
|
5246
5440
|
validateMetalProfile(profile);
|
|
5247
5441
|
profileValid = true;
|
|
5248
|
-
profileSha256 =
|
|
5442
|
+
profileSha256 = createHash3("sha256").update(JSON.stringify(profile)).digest("hex");
|
|
5249
5443
|
if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(profileHostname) || hostTelemetryEndpoint !== "http://127.0.0.1:4318") {
|
|
5250
5444
|
problems.push("persisted metal host coordinates are invalid");
|
|
5251
5445
|
}
|
|
@@ -5274,7 +5468,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
5274
5468
|
problems.push("local OTLP metrics receiver did not accept a proof request");
|
|
5275
5469
|
}
|
|
5276
5470
|
const image = profile.images[Object.keys(profile.images)[0]];
|
|
5277
|
-
if (
|
|
5471
|
+
if (existsSync6(image.path)) {
|
|
5278
5472
|
const digest = (await exec(["/usr/bin/sha256sum", image.path])).stdout.split(/\s+/)[0];
|
|
5279
5473
|
imageVerified = digest === image.sha256;
|
|
5280
5474
|
}
|
|
@@ -5285,9 +5479,9 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
5285
5479
|
}
|
|
5286
5480
|
} else
|
|
5287
5481
|
problems.push("metal profile is missing");
|
|
5288
|
-
if (
|
|
5482
|
+
if (existsSync6(STATE_PATH2)) {
|
|
5289
5483
|
try {
|
|
5290
|
-
const state = JSON.parse(
|
|
5484
|
+
const state = JSON.parse(readFileSync6(STATE_PATH2, "utf8"));
|
|
5291
5485
|
metalHostname = state.metalHostname;
|
|
5292
5486
|
if (state.role !== "metal" || !metalHostname || state.profileSha256 !== profileSha256) {
|
|
5293
5487
|
problems.push("metal initialized state does not bind the current profile");
|
|
@@ -5303,7 +5497,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
5303
5497
|
"forgezero-metal-agent-egress.service",
|
|
5304
5498
|
"forgezero-metal-agent.service"
|
|
5305
5499
|
]) {
|
|
5306
|
-
if (!
|
|
5500
|
+
if (!existsSync6(join5(UNIT_DIRECTORY, unit)))
|
|
5307
5501
|
units[unit] = "missing";
|
|
5308
5502
|
else
|
|
5309
5503
|
units[unit] = (await exec(["/usr/bin/systemctl", "is-active", "--quiet", unit])).exitCode === 0 ? "active" : "inactive";
|
|
@@ -5317,7 +5511,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
5317
5511
|
if (!updateSocketReady)
|
|
5318
5512
|
problems.push("Agent update helper socket is not ready");
|
|
5319
5513
|
return {
|
|
5320
|
-
initialized:
|
|
5514
|
+
initialized: existsSync6(STATE_PATH2),
|
|
5321
5515
|
profileValid,
|
|
5322
5516
|
profileMode,
|
|
5323
5517
|
imageVerified,
|
|
@@ -5330,8 +5524,8 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
5330
5524
|
}
|
|
5331
5525
|
|
|
5332
5526
|
// src/operator-bootstrap.ts
|
|
5333
|
-
import { createHash as
|
|
5334
|
-
import { lstatSync as
|
|
5527
|
+
import { createHash as createHash4, randomBytes as randomBytes5 } from "crypto";
|
|
5528
|
+
import { lstatSync as lstatSync5, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
5335
5529
|
import { isIP as isIP6 } from "net";
|
|
5336
5530
|
import { tmpdir } from "os";
|
|
5337
5531
|
import { basename, join as join6 } from "path";
|
|
@@ -5472,22 +5666,22 @@ var exactKeys3 = (value, keys, label) => {
|
|
|
5472
5666
|
var ownerFile = (path, limit, label) => {
|
|
5473
5667
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
5474
5668
|
throw new Error(`${label} path must be absolute`);
|
|
5475
|
-
const metadata =
|
|
5669
|
+
const metadata = lstatSync5(path);
|
|
5476
5670
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
5477
5671
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== uid || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size < 1 || metadata.size > limit) {
|
|
5478
5672
|
throw new Error(`${label} must be an owner-only regular file with one link and at most ${limit} bytes`);
|
|
5479
5673
|
}
|
|
5480
|
-
return
|
|
5674
|
+
return readFileSync7(path);
|
|
5481
5675
|
};
|
|
5482
5676
|
var publicIdentity = (path) => {
|
|
5483
5677
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
5484
5678
|
throw new Error("SSH public-key path must be absolute");
|
|
5485
|
-
const metadata =
|
|
5679
|
+
const metadata = lstatSync5(path);
|
|
5486
5680
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
5487
5681
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== uid || metadata.nlink !== 1 || metadata.size > 16384) {
|
|
5488
5682
|
throw new Error("SSH public key must be a caller-owned regular file with one link");
|
|
5489
5683
|
}
|
|
5490
|
-
const value =
|
|
5684
|
+
const value = readFileSync7(path, "utf8").trim();
|
|
5491
5685
|
if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(value)) {
|
|
5492
5686
|
throw new Error("SSH public key is malformed");
|
|
5493
5687
|
}
|
|
@@ -5496,7 +5690,7 @@ var publicIdentity = (path) => {
|
|
|
5496
5690
|
var socketPath = (path) => {
|
|
5497
5691
|
if (!path.startsWith("/") || /[\r\n]/.test(path))
|
|
5498
5692
|
throw new Error("SSH agent socket path must be absolute");
|
|
5499
|
-
const metadata =
|
|
5693
|
+
const metadata = lstatSync5(path);
|
|
5500
5694
|
const uid = process.getuid?.() ?? metadata.uid;
|
|
5501
5695
|
if (!metadata.isSocket() || metadata.isSymbolicLink() || metadata.uid !== uid) {
|
|
5502
5696
|
throw new Error("SSH agent socket must be a caller-owned Unix socket");
|
|
@@ -5510,7 +5704,7 @@ var fingerprint = (key) => {
|
|
|
5510
5704
|
if (!bytes.length || bytes.toString("base64").replace(/=+$/, "") !== encoded.replace(/=+$/, "")) {
|
|
5511
5705
|
throw new Error("SSH host key is malformed");
|
|
5512
5706
|
}
|
|
5513
|
-
return `SHA256:${
|
|
5707
|
+
return `SHA256:${createHash4("sha256").update(bytes).digest("base64").replace(/=+$/, "")}`;
|
|
5514
5708
|
};
|
|
5515
5709
|
var validateHop = (value, label) => {
|
|
5516
5710
|
const hop = exactKeys3(value, ["address", "port", "user", "hostKey", "hostKeySha256"], label);
|
|
@@ -5655,8 +5849,9 @@ function planOperatorPlatformBootstrap(request, mode) {
|
|
|
5655
5849
|
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : [
|
|
5656
5850
|
"verify pinned SSH transport and caller-approved SSH agent",
|
|
5657
5851
|
"copy pinned Bun and packaged fz artifacts to a private staging directory",
|
|
5852
|
+
"verify and copy the one immutable API Git bundle plus its manifest",
|
|
5658
5853
|
"stage the rewritten owner-only platform config and its named credential handoffs",
|
|
5659
|
-
|
|
5854
|
+
"run typed fz bootstrap platform --apply from the local bundle",
|
|
5660
5855
|
"remove transient local and remote staging data"
|
|
5661
5856
|
],
|
|
5662
5857
|
secretInputs: mode === "apply" ? [...attendedSecretNames(config), ...secretSources(config).map(([name]) => name)] : []
|
|
@@ -5685,7 +5880,7 @@ var defaultExec3 = async (argv, options = {}) => {
|
|
|
5685
5880
|
]);
|
|
5686
5881
|
return { exitCode, output: options.secret ? "" : `${stdout}${stderr}`.slice(0, 65536) };
|
|
5687
5882
|
};
|
|
5688
|
-
var
|
|
5883
|
+
var checked5 = async (exec, argv, label, options) => {
|
|
5689
5884
|
const result = await exec(argv, options);
|
|
5690
5885
|
if (result.exitCode !== 0)
|
|
5691
5886
|
throw new Error(`${label} failed${result.output ? `: ${result.output.trim()}` : ""}`);
|
|
@@ -5698,7 +5893,7 @@ function writeKnownHosts(request, directory) {
|
|
|
5698
5893
|
if (request.target.jump)
|
|
5699
5894
|
lines.push(`${hostLabel(request.target.jump.address, request.target.jump.port)} ${request.target.jump.hostKey}`);
|
|
5700
5895
|
const path = join6(directory, "known_hosts");
|
|
5701
|
-
|
|
5896
|
+
writeFileSync8(path, `${lines.join(`
|
|
5702
5897
|
`)}
|
|
5703
5898
|
`, { mode: 384, flag: "wx" });
|
|
5704
5899
|
return path;
|
|
@@ -5739,7 +5934,7 @@ var safeRemoteArg = (value) => {
|
|
|
5739
5934
|
};
|
|
5740
5935
|
var remote = async (exec, request, knownHosts, argv, label, secret = false, stdin) => {
|
|
5741
5936
|
argv.forEach(safeRemoteArg);
|
|
5742
|
-
return
|
|
5937
|
+
return checked5(exec, ["ssh", ...sshOptions(request, knownHosts), destination2(request.target), "--", ...argv], label, { secret, stdin });
|
|
5743
5938
|
};
|
|
5744
5939
|
var remoteRegularFileExists = async (exec, request, knownHosts, path) => {
|
|
5745
5940
|
safeRemoteArg(path);
|
|
@@ -5760,36 +5955,45 @@ var remoteRegularFileExists = async (exec, request, knownHosts, path) => {
|
|
|
5760
5955
|
};
|
|
5761
5956
|
var copy = async (exec, request, knownHosts, local, remotePath, secret = false) => {
|
|
5762
5957
|
safeRemoteArg(remotePath);
|
|
5763
|
-
await
|
|
5958
|
+
await checked5(exec, [
|
|
5764
5959
|
"scp",
|
|
5765
5960
|
...sshOptions(request, knownHosts, true),
|
|
5766
5961
|
local,
|
|
5767
5962
|
`${destination2(request.target)}:${remotePath}`
|
|
5768
5963
|
], "secure copy", { secret });
|
|
5769
5964
|
};
|
|
5770
|
-
function stageConfig(config, directory) {
|
|
5965
|
+
async function stageConfig(config, directory) {
|
|
5771
5966
|
const rewritten = structuredClone(config);
|
|
5772
5967
|
const staged = [];
|
|
5773
5968
|
for (const [name, source] of secretSources(config)) {
|
|
5774
5969
|
const bytes = ownerFile(source, SECRET_LIMIT, name);
|
|
5775
5970
|
const local = join6(directory, name);
|
|
5776
|
-
|
|
5971
|
+
writeFileSync8(local, bytes, { mode: 384, flag: "wx" });
|
|
5777
5972
|
staged.push(name);
|
|
5778
5973
|
const remotePath = `${REMOTE_STAGE}/${name}`;
|
|
5779
5974
|
if (name === "cloudflare-handoff")
|
|
5780
5975
|
rewritten.cloudflareHandoff.handoffFile = remotePath;
|
|
5781
5976
|
}
|
|
5977
|
+
const bundle = await readBootstrapBundle(config.bootstrapBundle.bundleFile, config.bootstrapBundle.manifestFile);
|
|
5978
|
+
const bundleFiles = [
|
|
5979
|
+
{ source: bundle.bundlePath, name: "bootstrap-api.bundle" },
|
|
5980
|
+
{ source: bundle.manifestPath, name: "bootstrap-api.bundle.json" }
|
|
5981
|
+
];
|
|
5982
|
+
rewritten.bootstrapBundle = {
|
|
5983
|
+
bundleFile: `${REMOTE_STAGE}/bootstrap-api.bundle`,
|
|
5984
|
+
manifestFile: `${REMOTE_STAGE}/bootstrap-api.bundle.json`
|
|
5985
|
+
};
|
|
5782
5986
|
const path = join6(directory, "platform-config.json");
|
|
5783
|
-
|
|
5987
|
+
writeFileSync8(path, `${JSON.stringify(rewritten, null, 2)}
|
|
5784
5988
|
`, { mode: 384, flag: "wx" });
|
|
5785
|
-
return { path, files: staged };
|
|
5989
|
+
return { path, files: staged, bundleFiles };
|
|
5786
5990
|
}
|
|
5787
5991
|
function stageMetalConfig(config, directory) {
|
|
5788
5992
|
const rewritten = structuredClone(config);
|
|
5789
5993
|
const files = [];
|
|
5790
5994
|
if (config.agentSeedFile) {
|
|
5791
5995
|
const name = "metal-agent-seed";
|
|
5792
|
-
|
|
5996
|
+
writeFileSync8(join6(directory, name), ownerFile(config.agentSeedFile, SECRET_LIMIT, name), {
|
|
5793
5997
|
mode: 384,
|
|
5794
5998
|
flag: "wx"
|
|
5795
5999
|
});
|
|
@@ -5797,7 +6001,7 @@ function stageMetalConfig(config, directory) {
|
|
|
5797
6001
|
files.push(name);
|
|
5798
6002
|
}
|
|
5799
6003
|
const path = join6(directory, "metal-config.json");
|
|
5800
|
-
|
|
6004
|
+
writeFileSync8(path, `${JSON.stringify(rewritten, null, 2)}
|
|
5801
6005
|
`, { mode: 384, flag: "wx" });
|
|
5802
6006
|
return { path, files };
|
|
5803
6007
|
}
|
|
@@ -5806,10 +6010,10 @@ async function verifiedBunArchive(directory, fetcher) {
|
|
|
5806
6010
|
if (!response.ok)
|
|
5807
6011
|
throw new Error("pinned Bun download failed");
|
|
5808
6012
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
5809
|
-
if (
|
|
6013
|
+
if (createHash4("sha256").update(bytes).digest("hex") !== BUN_RELEASE_SHA256)
|
|
5810
6014
|
throw new Error("pinned Bun checksum mismatch");
|
|
5811
6015
|
const path = join6(directory, "bun.zip");
|
|
5812
|
-
|
|
6016
|
+
writeFileSync8(path, bytes, { mode: 384, flag: "wx" });
|
|
5813
6017
|
return path;
|
|
5814
6018
|
}
|
|
5815
6019
|
async function installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options) {
|
|
@@ -5819,7 +6023,7 @@ async function installPackagedAgent(request, knownHosts, exec, directory, remote
|
|
|
5819
6023
|
[options.fzGitSshPath ?? fileURLToPath2(new URL("./fz-git-ssh.js", import.meta.url)), "fz-git-ssh.js"]
|
|
5820
6024
|
];
|
|
5821
6025
|
for (const [artifact] of artifacts) {
|
|
5822
|
-
if (!
|
|
6026
|
+
if (!readFileSync7(artifact).length)
|
|
5823
6027
|
throw new Error(`packaged artifact is empty: ${basename(artifact)}`);
|
|
5824
6028
|
}
|
|
5825
6029
|
await remote(exec, request, knownHosts, ["/usr/bin/mkdir", "-m", "0700", remoteTemp], "remote staging");
|
|
@@ -5860,7 +6064,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
5860
6064
|
socketPath(request.target.agentSocket);
|
|
5861
6065
|
const exec = options.exec ?? defaultExec3;
|
|
5862
6066
|
const directory = mkdtempSync(join6(tmpdir(), "forgezero-operator-bootstrap-"));
|
|
5863
|
-
const remoteTemp = `/tmp/forgezero-operator-${
|
|
6067
|
+
const remoteTemp = `/tmp/forgezero-operator-${randomBytes5(12).toString("hex")}`;
|
|
5864
6068
|
let knownHosts = "";
|
|
5865
6069
|
try {
|
|
5866
6070
|
knownHosts = writeKnownHosts(request, directory);
|
|
@@ -5872,18 +6076,16 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
5872
6076
|
if (config.kind !== "platform")
|
|
5873
6077
|
throw new Error("operator bootstrap requires a platform config");
|
|
5874
6078
|
const secrets = mode === "apply" ? validatePlatformBootstrapSecrets(config, options.secrets) : undefined;
|
|
5875
|
-
const staged = stageConfig(config, directory);
|
|
6079
|
+
const staged = await stageConfig(config, directory);
|
|
5876
6080
|
await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
|
|
5877
6081
|
await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
|
|
5878
6082
|
for (const name of staged.files)
|
|
5879
6083
|
await copy(exec, request, knownHosts, join6(directory, name), `${remoteTemp}/${name}`, true);
|
|
5880
|
-
for (const
|
|
6084
|
+
for (const bundle of staged.bundleFiles)
|
|
6085
|
+
await copy(exec, request, knownHosts, bundle.source, `${remoteTemp}/${bundle.name}`, true);
|
|
6086
|
+
for (const name of ["platform-config.json", ...staged.files, ...staged.bundleFiles.map(({ name: name2 }) => name2)])
|
|
5881
6087
|
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");
|
|
6088
|
+
const command = ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "platform", "credentials-stdin"];
|
|
5887
6089
|
command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
|
|
5888
6090
|
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
|
|
5889
6091
|
` : undefined);
|
|
@@ -5894,7 +6096,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
5894
6096
|
return;
|
|
5895
6097
|
});
|
|
5896
6098
|
}
|
|
5897
|
-
|
|
6099
|
+
rmSync6(directory, { recursive: true, force: true });
|
|
5898
6100
|
}
|
|
5899
6101
|
}
|
|
5900
6102
|
async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
@@ -5903,7 +6105,7 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
5903
6105
|
socketPath(request.target.agentSocket);
|
|
5904
6106
|
const exec = options.exec ?? defaultExec3;
|
|
5905
6107
|
const directory = mkdtempSync(join6(tmpdir(), "forgezero-operator-metal-"));
|
|
5906
|
-
const remoteTemp = `/tmp/forgezero-operator-${
|
|
6108
|
+
const remoteTemp = `/tmp/forgezero-operator-${randomBytes5(12).toString("hex")}`;
|
|
5907
6109
|
let knownHosts = "";
|
|
5908
6110
|
try {
|
|
5909
6111
|
knownHosts = writeKnownHosts(request, directory);
|
|
@@ -5947,7 +6149,7 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
5947
6149
|
return;
|
|
5948
6150
|
});
|
|
5949
6151
|
}
|
|
5950
|
-
|
|
6152
|
+
rmSync6(directory, { recursive: true, force: true });
|
|
5951
6153
|
}
|
|
5952
6154
|
}
|
|
5953
6155
|
export {
|