@forgezero/agent 0.1.86 → 0.1.88
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/dist/agent-handover.d.ts +19 -0
- package/dist/agent-heartbeat.d.ts +3 -3
- package/dist/agent-heartbeat.js +247 -60
- package/dist/agent-update-helper.d.ts +12 -2
- package/dist/agent-update-helper.js +210 -45
- package/dist/agent-update.d.ts +4 -0
- package/dist/agent-update.js +19 -1
- package/dist/bootstrap.js +48 -5
- package/dist/fz-agent.js +569 -256
- package/dist/fz.js +87 -5
- package/dist/metal-bootstrap.js +40 -1
- package/dist/operator-bootstrap.js +87 -5
- package/dist/platform-fleet-verification.js +415 -211
- package/dist/provision.d.ts +16 -1
- package/dist/provision.js +332 -124
- package/dist/socket.d.ts +6 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "node:fs";
|
|
17
17
|
import { dirname, join, resolve } from "node:path";
|
|
18
18
|
var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
|
|
19
|
+
var DEFAULT_AGENT_CANDIDATE_LINK = `${DEFAULT_AGENT_RELEASE_ROOT}/candidate`;
|
|
19
20
|
var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
|
|
20
21
|
var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
|
|
21
22
|
var VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
@@ -209,25 +210,90 @@ function restoreAgentRelease(staged) {
|
|
|
209
210
|
rmSync(next, { force: true });
|
|
210
211
|
}
|
|
211
212
|
}
|
|
213
|
+
function selectAgentCandidate(staged) {
|
|
214
|
+
const link = join(dirname(staged.currentLink), "candidate");
|
|
215
|
+
const next = `${link}.${randomUUID()}.next`;
|
|
216
|
+
try {
|
|
217
|
+
symlinkSync(staged.nextTarget, next);
|
|
218
|
+
renameSync(next, link);
|
|
219
|
+
syncPath(dirname(link));
|
|
220
|
+
} finally {
|
|
221
|
+
rmSync(next, { force: true });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function clearAgentCandidate(root = DEFAULT_AGENT_RELEASE_ROOT) {
|
|
225
|
+
rmSync(join(resolve(root), "candidate"), { force: true });
|
|
226
|
+
}
|
|
212
227
|
|
|
213
228
|
// src/agent-update-helper.ts
|
|
214
|
-
import { randomUUID as
|
|
229
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
215
230
|
import {
|
|
216
|
-
chmodSync as
|
|
231
|
+
chmodSync as chmodSync3,
|
|
217
232
|
closeSync as closeSync2,
|
|
218
|
-
existsSync as
|
|
233
|
+
existsSync as existsSync3,
|
|
219
234
|
fsyncSync as fsyncSync2,
|
|
220
235
|
mkdirSync as mkdirSync2,
|
|
221
236
|
openSync as openSync2,
|
|
222
237
|
readFileSync as readFileSync2,
|
|
223
|
-
renameSync as
|
|
224
|
-
rmSync as
|
|
225
|
-
unlinkSync,
|
|
238
|
+
renameSync as renameSync3,
|
|
239
|
+
rmSync as rmSync3,
|
|
240
|
+
unlinkSync as unlinkSync2,
|
|
226
241
|
writeFileSync as writeFileSync2
|
|
227
242
|
} from "node:fs";
|
|
228
|
-
import { connect, createServer } from "node:net";
|
|
229
|
-
import { dirname as
|
|
243
|
+
import { connect as connect2, createServer as createServer2 } from "node:net";
|
|
244
|
+
import { dirname as dirname3, join as join2, resolve as resolve2 } from "node:path";
|
|
230
245
|
import { DEFAULT_SOCKET } from "@forgezero/vault";
|
|
246
|
+
|
|
247
|
+
// src/agent-handover.ts
|
|
248
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
249
|
+
import { chmodSync as chmodSync2, existsSync as existsSync2, renameSync as renameSync2, rmSync as rmSync2, symlinkSync as symlinkSync2, unlinkSync } from "node:fs";
|
|
250
|
+
import { basename, dirname as dirname2 } from "node:path";
|
|
251
|
+
import { connect, createServer } from "node:net";
|
|
252
|
+
var DEFAULT_AGENT_CANDIDATE_READY_SOCKET = "/run/forgezero/candidate-ready.sock";
|
|
253
|
+
function switchAgentSocketRoute(route, backend) {
|
|
254
|
+
if (dirname2(route) !== dirname2(backend))
|
|
255
|
+
throw new Error("Agent handover sockets must share one runtime directory");
|
|
256
|
+
const target = basename(backend);
|
|
257
|
+
const next = `${route}.${randomUUID2()}.next`;
|
|
258
|
+
try {
|
|
259
|
+
symlinkSync2(target, next);
|
|
260
|
+
renameSync2(next, route);
|
|
261
|
+
} finally {
|
|
262
|
+
rmSync2(next, { force: true });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function probeAgentCandidate(expected, socketPath = DEFAULT_AGENT_CANDIDATE_READY_SOCKET, timeoutMs = 5000) {
|
|
266
|
+
return new Promise((resolve2) => {
|
|
267
|
+
const socket = connect(socketPath);
|
|
268
|
+
let buffer = "";
|
|
269
|
+
let settled = false;
|
|
270
|
+
const finish = (value) => {
|
|
271
|
+
if (settled)
|
|
272
|
+
return;
|
|
273
|
+
settled = true;
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
socket.destroy();
|
|
276
|
+
resolve2(value);
|
|
277
|
+
};
|
|
278
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
279
|
+
socket.on("data", (chunk) => {
|
|
280
|
+
buffer += chunk.toString("utf8");
|
|
281
|
+
const newline = buffer.indexOf(`
|
|
282
|
+
`);
|
|
283
|
+
if (newline < 0)
|
|
284
|
+
return;
|
|
285
|
+
try {
|
|
286
|
+
const value = JSON.parse(buffer.slice(0, newline));
|
|
287
|
+
finish(value.version === expected.version && value.nodeKey === expected.nodeKey && value.bound === expected.bound && expected.vault.includes(value.vault));
|
|
288
|
+
} catch {
|
|
289
|
+
finish(false);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
socket.on("error", () => finish(false));
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/agent-update-helper.ts
|
|
231
297
|
var AGENT_UPDATE_GROUP = "forgezero-update";
|
|
232
298
|
var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-helper.service";
|
|
233
299
|
var AGENT_UPDATE_JOURNAL = "/var/lib/forgezero/agent-update.json";
|
|
@@ -322,13 +388,13 @@ function validateJournal(value, root) {
|
|
|
322
388
|
return journal;
|
|
323
389
|
}
|
|
324
390
|
function readJournal(path, root) {
|
|
325
|
-
if (!
|
|
391
|
+
if (!existsSync3(path))
|
|
326
392
|
return;
|
|
327
393
|
return validateJournal(JSON.parse(readFileSync2(path, "utf8")), root);
|
|
328
394
|
}
|
|
329
395
|
function writeAtomic(path, value, mode) {
|
|
330
|
-
mkdirSync2(
|
|
331
|
-
const next = `${path}.${
|
|
396
|
+
mkdirSync2(dirname3(path), { recursive: true, mode: 493 });
|
|
397
|
+
const next = `${path}.${randomUUID3()}.next`;
|
|
332
398
|
let file;
|
|
333
399
|
try {
|
|
334
400
|
file = openSync2(next, "wx", mode);
|
|
@@ -337,8 +403,8 @@ function writeAtomic(path, value, mode) {
|
|
|
337
403
|
fsyncSync2(file);
|
|
338
404
|
closeSync2(file);
|
|
339
405
|
file = undefined;
|
|
340
|
-
|
|
341
|
-
const directory = openSync2(
|
|
406
|
+
renameSync3(next, path);
|
|
407
|
+
const directory = openSync2(dirname3(path), "r");
|
|
342
408
|
try {
|
|
343
409
|
fsyncSync2(directory);
|
|
344
410
|
} finally {
|
|
@@ -347,7 +413,7 @@ function writeAtomic(path, value, mode) {
|
|
|
347
413
|
} finally {
|
|
348
414
|
if (file !== undefined)
|
|
349
415
|
closeSync2(file);
|
|
350
|
-
|
|
416
|
+
rmSync3(next, { force: true });
|
|
351
417
|
}
|
|
352
418
|
}
|
|
353
419
|
var publicReceipt = (journal) => {
|
|
@@ -380,7 +446,7 @@ function writeUpdateState(journalPath, receiptPath, journal) {
|
|
|
380
446
|
}
|
|
381
447
|
function readAgentUpdateReceipt(path = AGENT_UPDATE_RECEIPT) {
|
|
382
448
|
try {
|
|
383
|
-
if (!
|
|
449
|
+
if (!existsSync3(path))
|
|
384
450
|
return;
|
|
385
451
|
return validateReceipt(JSON.parse(readFileSync2(path, "utf8")));
|
|
386
452
|
} catch {
|
|
@@ -420,10 +486,37 @@ var restartAgent = async (target, run) => {
|
|
|
420
486
|
throw new Error(`systemd could not restart ${service}`);
|
|
421
487
|
}
|
|
422
488
|
};
|
|
489
|
+
var candidateUnit = (target) => target === "compute" ? "forgezero-agent-candidate.service" : "forgezero-metal-agent-candidate.service";
|
|
490
|
+
var startCandidate = async (staged, target, expectedNodeKey, run, probe = (expected) => probeAgentCandidate(expected, DEFAULT_AGENT_CANDIDATE_READY_SOCKET)) => {
|
|
491
|
+
selectAgentCandidate(staged);
|
|
492
|
+
if (!await runOk(run, "/usr/bin/systemctl", ["restart", candidateUnit(target)])) {
|
|
493
|
+
clearAgentCandidate(dirname3(staged.currentLink));
|
|
494
|
+
throw new Error(`systemd could not start ${candidateUnit(target)}`);
|
|
495
|
+
}
|
|
496
|
+
if (!await probe({
|
|
497
|
+
version: staged.version,
|
|
498
|
+
nodeKey: expectedNodeKey,
|
|
499
|
+
bound: true,
|
|
500
|
+
vault: target === "compute" ? ["ready"] : ["unbound"]
|
|
501
|
+
})) {
|
|
502
|
+
await run({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target)] });
|
|
503
|
+
clearAgentCandidate(dirname3(staged.currentLink));
|
|
504
|
+
throw new Error("the candidate Agent did not prove its identity and durable state");
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
var stopCandidate = async (staged, target, run) => {
|
|
508
|
+
await run({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target)] });
|
|
509
|
+
clearAgentCandidate(dirname3(staged.currentLink));
|
|
510
|
+
};
|
|
511
|
+
var socketPaths = (publicSocket = process.env.FZ_AGENT_PUBLIC_SOCKET ?? DEFAULT_SOCKET) => ({
|
|
512
|
+
route: `${publicSocket}.backend`,
|
|
513
|
+
active: `${publicSocket}.backend.active`,
|
|
514
|
+
candidate: `${publicSocket}.backend.candidate`
|
|
515
|
+
});
|
|
423
516
|
var targetProbe = (target, run) => target === "compute" ? () => probeAgentSocket() : async () => await runOk(run, "/usr/bin/systemctl", ["is-active", "--quiet", "forgezero-metal-agent.service"]) && await runOk(run, "/usr/bin/systemctl", ["is-active", "--quiet", "forgezero-metal-helper.service"]);
|
|
424
517
|
function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
|
|
425
518
|
return new Promise((resolve3) => {
|
|
426
|
-
const socket =
|
|
519
|
+
const socket = connect2(socketPath);
|
|
427
520
|
let settled = false;
|
|
428
521
|
let buffer = "";
|
|
429
522
|
const finish = (value) => {
|
|
@@ -456,12 +549,13 @@ function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
|
|
|
456
549
|
async function activateAgentRelease(staged, options = {}) {
|
|
457
550
|
const run = options.run ?? runCommand;
|
|
458
551
|
const target = options.target ?? "compute";
|
|
459
|
-
const
|
|
552
|
+
const paths = socketPaths(options.publicSocketPath);
|
|
553
|
+
const probe = options.probe ?? (target === "compute" ? () => probeAgentSocket(paths.active) : targetProbe(target, run));
|
|
460
554
|
const now = options.now ?? Date.now;
|
|
461
555
|
const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
|
|
462
556
|
const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
|
|
463
|
-
const previous = readJournal(journalPath,
|
|
464
|
-
const attemptId = options.attemptId ??
|
|
557
|
+
const previous = readJournal(journalPath, dirname3(staged.currentLink));
|
|
558
|
+
const attemptId = options.attemptId ?? randomUUID3();
|
|
465
559
|
if (!ATTEMPT_ID.test(attemptId))
|
|
466
560
|
throw new Error("Agent update attempt ID is invalid");
|
|
467
561
|
const startedAtTs = now();
|
|
@@ -483,11 +577,19 @@ async function activateAgentRelease(staged, options = {}) {
|
|
|
483
577
|
writeUpdateState(journalPath, receiptPath, journal);
|
|
484
578
|
let selectionAttempted = false;
|
|
485
579
|
try {
|
|
580
|
+
if (!options.candidatePrepared) {
|
|
581
|
+
throw new Error("direct activation requires a separately identity-bound candidate preparation");
|
|
582
|
+
}
|
|
583
|
+
if (target === "compute")
|
|
584
|
+
switchAgentSocketRoute(paths.route, paths.candidate);
|
|
486
585
|
selectionAttempted = true;
|
|
487
586
|
selectAgentRelease(staged);
|
|
488
587
|
await restartAgent(target, run);
|
|
489
588
|
if (!await probe())
|
|
490
|
-
throw new Error("the replacement Agent did not answer its
|
|
589
|
+
throw new Error("the replacement Agent did not answer its active Vault backend");
|
|
590
|
+
if (target === "compute")
|
|
591
|
+
switchAgentSocketRoute(paths.route, paths.active);
|
|
592
|
+
await stopCandidate(staged, target, run);
|
|
491
593
|
journal = { ...journal, outcome: "active", updatedAtTs: now(), rollbackHealthy: undefined };
|
|
492
594
|
writeUpdateState(journalPath, receiptPath, journal);
|
|
493
595
|
run({
|
|
@@ -505,10 +607,13 @@ async function activateAgentRelease(staged, options = {}) {
|
|
|
505
607
|
restored = true;
|
|
506
608
|
await restartAgent(target, run);
|
|
507
609
|
rollbackHealthy = await probe();
|
|
610
|
+
if (rollbackHealthy && target === "compute")
|
|
611
|
+
switchAgentSocketRoute(paths.route, paths.active);
|
|
508
612
|
} catch {
|
|
509
613
|
rollbackHealthy = false;
|
|
510
614
|
}
|
|
511
615
|
}
|
|
616
|
+
await stopCandidate(staged, target, run).catch(() => {});
|
|
512
617
|
const failures = failureCount + 1;
|
|
513
618
|
const updatedAtTs = now();
|
|
514
619
|
journal = {
|
|
@@ -528,28 +633,38 @@ async function recoverInterruptedAgentUpdate(options = {}) {
|
|
|
528
633
|
const root = resolve2(options.root ?? DEFAULT_AGENT_RELEASE_ROOT);
|
|
529
634
|
const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
|
|
530
635
|
const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
|
|
636
|
+
const run = options.run ?? runCommand;
|
|
531
637
|
const journal = readJournal(journalPath, root);
|
|
532
|
-
if (!journal)
|
|
638
|
+
if (!journal) {
|
|
639
|
+
for (const target of ["compute", "metal"]) {
|
|
640
|
+
await run({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target)] });
|
|
641
|
+
}
|
|
642
|
+
clearAgentCandidate(root);
|
|
533
643
|
return;
|
|
644
|
+
}
|
|
534
645
|
if (journal.outcome !== "activating") {
|
|
646
|
+
await stopCandidate(stagedFromJournal(journal, root), journal.target, run).catch(() => {});
|
|
535
647
|
writeAtomic(receiptPath, publicReceipt(journal), 416);
|
|
536
648
|
return publicReceipt(journal);
|
|
537
649
|
}
|
|
538
650
|
const staged = stagedFromJournal(journal, root);
|
|
539
|
-
if (!
|
|
651
|
+
if (!existsSync3(join2(root, journal.previousTarget))) {
|
|
540
652
|
throw new Error("Agent update rollback release is missing");
|
|
541
653
|
}
|
|
542
|
-
const
|
|
543
|
-
const probe = options.probe ?? targetProbe(journal.target, run);
|
|
654
|
+
const paths = socketPaths(options.publicSocketPath);
|
|
655
|
+
const probe = options.probe ?? (journal.target === "compute" ? () => probeAgentSocket(paths.active) : targetProbe(journal.target, run));
|
|
544
656
|
restoreAgentRelease(staged);
|
|
545
657
|
let rollbackHealthy = false;
|
|
546
658
|
let failureMessage = "activation was interrupted before its health verdict became durable";
|
|
547
659
|
try {
|
|
548
660
|
await restartAgent(journal.target, run);
|
|
549
661
|
rollbackHealthy = await probe();
|
|
662
|
+
if (rollbackHealthy && journal.target === "compute")
|
|
663
|
+
switchAgentSocketRoute(paths.route, paths.active);
|
|
550
664
|
} catch (cause) {
|
|
551
665
|
failureMessage = `${failureMessage}; ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
552
666
|
}
|
|
667
|
+
await stopCandidate(staged, journal.target, run).catch(() => {});
|
|
553
668
|
const failures = journal.failureCount + 1;
|
|
554
669
|
const updatedAtTs = (options.now ?? Date.now)();
|
|
555
670
|
const recovered = {
|
|
@@ -566,15 +681,23 @@ async function recoverInterruptedAgentUpdate(options = {}) {
|
|
|
566
681
|
}
|
|
567
682
|
function startAgentUpdateHelper(options = {}) {
|
|
568
683
|
const socketPath = options.socketPath ?? DEFAULT_AGENT_UPDATE_SOCKET;
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
mkdirSync2(
|
|
572
|
-
const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
|
|
684
|
+
if (existsSync3(socketPath))
|
|
685
|
+
unlinkSync2(socketPath);
|
|
686
|
+
mkdirSync2(dirname3(socketPath), { recursive: true, mode: 488 });
|
|
573
687
|
const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
|
|
574
688
|
const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
|
|
575
689
|
const releaseRoot = options.root ?? DEFAULT_AGENT_RELEASE_ROOT;
|
|
576
|
-
const
|
|
690
|
+
const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
|
|
691
|
+
const activate = options.activate ?? ((staged, target, attemptId) => activateAgentRelease(staged, {
|
|
692
|
+
target,
|
|
693
|
+
attemptId,
|
|
694
|
+
journalPath,
|
|
695
|
+
receiptPath,
|
|
696
|
+
now: options.now,
|
|
697
|
+
candidatePrepared: true
|
|
698
|
+
}));
|
|
577
699
|
let busy = true;
|
|
700
|
+
let pending;
|
|
578
701
|
let blocked;
|
|
579
702
|
(options.recover ?? (() => recoverInterruptedAgentUpdate({
|
|
580
703
|
root: releaseRoot,
|
|
@@ -586,7 +709,7 @@ function startAgentUpdateHelper(options = {}) {
|
|
|
586
709
|
}).finally(() => {
|
|
587
710
|
busy = false;
|
|
588
711
|
});
|
|
589
|
-
const server =
|
|
712
|
+
const server = createServer2((socket) => {
|
|
590
713
|
let buffer = "";
|
|
591
714
|
socket.on("data", (chunk) => {
|
|
592
715
|
buffer += chunk.toString("utf8");
|
|
@@ -605,16 +728,52 @@ function startAgentUpdateHelper(options = {}) {
|
|
|
605
728
|
Promise.resolve().then(() => JSON.parse(line)).then(async (request) => {
|
|
606
729
|
if (blocked)
|
|
607
730
|
throw new Error(`update journal needs operator recovery: ${blocked}`);
|
|
608
|
-
if (busy)
|
|
609
|
-
throw new Error("another Agent update or recovery is already active");
|
|
610
|
-
if (request.op !== "apply")
|
|
611
|
-
throw new Error("unknown update operation");
|
|
612
731
|
if (request.target !== "compute" && request.target !== "metal") {
|
|
613
732
|
throw new Error("agent update target is invalid");
|
|
614
733
|
}
|
|
615
|
-
const attemptId = request.attemptId ??
|
|
734
|
+
const attemptId = request.attemptId ?? randomUUID3();
|
|
616
735
|
if (!ATTEMPT_ID.test(attemptId))
|
|
617
736
|
throw new Error("Agent update attempt ID is invalid");
|
|
737
|
+
if (request.op === "commit" || request.op === "abort") {
|
|
738
|
+
if (!pending || pending.attemptId !== attemptId || pending.target !== request.target || pending.staged.fromVersion !== request.currentVersion || pending.staged.version !== request.targetVersion) {
|
|
739
|
+
throw new Error("Agent update handover does not match the prepared candidate");
|
|
740
|
+
}
|
|
741
|
+
const selected = pending;
|
|
742
|
+
pending = undefined;
|
|
743
|
+
if (request.op === "abort") {
|
|
744
|
+
await stopCandidate(selected.staged, selected.target, runCommand);
|
|
745
|
+
busy = false;
|
|
746
|
+
const response3 = {
|
|
747
|
+
ok: true,
|
|
748
|
+
status: "aborted",
|
|
749
|
+
version: selected.staged.version,
|
|
750
|
+
attemptId
|
|
751
|
+
};
|
|
752
|
+
socket.end(`${JSON.stringify(response3)}
|
|
753
|
+
`);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const outcome = await activate(selected.staged, selected.target, attemptId);
|
|
757
|
+
busy = false;
|
|
758
|
+
if (outcome?.ok === false)
|
|
759
|
+
throw new Error(outcome.reason ?? "Agent handover failed");
|
|
760
|
+
const response2 = {
|
|
761
|
+
ok: true,
|
|
762
|
+
status: "active",
|
|
763
|
+
version: selected.staged.version,
|
|
764
|
+
attemptId
|
|
765
|
+
};
|
|
766
|
+
socket.end(`${JSON.stringify(response2)}
|
|
767
|
+
`);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (request.op !== "prepare")
|
|
771
|
+
throw new Error("unknown update operation");
|
|
772
|
+
if (typeof request.expectedNodeKey !== "string" || request.expectedNodeKey.length < 16 || Buffer.byteLength(request.expectedNodeKey, "utf8") > 512 || /[\0\r\n]/.test(request.expectedNodeKey)) {
|
|
773
|
+
throw new Error("Agent update expected node identity is invalid");
|
|
774
|
+
}
|
|
775
|
+
if (busy)
|
|
776
|
+
throw new Error("another Agent update or recovery is already active");
|
|
618
777
|
const prior = readJournal(journalPath, releaseRoot);
|
|
619
778
|
const now = (options.now ?? Date.now)();
|
|
620
779
|
if (prior?.targetVersion === request.release.version && (prior.outcome === "rolled-back" || prior.outcome === "failed") && (prior.retryAfterTs ?? 0) > now)
|
|
@@ -625,17 +784,23 @@ function startAgentUpdateHelper(options = {}) {
|
|
|
625
784
|
currentVersion: request.currentVersion,
|
|
626
785
|
root: releaseRoot
|
|
627
786
|
});
|
|
628
|
-
|
|
787
|
+
await startCandidate(staged, request.target, request.expectedNodeKey, runCommand);
|
|
788
|
+
pending = { staged, target: request.target, attemptId, expectedNodeKey: request.expectedNodeKey };
|
|
789
|
+
ownsBusy = false;
|
|
790
|
+
setTimer(() => {
|
|
791
|
+
if (pending?.attemptId !== attemptId)
|
|
792
|
+
return;
|
|
793
|
+
const expired = pending;
|
|
794
|
+
pending = undefined;
|
|
795
|
+
stopCandidate(expired.staged, expired.target, runCommand).finally(() => {
|
|
796
|
+
busy = false;
|
|
797
|
+
});
|
|
798
|
+
}, 180000);
|
|
799
|
+
const response = { ok: true, status: "prepared", version: staged.version, attemptId };
|
|
629
800
|
socket.end(`${JSON.stringify(response)}
|
|
630
801
|
`);
|
|
631
|
-
setTimer(() => void activate(staged, request.target, attemptId).catch((cause) => {
|
|
632
|
-
blocked = cause instanceof Error ? cause.message : String(cause);
|
|
633
|
-
}).finally(() => {
|
|
634
|
-
busy = false;
|
|
635
|
-
}), 100);
|
|
636
|
-
ownsBusy = false;
|
|
637
802
|
}).catch((cause) => {
|
|
638
|
-
if (ownsBusy)
|
|
803
|
+
if (ownsBusy && !pending)
|
|
639
804
|
busy = false;
|
|
640
805
|
const response = {
|
|
641
806
|
ok: false,
|
|
@@ -647,12 +812,12 @@ function startAgentUpdateHelper(options = {}) {
|
|
|
647
812
|
});
|
|
648
813
|
socket.on("error", () => socket.destroy());
|
|
649
814
|
});
|
|
650
|
-
server.listen(socketPath, () =>
|
|
815
|
+
server.listen(socketPath, () => chmodSync3(socketPath, 432));
|
|
651
816
|
return server;
|
|
652
817
|
}
|
|
653
818
|
function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, timeoutMs = 90000) {
|
|
654
819
|
return new Promise((resolve3, reject) => {
|
|
655
|
-
const socket =
|
|
820
|
+
const socket = connect2(socketPath, () => socket.write(`${JSON.stringify(request)}
|
|
656
821
|
`));
|
|
657
822
|
let buffer = "";
|
|
658
823
|
socket.setTimeout(timeoutMs, () => {
|
|
@@ -680,22 +845,22 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
|
|
|
680
845
|
import { createHash as createHash2 } from "node:crypto";
|
|
681
846
|
import {
|
|
682
847
|
accessSync,
|
|
683
|
-
chmodSync as
|
|
848
|
+
chmodSync as chmodSync4,
|
|
684
849
|
copyFileSync,
|
|
685
850
|
createReadStream,
|
|
686
|
-
existsSync as
|
|
851
|
+
existsSync as existsSync4,
|
|
687
852
|
mkdtempSync,
|
|
688
853
|
mkdirSync as mkdirSync3,
|
|
689
854
|
readFileSync as readFileSync3,
|
|
690
|
-
renameSync as
|
|
691
|
-
rmSync as
|
|
855
|
+
renameSync as renameSync4,
|
|
856
|
+
rmSync as rmSync4,
|
|
692
857
|
statSync,
|
|
693
|
-
symlinkSync as
|
|
694
|
-
unlinkSync as
|
|
858
|
+
symlinkSync as symlinkSync3,
|
|
859
|
+
unlinkSync as unlinkSync3,
|
|
695
860
|
writeFileSync as writeFileSync3
|
|
696
861
|
} from "node:fs";
|
|
697
862
|
import { tmpdir } from "node:os";
|
|
698
|
-
import { dirname as
|
|
863
|
+
import { dirname as dirname4, join as join3 } from "node:path";
|
|
699
864
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
700
865
|
var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
|
|
701
866
|
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
@@ -868,23 +1033,23 @@ var softwareDownloadCommand = (url, destination, maximumBytes) => {
|
|
|
868
1033
|
];
|
|
869
1034
|
};
|
|
870
1035
|
var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
|
|
871
|
-
if (
|
|
1036
|
+
if (existsSync4(destination))
|
|
872
1037
|
throw new Error("download destination already exists");
|
|
873
1038
|
const received = await run(softwareDownloadCommand(url, destination, maximumBytes));
|
|
874
1039
|
if (received.exitCode !== 0) {
|
|
875
|
-
|
|
1040
|
+
rmSync4(destination, { force: true });
|
|
876
1041
|
throw new Error(`software download failed (${received.exitCode}): ${received.output.trim()}`);
|
|
877
1042
|
}
|
|
878
|
-
|
|
1043
|
+
chmodSync4(destination, 384);
|
|
879
1044
|
if (statSync(destination).size > maximumBytes) {
|
|
880
|
-
|
|
1045
|
+
rmSync4(destination, { force: true });
|
|
881
1046
|
throw new Error("download exceeds reviewed size bound");
|
|
882
1047
|
}
|
|
883
1048
|
const digest = createHash2("sha256");
|
|
884
1049
|
for await (const chunk of createReadStream(destination))
|
|
885
1050
|
digest.update(chunk);
|
|
886
1051
|
if (digest.digest("hex") !== sha256) {
|
|
887
|
-
|
|
1052
|
+
rmSync4(destination, { force: true });
|
|
888
1053
|
throw new Error("download checksum mismatch");
|
|
889
1054
|
}
|
|
890
1055
|
};
|
|
@@ -900,12 +1065,12 @@ var aptInstallMany = async (names) => {
|
|
|
900
1065
|
return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", ...names], environment) : update;
|
|
901
1066
|
};
|
|
902
1067
|
var writeOwnedPolicy = (pathValue, content, mode = 420) => {
|
|
903
|
-
if (
|
|
1068
|
+
if (existsSync4(pathValue)) {
|
|
904
1069
|
if (readFileSync3(pathValue, "utf8") !== content)
|
|
905
1070
|
throw new Error(`refusing to overwrite non-ForgeZero policy at ${pathValue}`);
|
|
906
1071
|
return;
|
|
907
1072
|
}
|
|
908
|
-
mkdirSync3(
|
|
1073
|
+
mkdirSync3(dirname4(pathValue), { recursive: true, mode: 493 });
|
|
909
1074
|
writeFileSync3(pathValue, content, { mode, flag: "wx" });
|
|
910
1075
|
};
|
|
911
1076
|
var installContainerdRuntime = async (directory, osVersion) => {
|
|
@@ -917,7 +1082,7 @@ var installContainerdRuntime = async (directory, osVersion) => {
|
|
|
917
1082
|
const nerdctlExtract = await run(["/usr/bin/tar", "-xzf", nerdctl, "-C", "/usr/local/bin", "nerdctl"]);
|
|
918
1083
|
if (nerdctlExtract.exitCode !== 0)
|
|
919
1084
|
return nerdctlExtract;
|
|
920
|
-
|
|
1085
|
+
chmodSync4("/usr/local/bin/nerdctl", 493);
|
|
921
1086
|
const buildkit = join3(directory, "buildkit.tar.gz");
|
|
922
1087
|
await download(`https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-amd64.tar.gz`, buildkit, BUILDKIT_SHA256);
|
|
923
1088
|
const unpacked = join3(directory, "buildkit");
|
|
@@ -927,15 +1092,15 @@ var installContainerdRuntime = async (directory, osVersion) => {
|
|
|
927
1092
|
return buildkitExtract;
|
|
928
1093
|
for (const binary of ["buildctl", "buildkitd"]) {
|
|
929
1094
|
copyFileSync(join3(unpacked, "bin", binary), `/usr/local/bin/${binary}`);
|
|
930
|
-
|
|
1095
|
+
chmodSync4(`/usr/local/bin/${binary}`, 493);
|
|
931
1096
|
}
|
|
932
1097
|
mkdirSync3("/etc/containerd", { recursive: true, mode: 493 });
|
|
933
1098
|
const expected = CONTAINERD_CONFIG(false);
|
|
934
1099
|
const kataExpected = CONTAINERD_CONFIG(true);
|
|
935
|
-
if (
|
|
1100
|
+
if (existsSync4(CONTAINERD_CONFIG_PATH) && ![expected, kataExpected].includes(readFileSync3(CONTAINERD_CONFIG_PATH, "utf8"))) {
|
|
936
1101
|
return { exitCode: 2, output: "refusing to overwrite an existing containerd configuration that differs from the reviewed ForgeZero policy" };
|
|
937
1102
|
}
|
|
938
|
-
if (!
|
|
1103
|
+
if (!existsSync4(CONTAINERD_CONFIG_PATH))
|
|
939
1104
|
writeFileSync3(CONTAINERD_CONFIG_PATH, expected, { mode: 420, flag: "wx" });
|
|
940
1105
|
writeOwnedPolicy("/etc/systemd/system/forgezero-buildkit.service", BUILDKIT_UNIT);
|
|
941
1106
|
mkdirSync3("/var/lib/forgezero/containerd", { recursive: true, mode: 448 });
|
|
@@ -948,10 +1113,10 @@ var installContainerdRuntime = async (directory, osVersion) => {
|
|
|
948
1113
|
};
|
|
949
1114
|
var installKataRuntime = async (directory) => {
|
|
950
1115
|
for (const pathValue of ["/dev/kvm", "/dev/sev"])
|
|
951
|
-
if (!
|
|
1116
|
+
if (!existsSync4(pathValue))
|
|
952
1117
|
return { exitCode: 2, output: `${pathValue} is required for Kata SEV-SNP` };
|
|
953
1118
|
for (const parameter of ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"]) {
|
|
954
|
-
if (!
|
|
1119
|
+
if (!existsSync4(parameter) || !/^(1|Y)$/i.test(readFileSync3(parameter, "utf8").trim()))
|
|
955
1120
|
return { exitCode: 2, output: `${parameter} does not enable SEV-SNP` };
|
|
956
1121
|
}
|
|
957
1122
|
const dependencies = await aptInstallMany(["zstd"]);
|
|
@@ -966,7 +1131,7 @@ var installKataRuntime = async (directory) => {
|
|
|
966
1131
|
const extracted = await run(["/usr/bin/tar", "-xf", tar, "-C", "/"]);
|
|
967
1132
|
if (extracted.exitCode !== 0)
|
|
968
1133
|
return extracted;
|
|
969
|
-
if (!
|
|
1134
|
+
if (!existsSync4("/opt/kata/bin/containerd-shim-kata-v2") || !existsSync4(KATA_SNP_CONFIG_PATH)) {
|
|
970
1135
|
return { exitCode: 2, output: "Kata archive does not contain the reviewed QEMU SNP runtime and configuration" };
|
|
971
1136
|
}
|
|
972
1137
|
const snpConfig = readFileSync3(KATA_SNP_CONFIG_PATH, "utf8");
|
|
@@ -974,19 +1139,19 @@ var installKataRuntime = async (directory) => {
|
|
|
974
1139
|
return { exitCode: 2, output: "Kata QEMU configuration does not enable confidential SEV-SNP guests" };
|
|
975
1140
|
}
|
|
976
1141
|
mkdirSync3("/etc/kata-containers", { recursive: true, mode: 493 });
|
|
977
|
-
if (
|
|
1142
|
+
if (existsSync4(KATA_ACTIVE_CONFIG_PATH) && readFileSync3(KATA_ACTIVE_CONFIG_PATH, "utf8") !== snpConfig) {
|
|
978
1143
|
return { exitCode: 2, output: "refusing to overwrite a non-ForgeZero Kata runtime configuration" };
|
|
979
1144
|
}
|
|
980
|
-
if (!
|
|
1145
|
+
if (!existsSync4(KATA_ACTIVE_CONFIG_PATH))
|
|
981
1146
|
copyFileSync(KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH);
|
|
982
|
-
|
|
1147
|
+
chmodSync4(KATA_ACTIVE_CONFIG_PATH, 420);
|
|
983
1148
|
try {
|
|
984
|
-
|
|
1149
|
+
unlinkSync3("/usr/local/bin/containerd-shim-kata-v2");
|
|
985
1150
|
} catch {}
|
|
986
|
-
|
|
1151
|
+
symlinkSync3("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
|
|
987
1152
|
const base = CONTAINERD_CONFIG(false);
|
|
988
1153
|
const kata = CONTAINERD_CONFIG(true);
|
|
989
|
-
if (!
|
|
1154
|
+
if (!existsSync4(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync3(CONTAINERD_CONFIG_PATH, "utf8"))) {
|
|
990
1155
|
return { exitCode: 2, output: "containerd must use the reviewed ForgeZero policy before Kata is installed" };
|
|
991
1156
|
}
|
|
992
1157
|
if (readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") !== kata)
|
|
@@ -1006,7 +1171,7 @@ async function executeSoftwareOperation(operation) {
|
|
|
1006
1171
|
const binary = await run(["/usr/bin/docker", "--version"]);
|
|
1007
1172
|
if (!successful(binary, /Docker version/))
|
|
1008
1173
|
return { ...binary, exitCode: 1 };
|
|
1009
|
-
if (!
|
|
1174
|
+
if (!existsSync4(DOCKER_DAEMON_PATH) || readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG)
|
|
1010
1175
|
return { exitCode: 1, output: "Docker daemon policy is absent or differs from the reviewed ForgeZero policy" };
|
|
1011
1176
|
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
|
|
1012
1177
|
return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
|
|
@@ -1019,17 +1184,17 @@ async function executeSoftwareOperation(operation) {
|
|
|
1019
1184
|
run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]),
|
|
1020
1185
|
run(["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-buildkit.service"])
|
|
1021
1186
|
]);
|
|
1022
|
-
const policy =
|
|
1187
|
+
const policy = existsSync4(CONTAINERD_CONFIG_PATH) ? readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") : "";
|
|
1023
1188
|
const validPolicy = policy === CONTAINERD_CONFIG(false) || policy === CONTAINERD_CONFIG(true);
|
|
1024
1189
|
return successful(containerd, /containerd/) && successful(nerdctl, new RegExp(NERDCTL_VERSION.replaceAll(".", "\\."))) && successful(buildkit, new RegExp(BUILDKIT_VERSION.replaceAll(".", "\\."))) && active.exitCode === 0 && builder.exitCode === 0 && validPolicy ? { exitCode: 0, output: `${containerd.output}${nerdctl.output}${buildkit.output}` } : { exitCode: 1, output: "containerd, nerdctl, BuildKit, or ForgeZero storage/runtime policy is unavailable" };
|
|
1025
1190
|
}
|
|
1026
1191
|
if (software === "kata-containers") {
|
|
1027
1192
|
const shim = await run(["/opt/kata/bin/containerd-shim-kata-v2", "--version"]);
|
|
1028
|
-
const policy =
|
|
1029
|
-
const activeConfig =
|
|
1030
|
-
const host = ["/dev/kvm", "/dev/sev", KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH].every(
|
|
1193
|
+
const policy = existsSync4(CONTAINERD_CONFIG_PATH) ? readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") : "";
|
|
1194
|
+
const activeConfig = existsSync4(KATA_ACTIVE_CONFIG_PATH) ? readFileSync3(KATA_ACTIVE_CONFIG_PATH, "utf8") : "";
|
|
1195
|
+
const host = ["/dev/kvm", "/dev/sev", KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH].every(existsSync4) && ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"].every((parameter) => existsSync4(parameter) && /^(1|Y)$/i.test(readFileSync3(parameter, "utf8").trim()));
|
|
1031
1196
|
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]);
|
|
1032
|
-
return successful(shim, /kata.*4\.0\.0/i) && policy === CONTAINERD_CONFIG(true) && activeConfig === (
|
|
1197
|
+
return successful(shim, /kata.*4\.0\.0/i) && policy === CONTAINERD_CONFIG(true) && activeConfig === (existsSync4(KATA_SNP_CONFIG_PATH) ? readFileSync3(KATA_SNP_CONFIG_PATH, "utf8") : "") && host && active.exitCode === 0 ? { exitCode: 0, output: shim.output } : { exitCode: 1, output: "Kata QEMU SNP runtime, host capability, or containerd policy is unavailable" };
|
|
1033
1198
|
}
|
|
1034
1199
|
if (software === "nginx") {
|
|
1035
1200
|
const binary = await run(["/usr/sbin/nginx", "-v"]);
|
|
@@ -1070,10 +1235,10 @@ async function executeSoftwareOperation(operation) {
|
|
|
1070
1235
|
return installed;
|
|
1071
1236
|
if (software === "docker") {
|
|
1072
1237
|
mkdirSync3("/etc/docker", { recursive: true, mode: 493 });
|
|
1073
|
-
if (
|
|
1238
|
+
if (existsSync4(DOCKER_DAEMON_PATH) && readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG) {
|
|
1074
1239
|
return { exitCode: 2, output: "refusing to overwrite an existing Docker daemon configuration that differs from the reviewed ForgeZero policy" };
|
|
1075
1240
|
}
|
|
1076
|
-
if (!
|
|
1241
|
+
if (!existsSync4(DOCKER_DAEMON_PATH))
|
|
1077
1242
|
writeFileSync3(DOCKER_DAEMON_PATH, DOCKER_DAEMON_CONFIG, { mode: 420, flag: "wx" });
|
|
1078
1243
|
const enabled = await run(["/usr/bin/systemctl", "enable", "docker.service"]);
|
|
1079
1244
|
return enabled.exitCode === 0 ? run(["/usr/bin/systemctl", "restart", "docker.service"]) : enabled;
|
|
@@ -1123,20 +1288,20 @@ async function executeSoftwareOperation(operation) {
|
|
|
1123
1288
|
return unzipped;
|
|
1124
1289
|
mkdirSync3("/usr/local/lib/forgezero/runtime", { recursive: true, mode: 493 });
|
|
1125
1290
|
copyFileSync(join3(unpacked, "bun-linux-x64", "bun"), "/usr/local/lib/forgezero/runtime/bun.next");
|
|
1126
|
-
|
|
1127
|
-
|
|
1291
|
+
chmodSync4("/usr/local/lib/forgezero/runtime/bun.next", 493);
|
|
1292
|
+
renameSync4("/usr/local/lib/forgezero/runtime/bun.next", "/usr/local/lib/forgezero/runtime/bun");
|
|
1128
1293
|
try {
|
|
1129
|
-
|
|
1294
|
+
unlinkSync3("/usr/local/bin/bun");
|
|
1130
1295
|
} catch {}
|
|
1131
|
-
|
|
1296
|
+
symlinkSync3("/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun");
|
|
1132
1297
|
return { exitCode: 0, output: "" };
|
|
1133
1298
|
}
|
|
1134
1299
|
if (software === "cloudflared") {
|
|
1135
1300
|
const binary = join3(directory, "cloudflared");
|
|
1136
1301
|
await download("https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64", binary, CLOUDFLARED_SHA256);
|
|
1137
|
-
|
|
1302
|
+
chmodSync4(binary, 493);
|
|
1138
1303
|
copyFileSync(binary, "/usr/local/bin/cloudflared");
|
|
1139
|
-
|
|
1304
|
+
chmodSync4("/usr/local/bin/cloudflared", 493);
|
|
1140
1305
|
return { exitCode: 0, output: "" };
|
|
1141
1306
|
}
|
|
1142
1307
|
const deb = join3(directory, "arangodb.deb");
|
|
@@ -1149,7 +1314,7 @@ async function executeSoftwareOperation(operation) {
|
|
|
1149
1314
|
await run(["/usr/bin/systemctl", "disable", "--now", "arangodb3.service"]);
|
|
1150
1315
|
return { exitCode: 0, output: "" };
|
|
1151
1316
|
} finally {
|
|
1152
|
-
|
|
1317
|
+
rmSync4(directory, { recursive: true, force: true });
|
|
1153
1318
|
}
|
|
1154
1319
|
}
|
|
1155
1320
|
function observeSoftwareHost(osRelease = readFileSync3("/etc/os-release", "utf8"), architecture = process.arch) {
|
|
@@ -1648,9 +1813,9 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
|
|
|
1648
1813
|
|
|
1649
1814
|
// src/deployment-connectivity.ts
|
|
1650
1815
|
import { createHash as createHash3 } from "node:crypto";
|
|
1651
|
-
import { mkdirSync as mkdirSync4, renameSync as
|
|
1816
|
+
import { mkdirSync as mkdirSync4, renameSync as renameSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1652
1817
|
import { createConnection } from "node:net";
|
|
1653
|
-
import { dirname as
|
|
1818
|
+
import { dirname as dirname5 } from "node:path";
|
|
1654
1819
|
|
|
1655
1820
|
// src/process-input.ts
|
|
1656
1821
|
async function writeAndCloseProcessInput(input, value) {
|
|
@@ -1671,7 +1836,7 @@ var checked2 = async (host, argv2, label) => {
|
|
|
1671
1836
|
return result.output.trim();
|
|
1672
1837
|
};
|
|
1673
1838
|
async function sealWithSystemd(name, path2, value) {
|
|
1674
|
-
mkdirSync4(
|
|
1839
|
+
mkdirSync4(dirname5(path2), { recursive: true, mode: 448 });
|
|
1675
1840
|
const next = `${path2}.next`;
|
|
1676
1841
|
const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
|
|
1677
1842
|
stdin: "pipe",
|
|
@@ -1687,15 +1852,15 @@ async function sealWithSystemd(name, path2, value) {
|
|
|
1687
1852
|
]);
|
|
1688
1853
|
if (exitCode !== 0)
|
|
1689
1854
|
throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
|
|
1690
|
-
|
|
1855
|
+
renameSync5(next, path2);
|
|
1691
1856
|
}
|
|
1692
1857
|
var defaultHost = {
|
|
1693
1858
|
seal: sealWithSystemd,
|
|
1694
1859
|
write(path2, content, mode) {
|
|
1695
|
-
mkdirSync4(
|
|
1860
|
+
mkdirSync4(dirname5(path2), { recursive: true, mode: 493 });
|
|
1696
1861
|
const next = `${path2}.next`;
|
|
1697
1862
|
writeFileSync4(next, content, { mode });
|
|
1698
|
-
|
|
1863
|
+
renameSync5(next, path2);
|
|
1699
1864
|
},
|
|
1700
1865
|
async exec(argv2) {
|
|
1701
1866
|
const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
|
|
@@ -1927,14 +2092,14 @@ WantedBy=multi-user.target
|
|
|
1927
2092
|
}
|
|
1928
2093
|
|
|
1929
2094
|
// src/software-helper.ts
|
|
1930
|
-
import { chmodSync as
|
|
1931
|
-
import { connect as
|
|
1932
|
-
import { dirname as
|
|
2095
|
+
import { chmodSync as chmodSync5, existsSync as existsSync7, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4 } from "node:fs";
|
|
2096
|
+
import { connect as connect3, createServer as createServer3 } from "node:net";
|
|
2097
|
+
import { dirname as dirname8 } from "node:path";
|
|
1933
2098
|
|
|
1934
2099
|
// src/service-supervisor.ts
|
|
1935
2100
|
import { createHash as createHash4 } from "node:crypto";
|
|
1936
|
-
import { existsSync as
|
|
1937
|
-
import { dirname as
|
|
2101
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, realpathSync, renameSync as renameSync6, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2102
|
+
import { dirname as dirname6, join as join4, resolve as resolve3, sep } from "node:path";
|
|
1938
2103
|
var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
|
|
1939
2104
|
var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
|
|
1940
2105
|
var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
|
|
@@ -1944,17 +2109,17 @@ var statePath = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
|
|
|
1944
2109
|
var within = (root, path2) => path2 === root || path2.startsWith(`${root}${sep}`);
|
|
1945
2110
|
var defaultHost2 = {
|
|
1946
2111
|
write(path2, content, mode) {
|
|
1947
|
-
mkdirSync5(
|
|
2112
|
+
mkdirSync5(dirname6(path2), { recursive: true, mode: 493 });
|
|
1948
2113
|
const next = `${path2}.next`;
|
|
1949
2114
|
writeFileSync5(next, content, { mode });
|
|
1950
|
-
|
|
2115
|
+
renameSync6(next, path2);
|
|
1951
2116
|
},
|
|
1952
2117
|
read: (path2) => readFileSync4(path2, "utf8"),
|
|
1953
|
-
exists:
|
|
1954
|
-
list: (path2) =>
|
|
2118
|
+
exists: existsSync5,
|
|
2119
|
+
list: (path2) => existsSync5(path2) ? readdirSync(path2) : [],
|
|
1955
2120
|
realpath: realpathSync,
|
|
1956
2121
|
mkdir: (path2, mode) => mkdirSync5(path2, { recursive: true, mode }),
|
|
1957
|
-
remove: (path2) =>
|
|
2122
|
+
remove: (path2) => rmSync5(path2, { force: true }),
|
|
1958
2123
|
async exec(argv2) {
|
|
1959
2124
|
const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: {
|
|
1960
2125
|
PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
@@ -2150,20 +2315,20 @@ async function activateSupervisedService(request, host = defaultHost2) {
|
|
|
2150
2315
|
|
|
2151
2316
|
// src/container-supervisor.ts
|
|
2152
2317
|
import { createHash as createHash5 } from "node:crypto";
|
|
2153
|
-
import { existsSync as
|
|
2154
|
-
import { dirname as
|
|
2318
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync7, rmSync as rmSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2319
|
+
import { dirname as dirname7, resolve as resolve4, sep as sep2 } from "node:path";
|
|
2155
2320
|
var defaultHost3 = {
|
|
2156
2321
|
realpath: realpathSync2,
|
|
2157
|
-
exists:
|
|
2322
|
+
exists: existsSync6,
|
|
2158
2323
|
read: (path2) => readFileSync5(path2, "utf8"),
|
|
2159
2324
|
write(path2, content, mode) {
|
|
2160
|
-
mkdirSync6(
|
|
2325
|
+
mkdirSync6(dirname7(path2), { recursive: true, mode: 493 });
|
|
2161
2326
|
const next = `${path2}.next`;
|
|
2162
2327
|
writeFileSync6(next, content, { mode });
|
|
2163
|
-
|
|
2328
|
+
renameSync7(next, path2);
|
|
2164
2329
|
},
|
|
2165
|
-
remove: (path2) =>
|
|
2166
|
-
list: (path2) =>
|
|
2330
|
+
remove: (path2) => rmSync6(path2, { force: true }),
|
|
2331
|
+
list: (path2) => existsSync6(path2) ? readdirSync2(path2) : [],
|
|
2167
2332
|
mkdir: (path2, mode) => mkdirSync6(path2, { recursive: true, mode }),
|
|
2168
2333
|
async exec(argv2) {
|
|
2169
2334
|
const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
|
|
@@ -2458,9 +2623,9 @@ var MAX_REQUEST_BYTES2 = 128 * 1024;
|
|
|
2458
2623
|
var MAX_PENDING_REQUESTS = 128;
|
|
2459
2624
|
function startSoftwareHelper(options = {}) {
|
|
2460
2625
|
const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
|
|
2461
|
-
if (
|
|
2462
|
-
|
|
2463
|
-
mkdirSync7(
|
|
2626
|
+
if (existsSync7(socketPath))
|
|
2627
|
+
unlinkSync4(socketPath);
|
|
2628
|
+
mkdirSync7(dirname8(socketPath), { recursive: true, mode: 488 });
|
|
2464
2629
|
const ensure = options.ensure ?? ensureSoftwareRequirements;
|
|
2465
2630
|
const activate = options.activate ?? activateSupervisedService;
|
|
2466
2631
|
const buildContainer = options.buildContainer ?? buildContainerImage;
|
|
@@ -2468,7 +2633,7 @@ function startSoftwareHelper(options = {}) {
|
|
|
2468
2633
|
const applyConnectivity = options.applyConnectivity ?? applyDeploymentConnectivity;
|
|
2469
2634
|
let tail = Promise.resolve();
|
|
2470
2635
|
let pending = 0;
|
|
2471
|
-
const server =
|
|
2636
|
+
const server = createServer3((socket) => {
|
|
2472
2637
|
let buffer = "";
|
|
2473
2638
|
socket.on("data", (chunk) => {
|
|
2474
2639
|
buffer += chunk.toString("utf8");
|
|
@@ -2547,12 +2712,12 @@ function startSoftwareHelper(options = {}) {
|
|
|
2547
2712
|
});
|
|
2548
2713
|
socket.on("error", () => socket.destroy());
|
|
2549
2714
|
});
|
|
2550
|
-
server.listen(socketPath, () =>
|
|
2715
|
+
server.listen(socketPath, () => chmodSync5(socketPath, 432));
|
|
2551
2716
|
return server;
|
|
2552
2717
|
}
|
|
2553
2718
|
function requestContainer(op, request, socketPath, timeoutMs) {
|
|
2554
2719
|
return new Promise((resolve5, reject) => {
|
|
2555
|
-
const socket =
|
|
2720
|
+
const socket = connect3(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
|
|
2556
2721
|
`));
|
|
2557
2722
|
let buffer = "";
|
|
2558
2723
|
socket.setTimeout(timeoutMs, () => {
|
|
@@ -2590,7 +2755,7 @@ function requestDeploymentConnectivity(request, socketPath = DEFAULT_SOFTWARE_HE
|
|
|
2590
2755
|
}
|
|
2591
2756
|
function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
|
|
2592
2757
|
return new Promise((resolve5, reject) => {
|
|
2593
|
-
const socket =
|
|
2758
|
+
const socket = connect3(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
|
|
2594
2759
|
`));
|
|
2595
2760
|
let buffer = "";
|
|
2596
2761
|
socket.setTimeout(timeoutMs, () => {
|
|
@@ -2619,7 +2784,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
|
|
|
2619
2784
|
function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
2620
2785
|
validateSoftwareRequirements(requirements);
|
|
2621
2786
|
return new Promise((resolve5, reject) => {
|
|
2622
|
-
const socket =
|
|
2787
|
+
const socket = connect3(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
|
|
2623
2788
|
`));
|
|
2624
2789
|
let buffer = "";
|
|
2625
2790
|
socket.setTimeout(timeoutMs, () => {
|
|
@@ -2647,7 +2812,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
2647
2812
|
}
|
|
2648
2813
|
|
|
2649
2814
|
// src/version.ts
|
|
2650
|
-
var VERSION3 = "0.1.
|
|
2815
|
+
var VERSION3 = "0.1.88";
|
|
2651
2816
|
|
|
2652
2817
|
// src/egress-policy.ts
|
|
2653
2818
|
import { realpathSync as realpathSync3 } from "node:fs";
|
|
@@ -2763,6 +2928,7 @@ var LIFECYCLE_GROUP = "forgezero-lifecycle";
|
|
|
2763
2928
|
var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
|
|
2764
2929
|
var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
|
|
2765
2930
|
var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
|
|
2931
|
+
var AGENT_CANDIDATE_UNIT_PATH = "/etc/systemd/system/forgezero-agent-candidate.service";
|
|
2766
2932
|
var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
2767
2933
|
var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
|
|
2768
2934
|
var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
|
|
@@ -2874,6 +3040,7 @@ Type=simple
|
|
|
2874
3040
|
User=root
|
|
2875
3041
|
Group=${AGENT_UPDATE_GROUP}
|
|
2876
3042
|
Environment=FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}
|
|
3043
|
+
Environment=FZ_AGENT_PUBLIC_SOCKET=${systemdPath(options.socketPath, "agent socket")}
|
|
2877
3044
|
ExecStart=${bin} update-helper
|
|
2878
3045
|
Restart=always
|
|
2879
3046
|
RestartSec=2
|
|
@@ -2918,13 +3085,11 @@ WantedBy=sockets.target
|
|
|
2918
3085
|
`;
|
|
2919
3086
|
}
|
|
2920
3087
|
function agentSocketProxyUnit(options) {
|
|
2921
|
-
const backend =
|
|
3088
|
+
const backend = agentRoutingSocketPath(options.socketPath);
|
|
2922
3089
|
const user = options.user ?? "forgezero";
|
|
2923
3090
|
return `[Unit]
|
|
2924
3091
|
Description=ForgeZero application Vault socket proxy
|
|
2925
3092
|
Documentation=https://www.forgezero.net/docs/agent
|
|
2926
|
-
Requires=forgezero-agent.service
|
|
2927
|
-
After=forgezero-agent.service
|
|
2928
3093
|
|
|
2929
3094
|
[Service]
|
|
2930
3095
|
User=${user}
|
|
@@ -2945,12 +3110,26 @@ RestrictAddressFamilies=AF_UNIX
|
|
|
2945
3110
|
`;
|
|
2946
3111
|
}
|
|
2947
3112
|
function agentBackendSocketPath(publicSocketPath) {
|
|
3113
|
+
const socket = systemdPath(publicSocketPath, "agent socket");
|
|
3114
|
+
const backend = `${socket}.backend.active`;
|
|
3115
|
+
if (Buffer.byteLength(backend) > 100)
|
|
3116
|
+
throw new Error("agent socket path is too long for a Unix socket");
|
|
3117
|
+
return backend;
|
|
3118
|
+
}
|
|
3119
|
+
function agentRoutingSocketPath(publicSocketPath) {
|
|
2948
3120
|
const socket = systemdPath(publicSocketPath, "agent socket");
|
|
2949
3121
|
const backend = `${socket}.backend`;
|
|
2950
3122
|
if (Buffer.byteLength(backend) > 100)
|
|
2951
3123
|
throw new Error("agent socket path is too long for a Unix socket");
|
|
2952
3124
|
return backend;
|
|
2953
3125
|
}
|
|
3126
|
+
function agentCandidateSocketPath(publicSocketPath) {
|
|
3127
|
+
const socket = systemdPath(publicSocketPath, "agent socket");
|
|
3128
|
+
const backend = `${socket}.backend.candidate`;
|
|
3129
|
+
if (Buffer.byteLength(backend) > 100)
|
|
3130
|
+
throw new Error("agent socket path is too long for a Unix socket");
|
|
3131
|
+
return backend;
|
|
3132
|
+
}
|
|
2954
3133
|
var systemdPath = (value, label) => {
|
|
2955
3134
|
if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
|
|
2956
3135
|
throw new Error(`invalid ${label} path`);
|
|
@@ -3234,7 +3413,11 @@ function agentUnit(options) {
|
|
|
3234
3413
|
}
|
|
3235
3414
|
const environment = [
|
|
3236
3415
|
"NODE_ENV=production",
|
|
3237
|
-
`FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
|
|
3416
|
+
`FZ_SOCKET_PATH=${options.backendSocketPath ?? agentBackendSocketPath(options.socketPath)}`,
|
|
3417
|
+
`FZ_AGENT_PUBLIC_SOCKET=${options.socketPath}`,
|
|
3418
|
+
`FZ_AGENT_ROUTE_SOCKET=${agentRoutingSocketPath(options.socketPath)}`,
|
|
3419
|
+
options.handoverCandidate ? "FZ_AGENT_HANDOVER_CANDIDATE=true" : null,
|
|
3420
|
+
options.handoverCandidate ? `FZ_AGENT_HANDOVER_READY_SOCKET=${DEFAULT_AGENT_CANDIDATE_READY_SOCKET}` : null,
|
|
3238
3421
|
`FZ_CONTROL_SOCKET=${controlSocketPath}`,
|
|
3239
3422
|
`FZ_SEED_CREDENTIAL=agent-seed`,
|
|
3240
3423
|
`FZ_AGENT_MODE=${options.mode}`,
|
|
@@ -3371,6 +3554,26 @@ ${deploymentWrites}
|
|
|
3371
3554
|
WantedBy=multi-user.target
|
|
3372
3555
|
`;
|
|
3373
3556
|
}
|
|
3557
|
+
function agentCandidateUnit(options) {
|
|
3558
|
+
return agentUnit({
|
|
3559
|
+
...options,
|
|
3560
|
+
binPath: `${DEFAULT_AGENT_RELEASE_ROOT}/candidate/dist/fz-agent.js`,
|
|
3561
|
+
backendSocketPath: agentCandidateSocketPath(options.socketPath),
|
|
3562
|
+
handoverCandidate: true,
|
|
3563
|
+
gitCredentialPath: undefined,
|
|
3564
|
+
deploymentCredentials: {},
|
|
3565
|
+
bootstrapSshCredentialPath: undefined,
|
|
3566
|
+
bootstrapSshPublicKeyPath: undefined,
|
|
3567
|
+
pullBootstrap: false,
|
|
3568
|
+
pullDeployments: false,
|
|
3569
|
+
pullMigrations: false,
|
|
3570
|
+
lifecycleProfilePath: undefined,
|
|
3571
|
+
bootstrapTargetTelemetryEndpoint: undefined,
|
|
3572
|
+
repository: undefined,
|
|
3573
|
+
bootstrapBundlePath: undefined,
|
|
3574
|
+
bootstrapBundleManifestPath: undefined
|
|
3575
|
+
}).replace("Description=ForgeZero node agent", "Description=ForgeZero candidate node agent");
|
|
3576
|
+
}
|
|
3374
3577
|
var renderOperation = (operation) => {
|
|
3375
3578
|
if (operation.kind === "commands")
|
|
3376
3579
|
return operation.commands.map(({ argv: argv2 }) => argv2.join(" ")).join(`
|
|
@@ -3506,6 +3709,7 @@ function planProvision(options) {
|
|
|
3506
3709
|
auxiliaryUnits: [
|
|
3507
3710
|
{ path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
|
|
3508
3711
|
{ path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
|
|
3712
|
+
{ path: AGENT_CANDIDATE_UNIT_PATH, unit: agentCandidateUnit(options) },
|
|
3509
3713
|
{ path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
|
|
3510
3714
|
...options.enforceEgress ? [
|
|
3511
3715
|
{ path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
|
|
@@ -3773,19 +3977,19 @@ function platformCommunityRehearsalDeletionClaims(seedGuests, rehearsalGuest) {
|
|
|
3773
3977
|
|
|
3774
3978
|
// src/platform-bootstrap-runtime.ts
|
|
3775
3979
|
import {
|
|
3776
|
-
chmodSync as
|
|
3980
|
+
chmodSync as chmodSync6,
|
|
3777
3981
|
chownSync,
|
|
3778
3982
|
copyFileSync as copyFileSync2,
|
|
3779
|
-
existsSync as
|
|
3983
|
+
existsSync as existsSync8,
|
|
3780
3984
|
lstatSync,
|
|
3781
3985
|
mkdirSync as mkdirSync8,
|
|
3782
3986
|
readFileSync as readFileSync6,
|
|
3783
3987
|
readdirSync as readdirSync3,
|
|
3784
3988
|
realpathSync as realpathSync4,
|
|
3785
|
-
renameSync as
|
|
3786
|
-
rmSync as
|
|
3989
|
+
renameSync as renameSync8,
|
|
3990
|
+
rmSync as rmSync7,
|
|
3787
3991
|
statSync as statSync2,
|
|
3788
|
-
symlinkSync as
|
|
3992
|
+
symlinkSync as symlinkSync4,
|
|
3789
3993
|
writeFileSync as writeFileSync7
|
|
3790
3994
|
} from "node:fs";
|
|
3791
3995
|
import { join as join5 } from "node:path";
|
|
@@ -4216,13 +4420,13 @@ var platformExec = async (argv2) => {
|
|
|
4216
4420
|
};
|
|
4217
4421
|
var replaceLink = (path2, target) => {
|
|
4218
4422
|
const pending = `${path2}.next`;
|
|
4219
|
-
|
|
4423
|
+
rmSync7(pending, { force: true });
|
|
4220
4424
|
if (!target) {
|
|
4221
|
-
|
|
4425
|
+
rmSync7(path2, { force: true });
|
|
4222
4426
|
return;
|
|
4223
4427
|
}
|
|
4224
|
-
|
|
4225
|
-
|
|
4428
|
+
symlinkSync4(target, pending);
|
|
4429
|
+
renameSync8(pending, path2);
|
|
4226
4430
|
};
|
|
4227
4431
|
var secureRelease = (path2, uid, gid) => {
|
|
4228
4432
|
const visit = (current2) => {
|
|
@@ -4230,7 +4434,7 @@ var secureRelease = (path2, uid, gid) => {
|
|
|
4230
4434
|
if (metadata.isSymbolicLink())
|
|
4231
4435
|
throw new Error("release contains a symbolic link");
|
|
4232
4436
|
chownSync(current2, uid, gid);
|
|
4233
|
-
|
|
4437
|
+
chmodSync6(current2, metadata.isDirectory() ? 365 : 292);
|
|
4234
4438
|
if (metadata.isDirectory())
|
|
4235
4439
|
for (const name of readdirSync3(current2))
|
|
4236
4440
|
visit(join5(current2, name));
|
|
@@ -4255,7 +4459,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
4255
4459
|
const slots = join5(normalized.root, "slots");
|
|
4256
4460
|
mkdirSync8(slots, { recursive: true, mode: 493 });
|
|
4257
4461
|
const slotFile = join5(normalized.root, ".forge-slot");
|
|
4258
|
-
const previousSlot =
|
|
4462
|
+
const previousSlot = existsSync8(slotFile) ? readFileSync6(slotFile, "utf8").trim() : undefined;
|
|
4259
4463
|
const target = previousSlot === "blue" ? "green" : "blue";
|
|
4260
4464
|
const port = target === "blue" ? normalized.bluePort : normalized.greenPort;
|
|
4261
4465
|
const targetLink = join5(slots, target);
|
|
@@ -4295,25 +4499,25 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
4295
4499
|
}
|
|
4296
4500
|
const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
|
|
4297
4501
|
const backup = `${upstream}.forgezero-backup`;
|
|
4298
|
-
if (
|
|
4502
|
+
if (existsSync8(upstream))
|
|
4299
4503
|
copyFileSync2(upstream, backup);
|
|
4300
4504
|
else
|
|
4301
|
-
|
|
4505
|
+
rmSync7(backup, { force: true });
|
|
4302
4506
|
writeFileSync7(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
|
|
4303
4507
|
`, { mode: 420 });
|
|
4304
4508
|
const test = await exec(["/usr/sbin/nginx", "-t"]);
|
|
4305
4509
|
const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
|
|
4306
4510
|
if (reload.exitCode !== 0) {
|
|
4307
|
-
if (
|
|
4308
|
-
|
|
4511
|
+
if (existsSync8(backup))
|
|
4512
|
+
renameSync8(backup, upstream);
|
|
4309
4513
|
else
|
|
4310
|
-
|
|
4514
|
+
rmSync7(upstream, { force: true });
|
|
4311
4515
|
await exec(["/usr/sbin/nginx", "-t"]);
|
|
4312
4516
|
await exec(["/usr/sbin/nginx", "-s", "reload"]);
|
|
4313
4517
|
await stopTarget();
|
|
4314
4518
|
throw new Error("nginx refused the promoted upstream");
|
|
4315
4519
|
}
|
|
4316
|
-
|
|
4520
|
+
rmSync7(backup, { force: true });
|
|
4317
4521
|
writeFileSync7(slotFile, `${target}
|
|
4318
4522
|
`, { mode: 420 });
|
|
4319
4523
|
if (previousSlot && previousSlot !== target) {
|
|
@@ -4323,7 +4527,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
4323
4527
|
const old = readdirSync3(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join5(releases, entry.name)).sort((left, right) => statSync2(right).mtimeMs - statSync2(left).mtimeMs).slice(normalized.keepReleases);
|
|
4324
4528
|
for (const path2 of old)
|
|
4325
4529
|
if (path2 !== release)
|
|
4326
|
-
|
|
4530
|
+
rmSync7(path2, { recursive: true, force: true });
|
|
4327
4531
|
return { release, slot: target };
|
|
4328
4532
|
}
|
|
4329
4533
|
function validateCollectorUnit(unit2) {
|
|
@@ -4369,34 +4573,34 @@ import { lstatSync as lstatSync5 } from "node:fs";
|
|
|
4369
4573
|
// src/bootstrap.ts
|
|
4370
4574
|
import { createHash as createHash7, createHmac as createHmac2, randomBytes as randomBytes3 } from "node:crypto";
|
|
4371
4575
|
import {
|
|
4372
|
-
chmodSync as
|
|
4373
|
-
existsSync as
|
|
4576
|
+
chmodSync as chmodSync9,
|
|
4577
|
+
existsSync as existsSync11,
|
|
4374
4578
|
lstatSync as lstatSync4,
|
|
4375
4579
|
mkdirSync as mkdirSync11,
|
|
4376
4580
|
readFileSync as readFileSync9,
|
|
4377
|
-
renameSync as
|
|
4378
|
-
rmSync as
|
|
4581
|
+
renameSync as renameSync11,
|
|
4582
|
+
rmSync as rmSync10,
|
|
4379
4583
|
writeFileSync as writeFileSync10
|
|
4380
4584
|
} from "node:fs";
|
|
4381
|
-
import { dirname as
|
|
4585
|
+
import { dirname as dirname12 } from "node:path";
|
|
4382
4586
|
import { fileURLToPath } from "node:url";
|
|
4383
4587
|
|
|
4384
4588
|
// src/cli/agent-install.ts
|
|
4385
4589
|
import { randomBytes } from "node:crypto";
|
|
4386
4590
|
import {
|
|
4387
|
-
chmodSync as
|
|
4591
|
+
chmodSync as chmodSync7,
|
|
4388
4592
|
copyFileSync as copyFileSync3,
|
|
4389
|
-
existsSync as
|
|
4593
|
+
existsSync as existsSync9,
|
|
4390
4594
|
lstatSync as lstatSync2,
|
|
4391
4595
|
mkdirSync as mkdirSync9,
|
|
4392
4596
|
readFileSync as readFileSync7,
|
|
4393
4597
|
realpathSync as realpathSync5,
|
|
4394
|
-
renameSync as
|
|
4395
|
-
rmSync as
|
|
4396
|
-
symlinkSync as
|
|
4598
|
+
renameSync as renameSync9,
|
|
4599
|
+
rmSync as rmSync8,
|
|
4600
|
+
symlinkSync as symlinkSync5,
|
|
4397
4601
|
writeFileSync as writeFileSync8
|
|
4398
4602
|
} from "node:fs";
|
|
4399
|
-
import { dirname as
|
|
4603
|
+
import { dirname as dirname9 } from "node:path";
|
|
4400
4604
|
async function readCapabilities(run2) {
|
|
4401
4605
|
const answers = {};
|
|
4402
4606
|
const checks = Object.entries(CAPABILITY_CHECKS);
|
|
@@ -4454,51 +4658,51 @@ var runProvisionOperation = async (operation) => {
|
|
|
4454
4658
|
if (operation.kind === "install-runtime") {
|
|
4455
4659
|
const release = `/opt/forgezero/agent/versions/${operation.version}`;
|
|
4456
4660
|
mkdirSync9(`${release}/dist`, { recursive: true, mode: 493 });
|
|
4457
|
-
mkdirSync9(
|
|
4661
|
+
mkdirSync9(dirname9(operation.binary), { recursive: true, mode: 493 });
|
|
4458
4662
|
copyFileSync3(operation.source, `${release}/dist/fz-agent.js`);
|
|
4459
|
-
|
|
4460
|
-
const gitSshSource = `${
|
|
4461
|
-
if (!
|
|
4663
|
+
chmodSync7(`${release}/dist/fz-agent.js`, 493);
|
|
4664
|
+
const gitSshSource = `${dirname9(operation.source)}/fz-git-ssh.js`;
|
|
4665
|
+
if (!existsSync9(gitSshSource))
|
|
4462
4666
|
return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
|
|
4463
4667
|
copyFileSync3(gitSshSource, `${release}/dist/fz-git-ssh.js`);
|
|
4464
|
-
|
|
4668
|
+
chmodSync7(`${release}/dist/fz-git-ssh.js`, 493);
|
|
4465
4669
|
const pending = "/opt/forgezero/agent/current.next";
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4670
|
+
rmSync8(pending, { force: true });
|
|
4671
|
+
symlinkSync5(`versions/${operation.version}`, pending);
|
|
4672
|
+
renameSync9(pending, "/opt/forgezero/agent/current");
|
|
4673
|
+
rmSync8(operation.binary, { force: true });
|
|
4674
|
+
symlinkSync5("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
|
|
4471
4675
|
const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
|
|
4472
|
-
|
|
4473
|
-
|
|
4676
|
+
rmSync8(gitSshBinary, { force: true });
|
|
4677
|
+
symlinkSync5("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
|
|
4474
4678
|
return { stdout: "", exitCode: 0 };
|
|
4475
4679
|
}
|
|
4476
4680
|
if (operation.kind === "ensure-seed") {
|
|
4477
|
-
if (
|
|
4681
|
+
if (existsSync9(operation.credential) && lstatSync2(operation.credential).size > 0)
|
|
4478
4682
|
return { stdout: "", exitCode: 0 };
|
|
4479
4683
|
const seed = randomBytes(32).toString("base64url");
|
|
4480
4684
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
|
|
4481
4685
|
if (result.exitCode === 0)
|
|
4482
|
-
|
|
4686
|
+
chmodSync7(operation.credential, 256);
|
|
4483
4687
|
return result;
|
|
4484
4688
|
}
|
|
4485
4689
|
if (operation.kind === "ensure-git-identity") {
|
|
4486
4690
|
const key = "/run/forgezero-git-deploy-key";
|
|
4487
4691
|
const publicKey2 = `${key}.pub`;
|
|
4488
4692
|
try {
|
|
4489
|
-
if (!
|
|
4490
|
-
|
|
4491
|
-
|
|
4693
|
+
if (!existsSync9(operation.credential) || lstatSync2(operation.credential).size < 1) {
|
|
4694
|
+
rmSync8(key, { force: true });
|
|
4695
|
+
rmSync8(publicKey2, { force: true });
|
|
4492
4696
|
let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
|
|
4493
4697
|
if (result.exitCode !== 0)
|
|
4494
4698
|
return result;
|
|
4495
4699
|
result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=git-deploy-key", key, operation.credential]);
|
|
4496
4700
|
if (result.exitCode !== 0)
|
|
4497
4701
|
return result;
|
|
4498
|
-
|
|
4702
|
+
chmodSync7(operation.credential, 256);
|
|
4499
4703
|
}
|
|
4500
|
-
if (!
|
|
4501
|
-
if (!
|
|
4704
|
+
if (!existsSync9(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
|
|
4705
|
+
if (!existsSync9(key)) {
|
|
4502
4706
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
|
|
4503
4707
|
if (decrypted.exitCode !== 0)
|
|
4504
4708
|
return decrypted;
|
|
@@ -4511,25 +4715,25 @@ var runProvisionOperation = async (operation) => {
|
|
|
4511
4715
|
}
|
|
4512
4716
|
return { stdout: "", exitCode: 0 };
|
|
4513
4717
|
} finally {
|
|
4514
|
-
|
|
4515
|
-
|
|
4718
|
+
rmSync8(key, { force: true });
|
|
4719
|
+
rmSync8(publicKey2, { force: true });
|
|
4516
4720
|
}
|
|
4517
4721
|
}
|
|
4518
4722
|
if (operation.kind === "ensure-bootstrap-ssh-identity") {
|
|
4519
4723
|
const key = "/run/forgezero-bootstrap-ssh-key";
|
|
4520
4724
|
const generatedPublicKey = `${key}.pub`;
|
|
4521
4725
|
try {
|
|
4522
|
-
if (!
|
|
4523
|
-
|
|
4524
|
-
|
|
4726
|
+
if (!existsSync9(operation.credential) || lstatSync2(operation.credential).size < 1) {
|
|
4727
|
+
rmSync8(key, { force: true });
|
|
4728
|
+
rmSync8(generatedPublicKey, { force: true });
|
|
4525
4729
|
let result;
|
|
4526
4730
|
if (operation.source) {
|
|
4527
|
-
const source =
|
|
4731
|
+
const source = existsSync9(operation.source) ? lstatSync2(operation.source) : undefined;
|
|
4528
4732
|
if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
|
|
4529
4733
|
return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
|
|
4530
4734
|
}
|
|
4531
4735
|
copyFileSync3(operation.source, key);
|
|
4532
|
-
|
|
4736
|
+
chmodSync7(key, 384);
|
|
4533
4737
|
} else {
|
|
4534
4738
|
result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-bootstrap-runner", "-f", key]);
|
|
4535
4739
|
if (result.exitCode !== 0)
|
|
@@ -4542,41 +4746,41 @@ var runProvisionOperation = async (operation) => {
|
|
|
4542
4746
|
result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
|
|
4543
4747
|
if (result.exitCode !== 0)
|
|
4544
4748
|
return result;
|
|
4545
|
-
|
|
4749
|
+
chmodSync7(operation.credential, 256);
|
|
4546
4750
|
}
|
|
4547
|
-
if (!
|
|
4548
|
-
if (!
|
|
4751
|
+
if (!existsSync9(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
|
|
4752
|
+
if (!existsSync9(key)) {
|
|
4549
4753
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
|
|
4550
4754
|
if (decrypted.exitCode !== 0)
|
|
4551
4755
|
return decrypted;
|
|
4552
|
-
|
|
4756
|
+
chmodSync7(key, 384);
|
|
4553
4757
|
}
|
|
4554
4758
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
4555
4759
|
if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
|
|
4556
4760
|
return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
|
|
4557
4761
|
}
|
|
4558
|
-
mkdirSync9(
|
|
4762
|
+
mkdirSync9(dirname9(operation.publicKey), { recursive: true, mode: 493 });
|
|
4559
4763
|
writeFileSync8(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
|
|
4560
4764
|
`, { mode: 292 });
|
|
4561
|
-
|
|
4765
|
+
chmodSync7(operation.publicKey, 292);
|
|
4562
4766
|
}
|
|
4563
4767
|
if (operation.source)
|
|
4564
|
-
|
|
4768
|
+
rmSync8(operation.source, { force: true });
|
|
4565
4769
|
return { stdout: "", exitCode: 0 };
|
|
4566
4770
|
} finally {
|
|
4567
|
-
|
|
4568
|
-
|
|
4771
|
+
rmSync8(key, { force: true });
|
|
4772
|
+
rmSync8(generatedPublicKey, { force: true });
|
|
4569
4773
|
}
|
|
4570
4774
|
}
|
|
4571
4775
|
if (operation.kind === "ensure-enrolment") {
|
|
4572
|
-
if (
|
|
4776
|
+
if (existsSync9(operation.state) && lstatSync2(operation.state).size > 0 || existsSync9(operation.credential) && lstatSync2(operation.credential).size > 0)
|
|
4573
4777
|
return { stdout: "", exitCode: 0 };
|
|
4574
|
-
if (!
|
|
4778
|
+
if (!existsSync9(operation.source))
|
|
4575
4779
|
return { stdout: "enrolment source is missing", exitCode: 1 };
|
|
4576
4780
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
|
|
4577
4781
|
if (result.exitCode === 0) {
|
|
4578
|
-
|
|
4579
|
-
|
|
4782
|
+
chmodSync7(operation.credential, 256);
|
|
4783
|
+
rmSync8(operation.source, { force: true });
|
|
4580
4784
|
}
|
|
4581
4785
|
return result;
|
|
4582
4786
|
}
|
|
@@ -4591,7 +4795,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
4591
4795
|
return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
|
|
4592
4796
|
}
|
|
4593
4797
|
if (operation.kind === "verify-file")
|
|
4594
|
-
return
|
|
4798
|
+
return existsSync9(operation.path) && lstatSync2(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
|
|
4595
4799
|
if (operation.kind === "verify-egress") {
|
|
4596
4800
|
const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
|
|
4597
4801
|
if (active.exitCode !== 0)
|
|
@@ -4625,7 +4829,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
4625
4829
|
const key = "/run/cloudflare-warp-key.gpg";
|
|
4626
4830
|
writeFileSync8(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
|
|
4627
4831
|
let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
|
|
4628
|
-
|
|
4832
|
+
rmSync8(key, { force: true });
|
|
4629
4833
|
if (result.exitCode !== 0)
|
|
4630
4834
|
return result;
|
|
4631
4835
|
const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
|
|
@@ -4674,9 +4878,9 @@ async function applyPlan(plan, run2) {
|
|
|
4674
4878
|
|
|
4675
4879
|
// src/cloudflare-bootstrap.ts
|
|
4676
4880
|
import { constants } from "node:fs";
|
|
4677
|
-
import { createHmac, randomUUID as
|
|
4881
|
+
import { createHmac, randomUUID as randomUUID4 } from "node:crypto";
|
|
4678
4882
|
import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "node:fs/promises";
|
|
4679
|
-
import { dirname as
|
|
4883
|
+
import { dirname as dirname10, join as join6, resolve as resolve5 } from "node:path";
|
|
4680
4884
|
import { isIP as isIP3 } from "node:net";
|
|
4681
4885
|
|
|
4682
4886
|
// src/cloudflare-edge.ts
|
|
@@ -4692,17 +4896,17 @@ import { DEFAULT_SOCKET as DEFAULT_SOCKET2 } from "@forgezero/vault";
|
|
|
4692
4896
|
// src/bootstrap-bundle.ts
|
|
4693
4897
|
import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
|
|
4694
4898
|
import {
|
|
4695
|
-
chmodSync as
|
|
4899
|
+
chmodSync as chmodSync8,
|
|
4696
4900
|
createReadStream as createReadStream2,
|
|
4697
|
-
existsSync as
|
|
4901
|
+
existsSync as existsSync10,
|
|
4698
4902
|
lstatSync as lstatSync3,
|
|
4699
4903
|
mkdirSync as mkdirSync10,
|
|
4700
4904
|
readFileSync as readFileSync8,
|
|
4701
|
-
renameSync as
|
|
4702
|
-
rmSync as
|
|
4905
|
+
renameSync as renameSync10,
|
|
4906
|
+
rmSync as rmSync9,
|
|
4703
4907
|
writeFileSync as writeFileSync9
|
|
4704
4908
|
} from "node:fs";
|
|
4705
|
-
import { dirname as
|
|
4909
|
+
import { dirname as dirname11, isAbsolute, resolve as resolve6 } from "node:path";
|
|
4706
4910
|
var BOOTSTRAP_BUNDLE_FORMAT = 1;
|
|
4707
4911
|
var BOOTSTRAP_BUNDLE_KIND = "forgezero-api-git-bundle";
|
|
4708
4912
|
var MAX_BOOTSTRAP_BUNDLE_BYTES = 512 * 1024 * 1024;
|
|
@@ -5007,17 +5211,17 @@ function localBootstrapHost() {
|
|
|
5007
5211
|
};
|
|
5008
5212
|
return {
|
|
5009
5213
|
uid: () => process.getuid?.() ?? -1,
|
|
5010
|
-
exists:
|
|
5214
|
+
exists: existsSync11,
|
|
5011
5215
|
read: (path2) => readFileSync9(path2, "utf8"),
|
|
5012
5216
|
write(path2, content, mode) {
|
|
5013
|
-
mkdirSync11(
|
|
5217
|
+
mkdirSync11(dirname12(path2), { recursive: true, mode: 493 });
|
|
5014
5218
|
const temporary = `${path2}.next.${process.pid}`;
|
|
5015
5219
|
writeFileSync10(temporary, content, { mode });
|
|
5016
|
-
|
|
5017
|
-
|
|
5220
|
+
chmodSync9(temporary, mode);
|
|
5221
|
+
renameSync11(temporary, path2);
|
|
5018
5222
|
},
|
|
5019
5223
|
mkdir: (path2, mode) => mkdirSync11(path2, { recursive: true, mode }),
|
|
5020
|
-
remove: (path2) =>
|
|
5224
|
+
remove: (path2) => rmSync10(path2, { force: true }),
|
|
5021
5225
|
inspect(path2) {
|
|
5022
5226
|
const value = lstatSync4(path2);
|
|
5023
5227
|
return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
|
|
@@ -5040,9 +5244,9 @@ function localBootstrapHost() {
|
|
|
5040
5244
|
},
|
|
5041
5245
|
async installAgent(config, phase) {
|
|
5042
5246
|
const capabilities = await readCapabilities(localRunner);
|
|
5043
|
-
const hasBinding =
|
|
5044
|
-
const hasEnrolCredential =
|
|
5045
|
-
const initialBundle = config.kind === "platform" && !
|
|
5247
|
+
const hasBinding = existsSync11("/var/lib/forgezero/enrolment.json");
|
|
5248
|
+
const hasEnrolCredential = existsSync11(ENROL_CREDENTIAL);
|
|
5249
|
+
const initialBundle = config.kind === "platform" && !existsSync11(BOOTSTRAP_RELEASE_EVIDENCE) ? {
|
|
5046
5250
|
path: config.bootstrapBundle.bundleFile,
|
|
5047
5251
|
manifestPath: config.bootstrapBundle.manifestFile,
|
|
5048
5252
|
manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync9(config.bootstrapBundle.manifestFile, "utf8")))
|
|
@@ -5058,7 +5262,7 @@ function localBootstrapHost() {
|
|
|
5058
5262
|
databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
|
|
5059
5263
|
databasePorts: [8529]
|
|
5060
5264
|
};
|
|
5061
|
-
mkdirSync11(
|
|
5265
|
+
mkdirSync11(dirname12(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
|
|
5062
5266
|
writeFileSync10(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
|
|
5063
5267
|
`, { mode: 256 });
|
|
5064
5268
|
}
|
|
@@ -5069,7 +5273,7 @@ function localBootstrapHost() {
|
|
|
5069
5273
|
initialBundle
|
|
5070
5274
|
});
|
|
5071
5275
|
for (const unit2 of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
|
|
5072
|
-
mkdirSync11(
|
|
5276
|
+
mkdirSync11(dirname12(unit2.path), { recursive: true, mode: 493 });
|
|
5073
5277
|
writeFileSync10(unit2.path, unit2.unit, { mode: 420 });
|
|
5074
5278
|
}
|
|
5075
5279
|
await applyPlan(plan, localRunner);
|