@forgezero/agent 0.1.62 → 0.1.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -16
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap-bundle.d.ts +31 -0
- package/dist/bootstrap-bundle.js +165 -0
- package/dist/bootstrap.d.ts +24 -18
- package/dist/bootstrap.js +509 -269
- package/dist/cli/agent-install.d.ts +2 -0
- package/dist/community-rehearsal-host.js +7 -5
- package/dist/definition.js +7 -5
- package/dist/deploy-file.js +7 -5
- package/dist/deployment.d.ts +27 -0
- package/dist/fz-agent.js +594 -421
- package/dist/fz.js +847 -372
- package/dist/metal-bootstrap.js +1 -1
- package/dist/metal-helper-socket.js +0 -2
- package/dist/metal-provision.js +0 -2
- package/dist/operator-bootstrap.d.ts +43 -1
- package/dist/operator-bootstrap.js +788 -367
- package/dist/platform-bootstrap-runtime.d.ts +0 -2
- package/dist/platform-bootstrap-runtime.js +0 -4
- package/dist/platform-fleet-verification.js +190 -98
- package/dist/provision.d.ts +4 -1
- package/dist/provision.js +41 -32
- package/dist/software-helper.js +7 -5
- package/dist/software.d.ts +1 -1
- package/dist/software.js +7 -5
- package/dist/version.d.ts +1 -1
- package/package.json +5 -1
package/dist/bootstrap.js
CHANGED
|
@@ -1209,19 +1209,175 @@ async function finalizeCloudflareBootstrapAcceptance(request, fetcher = fetch) {
|
|
|
1209
1209
|
return evidence;
|
|
1210
1210
|
}
|
|
1211
1211
|
|
|
1212
|
+
// src/bootstrap-bundle.ts
|
|
1213
|
+
import { createHash, randomBytes } from "crypto";
|
|
1214
|
+
import {
|
|
1215
|
+
chmodSync,
|
|
1216
|
+
createReadStream,
|
|
1217
|
+
existsSync,
|
|
1218
|
+
lstatSync,
|
|
1219
|
+
mkdirSync,
|
|
1220
|
+
readFileSync,
|
|
1221
|
+
renameSync,
|
|
1222
|
+
rmSync,
|
|
1223
|
+
writeFileSync
|
|
1224
|
+
} from "fs";
|
|
1225
|
+
import { dirname as dirname2, isAbsolute, resolve as resolve2 } from "path";
|
|
1226
|
+
var BOOTSTRAP_BUNDLE_FORMAT = 1;
|
|
1227
|
+
var BOOTSTRAP_BUNDLE_KIND = "forgezero-api-git-bundle";
|
|
1228
|
+
var MAX_BOOTSTRAP_BUNDLE_BYTES = 512 * 1024 * 1024;
|
|
1229
|
+
var run = async (argv) => {
|
|
1230
|
+
const child = Bun.spawn([...argv], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
1231
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
1232
|
+
new Response(child.stdout).text(),
|
|
1233
|
+
new Response(child.stderr).text(),
|
|
1234
|
+
child.exited
|
|
1235
|
+
]);
|
|
1236
|
+
return { exitCode, output: `${stdout}${stderr}`.slice(0, 65536) };
|
|
1237
|
+
};
|
|
1238
|
+
var checked = async (exec, argv, label) => {
|
|
1239
|
+
const result = await exec(argv);
|
|
1240
|
+
if (result.exitCode !== 0)
|
|
1241
|
+
throw new Error(`${label} failed${result.output.trim() ? `: ${result.output.trim()}` : ""}`);
|
|
1242
|
+
return result.output.trim();
|
|
1243
|
+
};
|
|
1244
|
+
async function sha256File(path) {
|
|
1245
|
+
const hash = createHash("sha256");
|
|
1246
|
+
await new Promise((resolveDone, reject) => {
|
|
1247
|
+
const stream = createReadStream(path);
|
|
1248
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
1249
|
+
stream.on("error", reject);
|
|
1250
|
+
stream.on("end", resolveDone);
|
|
1251
|
+
});
|
|
1252
|
+
return hash.digest("hex");
|
|
1253
|
+
}
|
|
1254
|
+
var branchName = (value) => {
|
|
1255
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9](?:[A-Za-z0-9._/-]{0,126}[A-Za-z0-9])?$/.test(value) || value.includes("..") || value.includes("//") || value.startsWith("-")) {
|
|
1256
|
+
throw new Error("bootstrap bundle branch is malformed");
|
|
1257
|
+
}
|
|
1258
|
+
return value;
|
|
1259
|
+
};
|
|
1260
|
+
function parseBootstrapBundleManifest(value) {
|
|
1261
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1262
|
+
throw new Error("bootstrap bundle manifest must be an object");
|
|
1263
|
+
const source = value;
|
|
1264
|
+
const allowed = ["format", "kind", "branch", "revision", "sha256", "bytes", "createdAt"];
|
|
1265
|
+
const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
|
|
1266
|
+
if (unknown.length)
|
|
1267
|
+
throw new Error(`bootstrap bundle manifest contains unknown field ${unknown[0]}`);
|
|
1268
|
+
if (source.format !== BOOTSTRAP_BUNDLE_FORMAT || source.kind !== BOOTSTRAP_BUNDLE_KIND) {
|
|
1269
|
+
throw new Error("bootstrap bundle manifest format is unsupported");
|
|
1270
|
+
}
|
|
1271
|
+
branchName(source.branch);
|
|
1272
|
+
if (typeof source.revision !== "string" || !/^[a-f0-9]{40}$/.test(source.revision)) {
|
|
1273
|
+
throw new Error("bootstrap bundle revision must be an exact lowercase Git commit");
|
|
1274
|
+
}
|
|
1275
|
+
if (typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256)) {
|
|
1276
|
+
throw new Error("bootstrap bundle digest is malformed");
|
|
1277
|
+
}
|
|
1278
|
+
if (!Number.isSafeInteger(source.bytes) || Number(source.bytes) < 1 || Number(source.bytes) > MAX_BOOTSTRAP_BUNDLE_BYTES) {
|
|
1279
|
+
throw new Error("bootstrap bundle size is outside the supported boundary");
|
|
1280
|
+
}
|
|
1281
|
+
if (typeof source.createdAt !== "string" || Number.isNaN(Date.parse(source.createdAt))) {
|
|
1282
|
+
throw new Error("bootstrap bundle creation time is malformed");
|
|
1283
|
+
}
|
|
1284
|
+
return source;
|
|
1285
|
+
}
|
|
1286
|
+
function ownerRegularFile(path, maximum, label) {
|
|
1287
|
+
if (!isAbsolute(path) || resolve2(path) !== path || /[\r\n\0]/.test(path)) {
|
|
1288
|
+
throw new Error(`${label} path must be canonical and absolute`);
|
|
1289
|
+
}
|
|
1290
|
+
const value = lstatSync(path);
|
|
1291
|
+
const uid = process.getuid?.() ?? value.uid;
|
|
1292
|
+
if (!value.isFile() || value.isSymbolicLink() || value.uid !== uid || value.nlink !== 1 || (value.mode & 63) !== 0 || value.size < 1 || value.size > maximum) {
|
|
1293
|
+
throw new Error(`${label} must be an owner-only regular file with one link and at most ${maximum} bytes`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
async function readBootstrapBundle(bundlePath, manifestPath = `${bundlePath}.json`) {
|
|
1297
|
+
ownerRegularFile(bundlePath, MAX_BOOTSTRAP_BUNDLE_BYTES, "bootstrap bundle");
|
|
1298
|
+
ownerRegularFile(manifestPath, 16 * 1024, "bootstrap bundle manifest");
|
|
1299
|
+
let parsed;
|
|
1300
|
+
try {
|
|
1301
|
+
parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
1302
|
+
} catch {
|
|
1303
|
+
throw new Error("bootstrap bundle manifest is not valid JSON");
|
|
1304
|
+
}
|
|
1305
|
+
const manifest = parseBootstrapBundleManifest(parsed);
|
|
1306
|
+
const size = lstatSync(bundlePath).size;
|
|
1307
|
+
if (size !== manifest.bytes || await sha256File(bundlePath) !== manifest.sha256) {
|
|
1308
|
+
throw new Error("bootstrap bundle bytes do not match their manifest");
|
|
1309
|
+
}
|
|
1310
|
+
return { bundlePath, manifestPath, manifest };
|
|
1311
|
+
}
|
|
1312
|
+
async function buildBootstrapBundle(input, exec = run) {
|
|
1313
|
+
const repositoryRoot = resolve2(input.repositoryRoot);
|
|
1314
|
+
const outputPath = resolve2(input.outputPath);
|
|
1315
|
+
const manifestPath = `${outputPath}.json`;
|
|
1316
|
+
const branch = branchName(input.branch);
|
|
1317
|
+
if (!isAbsolute(input.outputPath) || outputPath !== input.outputPath) {
|
|
1318
|
+
throw new Error("bootstrap bundle output must be a canonical absolute path");
|
|
1319
|
+
}
|
|
1320
|
+
if (existsSync(outputPath) || existsSync(manifestPath)) {
|
|
1321
|
+
throw new Error("bootstrap bundle output already exists");
|
|
1322
|
+
}
|
|
1323
|
+
const root = lstatSync(repositoryRoot);
|
|
1324
|
+
if (!root.isDirectory() || root.isSymbolicLink())
|
|
1325
|
+
throw new Error("bootstrap bundle source must be a real directory");
|
|
1326
|
+
mkdirSync(dirname2(outputPath), { recursive: true, mode: 448 });
|
|
1327
|
+
const parent = lstatSync(dirname2(outputPath));
|
|
1328
|
+
const uid = process.getuid?.() ?? parent.uid;
|
|
1329
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== uid || (parent.mode & 63) !== 0) {
|
|
1330
|
+
throw new Error("bootstrap bundle output directory must be caller-owned and owner-only");
|
|
1331
|
+
}
|
|
1332
|
+
const dirty = await checked(exec, ["git", "-C", repositoryRoot, "status", "--porcelain=v1", "--untracked-files=no"], "Git worktree check");
|
|
1333
|
+
if (dirty)
|
|
1334
|
+
throw new Error("bootstrap bundle source has tracked changes; commit the reviewed API release first");
|
|
1335
|
+
const revision = (await checked(exec, ["git", "-C", repositoryRoot, "rev-parse", "--verify", `refs/heads/${branch}^{commit}`], "Git branch resolution")).toLowerCase();
|
|
1336
|
+
if (!/^[a-f0-9]{40}$/.test(revision))
|
|
1337
|
+
throw new Error("bootstrap bundle branch did not resolve to one exact commit");
|
|
1338
|
+
const temporary = `${outputPath}.next.${process.pid}.${randomBytes(6).toString("hex")}`;
|
|
1339
|
+
try {
|
|
1340
|
+
await checked(exec, ["git", "-C", repositoryRoot, "bundle", "create", temporary, `refs/heads/${branch}`], "Git bundle creation");
|
|
1341
|
+
chmodSync(temporary, 384);
|
|
1342
|
+
const file = lstatSync(temporary);
|
|
1343
|
+
if (!file.isFile() || file.isSymbolicLink() || file.size < 1 || file.size > MAX_BOOTSTRAP_BUNDLE_BYTES) {
|
|
1344
|
+
throw new Error("created bootstrap bundle is outside the supported boundary");
|
|
1345
|
+
}
|
|
1346
|
+
await checked(exec, ["git", "bundle", "verify", temporary], "Git bundle verification");
|
|
1347
|
+
const manifest = {
|
|
1348
|
+
format: BOOTSTRAP_BUNDLE_FORMAT,
|
|
1349
|
+
kind: BOOTSTRAP_BUNDLE_KIND,
|
|
1350
|
+
branch,
|
|
1351
|
+
revision,
|
|
1352
|
+
sha256: await sha256File(temporary),
|
|
1353
|
+
bytes: file.size,
|
|
1354
|
+
createdAt: new Date().toISOString()
|
|
1355
|
+
};
|
|
1356
|
+
const manifestTemporary = `${manifestPath}.next.${process.pid}.${randomBytes(6).toString("hex")}`;
|
|
1357
|
+
writeFileSync(manifestTemporary, `${JSON.stringify(manifest, null, 2)}
|
|
1358
|
+
`, { mode: 384, flag: "wx" });
|
|
1359
|
+
renameSync(temporary, outputPath);
|
|
1360
|
+
renameSync(manifestTemporary, manifestPath);
|
|
1361
|
+
return { bundlePath: outputPath, manifestPath, manifest };
|
|
1362
|
+
} catch (cause) {
|
|
1363
|
+
rmSync(temporary, { force: true });
|
|
1364
|
+
throw cause;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1212
1368
|
// src/bootstrap.ts
|
|
1213
|
-
import { createHash, createHmac as createHmac2, randomBytes as
|
|
1369
|
+
import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes3 } from "crypto";
|
|
1214
1370
|
import {
|
|
1215
|
-
chmodSync as
|
|
1216
|
-
existsSync as
|
|
1217
|
-
lstatSync as
|
|
1218
|
-
mkdirSync as
|
|
1219
|
-
readFileSync as
|
|
1220
|
-
renameSync as
|
|
1221
|
-
rmSync as
|
|
1222
|
-
writeFileSync as
|
|
1371
|
+
chmodSync as chmodSync3,
|
|
1372
|
+
existsSync as existsSync5,
|
|
1373
|
+
lstatSync as lstatSync3,
|
|
1374
|
+
mkdirSync as mkdirSync5,
|
|
1375
|
+
readFileSync as readFileSync5,
|
|
1376
|
+
renameSync as renameSync5,
|
|
1377
|
+
rmSync as rmSync5,
|
|
1378
|
+
writeFileSync as writeFileSync5
|
|
1223
1379
|
} from "fs";
|
|
1224
|
-
import { dirname as
|
|
1380
|
+
import { dirname as dirname6 } from "path";
|
|
1225
1381
|
import { fileURLToPath } from "url";
|
|
1226
1382
|
|
|
1227
1383
|
// src/agent-update-helper.ts
|
|
@@ -1240,7 +1396,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1240
1396
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1241
1397
|
|
|
1242
1398
|
// src/version.ts
|
|
1243
|
-
var VERSION = "0.1.
|
|
1399
|
+
var VERSION = "0.1.64";
|
|
1244
1400
|
|
|
1245
1401
|
// src/software.ts
|
|
1246
1402
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -1255,8 +1411,8 @@ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
|
1255
1411
|
`;
|
|
1256
1412
|
|
|
1257
1413
|
// src/service-supervisor.ts
|
|
1258
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
1259
|
-
import { dirname as
|
|
1414
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1415
|
+
import { dirname as dirname3, join as join2, resolve as resolve3, sep } from "path";
|
|
1260
1416
|
|
|
1261
1417
|
// src/definition.ts
|
|
1262
1418
|
var RESERVED_STEP_ENV = new Set([
|
|
@@ -1275,17 +1431,17 @@ var RESERVED_STEP_ENV = new Set([
|
|
|
1275
1431
|
// src/service-supervisor.ts
|
|
1276
1432
|
var defaultHost = {
|
|
1277
1433
|
write(path, content, mode) {
|
|
1278
|
-
|
|
1434
|
+
mkdirSync2(dirname3(path), { recursive: true, mode: 493 });
|
|
1279
1435
|
const next = `${path}.next`;
|
|
1280
|
-
|
|
1281
|
-
|
|
1436
|
+
writeFileSync2(next, content, { mode });
|
|
1437
|
+
renameSync2(next, path);
|
|
1282
1438
|
},
|
|
1283
|
-
read: (path) =>
|
|
1284
|
-
exists:
|
|
1285
|
-
list: (path) =>
|
|
1439
|
+
read: (path) => readFileSync2(path, "utf8"),
|
|
1440
|
+
exists: existsSync2,
|
|
1441
|
+
list: (path) => existsSync2(path) ? readdirSync(path) : [],
|
|
1286
1442
|
realpath: realpathSync,
|
|
1287
|
-
mkdir: (path, mode) =>
|
|
1288
|
-
remove: (path) =>
|
|
1443
|
+
mkdir: (path, mode) => mkdirSync2(path, { recursive: true, mode }),
|
|
1444
|
+
remove: (path) => rmSync2(path, { force: true }),
|
|
1289
1445
|
async exec(argv) {
|
|
1290
1446
|
const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe", env: {
|
|
1291
1447
|
PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
@@ -1315,21 +1471,21 @@ var defaultHost = {
|
|
|
1315
1471
|
};
|
|
1316
1472
|
|
|
1317
1473
|
// src/container-supervisor.ts
|
|
1318
|
-
import { existsSync as
|
|
1319
|
-
import { dirname as
|
|
1474
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1475
|
+
import { dirname as dirname4, resolve as resolve4, sep as sep2 } from "path";
|
|
1320
1476
|
var defaultHost2 = {
|
|
1321
1477
|
realpath: realpathSync2,
|
|
1322
|
-
exists:
|
|
1323
|
-
read: (path) =>
|
|
1478
|
+
exists: existsSync3,
|
|
1479
|
+
read: (path) => readFileSync3(path, "utf8"),
|
|
1324
1480
|
write(path, content, mode) {
|
|
1325
|
-
|
|
1481
|
+
mkdirSync3(dirname4(path), { recursive: true, mode: 493 });
|
|
1326
1482
|
const next = `${path}.next`;
|
|
1327
|
-
|
|
1328
|
-
|
|
1483
|
+
writeFileSync3(next, content, { mode });
|
|
1484
|
+
renameSync3(next, path);
|
|
1329
1485
|
},
|
|
1330
|
-
remove: (path) =>
|
|
1331
|
-
list: (path) =>
|
|
1332
|
-
mkdir: (path, mode) =>
|
|
1486
|
+
remove: (path) => rmSync3(path, { force: true }),
|
|
1487
|
+
list: (path) => existsSync3(path) ? readdirSync2(path) : [],
|
|
1488
|
+
mkdir: (path, mode) => mkdirSync3(path, { recursive: true, mode }),
|
|
1333
1489
|
async exec(argv) {
|
|
1334
1490
|
const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
|
|
1335
1491
|
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
@@ -1479,7 +1635,11 @@ function agentEgressUnit(options) {
|
|
|
1479
1635
|
const user = options.user ?? "forgezero";
|
|
1480
1636
|
if (!/^[a-z_][a-z0-9_-]{0,30}$/.test(user))
|
|
1481
1637
|
throw new Error("invalid Agent service user");
|
|
1482
|
-
const
|
|
1638
|
+
const bootstrapBundleEnabled = Boolean(options.bootstrapBundlePath && options.bootstrapBundleManifestPath);
|
|
1639
|
+
if (Boolean(options.bootstrapBundlePath) !== Boolean(options.bootstrapBundleManifestPath) || bootstrapBundleEnabled && ![options.bootstrapBundlePath, options.bootstrapBundleManifestPath].every((path) => path.startsWith("/") && !/[\r\n\0:]/.test(path))) {
|
|
1640
|
+
throw new Error("bootstrap bundle and manifest must be supplied together as absolute paths");
|
|
1641
|
+
}
|
|
1642
|
+
const deploymentEnabled = Boolean(options.repository || bootstrapBundleEnabled || options.pullDeployments);
|
|
1483
1643
|
const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
|
|
1484
1644
|
const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
|
|
1485
1645
|
if (deploymentEnabled && runnerPublicTcpPorts.length < 1) {
|
|
@@ -1885,17 +2045,16 @@ function agentUnit(options) {
|
|
|
1885
2045
|
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
1886
2046
|
throw new Error("migration pull and lifecycle profile must be supplied together");
|
|
1887
2047
|
}
|
|
1888
|
-
const
|
|
1889
|
-
if (
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
throw new Error("bootstrap pull, SSH credential, public key and target telemetry endpoint must be supplied together");
|
|
2048
|
+
const bootstrapIdentityEnabled = Boolean(options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath);
|
|
2049
|
+
if (Boolean(options.bootstrapSshCredentialPath) !== Boolean(options.bootstrapSshPublicKeyPath)) {
|
|
2050
|
+
throw new Error("bootstrap SSH credential and public key paths must be supplied together");
|
|
2051
|
+
}
|
|
2052
|
+
const bootstrapEnabled = Boolean(options.pullBootstrap);
|
|
2053
|
+
if (bootstrapEnabled && (!bootstrapIdentityEnabled || !options.bootstrapTargetTelemetryEndpoint) || !bootstrapEnabled && options.bootstrapTargetTelemetryEndpoint) {
|
|
2054
|
+
throw new Error("bootstrap pull, SSH identity and target telemetry endpoint must be supplied together");
|
|
1896
2055
|
}
|
|
1897
2056
|
const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
|
|
1898
|
-
const bootstrapSshCredentialPath =
|
|
2057
|
+
const bootstrapSshCredentialPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
|
|
1899
2058
|
const warpValues = [
|
|
1900
2059
|
options.warpOrganization,
|
|
1901
2060
|
options.warpClientIdCredentialPath,
|
|
@@ -1949,6 +2108,8 @@ function agentUnit(options) {
|
|
|
1949
2108
|
options.gitPublicKeyPath ? `FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}` : null,
|
|
1950
2109
|
options.repository ? `FZ_DEPLOY_REPO=${options.repository}` : null,
|
|
1951
2110
|
options.branch ? `FZ_DEPLOY_BRANCH=${options.branch}` : null,
|
|
2111
|
+
options.bootstrapBundlePath ? `FZ_BOOTSTRAP_BUNDLE=${options.bootstrapBundlePath}` : null,
|
|
2112
|
+
options.bootstrapBundleManifestPath ? `FZ_BOOTSTRAP_BUNDLE_MANIFEST=${options.bootstrapBundleManifestPath}` : null,
|
|
1952
2113
|
options.profile ? `FZ_DEPLOY_PROFILE=${options.profile}` : null,
|
|
1953
2114
|
options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
|
|
1954
2115
|
deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
|
|
@@ -2132,15 +2293,13 @@ function planProvision(options) {
|
|
|
2132
2293
|
if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
|
|
2133
2294
|
throw new Error("migration pull and lifecycle profile must be supplied together");
|
|
2134
2295
|
}
|
|
2135
|
-
const
|
|
2136
|
-
if (
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
].some(Boolean) && !bootstrapEnabled) {
|
|
2143
|
-
throw new Error("bootstrap pull, SSH credential, public key and target telemetry endpoint must be supplied together");
|
|
2296
|
+
const bootstrapIdentityEnabled = Boolean(options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath);
|
|
2297
|
+
if (Boolean(options.bootstrapSshCredentialPath) !== Boolean(options.bootstrapSshPublicKeyPath) || options.bootstrapSshSourcePath && !bootstrapIdentityEnabled) {
|
|
2298
|
+
throw new Error("bootstrap SSH credential and public key paths must be supplied together");
|
|
2299
|
+
}
|
|
2300
|
+
const bootstrapEnabled = Boolean(options.pullBootstrap);
|
|
2301
|
+
if (bootstrapEnabled && (!bootstrapIdentityEnabled || !options.bootstrapTargetTelemetryEndpoint) || !bootstrapEnabled && options.bootstrapTargetTelemetryEndpoint) {
|
|
2302
|
+
throw new Error("bootstrap pull, SSH identity and target telemetry endpoint must be supplied together");
|
|
2144
2303
|
}
|
|
2145
2304
|
const warpValues = [
|
|
2146
2305
|
options.warpOrganization,
|
|
@@ -2150,13 +2309,13 @@ function planProvision(options) {
|
|
|
2150
2309
|
const warpEnabled = warpValues.every(Boolean);
|
|
2151
2310
|
if (warpValues.some(Boolean) && !warpEnabled)
|
|
2152
2311
|
throw new Error("WARP configuration must be supplied together");
|
|
2153
|
-
const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath
|
|
2154
|
-
if (
|
|
2155
|
-
throw new Error("direct enrolment credential
|
|
2312
|
+
const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath);
|
|
2313
|
+
if (options.enrolTokenCredentialPath && !options.enrolStatePath || options.enrolTokenSourcePath && (!options.enrolTokenCredentialPath || !options.enrolStatePath)) {
|
|
2314
|
+
throw new Error("direct enrolment credential requires its durable state path");
|
|
2156
2315
|
}
|
|
2157
2316
|
const enrolTokenSourcePath = options.enrolTokenSourcePath ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
|
|
2158
2317
|
const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
|
|
2159
|
-
const enrolStatePath =
|
|
2318
|
+
const enrolStatePath = options.enrolStatePath ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
|
|
2160
2319
|
const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
|
|
2161
2320
|
const sourceBinPath = options.sourceBinPath ? systemdPath(options.sourceBinPath, "agent source binary") : undefined;
|
|
2162
2321
|
const binPath = options.binPath ? systemdPath(options.binPath, "agent binary") : undefined;
|
|
@@ -2168,8 +2327,8 @@ function planProvision(options) {
|
|
|
2168
2327
|
const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
|
|
2169
2328
|
const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
|
|
2170
2329
|
const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
|
|
2171
|
-
const bootstrapSshCredentialPath =
|
|
2172
|
-
const bootstrapSshPublicKeyPath =
|
|
2330
|
+
const bootstrapSshCredentialPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
|
|
2331
|
+
const bootstrapSshPublicKeyPath = bootstrapIdentityEnabled ? systemdPath(options.bootstrapSshPublicKeyPath, "bootstrap SSH public key") : undefined;
|
|
2173
2332
|
const bootstrapSshSourcePath = options.bootstrapSshSourcePath ? systemdPath(options.bootstrapSshSourcePath, "bootstrap SSH private-key source") : undefined;
|
|
2174
2333
|
const bootstrapSshPublicKeyDir = bootstrapSshPublicKeyPath?.replace(/\/[^/]+$/, "");
|
|
2175
2334
|
const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
|
|
@@ -2269,7 +2428,7 @@ function planProvision(options) {
|
|
|
2269
2428
|
step("Git deploy identity directory", { kind: "directories", directories: [{ path: gitPublicKeyDir, mode: 493, owner: "root", group: "root" }] }),
|
|
2270
2429
|
step("unique encrypted Git deploy identity", { kind: "ensure-git-identity", credential: gitCredentialPath, publicKey: gitPublicKeyPath })
|
|
2271
2430
|
] : [],
|
|
2272
|
-
...
|
|
2431
|
+
...bootstrapIdentityEnabled ? [
|
|
2273
2432
|
step("bootstrap SSH public identity directory", { kind: "directories", directories: [
|
|
2274
2433
|
{ path: bootstrapSshPublicKeyDir, mode: 493, owner: "root", group: "root" }
|
|
2275
2434
|
] }),
|
|
@@ -2305,6 +2464,10 @@ function planProvision(options) {
|
|
|
2305
2464
|
{ argv: ["/usr/bin/rm", "-f", "/etc/systemd/system/forgezero-deploy-runner.socket"] },
|
|
2306
2465
|
{ argv: ["/usr/bin/systemctl", "daemon-reload"] }
|
|
2307
2466
|
] })] : [],
|
|
2467
|
+
...enrolStatePath && !enrolmentEnabled ? [step("retire consumed enrolment unit", { kind: "commands", commands: [
|
|
2468
|
+
{ argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-agent-enrol.service"], acceptedExitCodes: [0, 1, 5] },
|
|
2469
|
+
{ argv: ["/usr/bin/rm", "-f", ENROLMENT_UNIT_PATH] }
|
|
2470
|
+
] })] : [],
|
|
2308
2471
|
step("enable and converge services", { kind: "commands", commands: [
|
|
2309
2472
|
{ argv: ["/usr/bin/systemctl", "enable", ...enabledUnits] },
|
|
2310
2473
|
{ argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
|
|
@@ -2331,27 +2494,27 @@ function planProvision(options) {
|
|
|
2331
2494
|
}
|
|
2332
2495
|
|
|
2333
2496
|
// src/cli/agent-install.ts
|
|
2334
|
-
import { randomBytes } from "crypto";
|
|
2497
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
2335
2498
|
import {
|
|
2336
|
-
chmodSync,
|
|
2499
|
+
chmodSync as chmodSync2,
|
|
2337
2500
|
copyFileSync,
|
|
2338
|
-
existsSync as
|
|
2339
|
-
lstatSync,
|
|
2340
|
-
mkdirSync as
|
|
2341
|
-
readFileSync as
|
|
2501
|
+
existsSync as existsSync4,
|
|
2502
|
+
lstatSync as lstatSync2,
|
|
2503
|
+
mkdirSync as mkdirSync4,
|
|
2504
|
+
readFileSync as readFileSync4,
|
|
2342
2505
|
realpathSync as realpathSync3,
|
|
2343
|
-
renameSync as
|
|
2344
|
-
rmSync as
|
|
2506
|
+
renameSync as renameSync4,
|
|
2507
|
+
rmSync as rmSync4,
|
|
2345
2508
|
symlinkSync,
|
|
2346
|
-
writeFileSync as
|
|
2509
|
+
writeFileSync as writeFileSync4
|
|
2347
2510
|
} from "fs";
|
|
2348
|
-
import { dirname as
|
|
2349
|
-
async function readCapabilities(
|
|
2511
|
+
import { dirname as dirname5 } from "path";
|
|
2512
|
+
async function readCapabilities(run2) {
|
|
2350
2513
|
const answers = {};
|
|
2351
2514
|
const checks = Object.entries(CAPABILITY_CHECKS);
|
|
2352
2515
|
for (const [id, check] of checks) {
|
|
2353
2516
|
try {
|
|
2354
|
-
const result = await
|
|
2517
|
+
const result = await run2(check.operation);
|
|
2355
2518
|
answers[id] = check.satisfied(result.stdout, result.exitCode);
|
|
2356
2519
|
} catch {
|
|
2357
2520
|
answers[id] = false;
|
|
@@ -2404,52 +2567,52 @@ var runProvisionOperation = async (operation) => {
|
|
|
2404
2567
|
}
|
|
2405
2568
|
if (operation.kind === "install-runtime") {
|
|
2406
2569
|
const release = `/opt/forgezero/agent/versions/${operation.version}`;
|
|
2407
|
-
|
|
2408
|
-
|
|
2570
|
+
mkdirSync4(`${release}/dist`, { recursive: true, mode: 493 });
|
|
2571
|
+
mkdirSync4(dirname5(operation.binary), { recursive: true, mode: 493 });
|
|
2409
2572
|
copyFileSync(operation.source, `${release}/dist/fz-agent.js`);
|
|
2410
|
-
|
|
2411
|
-
const gitSshSource = `${
|
|
2412
|
-
if (!
|
|
2573
|
+
chmodSync2(`${release}/dist/fz-agent.js`, 493);
|
|
2574
|
+
const gitSshSource = `${dirname5(operation.source)}/fz-git-ssh.js`;
|
|
2575
|
+
if (!existsSync4(gitSshSource))
|
|
2413
2576
|
return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
|
|
2414
2577
|
copyFileSync(gitSshSource, `${release}/dist/fz-git-ssh.js`);
|
|
2415
|
-
|
|
2578
|
+
chmodSync2(`${release}/dist/fz-git-ssh.js`, 493);
|
|
2416
2579
|
const pending = "/opt/forgezero/agent/current.next";
|
|
2417
|
-
|
|
2580
|
+
rmSync4(pending, { force: true });
|
|
2418
2581
|
symlinkSync(`versions/${operation.version}`, pending);
|
|
2419
|
-
|
|
2420
|
-
|
|
2582
|
+
renameSync4(pending, "/opt/forgezero/agent/current");
|
|
2583
|
+
rmSync4(operation.binary, { force: true });
|
|
2421
2584
|
symlinkSync("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
|
|
2422
2585
|
const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
|
|
2423
|
-
|
|
2586
|
+
rmSync4(gitSshBinary, { force: true });
|
|
2424
2587
|
symlinkSync("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
|
|
2425
2588
|
return { stdout: "", exitCode: 0 };
|
|
2426
2589
|
}
|
|
2427
2590
|
if (operation.kind === "ensure-seed") {
|
|
2428
|
-
if (
|
|
2591
|
+
if (existsSync4(operation.credential) && lstatSync2(operation.credential).size > 0)
|
|
2429
2592
|
return { stdout: "", exitCode: 0 };
|
|
2430
|
-
const seed =
|
|
2593
|
+
const seed = randomBytes2(32).toString("base64url");
|
|
2431
2594
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
|
|
2432
2595
|
if (result.exitCode === 0)
|
|
2433
|
-
|
|
2596
|
+
chmodSync2(operation.credential, 256);
|
|
2434
2597
|
return result;
|
|
2435
2598
|
}
|
|
2436
2599
|
if (operation.kind === "ensure-git-identity") {
|
|
2437
2600
|
const key = "/run/forgezero-git-deploy-key";
|
|
2438
2601
|
const publicKey = `${key}.pub`;
|
|
2439
2602
|
try {
|
|
2440
|
-
if (!
|
|
2441
|
-
|
|
2442
|
-
|
|
2603
|
+
if (!existsSync4(operation.credential) || lstatSync2(operation.credential).size < 1) {
|
|
2604
|
+
rmSync4(key, { force: true });
|
|
2605
|
+
rmSync4(publicKey, { force: true });
|
|
2443
2606
|
let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
|
|
2444
2607
|
if (result.exitCode !== 0)
|
|
2445
2608
|
return result;
|
|
2446
2609
|
result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=git-deploy-key", key, operation.credential]);
|
|
2447
2610
|
if (result.exitCode !== 0)
|
|
2448
2611
|
return result;
|
|
2449
|
-
|
|
2612
|
+
chmodSync2(operation.credential, 256);
|
|
2450
2613
|
}
|
|
2451
|
-
if (!
|
|
2452
|
-
if (!
|
|
2614
|
+
if (!existsSync4(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
|
|
2615
|
+
if (!existsSync4(key)) {
|
|
2453
2616
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
|
|
2454
2617
|
if (decrypted.exitCode !== 0)
|
|
2455
2618
|
return decrypted;
|
|
@@ -2457,30 +2620,30 @@ var runProvisionOperation = async (operation) => {
|
|
|
2457
2620
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
2458
2621
|
if (derived.exitCode !== 0)
|
|
2459
2622
|
return derived;
|
|
2460
|
-
|
|
2623
|
+
writeFileSync4(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
|
|
2461
2624
|
`, { mode: 292 });
|
|
2462
2625
|
}
|
|
2463
2626
|
return { stdout: "", exitCode: 0 };
|
|
2464
2627
|
} finally {
|
|
2465
|
-
|
|
2466
|
-
|
|
2628
|
+
rmSync4(key, { force: true });
|
|
2629
|
+
rmSync4(publicKey, { force: true });
|
|
2467
2630
|
}
|
|
2468
2631
|
}
|
|
2469
2632
|
if (operation.kind === "ensure-bootstrap-ssh-identity") {
|
|
2470
2633
|
const key = "/run/forgezero-bootstrap-ssh-key";
|
|
2471
2634
|
const generatedPublicKey = `${key}.pub`;
|
|
2472
2635
|
try {
|
|
2473
|
-
if (!
|
|
2474
|
-
|
|
2475
|
-
|
|
2636
|
+
if (!existsSync4(operation.credential) || lstatSync2(operation.credential).size < 1) {
|
|
2637
|
+
rmSync4(key, { force: true });
|
|
2638
|
+
rmSync4(generatedPublicKey, { force: true });
|
|
2476
2639
|
let result;
|
|
2477
2640
|
if (operation.source) {
|
|
2478
|
-
const source =
|
|
2641
|
+
const source = existsSync4(operation.source) ? lstatSync2(operation.source) : undefined;
|
|
2479
2642
|
if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
|
|
2480
2643
|
return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
|
|
2481
2644
|
}
|
|
2482
2645
|
copyFileSync(operation.source, key);
|
|
2483
|
-
|
|
2646
|
+
chmodSync2(key, 384);
|
|
2484
2647
|
} else {
|
|
2485
2648
|
result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-bootstrap-runner", "-f", key]);
|
|
2486
2649
|
if (result.exitCode !== 0)
|
|
@@ -2493,48 +2656,48 @@ var runProvisionOperation = async (operation) => {
|
|
|
2493
2656
|
result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
|
|
2494
2657
|
if (result.exitCode !== 0)
|
|
2495
2658
|
return result;
|
|
2496
|
-
|
|
2659
|
+
chmodSync2(operation.credential, 256);
|
|
2497
2660
|
}
|
|
2498
|
-
if (!
|
|
2499
|
-
if (!
|
|
2661
|
+
if (!existsSync4(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
|
|
2662
|
+
if (!existsSync4(key)) {
|
|
2500
2663
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
|
|
2501
2664
|
if (decrypted.exitCode !== 0)
|
|
2502
2665
|
return decrypted;
|
|
2503
|
-
|
|
2666
|
+
chmodSync2(key, 384);
|
|
2504
2667
|
}
|
|
2505
2668
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
2506
2669
|
if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
|
|
2507
2670
|
return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
|
|
2508
2671
|
}
|
|
2509
|
-
|
|
2510
|
-
|
|
2672
|
+
mkdirSync4(dirname5(operation.publicKey), { recursive: true, mode: 493 });
|
|
2673
|
+
writeFileSync4(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
|
|
2511
2674
|
`, { mode: 292 });
|
|
2512
|
-
|
|
2675
|
+
chmodSync2(operation.publicKey, 292);
|
|
2513
2676
|
}
|
|
2514
2677
|
if (operation.source)
|
|
2515
|
-
|
|
2678
|
+
rmSync4(operation.source, { force: true });
|
|
2516
2679
|
return { stdout: "", exitCode: 0 };
|
|
2517
2680
|
} finally {
|
|
2518
|
-
|
|
2519
|
-
|
|
2681
|
+
rmSync4(key, { force: true });
|
|
2682
|
+
rmSync4(generatedPublicKey, { force: true });
|
|
2520
2683
|
}
|
|
2521
2684
|
}
|
|
2522
2685
|
if (operation.kind === "ensure-enrolment") {
|
|
2523
|
-
if (
|
|
2686
|
+
if (existsSync4(operation.state) && lstatSync2(operation.state).size > 0 || existsSync4(operation.credential) && lstatSync2(operation.credential).size > 0)
|
|
2524
2687
|
return { stdout: "", exitCode: 0 };
|
|
2525
|
-
if (!
|
|
2688
|
+
if (!existsSync4(operation.source))
|
|
2526
2689
|
return { stdout: "enrolment source is missing", exitCode: 1 };
|
|
2527
2690
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
|
|
2528
2691
|
if (result.exitCode === 0) {
|
|
2529
|
-
|
|
2530
|
-
|
|
2692
|
+
chmodSync2(operation.credential, 256);
|
|
2693
|
+
rmSync4(operation.source, { force: true });
|
|
2531
2694
|
}
|
|
2532
2695
|
return result;
|
|
2533
2696
|
}
|
|
2534
2697
|
if (operation.kind === "wait-socket") {
|
|
2535
2698
|
for (let attempt = 0;attempt < operation.attempts; attempt += 1) {
|
|
2536
2699
|
try {
|
|
2537
|
-
if (
|
|
2700
|
+
if (lstatSync2(operation.path).isSocket())
|
|
2538
2701
|
return { stdout: "", exitCode: 0 };
|
|
2539
2702
|
} catch {}
|
|
2540
2703
|
await Bun.sleep(operation.intervalMs);
|
|
@@ -2542,7 +2705,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
2542
2705
|
return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
|
|
2543
2706
|
}
|
|
2544
2707
|
if (operation.kind === "verify-file")
|
|
2545
|
-
return
|
|
2708
|
+
return existsSync4(operation.path) && lstatSync2(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
|
|
2546
2709
|
if (operation.kind === "verify-egress") {
|
|
2547
2710
|
const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
|
|
2548
2711
|
if (active.exitCode !== 0)
|
|
@@ -2564,25 +2727,25 @@ var runProvisionOperation = async (operation) => {
|
|
|
2564
2727
|
}
|
|
2565
2728
|
}
|
|
2566
2729
|
if (operation.kind === "install-warp") {
|
|
2567
|
-
const os =
|
|
2730
|
+
const os = readFileSync4("/etc/os-release", "utf8");
|
|
2568
2731
|
if (!/^ID=ubuntu$/m.test(os) || !/^VERSION_ID="?26\.04"?$/m.test(os))
|
|
2569
2732
|
return { stdout: "unsupported WARP host OS", exitCode: 1 };
|
|
2570
2733
|
const response = await fetch("https://pkg.cloudflareclient.com/pubkey.gpg", { signal: AbortSignal.timeout(30000) });
|
|
2571
2734
|
if (!response.ok)
|
|
2572
2735
|
return { stdout: `WARP key HTTP ${response.status}`, exitCode: 1 };
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2736
|
+
mkdirSync4("/usr/share/keyrings", { recursive: true, mode: 493 });
|
|
2737
|
+
mkdirSync4("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
|
|
2738
|
+
mkdirSync4("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
|
|
2576
2739
|
const key = "/run/cloudflare-warp-key.gpg";
|
|
2577
|
-
|
|
2740
|
+
writeFileSync4(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
|
|
2578
2741
|
let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
|
|
2579
|
-
|
|
2742
|
+
rmSync4(key, { force: true });
|
|
2580
2743
|
if (result.exitCode !== 0)
|
|
2581
2744
|
return result;
|
|
2582
2745
|
const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
|
|
2583
2746
|
if (!codename)
|
|
2584
2747
|
return { stdout: "Ubuntu codename missing", exitCode: 1 };
|
|
2585
|
-
|
|
2748
|
+
writeFileSync4("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ ${codename} main
|
|
2586
2749
|
`, { mode: 420 });
|
|
2587
2750
|
result = await fixed(["/usr/bin/apt-get", "update", "-qq"]);
|
|
2588
2751
|
return result.exitCode === 0 ? fixed(["/usr/bin/apt-get", "install", "-y", "cloudflare-warp"]) : result;
|
|
@@ -2597,7 +2760,7 @@ async function localRunner(operation) {
|
|
|
2597
2760
|
if (capability.kind === "version")
|
|
2598
2761
|
return fixed(capability.argv);
|
|
2599
2762
|
try {
|
|
2600
|
-
const metadata =
|
|
2763
|
+
const metadata = lstatSync2(capability.path);
|
|
2601
2764
|
const present = capability.nodeType === "directory" ? metadata.isDirectory() : true;
|
|
2602
2765
|
return { stdout: present ? `yes
|
|
2603
2766
|
` : `no
|
|
@@ -2611,10 +2774,10 @@ function planInstall(options) {
|
|
|
2611
2774
|
const { capabilities, ...unit } = options;
|
|
2612
2775
|
return planProvision({ ...unit, mode: modeFor(capabilities) });
|
|
2613
2776
|
}
|
|
2614
|
-
async function applyPlan(plan,
|
|
2777
|
+
async function applyPlan(plan, run2) {
|
|
2615
2778
|
const transcript = [];
|
|
2616
2779
|
for (const step2 of plan.steps) {
|
|
2617
|
-
const result = await
|
|
2780
|
+
const result = await run2(step2.operation);
|
|
2618
2781
|
transcript.push({ label: step2.label, command: step2.command, exitCode: result.exitCode });
|
|
2619
2782
|
if (result.exitCode !== 0 && !step2.optional) {
|
|
2620
2783
|
throw new Error(`${step2.label} failed (exit ${result.exitCode}): ${step2.command}`);
|
|
@@ -2708,8 +2871,6 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
2708
2871
|
databaseUser: input.databaseUser,
|
|
2709
2872
|
sharedDirectory: input.sharedDirectory,
|
|
2710
2873
|
seedSyncEpoch: input.seedSyncEpoch,
|
|
2711
|
-
repository: input.repository,
|
|
2712
|
-
branch: input.branch,
|
|
2713
2874
|
deployProfile: input.deployProfile
|
|
2714
2875
|
}))
|
|
2715
2876
|
safeAtom(name, value);
|
|
@@ -2792,8 +2953,6 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
2792
2953
|
FZ_AGENT_OTLP_ENDPOINT: value.agentOtlpEndpoint,
|
|
2793
2954
|
FZ_CUSTODIAN_EMAIL: value.custodianEmail ?? "",
|
|
2794
2955
|
FZ_PROFILE: value.deployProfile,
|
|
2795
|
-
FZ_REPO: value.repository,
|
|
2796
|
-
FZ_BRANCH: value.branch,
|
|
2797
2956
|
FZ_EMAIL_PROVIDER: value.email?.provider ?? "",
|
|
2798
2957
|
FZ_SMTP_HOST: value.email?.provider === "smtp" ? value.email.host : "",
|
|
2799
2958
|
FZ_SMTP_PORT: value.email?.provider === "smtp" ? String(value.email.port) : "",
|
|
@@ -3004,7 +3163,7 @@ var EXTRACT = "/run/forgezero-otelcol-release";
|
|
|
3004
3163
|
var BINARY = "/usr/local/lib/forgezero/otelcol/otelcol";
|
|
3005
3164
|
var CONFIG = "/etc/forgezero/otelcol.yaml";
|
|
3006
3165
|
var UNIT = `/etc/systemd/system/${FORGEZERO_OTEL_COLLECTOR_UNIT}`;
|
|
3007
|
-
var
|
|
3166
|
+
var checked2 = async (host, argv) => {
|
|
3008
3167
|
const result = await host.exec(argv);
|
|
3009
3168
|
const output = `${result.output ?? result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
3010
3169
|
if (result.exitCode !== 0)
|
|
@@ -3086,7 +3245,7 @@ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
|
|
|
3086
3245
|
});
|
|
3087
3246
|
const versionOutput = `${observed.output ?? observed.stdout ?? ""}${observed.stderr ?? ""}`;
|
|
3088
3247
|
if (observed.exitCode !== 0 || !versionOutput.includes(`otelcol version ${FORGEZERO_OTEL_COLLECTOR_VERSION}`)) {
|
|
3089
|
-
await
|
|
3248
|
+
await checked2(host, [
|
|
3090
3249
|
"/usr/bin/curl",
|
|
3091
3250
|
"--fail",
|
|
3092
3251
|
"--silent",
|
|
@@ -3098,19 +3257,19 @@ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
|
|
|
3098
3257
|
ARCHIVE,
|
|
3099
3258
|
`https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${FORGEZERO_OTEL_COLLECTOR_VERSION}/otelcol_${FORGEZERO_OTEL_COLLECTOR_VERSION}_linux_amd64.tar.gz`
|
|
3100
3259
|
]);
|
|
3101
|
-
const digest = (await
|
|
3260
|
+
const digest = (await checked2(host, ["/usr/bin/sha256sum", ARCHIVE])).split(/\s+/)[0];
|
|
3102
3261
|
if (digest !== FORGEZERO_OTEL_COLLECTOR_SHA256)
|
|
3103
3262
|
throw new Error("OTLP collector archive checksum mismatch");
|
|
3104
|
-
await
|
|
3105
|
-
await
|
|
3106
|
-
await
|
|
3107
|
-
await
|
|
3263
|
+
await checked2(host, ["/usr/bin/install", "-d", "-m", "0755", "/usr/local/lib/forgezero/otelcol", EXTRACT]);
|
|
3264
|
+
await checked2(host, ["/usr/bin/tar", "-xzf", ARCHIVE, "-C", EXTRACT, "otelcol"]);
|
|
3265
|
+
await checked2(host, ["/usr/bin/install", "-m", "0755", `${EXTRACT}/otelcol`, BINARY]);
|
|
3266
|
+
await checked2(host, ["/usr/bin/rm", "-f", ARCHIVE, `${EXTRACT}/otelcol`]);
|
|
3108
3267
|
}
|
|
3109
3268
|
if ((await host.exec(["/usr/bin/getent", "group", "forgezero-otel"])).exitCode !== 0) {
|
|
3110
|
-
await
|
|
3269
|
+
await checked2(host, ["/usr/sbin/groupadd", "--system", "forgezero-otel"]);
|
|
3111
3270
|
}
|
|
3112
3271
|
if ((await host.exec(["/usr/bin/id", "forgezero-otel"])).exitCode !== 0) {
|
|
3113
|
-
await
|
|
3272
|
+
await checked2(host, [
|
|
3114
3273
|
"/usr/sbin/useradd",
|
|
3115
3274
|
"--system",
|
|
3116
3275
|
"--no-create-home",
|
|
@@ -3123,8 +3282,8 @@ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
|
|
|
3123
3282
|
}
|
|
3124
3283
|
host.write(CONFIG, rendered.config, 420);
|
|
3125
3284
|
host.write(UNIT, rendered.unit, 420);
|
|
3126
|
-
await
|
|
3127
|
-
await
|
|
3285
|
+
await checked2(host, ["/usr/bin/systemctl", "daemon-reload"]);
|
|
3286
|
+
await checked2(host, ["/usr/bin/systemctl", "enable", "--now", FORGEZERO_OTEL_COLLECTOR_UNIT]);
|
|
3128
3287
|
let ready = false;
|
|
3129
3288
|
for (let attempt = 0;attempt < 100; attempt += 1) {
|
|
3130
3289
|
const probe = await host.exec([
|
|
@@ -3253,7 +3412,7 @@ var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
|
|
|
3253
3412
|
var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
|
|
3254
3413
|
var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
|
|
3255
3414
|
var BOOTSTRAP_SSH_PUBLIC_KEY = "/etc/forgezero/bootstrap/runner.pub";
|
|
3256
|
-
var
|
|
3415
|
+
var BOOTSTRAP_RELEASE_EVIDENCE = "/var/lib/forgezero/bootstrap-release.json";
|
|
3257
3416
|
var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
|
|
3258
3417
|
var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
|
|
3259
3418
|
var CONTROL_SOCKET = "/run/forgezero/control.sock";
|
|
@@ -3326,6 +3485,9 @@ function validateBootstrapConfig(value) {
|
|
|
3326
3485
|
throw new Error("Cloudflare Mesh/WARP requires a node-specific Cloudflare handoff");
|
|
3327
3486
|
}
|
|
3328
3487
|
if (value.kind === "enrolled-compute") {
|
|
3488
|
+
if (value.gitDeployKey !== undefined && typeof value.gitDeployKey !== "boolean") {
|
|
3489
|
+
throw new Error("gitDeployKey must be boolean");
|
|
3490
|
+
}
|
|
3329
3491
|
if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(value.realm))
|
|
3330
3492
|
throw new Error("tenant realm is malformed");
|
|
3331
3493
|
if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
|
|
@@ -3351,6 +3513,9 @@ function validateBootstrapConfig(value) {
|
|
|
3351
3513
|
throw new Error("unsupported platform software profile");
|
|
3352
3514
|
if (!["production", "development"].includes(value.environment))
|
|
3353
3515
|
throw new Error("platform environment must be production or development");
|
|
3516
|
+
if (!value.bootstrapBundle || ![value.bootstrapBundle.bundleFile, value.bootstrapBundle.manifestFile].every((path) => typeof path === "string" && path.startsWith("/") && !/[\r\n\0:]/.test(path))) {
|
|
3517
|
+
throw new Error("platform bootstrap requires absolute bundle and manifest paths");
|
|
3518
|
+
}
|
|
3354
3519
|
let api;
|
|
3355
3520
|
try {
|
|
3356
3521
|
api = new URL(value.apiUrl);
|
|
@@ -3383,7 +3548,7 @@ function validateBootstrapConfig(value) {
|
|
|
3383
3548
|
if (runtime.otlpCollectorUnit !== FORGEZERO_OTEL_COLLECTOR_UNIT) {
|
|
3384
3549
|
throw new Error(`platform bootstrap requires ${FORGEZERO_OTEL_COLLECTOR_UNIT}`);
|
|
3385
3550
|
}
|
|
3386
|
-
if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.
|
|
3551
|
+
if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.databaseCoordinators.join(",") !== write.join(",")) {
|
|
3387
3552
|
throw new Error("platform runtime coordinates disagree with immutable bootstrap coordinates");
|
|
3388
3553
|
}
|
|
3389
3554
|
if (runtime.deployProfile !== value.environment)
|
|
@@ -3413,6 +3578,7 @@ function planBootstrap(input, initialized = false) {
|
|
|
3413
3578
|
] : [
|
|
3414
3579
|
...config.firewall.enabled ? [{ id: "ufw", version: "ubuntu-26.04" }] : [],
|
|
3415
3580
|
{ id: "bun", version: "1.3.14" },
|
|
3581
|
+
{ id: "git", version: "ubuntu-26.04" },
|
|
3416
3582
|
{ id: "nginx", version: "ubuntu-26.04" },
|
|
3417
3583
|
...platformBootstrapRunner(config) ? [{ id: "openssh-client", version: "ubuntu-26.04" }] : [],
|
|
3418
3584
|
...config.profile === "platform-api" ? [] : [{ id: "arangodb", version: "3.11.14" }],
|
|
@@ -3441,7 +3607,7 @@ function planBootstrap(input, initialized = false) {
|
|
|
3441
3607
|
]
|
|
3442
3608
|
};
|
|
3443
3609
|
}
|
|
3444
|
-
var
|
|
3610
|
+
var checked3 = async (host, argv, label, options) => {
|
|
3445
3611
|
const result = await host.exec(argv, options);
|
|
3446
3612
|
if (result.exitCode !== 0)
|
|
3447
3613
|
throw new Error(`${label} failed: ${result.output.trim()}`);
|
|
@@ -3622,6 +3788,37 @@ async function seal(host, name, destination2, value) {
|
|
|
3622
3788
|
if (result.exitCode !== 0)
|
|
3623
3789
|
throw new Error(`could not seal ${name}: ${result.output.trim()}`);
|
|
3624
3790
|
}
|
|
3791
|
+
async function verifyBootstrapBundleOnHost(host, config, verifyGit = false) {
|
|
3792
|
+
const manifestMetadata = host.inspect?.(config.bootstrapBundle.manifestFile);
|
|
3793
|
+
const bundleMetadata = host.inspect?.(config.bootstrapBundle.bundleFile);
|
|
3794
|
+
if (manifestMetadata && (!manifestMetadata.regular || manifestMetadata.symbolic || manifestMetadata.uid !== 0 || manifestMetadata.links !== 1 || (manifestMetadata.mode & 63) !== 0 || manifestMetadata.size > 16 * 1024)) {
|
|
3795
|
+
throw new Error("bootstrap bundle manifest must be a root-owned owner-only regular file");
|
|
3796
|
+
}
|
|
3797
|
+
if (bundleMetadata && (!bundleMetadata.regular || bundleMetadata.symbolic || bundleMetadata.uid !== 0 || bundleMetadata.links !== 1 || (bundleMetadata.mode & 63) !== 0 || bundleMetadata.size < 1 || bundleMetadata.size > 512 * 1024 * 1024)) {
|
|
3798
|
+
throw new Error("bootstrap bundle must be a root-owned owner-only regular file within 512 MiB");
|
|
3799
|
+
}
|
|
3800
|
+
if (!host.exists(config.bootstrapBundle.bundleFile) || !host.exists(config.bootstrapBundle.manifestFile)) {
|
|
3801
|
+
throw new Error("attended bootstrap bundle and manifest are required for release generation one");
|
|
3802
|
+
}
|
|
3803
|
+
let parsed;
|
|
3804
|
+
try {
|
|
3805
|
+
parsed = JSON.parse(host.read(config.bootstrapBundle.manifestFile));
|
|
3806
|
+
} catch {
|
|
3807
|
+
throw new Error("bootstrap bundle manifest is not valid JSON");
|
|
3808
|
+
}
|
|
3809
|
+
const manifest = parseBootstrapBundleManifest(parsed);
|
|
3810
|
+
const expectedBranch = config.environment === "production" ? "main" : "dev";
|
|
3811
|
+
if (manifest.branch !== expectedBranch || bundleMetadata && bundleMetadata.size !== manifest.bytes) {
|
|
3812
|
+
throw new Error("bootstrap bundle manifest disagrees with the selected platform environment");
|
|
3813
|
+
}
|
|
3814
|
+
const digest = (await checked3(host, ["/usr/bin/sha256sum", config.bootstrapBundle.bundleFile], "bootstrap bundle digest")).trim().split(/\s+/)[0];
|
|
3815
|
+
if (digest !== manifest.sha256)
|
|
3816
|
+
throw new Error("bootstrap bundle digest does not match its manifest");
|
|
3817
|
+
if (verifyGit) {
|
|
3818
|
+
await checked3(host, ["/usr/bin/git", "bundle", "verify", config.bootstrapBundle.bundleFile], "bootstrap Git bundle verification");
|
|
3819
|
+
}
|
|
3820
|
+
return manifest;
|
|
3821
|
+
}
|
|
3625
3822
|
function bootstrapIdentity(config) {
|
|
3626
3823
|
if (config.kind === "enrolled-compute")
|
|
3627
3824
|
return {
|
|
@@ -3648,8 +3845,7 @@ function bootstrapIdentity(config) {
|
|
|
3648
3845
|
computeReference: config.computeReference,
|
|
3649
3846
|
nodeHostname: config.nodeHostname,
|
|
3650
3847
|
apiUrl: config.apiUrl,
|
|
3651
|
-
|
|
3652
|
-
branch: config.branch,
|
|
3848
|
+
bootstrapBundle: config.bootstrapBundle,
|
|
3653
3849
|
deployRoot: config.deployRoot ?? "/opt/forgezero",
|
|
3654
3850
|
telemetryEndpoint: config.telemetryEndpoint,
|
|
3655
3851
|
database: {
|
|
@@ -3679,7 +3875,7 @@ function bootstrapIdentity(config) {
|
|
|
3679
3875
|
};
|
|
3680
3876
|
}
|
|
3681
3877
|
function bootstrapIdentityDigest(config) {
|
|
3682
|
-
return
|
|
3878
|
+
return createHash2("sha256").update(JSON.stringify(bootstrapIdentity(config))).digest("hex");
|
|
3683
3879
|
}
|
|
3684
3880
|
function parseStoredState(raw) {
|
|
3685
3881
|
let value;
|
|
@@ -3729,34 +3925,6 @@ function bindBootstrapIntent(host, config) {
|
|
|
3729
3925
|
}
|
|
3730
3926
|
return identityDigest;
|
|
3731
3927
|
}
|
|
3732
|
-
async function preparePlatformBootstrap(input, host = localBootstrapHost()) {
|
|
3733
|
-
const config = validateBootstrapConfig(structuredClone(input));
|
|
3734
|
-
if (config.kind !== "platform")
|
|
3735
|
-
throw new Error("platform preparation requires a platform bootstrap config");
|
|
3736
|
-
if (host.uid() !== 0)
|
|
3737
|
-
throw new Error("fz bootstrap platform prepare --apply must run as root");
|
|
3738
|
-
if (host.exists(STATE_PATH)) {
|
|
3739
|
-
const installed = parseStoredState(host.read(STATE_PATH));
|
|
3740
|
-
if (installed.kind !== "platform" || installed.identityDigest !== bootstrapIdentityDigest(config)) {
|
|
3741
|
-
throw new Error("platform preparation coordinates do not match the installed host identity");
|
|
3742
|
-
}
|
|
3743
|
-
}
|
|
3744
|
-
const identityDigest = bindBootstrapIntent(host, config);
|
|
3745
|
-
await host.installAgent(config);
|
|
3746
|
-
if (!host.exists(GIT_PUBLIC_KEY))
|
|
3747
|
-
throw new Error("Agent installation did not produce its public Git deploy key");
|
|
3748
|
-
const gitPublicKey = host.read(GIT_PUBLIC_KEY).trim();
|
|
3749
|
-
if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(gitPublicKey)) {
|
|
3750
|
-
throw new Error("Agent public Git deploy key is malformed");
|
|
3751
|
-
}
|
|
3752
|
-
return {
|
|
3753
|
-
kind: "platform",
|
|
3754
|
-
prepared: true,
|
|
3755
|
-
identityDigest,
|
|
3756
|
-
gitPublicKey,
|
|
3757
|
-
next: "register this read-only deploy key, then run --apply concurrently on all three genesis Agency members"
|
|
3758
|
-
};
|
|
3759
|
-
}
|
|
3760
3928
|
function stateFor(config, cloudflare, previousCloudflareTunnelId) {
|
|
3761
3929
|
return `${JSON.stringify({
|
|
3762
3930
|
format: 2,
|
|
@@ -3910,6 +4078,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
3910
4078
|
let installed;
|
|
3911
4079
|
if (host.exists(STATE_PATH))
|
|
3912
4080
|
installed = parseStoredState(host.read(STATE_PATH));
|
|
4081
|
+
const bootstrapManifest = config.kind === "platform" && !host.exists(BOOTSTRAP_RELEASE_EVIDENCE) ? await verifyBootstrapBundleOnHost(host, config) : undefined;
|
|
3913
4082
|
let cloudflare;
|
|
3914
4083
|
if (config.cloudflareHandoff) {
|
|
3915
4084
|
if (host.exists(config.cloudflareHandoff.handoffFile)) {
|
|
@@ -3967,14 +4136,14 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
3967
4136
|
const platformPrivate = config.kind === "platform" ? (() => {
|
|
3968
4137
|
if (!secrets)
|
|
3969
4138
|
throw new Error("platform apply requires attended credentials on stdin");
|
|
3970
|
-
const
|
|
4139
|
+
const checked4 = validatePlatformBootstrapSecrets(config, secrets);
|
|
3971
4140
|
return {
|
|
3972
|
-
root:
|
|
3973
|
-
email:
|
|
3974
|
-
enrolmentToken:
|
|
3975
|
-
backup:
|
|
3976
|
-
cloudflareTunnelToken:
|
|
3977
|
-
cloudflareApiToken:
|
|
4141
|
+
root: checked4.clusterBootstrapCode,
|
|
4142
|
+
email: checked4.emailSecret,
|
|
4143
|
+
enrolmentToken: checked4.enrolmentToken,
|
|
4144
|
+
backup: checked4.backupS3Secret,
|
|
4145
|
+
cloudflareTunnelToken: checked4.cloudflareTunnelToken,
|
|
4146
|
+
cloudflareApiToken: checked4.cloudflareApiToken
|
|
3978
4147
|
};
|
|
3979
4148
|
})() : undefined;
|
|
3980
4149
|
const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
|
|
@@ -4013,24 +4182,35 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4013
4182
|
if (connectorCapabilities?.meshConnectorToken && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
|
|
4014
4183
|
await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, connectorCapabilities.meshConnectorToken);
|
|
4015
4184
|
}
|
|
4016
|
-
|
|
4185
|
+
if (alreadyEnrolled) {
|
|
4186
|
+
await host.installAgent(config, "bound");
|
|
4187
|
+
} else if (config.kind === "platform") {
|
|
4188
|
+
if (bootstrapManifest)
|
|
4189
|
+
await host.installAgent(config, "bootstrap");
|
|
4190
|
+
} else {
|
|
4191
|
+
await host.installAgent(config, "enrol");
|
|
4192
|
+
await host.installAgent(config, "bound");
|
|
4193
|
+
}
|
|
4017
4194
|
if (config.kind === "platform" && config.firewall.enabled) {
|
|
4018
4195
|
await host.ensureSoftware(plan.software.filter(({ id }) => id === "ufw"));
|
|
4019
4196
|
} else
|
|
4020
4197
|
await host.ensureSoftware(plan.software);
|
|
4021
4198
|
if (config.kind === "platform" && config.firewall.enabled) {
|
|
4022
|
-
await
|
|
4023
|
-
await
|
|
4024
|
-
await
|
|
4199
|
+
await checked3(host, ["ufw", "--force", "default", "deny", "incoming"], "firewall inbound policy");
|
|
4200
|
+
await checked3(host, ["ufw", "--force", "default", "allow", "outgoing"], "firewall outbound policy");
|
|
4201
|
+
await checked3(host, ["ufw", "allow", `${config.firewall.sshPort}/tcp`], "firewall SSH rule");
|
|
4025
4202
|
for (const cidr of config.firewall.privateCidrs) {
|
|
4026
4203
|
if (config.database.role !== "none")
|
|
4027
|
-
await
|
|
4204
|
+
await checked3(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
|
|
4028
4205
|
for (const port of [config.runtime.bluePort, config.runtime.greenPort])
|
|
4029
|
-
await
|
|
4206
|
+
await checked3(host, ["ufw", "allow", "from", cidr, "to", "any", "port", String(port), "proto", "tcp"], "seed-mesh firewall rule");
|
|
4030
4207
|
}
|
|
4031
|
-
await
|
|
4208
|
+
await checked3(host, ["ufw", "--force", "enable"], "firewall activation");
|
|
4032
4209
|
await host.ensureSoftware(plan.software.filter(({ id }) => id !== "ufw"));
|
|
4033
4210
|
}
|
|
4211
|
+
if (config.kind === "platform" && bootstrapManifest) {
|
|
4212
|
+
await verifyBootstrapBundleOnHost(host, config, true);
|
|
4213
|
+
}
|
|
4034
4214
|
if (config.kind === "platform") {
|
|
4035
4215
|
const root = platformPrivate.root;
|
|
4036
4216
|
if (!host.exists(JWT_CREDENTIAL)) {
|
|
@@ -4085,8 +4265,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4085
4265
|
keepReleases: runtime.keepReleases,
|
|
4086
4266
|
drainDeadlineMs: runtime.environment.drainDeadlineMs
|
|
4087
4267
|
});
|
|
4088
|
-
await
|
|
4089
|
-
await
|
|
4268
|
+
await checked3(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
|
|
4269
|
+
await checked3(host, ["id", runtime.serviceUser], "existing API service account");
|
|
4090
4270
|
});
|
|
4091
4271
|
host.mkdir(runtime.environment.sharedDirectory, 488);
|
|
4092
4272
|
host.mkdir(runtime.slotsDirectory, 493);
|
|
@@ -4095,7 +4275,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4095
4275
|
host.write("/etc/forgezero/capacity.env", `FZ_CONCURRENCY_LIMIT=${runtime.environment.concurrencyLimit}
|
|
4096
4276
|
`, 420);
|
|
4097
4277
|
}
|
|
4098
|
-
await
|
|
4278
|
+
await checked3(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
|
|
4099
4279
|
host.write("/etc/systemd/system/forgezero@.service", units.template, 420);
|
|
4100
4280
|
host.write("/etc/systemd/system/forgezero@blue.service.d/port.conf", units.dropIns.blue, 420);
|
|
4101
4281
|
host.write("/etc/systemd/system/forgezero@green.service.d/port.conf", units.dropIns.green, 420);
|
|
@@ -4104,30 +4284,30 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4104
4284
|
host.write("/etc/forgezero/deploy.env", activation.environment, 420);
|
|
4105
4285
|
host.write("/etc/forgezero/deploy-activation.json", activation.helper, 384);
|
|
4106
4286
|
host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
|
|
4107
|
-
await
|
|
4287
|
+
await checked3(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
|
|
4108
4288
|
const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
|
|
4109
|
-
await
|
|
4110
|
-
const otlpStatus = (await
|
|
4289
|
+
await checked3(host, telemetry.unitCheck.argv, "OTLP collector supervision");
|
|
4290
|
+
const otlpStatus = (await checked3(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
|
|
4111
4291
|
if (!/^2\d\d$/.test(otlpStatus))
|
|
4112
4292
|
throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
|
|
4113
|
-
await
|
|
4114
|
-
await
|
|
4115
|
-
await
|
|
4293
|
+
await checked3(host, ["nginx", "-t"], "nginx configuration");
|
|
4294
|
+
await checked3(host, ["systemctl", "daemon-reload"], "systemd reload");
|
|
4295
|
+
await checked3(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
|
|
4116
4296
|
if (config.database.role === "master") {
|
|
4117
4297
|
const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
4118
4298
|
if (!host.exists(invite)) {
|
|
4119
|
-
host.write(invite, `plt_${
|
|
4299
|
+
host.write(invite, `plt_${randomBytes3(24).toString("hex")}
|
|
4120
4300
|
`, 384);
|
|
4121
|
-
await
|
|
4301
|
+
await checked3(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
|
|
4122
4302
|
}
|
|
4123
4303
|
}
|
|
4124
4304
|
if (config.database.role !== "none") {
|
|
4125
4305
|
host.mkdir("/var/lib/forgezero-cluster", 448);
|
|
4126
|
-
await
|
|
4306
|
+
await checked3(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
|
|
4127
4307
|
host.write("/etc/systemd/system/forgezero-db.service", databaseUnit(config), 420);
|
|
4128
4308
|
host.write("/etc/systemd/system/forgezero-db-verify.service", databaseVerifyUnit(config), 420);
|
|
4129
|
-
await
|
|
4130
|
-
await
|
|
4309
|
+
await checked3(host, ["systemctl", "daemon-reload"], "database unit reload");
|
|
4310
|
+
await checked3(host, ["systemctl", "enable", "--now", "forgezero-db.service", "forgezero-db-verify.service"], "database supervision");
|
|
4131
4311
|
const evidence = {
|
|
4132
4312
|
expectedMode: "default",
|
|
4133
4313
|
role: "COORDINATOR",
|
|
@@ -4138,15 +4318,42 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4138
4318
|
host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
|
|
4139
4319
|
`, 384);
|
|
4140
4320
|
}
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4321
|
+
if (bootstrapManifest) {
|
|
4322
|
+
await checked3(host, [
|
|
4323
|
+
"runuser",
|
|
4324
|
+
"-u",
|
|
4325
|
+
"forgezero-agent",
|
|
4326
|
+
"--",
|
|
4327
|
+
"/usr/local/bin/fz-agent",
|
|
4328
|
+
"deploy",
|
|
4329
|
+
`--revision=${bootstrapManifest.revision}`,
|
|
4330
|
+
...config.database.role === "master" ? ["--release-executor"] : []
|
|
4331
|
+
], "initial Agent deployment");
|
|
4332
|
+
host.write(BOOTSTRAP_RELEASE_EVIDENCE, `${JSON.stringify({
|
|
4333
|
+
format: 1,
|
|
4334
|
+
kind: "forgezero-bootstrap-release",
|
|
4335
|
+
revision: bootstrapManifest.revision,
|
|
4336
|
+
sha256: bootstrapManifest.sha256,
|
|
4337
|
+
branch: bootstrapManifest.branch,
|
|
4338
|
+
deployedAt: new Date().toISOString()
|
|
4339
|
+
}, null, 2)}
|
|
4340
|
+
`, 384);
|
|
4341
|
+
host.remove(config.bootstrapBundle.bundleFile);
|
|
4342
|
+
host.remove(config.bootstrapBundle.manifestFile);
|
|
4343
|
+
}
|
|
4344
|
+
if (!alreadyEnrolled) {
|
|
4345
|
+
await checked3(host, [
|
|
4346
|
+
"curl",
|
|
4347
|
+
"--fail",
|
|
4348
|
+
"--silent",
|
|
4349
|
+
"--show-error",
|
|
4350
|
+
"--max-time",
|
|
4351
|
+
"10",
|
|
4352
|
+
`http://127.0.0.1:3000${runtime.healthPath}`
|
|
4353
|
+
], "release-one API health");
|
|
4354
|
+
await host.installAgent(config, "enrol");
|
|
4355
|
+
await host.installAgent(config, "bound");
|
|
4356
|
+
}
|
|
4150
4357
|
}
|
|
4151
4358
|
if (config.cloudflareHandoff) {
|
|
4152
4359
|
if (!host.exists(TUNNEL_CREDENTIAL) && connectorCapabilities) {
|
|
@@ -4155,8 +4362,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4155
4362
|
if (!host.exists(TUNNEL_CREDENTIAL))
|
|
4156
4363
|
throw new Error("sealed cloudflared connector credential is missing");
|
|
4157
4364
|
host.write("/etc/systemd/system/cloudflared.service", tunnelUnit(), 420);
|
|
4158
|
-
await
|
|
4159
|
-
await
|
|
4365
|
+
await checked3(host, ["systemctl", "daemon-reload"], "cloudflared unit reload");
|
|
4366
|
+
await checked3(host, ["systemctl", "enable", "--now", "cloudflared.service"], "cloudflared connector supervision");
|
|
4160
4367
|
const tunnelId = cloudflare?.tunnelId ?? installed?.cloudflareTunnelId ?? (config.kind === "platform" ? config.runtime.environment.cloudflare?.tunnelId : undefined);
|
|
4161
4368
|
if (!tunnelId)
|
|
4162
4369
|
throw new Error("Cloudflare tunnel identity is missing after handoff validation");
|
|
@@ -4166,8 +4373,8 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4166
4373
|
if (!host.exists(WARP_CONNECTOR_CREDENTIAL))
|
|
4167
4374
|
throw new Error("sealed Cloudflare Mesh connector credential is missing");
|
|
4168
4375
|
host.write("/etc/systemd/system/forgezero-mesh-config.service", meshConnectorUnit(), 420);
|
|
4169
|
-
await
|
|
4170
|
-
await
|
|
4376
|
+
await checked3(host, ["systemctl", "daemon-reload"], "Cloudflare Mesh unit reload");
|
|
4377
|
+
await checked3(host, ["systemctl", "enable", "--now", "warp-svc.service", "forgezero-mesh-config.service"], "Cloudflare Mesh connector supervision");
|
|
4171
4378
|
}
|
|
4172
4379
|
host.write(STATE_PATH, stateFor(config, cloudflare, installed?.cloudflareTunnelId), 384);
|
|
4173
4380
|
const status = await bootstrapStatus(host);
|
|
@@ -4212,8 +4419,7 @@ function strictBootstrapDocument(value) {
|
|
|
4212
4419
|
"computeReference",
|
|
4213
4420
|
"nodeHostname",
|
|
4214
4421
|
"apiUrl",
|
|
4215
|
-
"
|
|
4216
|
-
"branch",
|
|
4422
|
+
"bootstrapBundle",
|
|
4217
4423
|
"deployRoot",
|
|
4218
4424
|
"telemetryEndpoint",
|
|
4219
4425
|
"database",
|
|
@@ -4226,9 +4432,11 @@ function strictBootstrapDocument(value) {
|
|
|
4226
4432
|
"realm",
|
|
4227
4433
|
"software",
|
|
4228
4434
|
"deploymentCredentials",
|
|
4435
|
+
"gitDeployKey",
|
|
4229
4436
|
"bootstrapRunner"
|
|
4230
4437
|
], "bootstrap config");
|
|
4231
4438
|
if (root.kind === "platform") {
|
|
4439
|
+
exactKeys(root.bootstrapBundle, ["bundleFile", "manifestFile"], "bootstrap bundle config");
|
|
4232
4440
|
exactKeys(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
|
|
4233
4441
|
if (root.cloudflareHandoff !== undefined)
|
|
4234
4442
|
exactKeys(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
|
|
@@ -4270,8 +4478,6 @@ function strictBootstrapDocument(value) {
|
|
|
4270
4478
|
"agentOtlpEndpoint",
|
|
4271
4479
|
"custodianEmail",
|
|
4272
4480
|
"email",
|
|
4273
|
-
"repository",
|
|
4274
|
-
"branch",
|
|
4275
4481
|
"deployProfile",
|
|
4276
4482
|
"otlpFlushIntervalMs",
|
|
4277
4483
|
"otlpTraceSampleRatio",
|
|
@@ -4309,11 +4515,70 @@ function strictBootstrapDocument(value) {
|
|
|
4309
4515
|
return value;
|
|
4310
4516
|
}
|
|
4311
4517
|
function readBootstrapConfig(path) {
|
|
4312
|
-
const metadata =
|
|
4518
|
+
const metadata = lstatSync3(path);
|
|
4313
4519
|
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== (process.getuid?.() ?? metadata.uid) || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 64 * 1024) {
|
|
4314
4520
|
throw new Error("bootstrap config must be an owner-only regular file with one link and at most 64 KiB");
|
|
4315
4521
|
}
|
|
4316
|
-
return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(
|
|
4522
|
+
return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync5(path, "utf8"))));
|
|
4523
|
+
}
|
|
4524
|
+
function planBootstrapAgentInstall(config, phase, context) {
|
|
4525
|
+
const { capabilities, hasBinding, hasEnrolCredential, initialBundle } = context;
|
|
4526
|
+
if (phase === "bootstrap") {
|
|
4527
|
+
if (config.kind !== "platform" || hasBinding || !initialBundle) {
|
|
4528
|
+
throw new Error("release-one Agent install requires an unbound platform host and verified bootstrap bundle");
|
|
4529
|
+
}
|
|
4530
|
+
} else if (phase === "enrol") {
|
|
4531
|
+
if (hasBinding || !hasEnrolCredential) {
|
|
4532
|
+
throw new Error("Agent enrolment requires one unbound host and its sealed one-use credential");
|
|
4533
|
+
}
|
|
4534
|
+
} else if (!hasBinding) {
|
|
4535
|
+
throw new Error("bound Agent convergence requires durable enrolment state");
|
|
4536
|
+
}
|
|
4537
|
+
const bound = phase === "bound";
|
|
4538
|
+
const enrolling = phase === "enrol";
|
|
4539
|
+
const authenticated = enrolling || bound;
|
|
4540
|
+
const bootstrapRunner = platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner);
|
|
4541
|
+
const options = {
|
|
4542
|
+
capabilities,
|
|
4543
|
+
socketPath: DEFAULT_SOCKET2,
|
|
4544
|
+
seedPath: "/var/lib/forgezero/node.seed",
|
|
4545
|
+
controlSocketPath: CONTROL_SOCKET,
|
|
4546
|
+
repository: phase === "bootstrap" ? initialBundle.path : bound && config.kind === "enrolled-compute" ? config.repository : undefined,
|
|
4547
|
+
branch: phase === "bootstrap" ? initialBundle.manifest.branch : bound && config.kind === "enrolled-compute" ? config.branch : undefined,
|
|
4548
|
+
bootstrapBundlePath: phase === "bootstrap" ? initialBundle.path : undefined,
|
|
4549
|
+
bootstrapBundleManifestPath: phase === "bootstrap" ? initialBundle.manifestPath : undefined,
|
|
4550
|
+
profile: config.profile,
|
|
4551
|
+
deployRoot: config.deployRoot ?? "/opt/forgezero",
|
|
4552
|
+
deploymentCredentials: config.deploymentCredentials,
|
|
4553
|
+
publicApiUrl: config.apiUrl,
|
|
4554
|
+
gitCredentialPath: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/creds/git-deploy-key.cred" : undefined,
|
|
4555
|
+
gitPublicKeyPath: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey ? "/etc/forgezero/git/deploy.pub" : undefined,
|
|
4556
|
+
generateGitIdentity: authenticated && config.kind === "enrolled-compute" && config.gitDeployKey === true,
|
|
4557
|
+
pullDeployments: bound,
|
|
4558
|
+
pullMigrations: bound && config.kind === "platform",
|
|
4559
|
+
pullBootstrap: bound && bootstrapRunner,
|
|
4560
|
+
bootstrapSshCredentialPath: authenticated && bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
|
|
4561
|
+
bootstrapSshSourcePath: enrolling && config.kind === "enrolled-compute" ? config.bootstrapRunner?.sshPrivateKeyFile : undefined,
|
|
4562
|
+
bootstrapSshPublicKeyPath: authenticated && bootstrapRunner ? BOOTSTRAP_SSH_PUBLIC_KEY : undefined,
|
|
4563
|
+
bootstrapTargetTelemetryEndpoint: bound && bootstrapRunner ? platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined : undefined,
|
|
4564
|
+
lifecycleProfilePath: bound && config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
|
|
4565
|
+
enforceEgress: true,
|
|
4566
|
+
nodeHostname: config.nodeHostname,
|
|
4567
|
+
telemetryEndpoint: config.telemetryEndpoint,
|
|
4568
|
+
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
4569
|
+
sourceBinPath: PACKAGED_AGENT_BIN,
|
|
4570
|
+
...enrolling ? {
|
|
4571
|
+
enrolTokenCredentialPath: ENROL_CREDENTIAL,
|
|
4572
|
+
enrolStatePath: "/var/lib/forgezero/enrolment.json"
|
|
4573
|
+
} : bound ? { enrolStatePath: "/var/lib/forgezero/enrolment.json" } : {},
|
|
4574
|
+
...authenticated ? {
|
|
4575
|
+
apiUrl: config.apiUrl,
|
|
4576
|
+
project: config.kind === "enrolled-compute" ? config.realm : "platform",
|
|
4577
|
+
environment: config.kind === "enrolled-compute" ? undefined : config.environment,
|
|
4578
|
+
nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
|
|
4579
|
+
} : {}
|
|
4580
|
+
};
|
|
4581
|
+
return planInstall(options);
|
|
4317
4582
|
}
|
|
4318
4583
|
function localBootstrapHost() {
|
|
4319
4584
|
const execute = async (argv, options = {}) => {
|
|
@@ -4331,19 +4596,19 @@ function localBootstrapHost() {
|
|
|
4331
4596
|
};
|
|
4332
4597
|
return {
|
|
4333
4598
|
uid: () => process.getuid?.() ?? -1,
|
|
4334
|
-
exists:
|
|
4335
|
-
read: (path) =>
|
|
4599
|
+
exists: existsSync5,
|
|
4600
|
+
read: (path) => readFileSync5(path, "utf8"),
|
|
4336
4601
|
write(path, content, mode) {
|
|
4337
|
-
|
|
4602
|
+
mkdirSync5(dirname6(path), { recursive: true, mode: 493 });
|
|
4338
4603
|
const temporary = `${path}.next.${process.pid}`;
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4604
|
+
writeFileSync5(temporary, content, { mode });
|
|
4605
|
+
chmodSync3(temporary, mode);
|
|
4606
|
+
renameSync5(temporary, path);
|
|
4342
4607
|
},
|
|
4343
|
-
mkdir: (path, mode) =>
|
|
4344
|
-
remove: (path) =>
|
|
4608
|
+
mkdir: (path, mode) => mkdirSync5(path, { recursive: true, mode }),
|
|
4609
|
+
remove: (path) => rmSync5(path, { force: true }),
|
|
4345
4610
|
inspect(path) {
|
|
4346
|
-
const value =
|
|
4611
|
+
const value = lstatSync3(path);
|
|
4347
4612
|
return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
|
|
4348
4613
|
},
|
|
4349
4614
|
exec: execute,
|
|
@@ -4362,10 +4627,15 @@ function localBootstrapHost() {
|
|
|
4362
4627
|
throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
|
|
4363
4628
|
return result;
|
|
4364
4629
|
},
|
|
4365
|
-
async installAgent(config) {
|
|
4630
|
+
async installAgent(config, phase) {
|
|
4366
4631
|
const capabilities = await readCapabilities(localRunner);
|
|
4367
|
-
const
|
|
4368
|
-
const
|
|
4632
|
+
const hasBinding = existsSync5("/var/lib/forgezero/enrolment.json");
|
|
4633
|
+
const hasEnrolCredential = existsSync5(ENROL_CREDENTIAL);
|
|
4634
|
+
const initialBundle = config.kind === "platform" && !existsSync5(BOOTSTRAP_RELEASE_EVIDENCE) ? {
|
|
4635
|
+
path: config.bootstrapBundle.bundleFile,
|
|
4636
|
+
manifestPath: config.bootstrapBundle.manifestFile,
|
|
4637
|
+
manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync5(config.bootstrapBundle.manifestFile, "utf8")))
|
|
4638
|
+
} : undefined;
|
|
4369
4639
|
if (config.kind === "platform") {
|
|
4370
4640
|
const lifecycle = config.database.role === "none" ? {
|
|
4371
4641
|
apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
|
|
@@ -4377,49 +4647,19 @@ function localBootstrapHost() {
|
|
|
4377
4647
|
databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
|
|
4378
4648
|
databasePorts: [8529]
|
|
4379
4649
|
};
|
|
4380
|
-
|
|
4381
|
-
|
|
4650
|
+
mkdirSync5(dirname6(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
|
|
4651
|
+
writeFileSync5(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
|
|
4382
4652
|
`, { mode: 256 });
|
|
4383
4653
|
}
|
|
4384
|
-
const plan =
|
|
4654
|
+
const plan = planBootstrapAgentInstall(config, phase, {
|
|
4385
4655
|
capabilities,
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
repository: config.repository,
|
|
4390
|
-
branch: config.branch,
|
|
4391
|
-
profile: config.kind === "platform" ? config.profile : config.profile,
|
|
4392
|
-
deployRoot,
|
|
4393
|
-
deploymentCredentials: config.deploymentCredentials,
|
|
4394
|
-
publicApiUrl: config.apiUrl,
|
|
4395
|
-
gitCredentialPath: "/etc/forgezero/creds/git-deploy-key.cred",
|
|
4396
|
-
gitPublicKeyPath: "/etc/forgezero/git/deploy.pub",
|
|
4397
|
-
generateGitIdentity: true,
|
|
4398
|
-
pullDeployments: hasBinding,
|
|
4399
|
-
pullMigrations: config.kind === "platform" && hasBinding,
|
|
4400
|
-
pullBootstrap: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner),
|
|
4401
|
-
bootstrapSshCredentialPath: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && config.bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
|
|
4402
|
-
bootstrapSshSourcePath: config.kind === "enrolled-compute" ? config.bootstrapRunner?.sshPrivateKeyFile : undefined,
|
|
4403
|
-
bootstrapSshPublicKeyPath: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && config.bootstrapRunner ? BOOTSTRAP_SSH_PUBLIC_KEY : undefined,
|
|
4404
|
-
bootstrapTargetTelemetryEndpoint: platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined,
|
|
4405
|
-
lifecycleProfilePath: config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
|
|
4406
|
-
enforceEgress: true,
|
|
4407
|
-
nodeHostname: config.nodeHostname,
|
|
4408
|
-
telemetryEndpoint: config.telemetryEndpoint,
|
|
4409
|
-
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
4410
|
-
sourceBinPath: PACKAGED_AGENT_BIN,
|
|
4411
|
-
...hasBinding ? {
|
|
4412
|
-
enrolTokenCredentialPath: ENROL_CREDENTIAL,
|
|
4413
|
-
enrolStatePath: "/var/lib/forgezero/enrolment.json",
|
|
4414
|
-
apiUrl: config.apiUrl,
|
|
4415
|
-
project: config.kind === "enrolled-compute" ? config.realm : "platform",
|
|
4416
|
-
environment: config.kind === "enrolled-compute" ? undefined : config.environment,
|
|
4417
|
-
nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
|
|
4418
|
-
} : {}
|
|
4656
|
+
hasBinding,
|
|
4657
|
+
hasEnrolCredential,
|
|
4658
|
+
initialBundle
|
|
4419
4659
|
});
|
|
4420
4660
|
for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
|
|
4421
|
-
|
|
4422
|
-
|
|
4661
|
+
mkdirSync5(dirname6(unit.path), { recursive: true, mode: 493 });
|
|
4662
|
+
writeFileSync5(unit.path, unit.unit, { mode: 420 });
|
|
4423
4663
|
}
|
|
4424
4664
|
await applyPlan(plan, localRunner);
|
|
4425
4665
|
return plan;
|
|
@@ -4432,7 +4672,7 @@ export {
|
|
|
4432
4672
|
validateBootstrapConfig,
|
|
4433
4673
|
resolveInstalledBootstrapKind,
|
|
4434
4674
|
readBootstrapConfig,
|
|
4435
|
-
|
|
4675
|
+
planBootstrapAgentInstall,
|
|
4436
4676
|
planBootstrap,
|
|
4437
4677
|
localBootstrapHost,
|
|
4438
4678
|
bootstrapStatus,
|