@forgezero/agent 0.1.85 → 0.1.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -14
- package/dist/agent-handover.d.ts +13 -0
- package/dist/agent-heartbeat.d.ts +3 -3
- package/dist/agent-heartbeat.js +235 -62
- package/dist/agent-update-helper.d.ts +11 -2
- package/dist/agent-update-helper.js +201 -45
- package/dist/agent-update.d.ts +4 -0
- package/dist/agent-update.js +19 -1
- package/dist/bootstrap.js +56 -8
- package/dist/credential-schema.js +16 -4
- package/dist/fz-agent.js +569 -260
- package/dist/fz.js +415 -43
- package/dist/guest-enrolment.js +4 -2
- package/dist/metal-bootstrap.js +40 -1
- package/dist/migration-pull.js +4 -2
- package/dist/node-vault.js +16 -4
- package/dist/operator-bootstrap.js +95 -8
- package/dist/platform-bootstrap-runtime.d.ts +7 -0
- package/dist/platform-bootstrap-runtime.js +5 -2
- package/dist/platform-fleet-verification.js +411 -213
- package/dist/platform-genesis-config.d.ts +6 -1
- package/dist/platform-launch-profile.d.ts +44 -0
- package/dist/provision.d.ts +16 -1
- package/dist/provision.js +323 -124
- package/dist/provisioning-pull.js +4 -2
- package/dist/signed-node-http.d.ts +2 -1
- package/dist/socket.d.ts +6 -0
- package/dist/version.d.ts +1 -1
- package/package.json +4 -4
package/dist/provision.js
CHANGED
|
@@ -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 && typeof value.nodeKey === "string" && typeof value.bound === "boolean" && ["ready", "partial", "unavailable", "unbound"].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,32 @@ 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, 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({ version: staged.version })) {
|
|
497
|
+
await run({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target)] });
|
|
498
|
+
clearAgentCandidate(dirname3(staged.currentLink));
|
|
499
|
+
throw new Error("the candidate Agent did not prove its identity and durable state");
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
var stopCandidate = async (staged, target, run) => {
|
|
503
|
+
await run({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target)] });
|
|
504
|
+
clearAgentCandidate(dirname3(staged.currentLink));
|
|
505
|
+
};
|
|
506
|
+
var socketPaths = (publicSocket = process.env.FZ_AGENT_PUBLIC_SOCKET ?? DEFAULT_SOCKET) => ({
|
|
507
|
+
route: `${publicSocket}.backend`,
|
|
508
|
+
active: `${publicSocket}.backend.active`,
|
|
509
|
+
candidate: `${publicSocket}.backend.candidate`
|
|
510
|
+
});
|
|
423
511
|
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
512
|
function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
|
|
425
513
|
return new Promise((resolve3) => {
|
|
426
|
-
const socket =
|
|
514
|
+
const socket = connect2(socketPath);
|
|
427
515
|
let settled = false;
|
|
428
516
|
let buffer = "";
|
|
429
517
|
const finish = (value) => {
|
|
@@ -456,12 +544,13 @@ function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
|
|
|
456
544
|
async function activateAgentRelease(staged, options = {}) {
|
|
457
545
|
const run = options.run ?? runCommand;
|
|
458
546
|
const target = options.target ?? "compute";
|
|
459
|
-
const
|
|
547
|
+
const paths = socketPaths(options.publicSocketPath);
|
|
548
|
+
const probe = options.probe ?? (target === "compute" ? () => probeAgentSocket(paths.active) : targetProbe(target, run));
|
|
460
549
|
const now = options.now ?? Date.now;
|
|
461
550
|
const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
|
|
462
551
|
const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
|
|
463
|
-
const previous = readJournal(journalPath,
|
|
464
|
-
const attemptId = options.attemptId ??
|
|
552
|
+
const previous = readJournal(journalPath, dirname3(staged.currentLink));
|
|
553
|
+
const attemptId = options.attemptId ?? randomUUID3();
|
|
465
554
|
if (!ATTEMPT_ID.test(attemptId))
|
|
466
555
|
throw new Error("Agent update attempt ID is invalid");
|
|
467
556
|
const startedAtTs = now();
|
|
@@ -483,11 +572,18 @@ async function activateAgentRelease(staged, options = {}) {
|
|
|
483
572
|
writeUpdateState(journalPath, receiptPath, journal);
|
|
484
573
|
let selectionAttempted = false;
|
|
485
574
|
try {
|
|
575
|
+
if (!options.candidatePrepared)
|
|
576
|
+
await startCandidate(staged, target, run);
|
|
577
|
+
if (target === "compute")
|
|
578
|
+
switchAgentSocketRoute(paths.route, paths.candidate);
|
|
486
579
|
selectionAttempted = true;
|
|
487
580
|
selectAgentRelease(staged);
|
|
488
581
|
await restartAgent(target, run);
|
|
489
582
|
if (!await probe())
|
|
490
|
-
throw new Error("the replacement Agent did not answer its
|
|
583
|
+
throw new Error("the replacement Agent did not answer its active Vault backend");
|
|
584
|
+
if (target === "compute")
|
|
585
|
+
switchAgentSocketRoute(paths.route, paths.active);
|
|
586
|
+
await stopCandidate(staged, target, run);
|
|
491
587
|
journal = { ...journal, outcome: "active", updatedAtTs: now(), rollbackHealthy: undefined };
|
|
492
588
|
writeUpdateState(journalPath, receiptPath, journal);
|
|
493
589
|
run({
|
|
@@ -505,10 +601,13 @@ async function activateAgentRelease(staged, options = {}) {
|
|
|
505
601
|
restored = true;
|
|
506
602
|
await restartAgent(target, run);
|
|
507
603
|
rollbackHealthy = await probe();
|
|
604
|
+
if (rollbackHealthy && target === "compute")
|
|
605
|
+
switchAgentSocketRoute(paths.route, paths.active);
|
|
508
606
|
} catch {
|
|
509
607
|
rollbackHealthy = false;
|
|
510
608
|
}
|
|
511
609
|
}
|
|
610
|
+
await stopCandidate(staged, target, run).catch(() => {});
|
|
512
611
|
const failures = failureCount + 1;
|
|
513
612
|
const updatedAtTs = now();
|
|
514
613
|
journal = {
|
|
@@ -528,28 +627,38 @@ async function recoverInterruptedAgentUpdate(options = {}) {
|
|
|
528
627
|
const root = resolve2(options.root ?? DEFAULT_AGENT_RELEASE_ROOT);
|
|
529
628
|
const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
|
|
530
629
|
const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
|
|
630
|
+
const run = options.run ?? runCommand;
|
|
531
631
|
const journal = readJournal(journalPath, root);
|
|
532
|
-
if (!journal)
|
|
632
|
+
if (!journal) {
|
|
633
|
+
for (const target of ["compute", "metal"]) {
|
|
634
|
+
await run({ command: "/usr/bin/systemctl", args: ["stop", candidateUnit(target)] });
|
|
635
|
+
}
|
|
636
|
+
clearAgentCandidate(root);
|
|
533
637
|
return;
|
|
638
|
+
}
|
|
534
639
|
if (journal.outcome !== "activating") {
|
|
640
|
+
await stopCandidate(stagedFromJournal(journal, root), journal.target, run).catch(() => {});
|
|
535
641
|
writeAtomic(receiptPath, publicReceipt(journal), 416);
|
|
536
642
|
return publicReceipt(journal);
|
|
537
643
|
}
|
|
538
644
|
const staged = stagedFromJournal(journal, root);
|
|
539
|
-
if (!
|
|
645
|
+
if (!existsSync3(join2(root, journal.previousTarget))) {
|
|
540
646
|
throw new Error("Agent update rollback release is missing");
|
|
541
647
|
}
|
|
542
|
-
const
|
|
543
|
-
const probe = options.probe ?? targetProbe(journal.target, run);
|
|
648
|
+
const paths = socketPaths(options.publicSocketPath);
|
|
649
|
+
const probe = options.probe ?? (journal.target === "compute" ? () => probeAgentSocket(paths.active) : targetProbe(journal.target, run));
|
|
544
650
|
restoreAgentRelease(staged);
|
|
545
651
|
let rollbackHealthy = false;
|
|
546
652
|
let failureMessage = "activation was interrupted before its health verdict became durable";
|
|
547
653
|
try {
|
|
548
654
|
await restartAgent(journal.target, run);
|
|
549
655
|
rollbackHealthy = await probe();
|
|
656
|
+
if (rollbackHealthy && journal.target === "compute")
|
|
657
|
+
switchAgentSocketRoute(paths.route, paths.active);
|
|
550
658
|
} catch (cause) {
|
|
551
659
|
failureMessage = `${failureMessage}; ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
552
660
|
}
|
|
661
|
+
await stopCandidate(staged, journal.target, run).catch(() => {});
|
|
553
662
|
const failures = journal.failureCount + 1;
|
|
554
663
|
const updatedAtTs = (options.now ?? Date.now)();
|
|
555
664
|
const recovered = {
|
|
@@ -566,15 +675,23 @@ async function recoverInterruptedAgentUpdate(options = {}) {
|
|
|
566
675
|
}
|
|
567
676
|
function startAgentUpdateHelper(options = {}) {
|
|
568
677
|
const socketPath = options.socketPath ?? DEFAULT_AGENT_UPDATE_SOCKET;
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
mkdirSync2(
|
|
572
|
-
const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
|
|
678
|
+
if (existsSync3(socketPath))
|
|
679
|
+
unlinkSync2(socketPath);
|
|
680
|
+
mkdirSync2(dirname3(socketPath), { recursive: true, mode: 488 });
|
|
573
681
|
const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
|
|
574
682
|
const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
|
|
575
683
|
const releaseRoot = options.root ?? DEFAULT_AGENT_RELEASE_ROOT;
|
|
576
|
-
const
|
|
684
|
+
const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
|
|
685
|
+
const activate = options.activate ?? ((staged, target, attemptId) => activateAgentRelease(staged, {
|
|
686
|
+
target,
|
|
687
|
+
attemptId,
|
|
688
|
+
journalPath,
|
|
689
|
+
receiptPath,
|
|
690
|
+
now: options.now,
|
|
691
|
+
candidatePrepared: true
|
|
692
|
+
}));
|
|
577
693
|
let busy = true;
|
|
694
|
+
let pending;
|
|
578
695
|
let blocked;
|
|
579
696
|
(options.recover ?? (() => recoverInterruptedAgentUpdate({
|
|
580
697
|
root: releaseRoot,
|
|
@@ -586,7 +703,7 @@ function startAgentUpdateHelper(options = {}) {
|
|
|
586
703
|
}).finally(() => {
|
|
587
704
|
busy = false;
|
|
588
705
|
});
|
|
589
|
-
const server =
|
|
706
|
+
const server = createServer2((socket) => {
|
|
590
707
|
let buffer = "";
|
|
591
708
|
socket.on("data", (chunk) => {
|
|
592
709
|
buffer += chunk.toString("utf8");
|
|
@@ -605,16 +722,49 @@ function startAgentUpdateHelper(options = {}) {
|
|
|
605
722
|
Promise.resolve().then(() => JSON.parse(line)).then(async (request) => {
|
|
606
723
|
if (blocked)
|
|
607
724
|
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
725
|
if (request.target !== "compute" && request.target !== "metal") {
|
|
613
726
|
throw new Error("agent update target is invalid");
|
|
614
727
|
}
|
|
615
|
-
const attemptId = request.attemptId ??
|
|
728
|
+
const attemptId = request.attemptId ?? randomUUID3();
|
|
616
729
|
if (!ATTEMPT_ID.test(attemptId))
|
|
617
730
|
throw new Error("Agent update attempt ID is invalid");
|
|
731
|
+
if (request.op === "commit" || request.op === "abort") {
|
|
732
|
+
if (!pending || pending.attemptId !== attemptId || pending.target !== request.target || pending.staged.fromVersion !== request.currentVersion || pending.staged.version !== request.targetVersion) {
|
|
733
|
+
throw new Error("Agent update handover does not match the prepared candidate");
|
|
734
|
+
}
|
|
735
|
+
const selected = pending;
|
|
736
|
+
pending = undefined;
|
|
737
|
+
if (request.op === "abort") {
|
|
738
|
+
await stopCandidate(selected.staged, selected.target, runCommand);
|
|
739
|
+
busy = false;
|
|
740
|
+
const response3 = {
|
|
741
|
+
ok: true,
|
|
742
|
+
status: "aborted",
|
|
743
|
+
version: selected.staged.version,
|
|
744
|
+
attemptId
|
|
745
|
+
};
|
|
746
|
+
socket.end(`${JSON.stringify(response3)}
|
|
747
|
+
`);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const outcome = await activate(selected.staged, selected.target, attemptId);
|
|
751
|
+
busy = false;
|
|
752
|
+
if (outcome?.ok === false)
|
|
753
|
+
throw new Error(outcome.reason ?? "Agent handover failed");
|
|
754
|
+
const response2 = {
|
|
755
|
+
ok: true,
|
|
756
|
+
status: "active",
|
|
757
|
+
version: selected.staged.version,
|
|
758
|
+
attemptId
|
|
759
|
+
};
|
|
760
|
+
socket.end(`${JSON.stringify(response2)}
|
|
761
|
+
`);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (request.op !== "prepare")
|
|
765
|
+
throw new Error("unknown update operation");
|
|
766
|
+
if (busy)
|
|
767
|
+
throw new Error("another Agent update or recovery is already active");
|
|
618
768
|
const prior = readJournal(journalPath, releaseRoot);
|
|
619
769
|
const now = (options.now ?? Date.now)();
|
|
620
770
|
if (prior?.targetVersion === request.release.version && (prior.outcome === "rolled-back" || prior.outcome === "failed") && (prior.retryAfterTs ?? 0) > now)
|
|
@@ -625,17 +775,23 @@ function startAgentUpdateHelper(options = {}) {
|
|
|
625
775
|
currentVersion: request.currentVersion,
|
|
626
776
|
root: releaseRoot
|
|
627
777
|
});
|
|
628
|
-
|
|
778
|
+
await startCandidate(staged, request.target, runCommand);
|
|
779
|
+
pending = { staged, target: request.target, attemptId };
|
|
780
|
+
ownsBusy = false;
|
|
781
|
+
setTimer(() => {
|
|
782
|
+
if (pending?.attemptId !== attemptId)
|
|
783
|
+
return;
|
|
784
|
+
const expired = pending;
|
|
785
|
+
pending = undefined;
|
|
786
|
+
stopCandidate(expired.staged, expired.target, runCommand).finally(() => {
|
|
787
|
+
busy = false;
|
|
788
|
+
});
|
|
789
|
+
}, 180000);
|
|
790
|
+
const response = { ok: true, status: "prepared", version: staged.version, attemptId };
|
|
629
791
|
socket.end(`${JSON.stringify(response)}
|
|
630
792
|
`);
|
|
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
793
|
}).catch((cause) => {
|
|
638
|
-
if (ownsBusy)
|
|
794
|
+
if (ownsBusy && !pending)
|
|
639
795
|
busy = false;
|
|
640
796
|
const response = {
|
|
641
797
|
ok: false,
|
|
@@ -647,12 +803,12 @@ function startAgentUpdateHelper(options = {}) {
|
|
|
647
803
|
});
|
|
648
804
|
socket.on("error", () => socket.destroy());
|
|
649
805
|
});
|
|
650
|
-
server.listen(socketPath, () =>
|
|
806
|
+
server.listen(socketPath, () => chmodSync3(socketPath, 432));
|
|
651
807
|
return server;
|
|
652
808
|
}
|
|
653
809
|
function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, timeoutMs = 90000) {
|
|
654
810
|
return new Promise((resolve3, reject) => {
|
|
655
|
-
const socket =
|
|
811
|
+
const socket = connect2(socketPath, () => socket.write(`${JSON.stringify(request)}
|
|
656
812
|
`));
|
|
657
813
|
let buffer = "";
|
|
658
814
|
socket.setTimeout(timeoutMs, () => {
|
|
@@ -680,22 +836,22 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
|
|
|
680
836
|
import { createHash as createHash2 } from "node:crypto";
|
|
681
837
|
import {
|
|
682
838
|
accessSync,
|
|
683
|
-
chmodSync as
|
|
839
|
+
chmodSync as chmodSync4,
|
|
684
840
|
copyFileSync,
|
|
685
841
|
createReadStream,
|
|
686
|
-
existsSync as
|
|
842
|
+
existsSync as existsSync4,
|
|
687
843
|
mkdtempSync,
|
|
688
844
|
mkdirSync as mkdirSync3,
|
|
689
845
|
readFileSync as readFileSync3,
|
|
690
|
-
renameSync as
|
|
691
|
-
rmSync as
|
|
846
|
+
renameSync as renameSync4,
|
|
847
|
+
rmSync as rmSync4,
|
|
692
848
|
statSync,
|
|
693
|
-
symlinkSync as
|
|
694
|
-
unlinkSync as
|
|
849
|
+
symlinkSync as symlinkSync3,
|
|
850
|
+
unlinkSync as unlinkSync3,
|
|
695
851
|
writeFileSync as writeFileSync3
|
|
696
852
|
} from "node:fs";
|
|
697
853
|
import { tmpdir } from "node:os";
|
|
698
|
-
import { dirname as
|
|
854
|
+
import { dirname as dirname4, join as join3 } from "node:path";
|
|
699
855
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
700
856
|
var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
|
|
701
857
|
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
@@ -868,23 +1024,23 @@ var softwareDownloadCommand = (url, destination, maximumBytes) => {
|
|
|
868
1024
|
];
|
|
869
1025
|
};
|
|
870
1026
|
var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
|
|
871
|
-
if (
|
|
1027
|
+
if (existsSync4(destination))
|
|
872
1028
|
throw new Error("download destination already exists");
|
|
873
1029
|
const received = await run(softwareDownloadCommand(url, destination, maximumBytes));
|
|
874
1030
|
if (received.exitCode !== 0) {
|
|
875
|
-
|
|
1031
|
+
rmSync4(destination, { force: true });
|
|
876
1032
|
throw new Error(`software download failed (${received.exitCode}): ${received.output.trim()}`);
|
|
877
1033
|
}
|
|
878
|
-
|
|
1034
|
+
chmodSync4(destination, 384);
|
|
879
1035
|
if (statSync(destination).size > maximumBytes) {
|
|
880
|
-
|
|
1036
|
+
rmSync4(destination, { force: true });
|
|
881
1037
|
throw new Error("download exceeds reviewed size bound");
|
|
882
1038
|
}
|
|
883
1039
|
const digest = createHash2("sha256");
|
|
884
1040
|
for await (const chunk of createReadStream(destination))
|
|
885
1041
|
digest.update(chunk);
|
|
886
1042
|
if (digest.digest("hex") !== sha256) {
|
|
887
|
-
|
|
1043
|
+
rmSync4(destination, { force: true });
|
|
888
1044
|
throw new Error("download checksum mismatch");
|
|
889
1045
|
}
|
|
890
1046
|
};
|
|
@@ -900,12 +1056,12 @@ var aptInstallMany = async (names) => {
|
|
|
900
1056
|
return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", ...names], environment) : update;
|
|
901
1057
|
};
|
|
902
1058
|
var writeOwnedPolicy = (pathValue, content, mode = 420) => {
|
|
903
|
-
if (
|
|
1059
|
+
if (existsSync4(pathValue)) {
|
|
904
1060
|
if (readFileSync3(pathValue, "utf8") !== content)
|
|
905
1061
|
throw new Error(`refusing to overwrite non-ForgeZero policy at ${pathValue}`);
|
|
906
1062
|
return;
|
|
907
1063
|
}
|
|
908
|
-
mkdirSync3(
|
|
1064
|
+
mkdirSync3(dirname4(pathValue), { recursive: true, mode: 493 });
|
|
909
1065
|
writeFileSync3(pathValue, content, { mode, flag: "wx" });
|
|
910
1066
|
};
|
|
911
1067
|
var installContainerdRuntime = async (directory, osVersion) => {
|
|
@@ -917,7 +1073,7 @@ var installContainerdRuntime = async (directory, osVersion) => {
|
|
|
917
1073
|
const nerdctlExtract = await run(["/usr/bin/tar", "-xzf", nerdctl, "-C", "/usr/local/bin", "nerdctl"]);
|
|
918
1074
|
if (nerdctlExtract.exitCode !== 0)
|
|
919
1075
|
return nerdctlExtract;
|
|
920
|
-
|
|
1076
|
+
chmodSync4("/usr/local/bin/nerdctl", 493);
|
|
921
1077
|
const buildkit = join3(directory, "buildkit.tar.gz");
|
|
922
1078
|
await download(`https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-amd64.tar.gz`, buildkit, BUILDKIT_SHA256);
|
|
923
1079
|
const unpacked = join3(directory, "buildkit");
|
|
@@ -927,15 +1083,15 @@ var installContainerdRuntime = async (directory, osVersion) => {
|
|
|
927
1083
|
return buildkitExtract;
|
|
928
1084
|
for (const binary of ["buildctl", "buildkitd"]) {
|
|
929
1085
|
copyFileSync(join3(unpacked, "bin", binary), `/usr/local/bin/${binary}`);
|
|
930
|
-
|
|
1086
|
+
chmodSync4(`/usr/local/bin/${binary}`, 493);
|
|
931
1087
|
}
|
|
932
1088
|
mkdirSync3("/etc/containerd", { recursive: true, mode: 493 });
|
|
933
1089
|
const expected = CONTAINERD_CONFIG(false);
|
|
934
1090
|
const kataExpected = CONTAINERD_CONFIG(true);
|
|
935
|
-
if (
|
|
1091
|
+
if (existsSync4(CONTAINERD_CONFIG_PATH) && ![expected, kataExpected].includes(readFileSync3(CONTAINERD_CONFIG_PATH, "utf8"))) {
|
|
936
1092
|
return { exitCode: 2, output: "refusing to overwrite an existing containerd configuration that differs from the reviewed ForgeZero policy" };
|
|
937
1093
|
}
|
|
938
|
-
if (!
|
|
1094
|
+
if (!existsSync4(CONTAINERD_CONFIG_PATH))
|
|
939
1095
|
writeFileSync3(CONTAINERD_CONFIG_PATH, expected, { mode: 420, flag: "wx" });
|
|
940
1096
|
writeOwnedPolicy("/etc/systemd/system/forgezero-buildkit.service", BUILDKIT_UNIT);
|
|
941
1097
|
mkdirSync3("/var/lib/forgezero/containerd", { recursive: true, mode: 448 });
|
|
@@ -948,10 +1104,10 @@ var installContainerdRuntime = async (directory, osVersion) => {
|
|
|
948
1104
|
};
|
|
949
1105
|
var installKataRuntime = async (directory) => {
|
|
950
1106
|
for (const pathValue of ["/dev/kvm", "/dev/sev"])
|
|
951
|
-
if (!
|
|
1107
|
+
if (!existsSync4(pathValue))
|
|
952
1108
|
return { exitCode: 2, output: `${pathValue} is required for Kata SEV-SNP` };
|
|
953
1109
|
for (const parameter of ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"]) {
|
|
954
|
-
if (!
|
|
1110
|
+
if (!existsSync4(parameter) || !/^(1|Y)$/i.test(readFileSync3(parameter, "utf8").trim()))
|
|
955
1111
|
return { exitCode: 2, output: `${parameter} does not enable SEV-SNP` };
|
|
956
1112
|
}
|
|
957
1113
|
const dependencies = await aptInstallMany(["zstd"]);
|
|
@@ -966,7 +1122,7 @@ var installKataRuntime = async (directory) => {
|
|
|
966
1122
|
const extracted = await run(["/usr/bin/tar", "-xf", tar, "-C", "/"]);
|
|
967
1123
|
if (extracted.exitCode !== 0)
|
|
968
1124
|
return extracted;
|
|
969
|
-
if (!
|
|
1125
|
+
if (!existsSync4("/opt/kata/bin/containerd-shim-kata-v2") || !existsSync4(KATA_SNP_CONFIG_PATH)) {
|
|
970
1126
|
return { exitCode: 2, output: "Kata archive does not contain the reviewed QEMU SNP runtime and configuration" };
|
|
971
1127
|
}
|
|
972
1128
|
const snpConfig = readFileSync3(KATA_SNP_CONFIG_PATH, "utf8");
|
|
@@ -974,19 +1130,19 @@ var installKataRuntime = async (directory) => {
|
|
|
974
1130
|
return { exitCode: 2, output: "Kata QEMU configuration does not enable confidential SEV-SNP guests" };
|
|
975
1131
|
}
|
|
976
1132
|
mkdirSync3("/etc/kata-containers", { recursive: true, mode: 493 });
|
|
977
|
-
if (
|
|
1133
|
+
if (existsSync4(KATA_ACTIVE_CONFIG_PATH) && readFileSync3(KATA_ACTIVE_CONFIG_PATH, "utf8") !== snpConfig) {
|
|
978
1134
|
return { exitCode: 2, output: "refusing to overwrite a non-ForgeZero Kata runtime configuration" };
|
|
979
1135
|
}
|
|
980
|
-
if (!
|
|
1136
|
+
if (!existsSync4(KATA_ACTIVE_CONFIG_PATH))
|
|
981
1137
|
copyFileSync(KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH);
|
|
982
|
-
|
|
1138
|
+
chmodSync4(KATA_ACTIVE_CONFIG_PATH, 420);
|
|
983
1139
|
try {
|
|
984
|
-
|
|
1140
|
+
unlinkSync3("/usr/local/bin/containerd-shim-kata-v2");
|
|
985
1141
|
} catch {}
|
|
986
|
-
|
|
1142
|
+
symlinkSync3("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
|
|
987
1143
|
const base = CONTAINERD_CONFIG(false);
|
|
988
1144
|
const kata = CONTAINERD_CONFIG(true);
|
|
989
|
-
if (!
|
|
1145
|
+
if (!existsSync4(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync3(CONTAINERD_CONFIG_PATH, "utf8"))) {
|
|
990
1146
|
return { exitCode: 2, output: "containerd must use the reviewed ForgeZero policy before Kata is installed" };
|
|
991
1147
|
}
|
|
992
1148
|
if (readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") !== kata)
|
|
@@ -1006,7 +1162,7 @@ async function executeSoftwareOperation(operation) {
|
|
|
1006
1162
|
const binary = await run(["/usr/bin/docker", "--version"]);
|
|
1007
1163
|
if (!successful(binary, /Docker version/))
|
|
1008
1164
|
return { ...binary, exitCode: 1 };
|
|
1009
|
-
if (!
|
|
1165
|
+
if (!existsSync4(DOCKER_DAEMON_PATH) || readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG)
|
|
1010
1166
|
return { exitCode: 1, output: "Docker daemon policy is absent or differs from the reviewed ForgeZero policy" };
|
|
1011
1167
|
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
|
|
1012
1168
|
return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
|
|
@@ -1019,17 +1175,17 @@ async function executeSoftwareOperation(operation) {
|
|
|
1019
1175
|
run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]),
|
|
1020
1176
|
run(["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-buildkit.service"])
|
|
1021
1177
|
]);
|
|
1022
|
-
const policy =
|
|
1178
|
+
const policy = existsSync4(CONTAINERD_CONFIG_PATH) ? readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") : "";
|
|
1023
1179
|
const validPolicy = policy === CONTAINERD_CONFIG(false) || policy === CONTAINERD_CONFIG(true);
|
|
1024
1180
|
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
1181
|
}
|
|
1026
1182
|
if (software === "kata-containers") {
|
|
1027
1183
|
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(
|
|
1184
|
+
const policy = existsSync4(CONTAINERD_CONFIG_PATH) ? readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") : "";
|
|
1185
|
+
const activeConfig = existsSync4(KATA_ACTIVE_CONFIG_PATH) ? readFileSync3(KATA_ACTIVE_CONFIG_PATH, "utf8") : "";
|
|
1186
|
+
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
1187
|
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 === (
|
|
1188
|
+
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
1189
|
}
|
|
1034
1190
|
if (software === "nginx") {
|
|
1035
1191
|
const binary = await run(["/usr/sbin/nginx", "-v"]);
|
|
@@ -1070,10 +1226,10 @@ async function executeSoftwareOperation(operation) {
|
|
|
1070
1226
|
return installed;
|
|
1071
1227
|
if (software === "docker") {
|
|
1072
1228
|
mkdirSync3("/etc/docker", { recursive: true, mode: 493 });
|
|
1073
|
-
if (
|
|
1229
|
+
if (existsSync4(DOCKER_DAEMON_PATH) && readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG) {
|
|
1074
1230
|
return { exitCode: 2, output: "refusing to overwrite an existing Docker daemon configuration that differs from the reviewed ForgeZero policy" };
|
|
1075
1231
|
}
|
|
1076
|
-
if (!
|
|
1232
|
+
if (!existsSync4(DOCKER_DAEMON_PATH))
|
|
1077
1233
|
writeFileSync3(DOCKER_DAEMON_PATH, DOCKER_DAEMON_CONFIG, { mode: 420, flag: "wx" });
|
|
1078
1234
|
const enabled = await run(["/usr/bin/systemctl", "enable", "docker.service"]);
|
|
1079
1235
|
return enabled.exitCode === 0 ? run(["/usr/bin/systemctl", "restart", "docker.service"]) : enabled;
|
|
@@ -1123,20 +1279,20 @@ async function executeSoftwareOperation(operation) {
|
|
|
1123
1279
|
return unzipped;
|
|
1124
1280
|
mkdirSync3("/usr/local/lib/forgezero/runtime", { recursive: true, mode: 493 });
|
|
1125
1281
|
copyFileSync(join3(unpacked, "bun-linux-x64", "bun"), "/usr/local/lib/forgezero/runtime/bun.next");
|
|
1126
|
-
|
|
1127
|
-
|
|
1282
|
+
chmodSync4("/usr/local/lib/forgezero/runtime/bun.next", 493);
|
|
1283
|
+
renameSync4("/usr/local/lib/forgezero/runtime/bun.next", "/usr/local/lib/forgezero/runtime/bun");
|
|
1128
1284
|
try {
|
|
1129
|
-
|
|
1285
|
+
unlinkSync3("/usr/local/bin/bun");
|
|
1130
1286
|
} catch {}
|
|
1131
|
-
|
|
1287
|
+
symlinkSync3("/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun");
|
|
1132
1288
|
return { exitCode: 0, output: "" };
|
|
1133
1289
|
}
|
|
1134
1290
|
if (software === "cloudflared") {
|
|
1135
1291
|
const binary = join3(directory, "cloudflared");
|
|
1136
1292
|
await download("https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64", binary, CLOUDFLARED_SHA256);
|
|
1137
|
-
|
|
1293
|
+
chmodSync4(binary, 493);
|
|
1138
1294
|
copyFileSync(binary, "/usr/local/bin/cloudflared");
|
|
1139
|
-
|
|
1295
|
+
chmodSync4("/usr/local/bin/cloudflared", 493);
|
|
1140
1296
|
return { exitCode: 0, output: "" };
|
|
1141
1297
|
}
|
|
1142
1298
|
const deb = join3(directory, "arangodb.deb");
|
|
@@ -1149,7 +1305,7 @@ async function executeSoftwareOperation(operation) {
|
|
|
1149
1305
|
await run(["/usr/bin/systemctl", "disable", "--now", "arangodb3.service"]);
|
|
1150
1306
|
return { exitCode: 0, output: "" };
|
|
1151
1307
|
} finally {
|
|
1152
|
-
|
|
1308
|
+
rmSync4(directory, { recursive: true, force: true });
|
|
1153
1309
|
}
|
|
1154
1310
|
}
|
|
1155
1311
|
function observeSoftwareHost(osRelease = readFileSync3("/etc/os-release", "utf8"), architecture = process.arch) {
|
|
@@ -1648,9 +1804,9 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
|
|
|
1648
1804
|
|
|
1649
1805
|
// src/deployment-connectivity.ts
|
|
1650
1806
|
import { createHash as createHash3 } from "node:crypto";
|
|
1651
|
-
import { mkdirSync as mkdirSync4, renameSync as
|
|
1807
|
+
import { mkdirSync as mkdirSync4, renameSync as renameSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1652
1808
|
import { createConnection } from "node:net";
|
|
1653
|
-
import { dirname as
|
|
1809
|
+
import { dirname as dirname5 } from "node:path";
|
|
1654
1810
|
|
|
1655
1811
|
// src/process-input.ts
|
|
1656
1812
|
async function writeAndCloseProcessInput(input, value) {
|
|
@@ -1671,7 +1827,7 @@ var checked2 = async (host, argv2, label) => {
|
|
|
1671
1827
|
return result.output.trim();
|
|
1672
1828
|
};
|
|
1673
1829
|
async function sealWithSystemd(name, path2, value) {
|
|
1674
|
-
mkdirSync4(
|
|
1830
|
+
mkdirSync4(dirname5(path2), { recursive: true, mode: 448 });
|
|
1675
1831
|
const next = `${path2}.next`;
|
|
1676
1832
|
const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
|
|
1677
1833
|
stdin: "pipe",
|
|
@@ -1687,15 +1843,15 @@ async function sealWithSystemd(name, path2, value) {
|
|
|
1687
1843
|
]);
|
|
1688
1844
|
if (exitCode !== 0)
|
|
1689
1845
|
throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
|
|
1690
|
-
|
|
1846
|
+
renameSync5(next, path2);
|
|
1691
1847
|
}
|
|
1692
1848
|
var defaultHost = {
|
|
1693
1849
|
seal: sealWithSystemd,
|
|
1694
1850
|
write(path2, content, mode) {
|
|
1695
|
-
mkdirSync4(
|
|
1851
|
+
mkdirSync4(dirname5(path2), { recursive: true, mode: 493 });
|
|
1696
1852
|
const next = `${path2}.next`;
|
|
1697
1853
|
writeFileSync4(next, content, { mode });
|
|
1698
|
-
|
|
1854
|
+
renameSync5(next, path2);
|
|
1699
1855
|
},
|
|
1700
1856
|
async exec(argv2) {
|
|
1701
1857
|
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 +2083,14 @@ WantedBy=multi-user.target
|
|
|
1927
2083
|
}
|
|
1928
2084
|
|
|
1929
2085
|
// src/software-helper.ts
|
|
1930
|
-
import { chmodSync as
|
|
1931
|
-
import { connect as
|
|
1932
|
-
import { dirname as
|
|
2086
|
+
import { chmodSync as chmodSync5, existsSync as existsSync7, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4 } from "node:fs";
|
|
2087
|
+
import { connect as connect3, createServer as createServer3 } from "node:net";
|
|
2088
|
+
import { dirname as dirname8 } from "node:path";
|
|
1933
2089
|
|
|
1934
2090
|
// src/service-supervisor.ts
|
|
1935
2091
|
import { createHash as createHash4 } from "node:crypto";
|
|
1936
|
-
import { existsSync as
|
|
1937
|
-
import { dirname as
|
|
2092
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, realpathSync, renameSync as renameSync6, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2093
|
+
import { dirname as dirname6, join as join4, resolve as resolve3, sep } from "node:path";
|
|
1938
2094
|
var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
|
|
1939
2095
|
var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
|
|
1940
2096
|
var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
|
|
@@ -1944,17 +2100,17 @@ var statePath = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
|
|
|
1944
2100
|
var within = (root, path2) => path2 === root || path2.startsWith(`${root}${sep}`);
|
|
1945
2101
|
var defaultHost2 = {
|
|
1946
2102
|
write(path2, content, mode) {
|
|
1947
|
-
mkdirSync5(
|
|
2103
|
+
mkdirSync5(dirname6(path2), { recursive: true, mode: 493 });
|
|
1948
2104
|
const next = `${path2}.next`;
|
|
1949
2105
|
writeFileSync5(next, content, { mode });
|
|
1950
|
-
|
|
2106
|
+
renameSync6(next, path2);
|
|
1951
2107
|
},
|
|
1952
2108
|
read: (path2) => readFileSync4(path2, "utf8"),
|
|
1953
|
-
exists:
|
|
1954
|
-
list: (path2) =>
|
|
2109
|
+
exists: existsSync5,
|
|
2110
|
+
list: (path2) => existsSync5(path2) ? readdirSync(path2) : [],
|
|
1955
2111
|
realpath: realpathSync,
|
|
1956
2112
|
mkdir: (path2, mode) => mkdirSync5(path2, { recursive: true, mode }),
|
|
1957
|
-
remove: (path2) =>
|
|
2113
|
+
remove: (path2) => rmSync5(path2, { force: true }),
|
|
1958
2114
|
async exec(argv2) {
|
|
1959
2115
|
const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: {
|
|
1960
2116
|
PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
@@ -2150,20 +2306,20 @@ async function activateSupervisedService(request, host = defaultHost2) {
|
|
|
2150
2306
|
|
|
2151
2307
|
// src/container-supervisor.ts
|
|
2152
2308
|
import { createHash as createHash5 } from "node:crypto";
|
|
2153
|
-
import { existsSync as
|
|
2154
|
-
import { dirname as
|
|
2309
|
+
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";
|
|
2310
|
+
import { dirname as dirname7, resolve as resolve4, sep as sep2 } from "node:path";
|
|
2155
2311
|
var defaultHost3 = {
|
|
2156
2312
|
realpath: realpathSync2,
|
|
2157
|
-
exists:
|
|
2313
|
+
exists: existsSync6,
|
|
2158
2314
|
read: (path2) => readFileSync5(path2, "utf8"),
|
|
2159
2315
|
write(path2, content, mode) {
|
|
2160
|
-
mkdirSync6(
|
|
2316
|
+
mkdirSync6(dirname7(path2), { recursive: true, mode: 493 });
|
|
2161
2317
|
const next = `${path2}.next`;
|
|
2162
2318
|
writeFileSync6(next, content, { mode });
|
|
2163
|
-
|
|
2319
|
+
renameSync7(next, path2);
|
|
2164
2320
|
},
|
|
2165
|
-
remove: (path2) =>
|
|
2166
|
-
list: (path2) =>
|
|
2321
|
+
remove: (path2) => rmSync6(path2, { force: true }),
|
|
2322
|
+
list: (path2) => existsSync6(path2) ? readdirSync2(path2) : [],
|
|
2167
2323
|
mkdir: (path2, mode) => mkdirSync6(path2, { recursive: true, mode }),
|
|
2168
2324
|
async exec(argv2) {
|
|
2169
2325
|
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 +2614,9 @@ var MAX_REQUEST_BYTES2 = 128 * 1024;
|
|
|
2458
2614
|
var MAX_PENDING_REQUESTS = 128;
|
|
2459
2615
|
function startSoftwareHelper(options = {}) {
|
|
2460
2616
|
const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
|
|
2461
|
-
if (
|
|
2462
|
-
|
|
2463
|
-
mkdirSync7(
|
|
2617
|
+
if (existsSync7(socketPath))
|
|
2618
|
+
unlinkSync4(socketPath);
|
|
2619
|
+
mkdirSync7(dirname8(socketPath), { recursive: true, mode: 488 });
|
|
2464
2620
|
const ensure = options.ensure ?? ensureSoftwareRequirements;
|
|
2465
2621
|
const activate = options.activate ?? activateSupervisedService;
|
|
2466
2622
|
const buildContainer = options.buildContainer ?? buildContainerImage;
|
|
@@ -2468,7 +2624,7 @@ function startSoftwareHelper(options = {}) {
|
|
|
2468
2624
|
const applyConnectivity = options.applyConnectivity ?? applyDeploymentConnectivity;
|
|
2469
2625
|
let tail = Promise.resolve();
|
|
2470
2626
|
let pending = 0;
|
|
2471
|
-
const server =
|
|
2627
|
+
const server = createServer3((socket) => {
|
|
2472
2628
|
let buffer = "";
|
|
2473
2629
|
socket.on("data", (chunk) => {
|
|
2474
2630
|
buffer += chunk.toString("utf8");
|
|
@@ -2547,12 +2703,12 @@ function startSoftwareHelper(options = {}) {
|
|
|
2547
2703
|
});
|
|
2548
2704
|
socket.on("error", () => socket.destroy());
|
|
2549
2705
|
});
|
|
2550
|
-
server.listen(socketPath, () =>
|
|
2706
|
+
server.listen(socketPath, () => chmodSync5(socketPath, 432));
|
|
2551
2707
|
return server;
|
|
2552
2708
|
}
|
|
2553
2709
|
function requestContainer(op, request, socketPath, timeoutMs) {
|
|
2554
2710
|
return new Promise((resolve5, reject) => {
|
|
2555
|
-
const socket =
|
|
2711
|
+
const socket = connect3(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
|
|
2556
2712
|
`));
|
|
2557
2713
|
let buffer = "";
|
|
2558
2714
|
socket.setTimeout(timeoutMs, () => {
|
|
@@ -2590,7 +2746,7 @@ function requestDeploymentConnectivity(request, socketPath = DEFAULT_SOFTWARE_HE
|
|
|
2590
2746
|
}
|
|
2591
2747
|
function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
|
|
2592
2748
|
return new Promise((resolve5, reject) => {
|
|
2593
|
-
const socket =
|
|
2749
|
+
const socket = connect3(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
|
|
2594
2750
|
`));
|
|
2595
2751
|
let buffer = "";
|
|
2596
2752
|
socket.setTimeout(timeoutMs, () => {
|
|
@@ -2619,7 +2775,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
|
|
|
2619
2775
|
function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
2620
2776
|
validateSoftwareRequirements(requirements);
|
|
2621
2777
|
return new Promise((resolve5, reject) => {
|
|
2622
|
-
const socket =
|
|
2778
|
+
const socket = connect3(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
|
|
2623
2779
|
`));
|
|
2624
2780
|
let buffer = "";
|
|
2625
2781
|
socket.setTimeout(timeoutMs, () => {
|
|
@@ -2647,7 +2803,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
2647
2803
|
}
|
|
2648
2804
|
|
|
2649
2805
|
// src/version.ts
|
|
2650
|
-
var VERSION3 = "0.1.
|
|
2806
|
+
var VERSION3 = "0.1.87";
|
|
2651
2807
|
|
|
2652
2808
|
// src/egress-policy.ts
|
|
2653
2809
|
import { realpathSync as realpathSync3 } from "node:fs";
|
|
@@ -2763,6 +2919,7 @@ var LIFECYCLE_GROUP = "forgezero-lifecycle";
|
|
|
2763
2919
|
var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
|
|
2764
2920
|
var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
|
|
2765
2921
|
var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
|
|
2922
|
+
var AGENT_CANDIDATE_UNIT_PATH = "/etc/systemd/system/forgezero-agent-candidate.service";
|
|
2766
2923
|
var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
2767
2924
|
var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
|
|
2768
2925
|
var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
|
|
@@ -2874,6 +3031,7 @@ Type=simple
|
|
|
2874
3031
|
User=root
|
|
2875
3032
|
Group=${AGENT_UPDATE_GROUP}
|
|
2876
3033
|
Environment=FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}
|
|
3034
|
+
Environment=FZ_AGENT_PUBLIC_SOCKET=${systemdPath(options.socketPath, "agent socket")}
|
|
2877
3035
|
ExecStart=${bin} update-helper
|
|
2878
3036
|
Restart=always
|
|
2879
3037
|
RestartSec=2
|
|
@@ -2918,13 +3076,11 @@ WantedBy=sockets.target
|
|
|
2918
3076
|
`;
|
|
2919
3077
|
}
|
|
2920
3078
|
function agentSocketProxyUnit(options) {
|
|
2921
|
-
const backend =
|
|
3079
|
+
const backend = agentRoutingSocketPath(options.socketPath);
|
|
2922
3080
|
const user = options.user ?? "forgezero";
|
|
2923
3081
|
return `[Unit]
|
|
2924
3082
|
Description=ForgeZero application Vault socket proxy
|
|
2925
3083
|
Documentation=https://www.forgezero.net/docs/agent
|
|
2926
|
-
Requires=forgezero-agent.service
|
|
2927
|
-
After=forgezero-agent.service
|
|
2928
3084
|
|
|
2929
3085
|
[Service]
|
|
2930
3086
|
User=${user}
|
|
@@ -2945,12 +3101,26 @@ RestrictAddressFamilies=AF_UNIX
|
|
|
2945
3101
|
`;
|
|
2946
3102
|
}
|
|
2947
3103
|
function agentBackendSocketPath(publicSocketPath) {
|
|
3104
|
+
const socket = systemdPath(publicSocketPath, "agent socket");
|
|
3105
|
+
const backend = `${socket}.backend.active`;
|
|
3106
|
+
if (Buffer.byteLength(backend) > 100)
|
|
3107
|
+
throw new Error("agent socket path is too long for a Unix socket");
|
|
3108
|
+
return backend;
|
|
3109
|
+
}
|
|
3110
|
+
function agentRoutingSocketPath(publicSocketPath) {
|
|
2948
3111
|
const socket = systemdPath(publicSocketPath, "agent socket");
|
|
2949
3112
|
const backend = `${socket}.backend`;
|
|
2950
3113
|
if (Buffer.byteLength(backend) > 100)
|
|
2951
3114
|
throw new Error("agent socket path is too long for a Unix socket");
|
|
2952
3115
|
return backend;
|
|
2953
3116
|
}
|
|
3117
|
+
function agentCandidateSocketPath(publicSocketPath) {
|
|
3118
|
+
const socket = systemdPath(publicSocketPath, "agent socket");
|
|
3119
|
+
const backend = `${socket}.backend.candidate`;
|
|
3120
|
+
if (Buffer.byteLength(backend) > 100)
|
|
3121
|
+
throw new Error("agent socket path is too long for a Unix socket");
|
|
3122
|
+
return backend;
|
|
3123
|
+
}
|
|
2954
3124
|
var systemdPath = (value, label) => {
|
|
2955
3125
|
if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
|
|
2956
3126
|
throw new Error(`invalid ${label} path`);
|
|
@@ -3234,7 +3404,11 @@ function agentUnit(options) {
|
|
|
3234
3404
|
}
|
|
3235
3405
|
const environment = [
|
|
3236
3406
|
"NODE_ENV=production",
|
|
3237
|
-
`FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
|
|
3407
|
+
`FZ_SOCKET_PATH=${options.backendSocketPath ?? agentBackendSocketPath(options.socketPath)}`,
|
|
3408
|
+
`FZ_AGENT_PUBLIC_SOCKET=${options.socketPath}`,
|
|
3409
|
+
`FZ_AGENT_ROUTE_SOCKET=${agentRoutingSocketPath(options.socketPath)}`,
|
|
3410
|
+
options.handoverCandidate ? "FZ_AGENT_HANDOVER_CANDIDATE=true" : null,
|
|
3411
|
+
options.handoverCandidate ? `FZ_AGENT_HANDOVER_READY_SOCKET=${DEFAULT_AGENT_CANDIDATE_READY_SOCKET}` : null,
|
|
3238
3412
|
`FZ_CONTROL_SOCKET=${controlSocketPath}`,
|
|
3239
3413
|
`FZ_SEED_CREDENTIAL=agent-seed`,
|
|
3240
3414
|
`FZ_AGENT_MODE=${options.mode}`,
|
|
@@ -3371,6 +3545,26 @@ ${deploymentWrites}
|
|
|
3371
3545
|
WantedBy=multi-user.target
|
|
3372
3546
|
`;
|
|
3373
3547
|
}
|
|
3548
|
+
function agentCandidateUnit(options) {
|
|
3549
|
+
return agentUnit({
|
|
3550
|
+
...options,
|
|
3551
|
+
binPath: `${DEFAULT_AGENT_RELEASE_ROOT}/candidate/dist/fz-agent.js`,
|
|
3552
|
+
backendSocketPath: agentCandidateSocketPath(options.socketPath),
|
|
3553
|
+
handoverCandidate: true,
|
|
3554
|
+
gitCredentialPath: undefined,
|
|
3555
|
+
deploymentCredentials: {},
|
|
3556
|
+
bootstrapSshCredentialPath: undefined,
|
|
3557
|
+
bootstrapSshPublicKeyPath: undefined,
|
|
3558
|
+
pullBootstrap: false,
|
|
3559
|
+
pullDeployments: false,
|
|
3560
|
+
pullMigrations: false,
|
|
3561
|
+
lifecycleProfilePath: undefined,
|
|
3562
|
+
bootstrapTargetTelemetryEndpoint: undefined,
|
|
3563
|
+
repository: undefined,
|
|
3564
|
+
bootstrapBundlePath: undefined,
|
|
3565
|
+
bootstrapBundleManifestPath: undefined
|
|
3566
|
+
}).replace("Description=ForgeZero node agent", "Description=ForgeZero candidate node agent");
|
|
3567
|
+
}
|
|
3374
3568
|
var renderOperation = (operation) => {
|
|
3375
3569
|
if (operation.kind === "commands")
|
|
3376
3570
|
return operation.commands.map(({ argv: argv2 }) => argv2.join(" ")).join(`
|
|
@@ -3506,6 +3700,7 @@ function planProvision(options) {
|
|
|
3506
3700
|
auxiliaryUnits: [
|
|
3507
3701
|
{ path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
|
|
3508
3702
|
{ path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
|
|
3703
|
+
{ path: AGENT_CANDIDATE_UNIT_PATH, unit: agentCandidateUnit(options) },
|
|
3509
3704
|
{ path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
|
|
3510
3705
|
...options.enforceEgress ? [
|
|
3511
3706
|
{ path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
|
|
@@ -3647,8 +3842,11 @@ export {
|
|
|
3647
3842
|
agentUnit,
|
|
3648
3843
|
agentSocketUnit,
|
|
3649
3844
|
agentSocketProxyUnit,
|
|
3845
|
+
agentRoutingSocketPath,
|
|
3650
3846
|
agentEnrolmentUnit,
|
|
3651
3847
|
agentEgressUnit,
|
|
3848
|
+
agentCandidateUnit,
|
|
3849
|
+
agentCandidateSocketPath,
|
|
3652
3850
|
agentBackendSocketPath,
|
|
3653
3851
|
WARP_SERVICE_DROP_IN_PATH,
|
|
3654
3852
|
WARP_CONFIG_UNIT_PATH,
|
|
@@ -3667,5 +3865,6 @@ export {
|
|
|
3667
3865
|
APPLICATION_RUNTIME_USER,
|
|
3668
3866
|
AGENT_SOCKET_UNIT_PATH,
|
|
3669
3867
|
AGENT_SOCKET_PROXY_UNIT_PATH,
|
|
3670
|
-
AGENT_EGRESS_UNIT_PATH
|
|
3868
|
+
AGENT_EGRESS_UNIT_PATH,
|
|
3869
|
+
AGENT_CANDIDATE_UNIT_PATH
|
|
3671
3870
|
};
|