@forgezero/agent 0.1.159 → 0.1.161
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-heartbeat.js +1 -1
- package/dist/bootstrap.js +76 -9
- package/dist/fz-agent.js +143 -36
- package/dist/fz.js +76 -9
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.js +76 -9
- package/dist/platform-bootstrap-runtime.d.ts +13 -2
- package/dist/platform-bootstrap-runtime.js +98 -35
- package/dist/platform-fleet-verification.js +143 -36
- package/dist/provision.js +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -3081,7 +3081,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
3081
3081
|
}
|
|
3082
3082
|
|
|
3083
3083
|
// src/version.ts
|
|
3084
|
-
var VERSION3 = "0.1.
|
|
3084
|
+
var VERSION3 = "0.1.161";
|
|
3085
3085
|
|
|
3086
3086
|
// src/egress-policy.ts
|
|
3087
3087
|
import { realpathSync as realpathSync3 } from "node:fs";
|
|
@@ -4712,12 +4712,23 @@ function renderPlatformActivationFiles(input) {
|
|
|
4712
4712
|
throw new Error("Activation root must be absolute and single-line.");
|
|
4713
4713
|
if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(input.serviceUser))
|
|
4714
4714
|
throw new Error("Invalid activation service user.");
|
|
4715
|
+
boundedInteger("publicPort", input.publicPort, 1024, 65535);
|
|
4715
4716
|
boundedInteger("bluePort", input.bluePort, 1024, 65535);
|
|
4716
4717
|
boundedInteger("greenPort", input.greenPort, 1024, 65535);
|
|
4717
|
-
if (input.bluePort
|
|
4718
|
-
throw new Error("Activation slot ports must differ.");
|
|
4719
|
-
|
|
4720
|
-
|
|
4718
|
+
if (new Set([input.publicPort, input.bluePort, input.greenPort]).size !== 3) {
|
|
4719
|
+
throw new Error("Activation public and slot ports must differ.");
|
|
4720
|
+
}
|
|
4721
|
+
const runtimeHandoff = input.runtimeHandoff;
|
|
4722
|
+
if (!/^[A-Za-z0-9.-]{1,253}$/.test(runtimeHandoff.address) || runtimeHandoff.address.startsWith(".") || runtimeHandoff.address.endsWith(".")) {
|
|
4723
|
+
throw new Error("Runtime handoff address is malformed.");
|
|
4724
|
+
}
|
|
4725
|
+
for (const [name, path2] of Object.entries({
|
|
4726
|
+
residentPath: runtimeHandoff.residentPath,
|
|
4727
|
+
readyPath: runtimeHandoff.readyPath
|
|
4728
|
+
})) {
|
|
4729
|
+
if (!/^\/[A-Za-z0-9/_.-]{1,192}$/.test(path2) || path2.includes("..")) {
|
|
4730
|
+
throw new Error(`Runtime handoff ${name} is malformed.`);
|
|
4731
|
+
}
|
|
4721
4732
|
}
|
|
4722
4733
|
boundedInteger("keepReleases", input.keepReleases, 2, 100);
|
|
4723
4734
|
const drainDeadlineMs = boundedInteger("drainDeadlineMs", input.drainDeadlineMs ?? 35000, 1000, 300000);
|
|
@@ -4727,16 +4738,17 @@ function renderPlatformActivationFiles(input) {
|
|
|
4727
4738
|
const environment = [
|
|
4728
4739
|
`FZ_DIR=${input.root}`,
|
|
4729
4740
|
`FZ_USER=${input.serviceUser}`,
|
|
4741
|
+
`FZ_PUBLIC_PORT=${input.publicPort}`,
|
|
4730
4742
|
`FZ_BLUE_PORT=${input.bluePort}`,
|
|
4731
4743
|
`FZ_GREEN_PORT=${input.greenPort}`,
|
|
4732
|
-
`
|
|
4744
|
+
`FZ_RUNTIME_HANDOFF_ADDRESS=${runtimeHandoff.address}`,
|
|
4733
4745
|
`FZ_HEALTH_PATH=${input.healthPath}`,
|
|
4734
4746
|
`FZ_KEEP_RELEASES=${input.keepReleases}`,
|
|
4735
4747
|
`FZ_DRAIN_DEADLINE_MS=${drainDeadlineMs}`
|
|
4736
4748
|
].join(`
|
|
4737
4749
|
`) + `
|
|
4738
4750
|
`;
|
|
4739
|
-
const helper = `${JSON.stringify({ ...input, drainDeadlineMs }, null, 2)}
|
|
4751
|
+
const helper = `${JSON.stringify({ ...input, runtimeHandoff, drainDeadlineMs }, null, 2)}
|
|
4740
4752
|
`;
|
|
4741
4753
|
return {
|
|
4742
4754
|
environment,
|
|
@@ -4814,27 +4826,66 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
4814
4826
|
mkdirSync8(slots, { recursive: true, mode: 493 });
|
|
4815
4827
|
const slotFile = join5(slots, ".active");
|
|
4816
4828
|
let previousSlot = existsSync8(slotFile) ? readFileSync6(slotFile, "utf8").trim() : undefined;
|
|
4817
|
-
if (previousSlot !== "blue" && previousSlot !== "green")
|
|
4829
|
+
if (previousSlot !== "blue" && previousSlot !== "green")
|
|
4818
4830
|
previousSlot = undefined;
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
}
|
|
4831
|
+
const slotPort = (slot) => slot === "blue" ? normalized.bluePort : normalized.greenPort;
|
|
4832
|
+
const healthy = async (port2) => {
|
|
4833
|
+
try {
|
|
4834
|
+
const response = await request(`http://127.0.0.1:${port2}${normalized.healthPath}`, {
|
|
4835
|
+
signal: AbortSignal.timeout(2000)
|
|
4836
|
+
});
|
|
4837
|
+
return response.ok;
|
|
4838
|
+
} catch {
|
|
4839
|
+
return false;
|
|
4840
|
+
}
|
|
4841
|
+
};
|
|
4842
|
+
const runningHealthySlots = [];
|
|
4843
|
+
for (const candidate of ["blue", "green"]) {
|
|
4844
|
+
const active = await exec(["/usr/bin/systemctl", "is-active", "--quiet", `forgezero@${candidate}.service`]);
|
|
4845
|
+
if (active.exitCode === 0 && await healthy(slotPort(candidate)))
|
|
4846
|
+
runningHealthySlots.push(candidate);
|
|
4847
|
+
}
|
|
4848
|
+
if (previousSlot && !runningHealthySlots.includes(previousSlot)) {
|
|
4849
|
+
throw new Error("active slot evidence disagrees with the running healthy API slot");
|
|
4850
|
+
}
|
|
4851
|
+
if (!previousSlot) {
|
|
4852
|
+
if (runningHealthySlots.length !== 1) {
|
|
4853
|
+
throw new Error("exactly one running healthy API slot is required before promotion");
|
|
4825
4854
|
}
|
|
4855
|
+
previousSlot = runningHealthySlots[0];
|
|
4826
4856
|
}
|
|
4827
4857
|
const target = previousSlot === "blue" ? "green" : "blue";
|
|
4828
|
-
const
|
|
4829
|
-
const
|
|
4858
|
+
const previousPort = slotPort(previousSlot);
|
|
4859
|
+
const port = slotPort(target);
|
|
4860
|
+
const handoffProbe = async (slotPort2, path2) => {
|
|
4830
4861
|
try {
|
|
4831
|
-
const response = await request(`http://${normalized.
|
|
4862
|
+
const response = await request(`http://${normalized.runtimeHandoff.address}:${slotPort2}${path2}`, { signal: AbortSignal.timeout(2000) });
|
|
4832
4863
|
return response.ok;
|
|
4833
4864
|
} catch {
|
|
4834
4865
|
return false;
|
|
4835
4866
|
}
|
|
4836
4867
|
};
|
|
4837
|
-
const
|
|
4868
|
+
const upstream = options.upstreamPath ?? "/etc/nginx/conf.d/forgezero-upstream.conf";
|
|
4869
|
+
const renderUpstream = (upstreamPort) => `upstream forgezero { server 127.0.0.1:${upstreamPort}; }
|
|
4870
|
+
`;
|
|
4871
|
+
const reloadNginx = async (upstreamPort) => {
|
|
4872
|
+
const pending = `${upstream}.next`;
|
|
4873
|
+
writeFileSync7(pending, renderUpstream(upstreamPort), { mode: 420, flush: true });
|
|
4874
|
+
renameSync8(pending, upstream);
|
|
4875
|
+
const test = await exec(["/usr/sbin/nginx", "-t"]);
|
|
4876
|
+
const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
|
|
4877
|
+
if (reload.exitCode !== 0) {
|
|
4878
|
+
const refusal = (reload.output || test.output).trim().slice(0, 2000);
|
|
4879
|
+
throw new Error(`nginx refused the active-slot upstream${refusal ? `: ${refusal}` : ""}`);
|
|
4880
|
+
}
|
|
4881
|
+
};
|
|
4882
|
+
if (!existsSync8(upstream) || readFileSync6(upstream, "utf8") !== renderUpstream(previousPort)) {
|
|
4883
|
+
await reloadNginx(previousPort);
|
|
4884
|
+
}
|
|
4885
|
+
if (!await healthy(normalized.publicPort)) {
|
|
4886
|
+
throw new Error("stable public listener does not reach the active API slot");
|
|
4887
|
+
}
|
|
4888
|
+
const previousHeldSeed = previousSlot ? await handoffProbe(previousSlot === "blue" ? normalized.bluePort : normalized.greenPort, normalized.runtimeHandoff.residentPath) : false;
|
|
4838
4889
|
if (previousSlot && !previousHeldSeed) {
|
|
4839
4890
|
throw new Error("active slot has no resident Vault seed; unlock custody before promotion");
|
|
4840
4891
|
}
|
|
@@ -4855,13 +4906,10 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
4855
4906
|
replaceLink(targetLink, previousTarget);
|
|
4856
4907
|
throw new Error("target slot failed to start");
|
|
4857
4908
|
}
|
|
4858
|
-
let
|
|
4909
|
+
let targetHealthy = false;
|
|
4859
4910
|
for (let attempt = 0;attempt < 30; attempt += 1) {
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
healthy = response.ok;
|
|
4863
|
-
} catch {}
|
|
4864
|
-
if (healthy)
|
|
4911
|
+
targetHealthy = await healthy(port);
|
|
4912
|
+
if (targetHealthy)
|
|
4865
4913
|
break;
|
|
4866
4914
|
await sleep(1000);
|
|
4867
4915
|
}
|
|
@@ -4871,14 +4919,18 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
4871
4919
|
if (previousSlot)
|
|
4872
4920
|
await exec(["/usr/bin/systemctl", "start", `forgezero@${previousSlot}.service`]);
|
|
4873
4921
|
};
|
|
4874
|
-
if (!
|
|
4922
|
+
if (!targetHealthy) {
|
|
4875
4923
|
await stopTarget();
|
|
4876
4924
|
throw new Error("target slot failed its health deadline");
|
|
4877
4925
|
}
|
|
4878
4926
|
if (previousHeldSeed) {
|
|
4879
4927
|
let seedReady = false;
|
|
4880
4928
|
for (let attempt = 0;attempt < 30; attempt += 1) {
|
|
4881
|
-
|
|
4929
|
+
const [resident, converged] = await Promise.all([
|
|
4930
|
+
handoffProbe(port, normalized.runtimeHandoff.residentPath),
|
|
4931
|
+
handoffProbe(port, normalized.runtimeHandoff.readyPath)
|
|
4932
|
+
]);
|
|
4933
|
+
seedReady = resident && converged;
|
|
4882
4934
|
if (seedReady)
|
|
4883
4935
|
break;
|
|
4884
4936
|
await sleep(1000);
|
|
@@ -4888,18 +4940,14 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
4888
4940
|
throw new Error("target slot failed RAM-seed handover deadline");
|
|
4889
4941
|
}
|
|
4890
4942
|
}
|
|
4891
|
-
const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
|
|
4892
4943
|
const backup = `${upstream}.forgezero-backup`;
|
|
4893
4944
|
if (existsSync8(upstream))
|
|
4894
4945
|
copyFileSync2(upstream, backup);
|
|
4895
4946
|
else
|
|
4896
4947
|
rmSync7(backup, { force: true });
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
|
|
4901
|
-
if (reload.exitCode !== 0) {
|
|
4902
|
-
const refusal = (reload.output || test.output).trim().slice(0, 2000);
|
|
4948
|
+
try {
|
|
4949
|
+
await reloadNginx(port);
|
|
4950
|
+
} catch (cause) {
|
|
4903
4951
|
if (existsSync8(backup))
|
|
4904
4952
|
renameSync8(backup, upstream);
|
|
4905
4953
|
else
|
|
@@ -4907,14 +4955,29 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
4907
4955
|
await exec(["/usr/sbin/nginx", "-t"]);
|
|
4908
4956
|
await exec(["/usr/sbin/nginx", "-s", "reload"]);
|
|
4909
4957
|
await stopTarget();
|
|
4910
|
-
throw
|
|
4958
|
+
throw cause;
|
|
4959
|
+
}
|
|
4960
|
+
if (!await healthy(normalized.publicPort)) {
|
|
4961
|
+
if (existsSync8(backup))
|
|
4962
|
+
renameSync8(backup, upstream);
|
|
4963
|
+
else
|
|
4964
|
+
rmSync7(upstream, { force: true });
|
|
4965
|
+
await exec(["/usr/sbin/nginx", "-t"]);
|
|
4966
|
+
await exec(["/usr/sbin/nginx", "-s", "reload"]);
|
|
4967
|
+
await stopTarget();
|
|
4968
|
+
throw new Error("stable public listener failed after the slot switch");
|
|
4911
4969
|
}
|
|
4912
4970
|
rmSync7(backup, { force: true });
|
|
4913
4971
|
writeFileSync7(slotFile, `${target}
|
|
4914
4972
|
`, { mode: 420 });
|
|
4915
4973
|
if (previousSlot && previousSlot !== target) {
|
|
4916
|
-
|
|
4917
|
-
await exec(["/usr/bin/systemctl", "stop",
|
|
4974
|
+
const previousService = `forgezero@${previousSlot}.service`;
|
|
4975
|
+
const stopped = await exec(["/usr/bin/systemctl", "stop", previousService]);
|
|
4976
|
+
const inactive = await exec(["/usr/bin/systemctl", "is-active", "--quiet", previousService]);
|
|
4977
|
+
const status = await exec(["/usr/bin/systemctl", "show", "--property=ExecMainStatus", "--value", previousService]);
|
|
4978
|
+
if (stopped.exitCode !== 0 || inactive.exitCode === 0 || status.exitCode !== 0 || status.output.trim() !== "0") {
|
|
4979
|
+
throw new Error("old API slot failed its clean request and job drain; target remains active");
|
|
4980
|
+
}
|
|
4918
4981
|
}
|
|
4919
4982
|
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);
|
|
4920
4983
|
for (const path2 of old)
|
|
@@ -5506,6 +5569,50 @@ async function bootstrapStatus(host = localBootstrapHost()) {
|
|
|
5506
5569
|
services["forgezero@active.service"] = blue.exitCode === 0 || green.exitCode === 0;
|
|
5507
5570
|
if (!services["forgezero@active.service"])
|
|
5508
5571
|
problems.push("neither API slot is active");
|
|
5572
|
+
const activeSlots = [
|
|
5573
|
+
...blue.exitCode === 0 ? ["blue"] : [],
|
|
5574
|
+
...green.exitCode === 0 ? ["green"] : []
|
|
5575
|
+
];
|
|
5576
|
+
const activeSlotFile = state.slotsDirectory ? `${state.slotsDirectory}/.active` : "";
|
|
5577
|
+
const recordedSlot = activeSlotFile && host.exists(activeSlotFile) ? host.read(activeSlotFile).trim() : "";
|
|
5578
|
+
const coordinatesValid = Number.isSafeInteger(state.bluePort) && Number.isSafeInteger(state.greenPort) && Boolean(state.slotsDirectory) && Boolean(state.seedMeshAddress) && Boolean(state.healthPath) && Boolean(state.runtimeHandoffResidentPath) && Boolean(state.runtimeHandoffReadyPath);
|
|
5579
|
+
services["platform-active-slot-consistent"] = coordinatesValid && activeSlots.length === 1 && (recordedSlot === "blue" || recordedSlot === "green") && activeSlots[0] === recordedSlot;
|
|
5580
|
+
if (!services["platform-active-slot-consistent"]) {
|
|
5581
|
+
problems.push("active slot marker, service state, or runtime coordinates disagree");
|
|
5582
|
+
}
|
|
5583
|
+
if (services["platform-active-slot-consistent"]) {
|
|
5584
|
+
const slotPort = recordedSlot === "blue" ? state.bluePort : state.greenPort;
|
|
5585
|
+
const directHealth = await host.exec([
|
|
5586
|
+
"curl",
|
|
5587
|
+
"--fail",
|
|
5588
|
+
"--silent",
|
|
5589
|
+
"--show-error",
|
|
5590
|
+
"--max-time",
|
|
5591
|
+
"5",
|
|
5592
|
+
`http://127.0.0.1:${slotPort}${state.healthPath}`
|
|
5593
|
+
]);
|
|
5594
|
+
services["platform-active-slot-health"] = directHealth.exitCode === 0;
|
|
5595
|
+
if (directHealth.exitCode !== 0)
|
|
5596
|
+
problems.push("active API slot health check failed");
|
|
5597
|
+
const expectedUpstream = `upstream forgezero { server 127.0.0.1:${slotPort}; }
|
|
5598
|
+
`;
|
|
5599
|
+
const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
|
|
5600
|
+
services["platform-nginx-active-slot"] = host.exists(upstream) && host.read(upstream) === expectedUpstream;
|
|
5601
|
+
if (!services["platform-nginx-active-slot"])
|
|
5602
|
+
problems.push("nginx upstream does not match the active API slot");
|
|
5603
|
+
const resident = await host.exec([
|
|
5604
|
+
"curl",
|
|
5605
|
+
"--fail",
|
|
5606
|
+
"--silent",
|
|
5607
|
+
"--show-error",
|
|
5608
|
+
"--max-time",
|
|
5609
|
+
"5",
|
|
5610
|
+
`http://${state.seedMeshAddress}:${slotPort}${state.runtimeHandoffResidentPath}`
|
|
5611
|
+
]);
|
|
5612
|
+
services["platform-vault-resident"] = resident.exitCode === 0;
|
|
5613
|
+
if (resident.exitCode !== 0)
|
|
5614
|
+
problems.push("active API slot has no resident Vault seed");
|
|
5615
|
+
}
|
|
5509
5616
|
if (!Number.isSafeInteger(state.publicApiPort) || !state.healthPath) {
|
|
5510
5617
|
problems.push("platform health coordinates are missing from bootstrap state");
|
|
5511
5618
|
} else {
|
package/dist/provision.js
CHANGED
|
@@ -3081,7 +3081,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
3081
3081
|
}
|
|
3082
3082
|
|
|
3083
3083
|
// src/version.ts
|
|
3084
|
-
var VERSION3 = "0.1.
|
|
3084
|
+
var VERSION3 = "0.1.161";
|
|
3085
3085
|
|
|
3086
3086
|
// src/egress-policy.ts
|
|
3087
3087
|
import { realpathSync as realpathSync3 } from "node:fs";
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** One package version shared by both public binaries. Pinned to package.json by tests. */
|
|
2
|
-
export declare const VERSION = "0.1.
|
|
2
|
+
export declare const VERSION = "0.1.161";
|