@forgezero/agent 0.1.52 → 0.1.55
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 +118 -9
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.js +110 -69
- package/dist/community-rehearsal-host.js +40 -8
- package/dist/container-supervisor.d.ts +36 -0
- package/dist/definition.js +38 -6
- package/dist/deploy-actions.d.ts +87 -0
- package/dist/deploy-compiler.d.ts +26 -0
- package/dist/deploy-compiler.js +1050 -0
- package/dist/deploy-file.js +45 -13
- package/dist/deploy-plan-runner.d.ts +65 -0
- package/dist/deploy-plan-runner.js +1205 -0
- package/dist/deploy-plan.d.ts +41 -0
- package/dist/deploy-plan.js +925 -0
- package/dist/deploy-providers.d.ts +85 -0
- package/dist/deploy.d.ts +439 -0
- package/dist/deploy.js +169 -0
- package/dist/deployment.d.ts +5 -0
- package/dist/fz-agent.js +2116 -329
- package/dist/fz.js +1444 -313
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.js +167 -126
- package/dist/platform-bootstrap-runtime.js +1 -1
- package/dist/platform-fleet-verification.js +500 -121
- package/dist/provision.js +400 -21
- package/dist/software-helper.d.ts +5 -0
- package/dist/software-helper.js +400 -19
- package/dist/software.d.ts +1 -1
- package/dist/software.js +38 -6
- package/dist/version.d.ts +1 -1
- package/package.json +17 -1
package/dist/provision.js
CHANGED
|
@@ -682,6 +682,7 @@ import {
|
|
|
682
682
|
accessSync,
|
|
683
683
|
chmodSync as chmodSync3,
|
|
684
684
|
copyFileSync,
|
|
685
|
+
existsSync as existsSync3,
|
|
685
686
|
mkdtempSync,
|
|
686
687
|
mkdirSync as mkdirSync3,
|
|
687
688
|
readFileSync as readFileSync3,
|
|
@@ -702,6 +703,7 @@ var OS_CATALOG = [
|
|
|
702
703
|
];
|
|
703
704
|
var SOFTWARE_CATALOG = [
|
|
704
705
|
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
706
|
+
{ id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
705
707
|
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
706
708
|
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
707
709
|
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -711,6 +713,7 @@ var SOFTWARE_CATALOG = [
|
|
|
711
713
|
];
|
|
712
714
|
var UBUNTU_2604_X64 = [
|
|
713
715
|
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
716
|
+
{ requirement: { id: "docker", version: "ubuntu-26.04" } },
|
|
714
717
|
{ requirement: { id: "nginx", version: "ubuntu-26.04" } },
|
|
715
718
|
{ requirement: { id: "arangodb", version: "3.11.14" } },
|
|
716
719
|
{ requirement: { id: "cloudflared", version: "2026.7.3" } },
|
|
@@ -719,6 +722,15 @@ var UBUNTU_2604_X64 = [
|
|
|
719
722
|
{ requirement: { id: "openssh-client", version: "ubuntu-26.04" } }
|
|
720
723
|
];
|
|
721
724
|
var path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
725
|
+
var DOCKER_DAEMON_PATH = "/etc/docker/daemon.json";
|
|
726
|
+
var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
727
|
+
"data-root": "/var/lib/docker",
|
|
728
|
+
"storage-driver": "overlay2",
|
|
729
|
+
"live-restore": true,
|
|
730
|
+
"log-driver": "local",
|
|
731
|
+
"log-opts": { "max-size": "10m", "max-file": "3" }
|
|
732
|
+
}, null, 2)}
|
|
733
|
+
`;
|
|
722
734
|
var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
|
|
723
735
|
var runSoftwareCommand = async (argv, env = {}) => {
|
|
724
736
|
let child;
|
|
@@ -761,12 +773,21 @@ async function executeSoftwareOperation(operation) {
|
|
|
761
773
|
if (operation.kind === "check") {
|
|
762
774
|
if (software === "bun")
|
|
763
775
|
return run(["/usr/local/bin/bun", "--version"]).then((r) => ({ ...r, exitCode: successful(r, /^1\.3\.14\s*$/m) ? 0 : 1 }));
|
|
776
|
+
if (software === "docker") {
|
|
777
|
+
const binary = await run(["/usr/bin/docker", "--version"]);
|
|
778
|
+
if (!successful(binary, /Docker version/))
|
|
779
|
+
return { ...binary, exitCode: 1 };
|
|
780
|
+
if (!existsSync3(DOCKER_DAEMON_PATH) || readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG)
|
|
781
|
+
return { exitCode: 1, output: "Docker daemon policy is absent or differs from the reviewed ForgeZero policy" };
|
|
782
|
+
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
|
|
783
|
+
return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
|
|
784
|
+
}
|
|
764
785
|
if (software === "nginx") {
|
|
765
786
|
const binary = await run(["/usr/sbin/nginx", "-v"]);
|
|
766
787
|
return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
|
|
767
788
|
}
|
|
768
789
|
if (software === "arangodb") {
|
|
769
|
-
const binary = await run(["/usr/
|
|
790
|
+
const binary = await run(["/usr/sbin/arangod", "--version"]);
|
|
770
791
|
if (!successful(binary, /3\.11\.14/))
|
|
771
792
|
return { ...binary, exitCode: 1 };
|
|
772
793
|
const [active, enabled] = await Promise.all([
|
|
@@ -793,11 +814,22 @@ async function executeSoftwareOperation(operation) {
|
|
|
793
814
|
return { exitCode: 1, output: cause instanceof Error ? cause.message : String(cause) };
|
|
794
815
|
}
|
|
795
816
|
}
|
|
796
|
-
if (software === "nginx" || software === "ufw" || software === "openssh-client") {
|
|
797
|
-
const
|
|
798
|
-
|
|
817
|
+
if (software === "docker" || software === "nginx" || software === "ufw" || software === "openssh-client") {
|
|
818
|
+
const packageName = software === "openssh-client" ? "openssh-client" : software === "docker" ? "docker.io" : software;
|
|
819
|
+
const installed = await aptInstall(packageName);
|
|
820
|
+
if (installed.exitCode !== 0 || software !== "nginx" && software !== "docker")
|
|
799
821
|
return installed;
|
|
800
|
-
|
|
822
|
+
if (software === "docker") {
|
|
823
|
+
mkdirSync3("/etc/docker", { recursive: true, mode: 493 });
|
|
824
|
+
if (existsSync3(DOCKER_DAEMON_PATH) && readFileSync3(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG) {
|
|
825
|
+
return { exitCode: 2, output: "refusing to overwrite an existing Docker daemon configuration that differs from the reviewed ForgeZero policy" };
|
|
826
|
+
}
|
|
827
|
+
if (!existsSync3(DOCKER_DAEMON_PATH))
|
|
828
|
+
writeFileSync3(DOCKER_DAEMON_PATH, DOCKER_DAEMON_CONFIG, { mode: 420, flag: "wx" });
|
|
829
|
+
const enabled = await run(["/usr/bin/systemctl", "enable", "docker.service"]);
|
|
830
|
+
return enabled.exitCode === 0 ? run(["/usr/bin/systemctl", "restart", "docker.service"]) : enabled;
|
|
831
|
+
}
|
|
832
|
+
return run(["/usr/bin/systemctl", "enable", "--now", `${software}.service`]);
|
|
801
833
|
}
|
|
802
834
|
const directory = mkdtempSync(join3(tmpdir(), "forgezero-software-"));
|
|
803
835
|
try {
|
|
@@ -882,7 +914,7 @@ function validateSoftwareRequirements(value, _options = {}) {
|
|
|
882
914
|
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
883
915
|
throw new Error("software requirement contains an unknown field");
|
|
884
916
|
}
|
|
885
|
-
if (!["bun", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
917
|
+
if (!["bun", "docker", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
886
918
|
throw new Error("software requirement coordinate is invalid");
|
|
887
919
|
}
|
|
888
920
|
const requirement = { id: row.id, version: row.version };
|
|
@@ -1355,13 +1387,13 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
|
|
|
1355
1387
|
}
|
|
1356
1388
|
|
|
1357
1389
|
// src/software-helper.ts
|
|
1358
|
-
import { chmodSync as chmodSync4, existsSync as
|
|
1390
|
+
import { chmodSync as chmodSync4, existsSync as existsSync6, mkdirSync as mkdirSync6, unlinkSync as unlinkSync3 } from "node:fs";
|
|
1359
1391
|
import { connect as connect2, createServer as createServer2 } from "node:net";
|
|
1360
|
-
import { dirname as
|
|
1392
|
+
import { dirname as dirname5 } from "node:path";
|
|
1361
1393
|
|
|
1362
1394
|
// src/service-supervisor.ts
|
|
1363
1395
|
import { createHash as createHash3 } from "node:crypto";
|
|
1364
|
-
import { existsSync as
|
|
1396
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync, realpathSync, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1365
1397
|
import { dirname as dirname3, join as join4, resolve as resolve3, sep } from "node:path";
|
|
1366
1398
|
var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
|
|
1367
1399
|
var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
|
|
@@ -1378,8 +1410,8 @@ var defaultHost = {
|
|
|
1378
1410
|
renameSync4(next, path2);
|
|
1379
1411
|
},
|
|
1380
1412
|
read: (path2) => readFileSync4(path2, "utf8"),
|
|
1381
|
-
exists:
|
|
1382
|
-
list: (path2) =>
|
|
1413
|
+
exists: existsSync4,
|
|
1414
|
+
list: (path2) => existsSync4(path2) ? readdirSync(path2) : [],
|
|
1383
1415
|
realpath: realpathSync,
|
|
1384
1416
|
mkdir: (path2, mode) => mkdirSync4(path2, { recursive: true, mode }),
|
|
1385
1417
|
remove: (path2) => rmSync4(path2, { force: true }),
|
|
@@ -1576,6 +1608,297 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
1576
1608
|
return state;
|
|
1577
1609
|
}
|
|
1578
1610
|
|
|
1611
|
+
// src/container-supervisor.ts
|
|
1612
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1613
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync5, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1614
|
+
import { dirname as dirname4, resolve as resolve4, sep as sep2 } from "node:path";
|
|
1615
|
+
var defaultHost2 = {
|
|
1616
|
+
realpath: realpathSync2,
|
|
1617
|
+
exists: existsSync5,
|
|
1618
|
+
read: (path2) => readFileSync5(path2, "utf8"),
|
|
1619
|
+
write(path2, content, mode) {
|
|
1620
|
+
mkdirSync5(dirname4(path2), { recursive: true, mode: 493 });
|
|
1621
|
+
const next = `${path2}.next`;
|
|
1622
|
+
writeFileSync5(next, content, { mode });
|
|
1623
|
+
renameSync5(next, path2);
|
|
1624
|
+
},
|
|
1625
|
+
remove: (path2) => rmSync5(path2, { force: true }),
|
|
1626
|
+
list: (path2) => existsSync5(path2) ? readdirSync2(path2) : [],
|
|
1627
|
+
mkdir: (path2, mode) => mkdirSync5(path2, { recursive: true, mode }),
|
|
1628
|
+
async exec(argv2) {
|
|
1629
|
+
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" } });
|
|
1630
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
1631
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
1632
|
+
},
|
|
1633
|
+
async health(port, path2, timeoutMs, method, expectedStatus) {
|
|
1634
|
+
try {
|
|
1635
|
+
const response = await fetch(`http://127.0.0.1:${port}${path2}`, { method, redirect: "manual", signal: AbortSignal.timeout(timeoutMs) });
|
|
1636
|
+
return expectedStatus.includes(response.status);
|
|
1637
|
+
} catch {
|
|
1638
|
+
return false;
|
|
1639
|
+
}
|
|
1640
|
+
},
|
|
1641
|
+
sleep: (ms) => Bun.sleep(ms),
|
|
1642
|
+
now: Date.now
|
|
1643
|
+
};
|
|
1644
|
+
var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
|
|
1645
|
+
var idFor2 = (key) => createHash4("sha256").update(key).digest("hex").slice(0, 24);
|
|
1646
|
+
var statePath2 = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
|
|
1647
|
+
var nameFor = (id, slot) => `forgezero-${id}-slot${slot}`;
|
|
1648
|
+
function validateRequest(request, host) {
|
|
1649
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/.test(request.key) || !/^[a-f0-9]{40}$/.test(request.revision) || !/^[a-z][A-Za-z0-9-]{0,62}$/.test(request.component))
|
|
1650
|
+
throw new Error("CONTAINER_IDENTITY_INVALID");
|
|
1651
|
+
const root = host.realpath(resolve4(request.root));
|
|
1652
|
+
const release = host.realpath(resolve4(request.release));
|
|
1653
|
+
if (!within2(root, release))
|
|
1654
|
+
throw new Error("CONTAINER_RELEASE_OUTSIDE_ROOT");
|
|
1655
|
+
const application = request.application;
|
|
1656
|
+
if (application.kind !== "application" || application.runtime.kind !== "container")
|
|
1657
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1658
|
+
if (!application.resources.cpu || !application.resources.memory || !application.resources.pids)
|
|
1659
|
+
throw new Error("CONTAINER_LIMITS_REQUIRED");
|
|
1660
|
+
if (application.runtime.security.privileged !== false || application.runtime.security.noNewPrivileges !== true || !application.runtime.security.dropCapabilities.includes("ALL"))
|
|
1661
|
+
throw new Error("CONTAINER_SECURITY_INVALID");
|
|
1662
|
+
if (!application.network?.container || !["bridge", "host"].includes(application.network.container.mode))
|
|
1663
|
+
throw new Error("CONTAINER_NETWORK_INVALID");
|
|
1664
|
+
if (!application.network.ingress?.stablePort)
|
|
1665
|
+
throw new Error("CONTAINER_STABLE_PORT_REQUIRED");
|
|
1666
|
+
return { ...request, root, release };
|
|
1667
|
+
}
|
|
1668
|
+
async function checked3(host, argv2, code) {
|
|
1669
|
+
const result = await host.exec(argv2);
|
|
1670
|
+
if (result.exitCode !== 0)
|
|
1671
|
+
throw new Error(`${code}: ${result.output.slice(0, 512)}`);
|
|
1672
|
+
return result.output.trim();
|
|
1673
|
+
}
|
|
1674
|
+
async function buildContainerImage(requestValue, host = defaultHost2) {
|
|
1675
|
+
const request = validateRequest(requestValue, host);
|
|
1676
|
+
const runtime = request.application.runtime;
|
|
1677
|
+
if (runtime.kind !== "container")
|
|
1678
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1679
|
+
const source = runtime.image.source;
|
|
1680
|
+
if (source.kind === "registry") {
|
|
1681
|
+
if (!source.reference.includes("@sha256:"))
|
|
1682
|
+
throw new Error("CONTAINER_IMAGE_DIGEST_REQUIRED");
|
|
1683
|
+
await checked3(host, ["/usr/bin/docker", "pull", "--quiet", source.reference], "CONTAINER_PULL_FAILED");
|
|
1684
|
+
const digest2 = await checked3(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", source.reference], "CONTAINER_INSPECT_FAILED");
|
|
1685
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(digest2))
|
|
1686
|
+
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
1687
|
+
return { image: source.reference, digest: digest2 };
|
|
1688
|
+
}
|
|
1689
|
+
const context = host.realpath(resolve4(request.release, source.context));
|
|
1690
|
+
const dockerfile = host.realpath(resolve4(request.release, source.dockerfile));
|
|
1691
|
+
if (!within2(request.release, context) || !within2(context, dockerfile))
|
|
1692
|
+
throw new Error("CONTAINER_BUILD_PATH_INVALID");
|
|
1693
|
+
const tag = `forgezero/${idFor2(request.key)}:${request.revision}`;
|
|
1694
|
+
await checked3(host, [
|
|
1695
|
+
"/usr/bin/docker",
|
|
1696
|
+
"build",
|
|
1697
|
+
"--pull",
|
|
1698
|
+
...request.noCache ? ["--no-cache"] : [],
|
|
1699
|
+
"--file",
|
|
1700
|
+
dockerfile,
|
|
1701
|
+
"--tag",
|
|
1702
|
+
tag,
|
|
1703
|
+
"--label",
|
|
1704
|
+
`net.forgezero.revision=${request.revision}`,
|
|
1705
|
+
"--label",
|
|
1706
|
+
`net.forgezero.component=${request.component}`,
|
|
1707
|
+
context
|
|
1708
|
+
], "CONTAINER_BUILD_FAILED");
|
|
1709
|
+
const digest = await checked3(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", tag], "CONTAINER_INSPECT_FAILED");
|
|
1710
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(digest))
|
|
1711
|
+
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
1712
|
+
return { image: tag, digest };
|
|
1713
|
+
}
|
|
1714
|
+
function readState2(host, id) {
|
|
1715
|
+
if (!host.exists(statePath2(id)))
|
|
1716
|
+
return null;
|
|
1717
|
+
try {
|
|
1718
|
+
const value = JSON.parse(host.read(statePath2(id)));
|
|
1719
|
+
return value.format === 1 ? value : null;
|
|
1720
|
+
} catch {
|
|
1721
|
+
return null;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
function usedPorts(host, exceptId) {
|
|
1725
|
+
const ports = new Set;
|
|
1726
|
+
for (const name of host.list(SERVICE_STATE_DIRECTORY).filter((entry) => /^[a-f0-9]{24}\.json$/.test(entry))) {
|
|
1727
|
+
try {
|
|
1728
|
+
const state = JSON.parse(host.read(`${SERVICE_STATE_DIRECTORY}/${name}`));
|
|
1729
|
+
if (state.id !== exceptId) {
|
|
1730
|
+
ports.add(state.publicPort);
|
|
1731
|
+
state.applicationPorts.forEach((port) => ports.add(port));
|
|
1732
|
+
}
|
|
1733
|
+
} catch {}
|
|
1734
|
+
}
|
|
1735
|
+
return ports;
|
|
1736
|
+
}
|
|
1737
|
+
function allocatePorts(host, id, count, stablePort, previous) {
|
|
1738
|
+
const used = usedPorts(host, id);
|
|
1739
|
+
if (used.has(stablePort))
|
|
1740
|
+
throw new Error("CONTAINER_PUBLIC_PORT_CONFLICT");
|
|
1741
|
+
if (previous?.applicationPorts.length === count && previous.applicationPorts.every((port) => !used.has(port)))
|
|
1742
|
+
return [...previous.applicationPorts];
|
|
1743
|
+
const from = 20000;
|
|
1744
|
+
const to = 49999;
|
|
1745
|
+
const width = to - from + 1;
|
|
1746
|
+
const start = Number.parseInt(id.slice(0, 8), 16) % width;
|
|
1747
|
+
for (let offset = 0;offset < width; offset += 1) {
|
|
1748
|
+
const first = from + (start + offset) % width;
|
|
1749
|
+
const values = Array.from({ length: count }, (_, index) => first + index);
|
|
1750
|
+
if (values.at(-1) <= to && values.every((port) => port !== stablePort && !used.has(port)))
|
|
1751
|
+
return values;
|
|
1752
|
+
}
|
|
1753
|
+
throw new Error("CONTAINER_PORTS_EXHAUSTED");
|
|
1754
|
+
}
|
|
1755
|
+
function nginx2(request, id, port) {
|
|
1756
|
+
const service = request.application.service;
|
|
1757
|
+
const stablePort = request.application.network.ingress.stablePort;
|
|
1758
|
+
return `# ForgeZero container ${id}
|
|
1759
|
+
${service.maximumConnections ? `limit_conn_zone $server_name zone=fz_${id}:64k;
|
|
1760
|
+
` : ""}server {
|
|
1761
|
+
listen 127.0.0.1:${stablePort};
|
|
1762
|
+
server_name _;
|
|
1763
|
+
${service.maximumConnections ? ` limit_conn fz_${id} ${service.maximumConnections};
|
|
1764
|
+
limit_conn_status 503;
|
|
1765
|
+
` : ""} location / {
|
|
1766
|
+
proxy_pass http://127.0.0.1:${port};
|
|
1767
|
+
proxy_http_version 1.1;
|
|
1768
|
+
${service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
|
|
1769
|
+
proxy_set_header Connection "upgrade";
|
|
1770
|
+
` : ""} proxy_set_header Host $host;
|
|
1771
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
1772
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
`;
|
|
1776
|
+
}
|
|
1777
|
+
function runArgv(request, id, slot, port, networkName) {
|
|
1778
|
+
const { application } = request;
|
|
1779
|
+
const resources = application.resources;
|
|
1780
|
+
const runtime = application.runtime;
|
|
1781
|
+
if (runtime.kind !== "container")
|
|
1782
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1783
|
+
const memory = resources.memory;
|
|
1784
|
+
const network = application.network.container;
|
|
1785
|
+
const argv2 = [
|
|
1786
|
+
"/usr/bin/docker",
|
|
1787
|
+
"run",
|
|
1788
|
+
"--detach",
|
|
1789
|
+
"--name",
|
|
1790
|
+
nameFor(id, slot),
|
|
1791
|
+
"--restart",
|
|
1792
|
+
"unless-stopped",
|
|
1793
|
+
"--label",
|
|
1794
|
+
`net.forgezero.key=${request.key}`,
|
|
1795
|
+
"--label",
|
|
1796
|
+
`net.forgezero.revision=${request.revision}`,
|
|
1797
|
+
"--cpus",
|
|
1798
|
+
String(resources.cpu.limit),
|
|
1799
|
+
"--cpu-shares",
|
|
1800
|
+
String(resources.cpu.weight ?? 100),
|
|
1801
|
+
"--memory",
|
|
1802
|
+
`${memory.limitMiB}m`,
|
|
1803
|
+
...memory.reservationMiB ? ["--memory-reservation", `${memory.reservationMiB}m`] : [],
|
|
1804
|
+
...memory.swap === "disabled" ? ["--memory-swap", `${memory.limitMiB}m`] : [],
|
|
1805
|
+
"--pids-limit",
|
|
1806
|
+
String(resources.pids.limit),
|
|
1807
|
+
"--security-opt",
|
|
1808
|
+
"no-new-privileges=true",
|
|
1809
|
+
"--cap-drop",
|
|
1810
|
+
"ALL",
|
|
1811
|
+
...runtime.security.root === "read-only" ? ["--read-only"] : [],
|
|
1812
|
+
...network.mode === "host" ? ["--network", "host", "--env", `FZ_APP_PORT=${port}`] : ["--network", networkName, "--publish", `127.0.0.1:${port}:${application.service.port}`]
|
|
1813
|
+
];
|
|
1814
|
+
for (const mount of application.storage ?? []) {
|
|
1815
|
+
if (mount.class === "ephemeral")
|
|
1816
|
+
argv2.push("--tmpfs", `${mount.path}:rw,noexec,nosuid,size=${mount.sizeMiB}m`);
|
|
1817
|
+
else
|
|
1818
|
+
argv2.push("--volume", `forgezero-${id}-${mount.name}:${mount.path}`);
|
|
1819
|
+
}
|
|
1820
|
+
argv2.push(request.image, ...runtime.entrypoint ?? []);
|
|
1821
|
+
return argv2;
|
|
1822
|
+
}
|
|
1823
|
+
async function activateContainer(requestValue, host = defaultHost2) {
|
|
1824
|
+
const request = validateRequest(requestValue, host);
|
|
1825
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(request.image) && !/^forgezero\/[a-f0-9]{24}:[a-f0-9]{40}$/.test(request.image) && !request.image.includes("@sha256:"))
|
|
1826
|
+
throw new Error("CONTAINER_IMAGE_INVALID");
|
|
1827
|
+
const id = idFor2(request.key);
|
|
1828
|
+
host.mkdir(SERVICE_STATE_DIRECTORY, 448);
|
|
1829
|
+
host.mkdir(SERVICE_NGINX_DIRECTORY, 493);
|
|
1830
|
+
const previous = readState2(host, id);
|
|
1831
|
+
const slots = request.application.rollout.strategy === "blue-green" ? 2 : 1;
|
|
1832
|
+
if (!["direct", "blue-green"].includes(request.application.rollout.strategy))
|
|
1833
|
+
throw new Error("CONTAINER_ROLLOUT_REQUIRES_CONTROL_PLANE");
|
|
1834
|
+
const containerNetwork = request.application.network.container;
|
|
1835
|
+
let networkName = "host";
|
|
1836
|
+
if (containerNetwork.mode === "bridge") {
|
|
1837
|
+
networkName = containerNetwork.network ? `forgezero-${id}-${containerNetwork.network}` : "bridge";
|
|
1838
|
+
if (networkName !== "bridge") {
|
|
1839
|
+
const inspected = await host.exec(["/usr/bin/docker", "network", "inspect", '--format={{index .Labels "net.forgezero.owner"}}', networkName]);
|
|
1840
|
+
if (inspected.exitCode !== 0) {
|
|
1841
|
+
await checked3(host, ["/usr/bin/docker", "network", "create", "--driver", "bridge", "--label", `net.forgezero.owner=${id}`, networkName], "CONTAINER_NETWORK_CREATE_FAILED");
|
|
1842
|
+
} else if (inspected.output.trim() !== id)
|
|
1843
|
+
throw new Error("CONTAINER_NETWORK_OWNERSHIP_INVALID");
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
const ports = allocatePorts(host, id, slots, request.application.network.ingress.stablePort, previous);
|
|
1847
|
+
const nextSlot = slots === 2 && previous?.activeSlot === 0 ? 1 : 0;
|
|
1848
|
+
const port = ports[nextSlot];
|
|
1849
|
+
const name = nameFor(id, nextSlot);
|
|
1850
|
+
await host.exec(["/usr/bin/docker", "rm", "--force", name]);
|
|
1851
|
+
await checked3(host, runArgv(request, id, nextSlot, port, networkName), "CONTAINER_START_FAILED");
|
|
1852
|
+
let healthy = false;
|
|
1853
|
+
const health = request.application.service.health;
|
|
1854
|
+
for (let attempt = 0;attempt < (health.attempts ?? 30); attempt += 1) {
|
|
1855
|
+
if (await host.health(port, health.path, health.timeoutMs, health.method, health.expectedStatus)) {
|
|
1856
|
+
healthy = true;
|
|
1857
|
+
break;
|
|
1858
|
+
}
|
|
1859
|
+
await host.sleep(health.intervalMs ?? 1000);
|
|
1860
|
+
}
|
|
1861
|
+
if (!healthy) {
|
|
1862
|
+
await host.exec(["/usr/bin/docker", "rm", "--force", name]);
|
|
1863
|
+
throw new Error("CONTAINER_HEALTH_FAILED");
|
|
1864
|
+
}
|
|
1865
|
+
const nginxPath = `${SERVICE_NGINX_DIRECTORY}/forgezero-${id}.conf`;
|
|
1866
|
+
const previousNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
|
|
1867
|
+
host.write(nginxPath, nginx2(request, id, port), 420);
|
|
1868
|
+
try {
|
|
1869
|
+
await checked3(host, ["/usr/sbin/nginx", "-t"], "CONTAINER_NGINX_INVALID");
|
|
1870
|
+
await checked3(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "CONTAINER_NGINX_RELOAD_FAILED");
|
|
1871
|
+
} catch (cause) {
|
|
1872
|
+
if (previousNginx === null)
|
|
1873
|
+
host.remove(nginxPath);
|
|
1874
|
+
else
|
|
1875
|
+
host.write(nginxPath, previousNginx, 420);
|
|
1876
|
+
await host.exec(["/usr/sbin/nginx", "-t"]);
|
|
1877
|
+
await host.exec(["/usr/bin/systemctl", "reload", "nginx.service"]);
|
|
1878
|
+
await host.exec(["/usr/bin/docker", "rm", "--force", name]);
|
|
1879
|
+
throw cause;
|
|
1880
|
+
}
|
|
1881
|
+
const state = {
|
|
1882
|
+
format: 1,
|
|
1883
|
+
id,
|
|
1884
|
+
key: request.key,
|
|
1885
|
+
revision: request.revision,
|
|
1886
|
+
strategy: request.application.rollout.strategy,
|
|
1887
|
+
publicPort: request.application.network.ingress.stablePort,
|
|
1888
|
+
applicationPorts: ports,
|
|
1889
|
+
activeSlot: nextSlot,
|
|
1890
|
+
healthPath: health.path,
|
|
1891
|
+
updatedAtTs: host.now()
|
|
1892
|
+
};
|
|
1893
|
+
host.write(statePath2(id), `${JSON.stringify(state, null, 2)}
|
|
1894
|
+
`, 384);
|
|
1895
|
+
if (previous && previous.activeSlot !== nextSlot) {
|
|
1896
|
+
await host.sleep(request.application.rollout.drainMs ?? 30000);
|
|
1897
|
+
await host.exec(["/usr/bin/docker", "rm", "--force", nameFor(id, previous.activeSlot)]);
|
|
1898
|
+
}
|
|
1899
|
+
return state;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1579
1902
|
// src/software-helper.ts
|
|
1580
1903
|
var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
|
|
1581
1904
|
var SOFTWARE_HELPER_GROUP = "forgezero-software";
|
|
@@ -1584,11 +1907,13 @@ var MAX_REQUEST_BYTES2 = 128 * 1024;
|
|
|
1584
1907
|
var MAX_PENDING_REQUESTS = 128;
|
|
1585
1908
|
function startSoftwareHelper(options = {}) {
|
|
1586
1909
|
const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
|
|
1587
|
-
if (
|
|
1910
|
+
if (existsSync6(socketPath))
|
|
1588
1911
|
unlinkSync3(socketPath);
|
|
1589
|
-
|
|
1912
|
+
mkdirSync6(dirname5(socketPath), { recursive: true, mode: 488 });
|
|
1590
1913
|
const ensure = options.ensure ?? ensureSoftwareRequirements;
|
|
1591
1914
|
const activate = options.activate ?? activateSupervisedService;
|
|
1915
|
+
const buildContainer = options.buildContainer ?? buildContainerImage;
|
|
1916
|
+
const activateTypedContainer = options.activateContainer ?? activateContainer;
|
|
1592
1917
|
let tail = Promise.resolve();
|
|
1593
1918
|
let pending = 0;
|
|
1594
1919
|
const server = createServer2((socket) => {
|
|
@@ -1621,10 +1946,29 @@ function startSoftwareHelper(options = {}) {
|
|
|
1621
1946
|
return;
|
|
1622
1947
|
}
|
|
1623
1948
|
if (request.op === "activate-service" && request.request) {
|
|
1624
|
-
|
|
1949
|
+
const serviceRequest = request.request;
|
|
1950
|
+
if (!options.allowedRoot || serviceRequest.root !== options.allowedRoot) {
|
|
1625
1951
|
throw new Error("service deployment root is not owned by this helper");
|
|
1626
1952
|
}
|
|
1627
|
-
const state = await activate(
|
|
1953
|
+
const state = await activate(serviceRequest);
|
|
1954
|
+
socket.end(`${JSON.stringify({ ok: true, state })}
|
|
1955
|
+
`);
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
if (request.op === "build-container" && request.request) {
|
|
1959
|
+
const containerRequest = request.request;
|
|
1960
|
+
if (!options.allowedRoot || containerRequest.root !== options.allowedRoot)
|
|
1961
|
+
throw new Error("container deployment root is not owned by this helper");
|
|
1962
|
+
const result = await buildContainer(containerRequest);
|
|
1963
|
+
socket.end(`${JSON.stringify({ ok: true, result })}
|
|
1964
|
+
`);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
if (request.op === "activate-container" && request.request) {
|
|
1968
|
+
const containerRequest = request.request;
|
|
1969
|
+
if (!options.allowedRoot || containerRequest.root !== options.allowedRoot)
|
|
1970
|
+
throw new Error("container deployment root is not owned by this helper");
|
|
1971
|
+
const state = await activateTypedContainer(containerRequest);
|
|
1628
1972
|
socket.end(`${JSON.stringify({ ok: true, state })}
|
|
1629
1973
|
`);
|
|
1630
1974
|
return;
|
|
@@ -1648,8 +1992,43 @@ function startSoftwareHelper(options = {}) {
|
|
|
1648
1992
|
server.listen(socketPath, () => chmodSync4(socketPath, 432));
|
|
1649
1993
|
return server;
|
|
1650
1994
|
}
|
|
1995
|
+
function requestContainer(op, request, socketPath, timeoutMs) {
|
|
1996
|
+
return new Promise((resolve5, reject) => {
|
|
1997
|
+
const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
|
|
1998
|
+
`));
|
|
1999
|
+
let buffer = "";
|
|
2000
|
+
socket.setTimeout(timeoutMs, () => {
|
|
2001
|
+
socket.destroy();
|
|
2002
|
+
reject(new Error("container helper timed out"));
|
|
2003
|
+
});
|
|
2004
|
+
socket.on("data", (chunk) => {
|
|
2005
|
+
buffer += chunk.toString("utf8");
|
|
2006
|
+
const newline = buffer.indexOf(`
|
|
2007
|
+
`);
|
|
2008
|
+
if (newline < 0)
|
|
2009
|
+
return;
|
|
2010
|
+
socket.end();
|
|
2011
|
+
try {
|
|
2012
|
+
const response = JSON.parse(buffer.slice(0, newline));
|
|
2013
|
+
const result = response.result ?? response.state;
|
|
2014
|
+
if (!response.ok || !result)
|
|
2015
|
+
throw new Error(response.error?.message ?? "container helper refused request");
|
|
2016
|
+
resolve5(result);
|
|
2017
|
+
} catch (cause) {
|
|
2018
|
+
reject(cause);
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
socket.on("error", reject);
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
function requestContainerBuild(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 30 * 60000) {
|
|
2025
|
+
return requestContainer("build-container", request, socketPath, timeoutMs);
|
|
2026
|
+
}
|
|
2027
|
+
function requestContainerActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
2028
|
+
return requestContainer("activate-container", request, socketPath, timeoutMs);
|
|
2029
|
+
}
|
|
1651
2030
|
function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
|
|
1652
|
-
return new Promise((
|
|
2031
|
+
return new Promise((resolve5, reject) => {
|
|
1653
2032
|
const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
|
|
1654
2033
|
`));
|
|
1655
2034
|
let buffer = "";
|
|
@@ -1668,7 +2047,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
|
|
|
1668
2047
|
const response = JSON.parse(buffer.slice(0, newline));
|
|
1669
2048
|
if (!response.ok || !response.state)
|
|
1670
2049
|
throw new Error(response.error?.message ?? "service helper refused activation");
|
|
1671
|
-
|
|
2050
|
+
resolve5(response.state);
|
|
1672
2051
|
} catch (cause) {
|
|
1673
2052
|
reject(cause);
|
|
1674
2053
|
}
|
|
@@ -1678,7 +2057,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
|
|
|
1678
2057
|
}
|
|
1679
2058
|
function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
1680
2059
|
validateSoftwareRequirements(requirements);
|
|
1681
|
-
return new Promise((
|
|
2060
|
+
return new Promise((resolve5, reject) => {
|
|
1682
2061
|
const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
|
|
1683
2062
|
`));
|
|
1684
2063
|
let buffer = "";
|
|
@@ -1697,7 +2076,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
1697
2076
|
const response = JSON.parse(buffer.slice(0, newline));
|
|
1698
2077
|
if (!response.ok || !response.results)
|
|
1699
2078
|
throw new Error(response.error?.message ?? "software helper refused the request");
|
|
1700
|
-
|
|
2079
|
+
resolve5(response.results);
|
|
1701
2080
|
} catch (cause) {
|
|
1702
2081
|
reject(cause);
|
|
1703
2082
|
}
|
|
@@ -1707,10 +2086,10 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
1707
2086
|
}
|
|
1708
2087
|
|
|
1709
2088
|
// src/version.ts
|
|
1710
|
-
var VERSION3 = "0.1.
|
|
2089
|
+
var VERSION3 = "0.1.55";
|
|
1711
2090
|
|
|
1712
2091
|
// src/egress-policy.ts
|
|
1713
|
-
import { realpathSync as
|
|
2092
|
+
import { realpathSync as realpathSync3 } from "node:fs";
|
|
1714
2093
|
var SYSTEMD_RESOLVED_ADDRESS = "127.0.0.53";
|
|
1715
2094
|
var BLOCKED_IPV4 = [
|
|
1716
2095
|
"0.0.0.0/8",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Server } from 'node:net';
|
|
2
2
|
import { ensureSoftwareRequirements, type SoftwareRequirement } from './software';
|
|
3
3
|
import { activateSupervisedService, type ServiceActivationRequest, type ServiceActivationState } from './service-supervisor';
|
|
4
|
+
import { activateContainer, buildContainerImage, type ContainerActivateRequest, type ContainerBuildRequest, type ContainerBuildResult } from './container-supervisor';
|
|
4
5
|
export declare const DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
|
|
5
6
|
export declare const SOFTWARE_HELPER_GROUP = "forgezero-software";
|
|
6
7
|
export declare const SOFTWARE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-software-helper.service";
|
|
@@ -8,8 +9,12 @@ export declare function startSoftwareHelper(options?: {
|
|
|
8
9
|
socketPath?: string;
|
|
9
10
|
ensure?: typeof ensureSoftwareRequirements;
|
|
10
11
|
activate?: typeof activateSupervisedService;
|
|
12
|
+
buildContainer?: typeof buildContainerImage;
|
|
13
|
+
activateContainer?: typeof activateContainer;
|
|
11
14
|
allowedRoot?: string;
|
|
12
15
|
}): Server;
|
|
16
|
+
export declare function requestContainerBuild(request: ContainerBuildRequest, socketPath?: string, timeoutMs?: number): Promise<ContainerBuildResult>;
|
|
17
|
+
export declare function requestContainerActivation(request: ContainerActivateRequest, socketPath?: string, timeoutMs?: number): Promise<ServiceActivationState>;
|
|
13
18
|
export declare function requestServiceActivation(request: ServiceActivationRequest, socketPath?: string, timeoutMs?: number): Promise<ServiceActivationState>;
|
|
14
19
|
export declare function requestSoftware(requirements: readonly SoftwareRequirement[], socketPath?: string, timeoutMs?: number): Promise<Array<{
|
|
15
20
|
id: string;
|