@forgezero/agent 0.1.53 → 0.1.56
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.d.ts +7 -1
- package/dist/community-rehearsal-host.js +64 -6
- package/dist/container-supervisor.d.ts +36 -0
- package/dist/definition.js +37 -5
- 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 +44 -12
- 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 +2137 -326
- package/dist/fz.js +1492 -320
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.d.ts +1 -1
- package/dist/operator-bootstrap.js +212 -130
- package/dist/platform-bootstrap-runtime.js +1 -1
- package/dist/platform-fleet-verification.js +499 -120
- package/dist/platform-genesis.d.ts +9 -0
- package/dist/provision.js +399 -20
- package/dist/software-helper.d.ts +5 -0
- package/dist/software-helper.js +399 -18
- package/dist/software.d.ts +1 -1
- package/dist/software.js +37 -5
- package/dist/version.d.ts +1 -1
- package/package.json +17 -1
|
@@ -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,6 +773,15 @@ 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;
|
|
@@ -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.56";
|
|
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",
|
|
@@ -2686,17 +3065,17 @@ import {
|
|
|
2686
3065
|
chmodSync as chmodSync5,
|
|
2687
3066
|
chownSync,
|
|
2688
3067
|
copyFileSync as copyFileSync2,
|
|
2689
|
-
existsSync as
|
|
3068
|
+
existsSync as existsSync7,
|
|
2690
3069
|
lstatSync,
|
|
2691
|
-
mkdirSync as
|
|
2692
|
-
readFileSync as
|
|
2693
|
-
readdirSync as
|
|
2694
|
-
realpathSync as
|
|
2695
|
-
renameSync as
|
|
2696
|
-
rmSync as
|
|
3070
|
+
mkdirSync as mkdirSync7,
|
|
3071
|
+
readFileSync as readFileSync6,
|
|
3072
|
+
readdirSync as readdirSync3,
|
|
3073
|
+
realpathSync as realpathSync4,
|
|
3074
|
+
renameSync as renameSync6,
|
|
3075
|
+
rmSync as rmSync6,
|
|
2697
3076
|
statSync,
|
|
2698
3077
|
symlinkSync as symlinkSync3,
|
|
2699
|
-
writeFileSync as
|
|
3078
|
+
writeFileSync as writeFileSync6
|
|
2700
3079
|
} from "node:fs";
|
|
2701
3080
|
import { join as join5 } from "node:path";
|
|
2702
3081
|
var safeAtom = (name, value) => {
|
|
@@ -3058,13 +3437,13 @@ var platformExec = async (argv2) => {
|
|
|
3058
3437
|
};
|
|
3059
3438
|
var replaceLink = (path2, target) => {
|
|
3060
3439
|
const pending = `${path2}.next`;
|
|
3061
|
-
|
|
3440
|
+
rmSync6(pending, { force: true });
|
|
3062
3441
|
if (!target) {
|
|
3063
|
-
|
|
3442
|
+
rmSync6(path2, { force: true });
|
|
3064
3443
|
return;
|
|
3065
3444
|
}
|
|
3066
3445
|
symlinkSync3(target, pending);
|
|
3067
|
-
|
|
3446
|
+
renameSync6(pending, path2);
|
|
3068
3447
|
};
|
|
3069
3448
|
var secureRelease = (path2, uid, gid) => {
|
|
3070
3449
|
const visit = (current) => {
|
|
@@ -3074,7 +3453,7 @@ var secureRelease = (path2, uid, gid) => {
|
|
|
3074
3453
|
chownSync(current, uid, gid);
|
|
3075
3454
|
chmodSync5(current, metadata.isDirectory() ? 365 : 292);
|
|
3076
3455
|
if (metadata.isDirectory())
|
|
3077
|
-
for (const name of
|
|
3456
|
+
for (const name of readdirSync3(current))
|
|
3078
3457
|
visit(join5(current, name));
|
|
3079
3458
|
};
|
|
3080
3459
|
visit(path2);
|
|
@@ -3082,11 +3461,11 @@ var secureRelease = (path2, uid, gid) => {
|
|
|
3082
3461
|
async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
3083
3462
|
const rendered = renderPlatformActivationFiles(config);
|
|
3084
3463
|
const normalized = JSON.parse(rendered.helper);
|
|
3085
|
-
const releases =
|
|
3086
|
-
const release =
|
|
3464
|
+
const releases = realpathSync4(join5(normalized.root, "releases"));
|
|
3465
|
+
const release = realpathSync4(requestedRelease);
|
|
3087
3466
|
if (!release.startsWith(`${releases}/`) || release === releases)
|
|
3088
3467
|
throw new Error("release is outside configured releases directory");
|
|
3089
|
-
for (const required of [".fz/deploy.json", "src/index.ts", "bun.lock"]) {
|
|
3468
|
+
for (const required of ["forgezero.deploy.ts", ".fz/deploy.plan.json", "src/index.ts", "bun.lock"]) {
|
|
3090
3469
|
const metadata = statSync(join5(release, required));
|
|
3091
3470
|
if (!metadata.isFile() || metadata.size < 1)
|
|
3092
3471
|
throw new Error(`release is incomplete: ${required}`);
|
|
@@ -3095,15 +3474,15 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
3095
3474
|
const request = options.fetch ?? fetch;
|
|
3096
3475
|
const sleep = options.sleep ?? Bun.sleep;
|
|
3097
3476
|
const slots = join5(normalized.root, "slots");
|
|
3098
|
-
|
|
3477
|
+
mkdirSync7(slots, { recursive: true, mode: 493 });
|
|
3099
3478
|
const slotFile = join5(normalized.root, ".forge-slot");
|
|
3100
|
-
const previousSlot =
|
|
3479
|
+
const previousSlot = existsSync7(slotFile) ? readFileSync6(slotFile, "utf8").trim() : undefined;
|
|
3101
3480
|
const target = previousSlot === "blue" ? "green" : "blue";
|
|
3102
3481
|
const port = target === "blue" ? normalized.bluePort : normalized.greenPort;
|
|
3103
3482
|
const targetLink = join5(slots, target);
|
|
3104
3483
|
let previousTarget;
|
|
3105
3484
|
try {
|
|
3106
|
-
previousTarget =
|
|
3485
|
+
previousTarget = realpathSync4(targetLink);
|
|
3107
3486
|
} catch {}
|
|
3108
3487
|
const user = await exec(["/usr/bin/id", "-u", normalized.serviceUser]);
|
|
3109
3488
|
const group = await exec(["/usr/bin/id", "-g", normalized.serviceUser]);
|
|
@@ -3137,35 +3516,35 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
3137
3516
|
}
|
|
3138
3517
|
const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
|
|
3139
3518
|
const backup = `${upstream}.forgezero-backup`;
|
|
3140
|
-
if (
|
|
3519
|
+
if (existsSync7(upstream))
|
|
3141
3520
|
copyFileSync2(upstream, backup);
|
|
3142
3521
|
else
|
|
3143
|
-
|
|
3144
|
-
|
|
3522
|
+
rmSync6(backup, { force: true });
|
|
3523
|
+
writeFileSync6(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
|
|
3145
3524
|
`, { mode: 420 });
|
|
3146
3525
|
const test = await exec(["/usr/sbin/nginx", "-t"]);
|
|
3147
3526
|
const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
|
|
3148
3527
|
if (reload.exitCode !== 0) {
|
|
3149
|
-
if (
|
|
3150
|
-
|
|
3528
|
+
if (existsSync7(backup))
|
|
3529
|
+
renameSync6(backup, upstream);
|
|
3151
3530
|
else
|
|
3152
|
-
|
|
3531
|
+
rmSync6(upstream, { force: true });
|
|
3153
3532
|
await exec(["/usr/sbin/nginx", "-t"]);
|
|
3154
3533
|
await exec(["/usr/sbin/nginx", "-s", "reload"]);
|
|
3155
3534
|
await stopTarget();
|
|
3156
3535
|
throw new Error("nginx refused the promoted upstream");
|
|
3157
3536
|
}
|
|
3158
|
-
|
|
3159
|
-
|
|
3537
|
+
rmSync6(backup, { force: true });
|
|
3538
|
+
writeFileSync6(slotFile, `${target}
|
|
3160
3539
|
`, { mode: 420 });
|
|
3161
3540
|
if (previousSlot && previousSlot !== target) {
|
|
3162
3541
|
await sleep(normalized.drainDeadlineMs);
|
|
3163
3542
|
await exec(["/usr/bin/systemctl", "stop", `forgezero@${previousSlot}.service`]);
|
|
3164
3543
|
}
|
|
3165
|
-
const old =
|
|
3544
|
+
const old = readdirSync3(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join5(releases, entry.name)).sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs).slice(normalized.keepReleases);
|
|
3166
3545
|
for (const path2 of old)
|
|
3167
3546
|
if (path2 !== release)
|
|
3168
|
-
|
|
3547
|
+
rmSync6(path2, { recursive: true, force: true });
|
|
3169
3548
|
return { release, slot: target };
|
|
3170
3549
|
}
|
|
3171
3550
|
function validateCollectorUnit(unit2) {
|
|
@@ -3209,18 +3588,18 @@ function planLocalOtlpProof(endpoint, collectorUnit) {
|
|
|
3209
3588
|
import { lstatSync as lstatSync4 } from "node:fs";
|
|
3210
3589
|
|
|
3211
3590
|
// src/bootstrap.ts
|
|
3212
|
-
import { createHash as
|
|
3591
|
+
import { createHash as createHash5, createHmac, randomBytes as randomBytes3 } from "node:crypto";
|
|
3213
3592
|
import {
|
|
3214
3593
|
chmodSync as chmodSync7,
|
|
3215
|
-
existsSync as
|
|
3594
|
+
existsSync as existsSync9,
|
|
3216
3595
|
lstatSync as lstatSync3,
|
|
3217
|
-
mkdirSync as
|
|
3218
|
-
readFileSync as
|
|
3219
|
-
renameSync as
|
|
3220
|
-
rmSync as
|
|
3221
|
-
writeFileSync as
|
|
3596
|
+
mkdirSync as mkdirSync9,
|
|
3597
|
+
readFileSync as readFileSync8,
|
|
3598
|
+
renameSync as renameSync8,
|
|
3599
|
+
rmSync as rmSync8,
|
|
3600
|
+
writeFileSync as writeFileSync8
|
|
3222
3601
|
} from "node:fs";
|
|
3223
|
-
import { dirname as
|
|
3602
|
+
import { dirname as dirname8 } from "node:path";
|
|
3224
3603
|
import { fileURLToPath } from "node:url";
|
|
3225
3604
|
|
|
3226
3605
|
// src/cli/agent-install.ts
|
|
@@ -3228,17 +3607,17 @@ import { randomBytes } from "node:crypto";
|
|
|
3228
3607
|
import {
|
|
3229
3608
|
chmodSync as chmodSync6,
|
|
3230
3609
|
copyFileSync as copyFileSync3,
|
|
3231
|
-
existsSync as
|
|
3610
|
+
existsSync as existsSync8,
|
|
3232
3611
|
lstatSync as lstatSync2,
|
|
3233
|
-
mkdirSync as
|
|
3234
|
-
readFileSync as
|
|
3235
|
-
realpathSync as
|
|
3236
|
-
renameSync as
|
|
3237
|
-
rmSync as
|
|
3612
|
+
mkdirSync as mkdirSync8,
|
|
3613
|
+
readFileSync as readFileSync7,
|
|
3614
|
+
realpathSync as realpathSync5,
|
|
3615
|
+
renameSync as renameSync7,
|
|
3616
|
+
rmSync as rmSync7,
|
|
3238
3617
|
symlinkSync as symlinkSync4,
|
|
3239
|
-
writeFileSync as
|
|
3618
|
+
writeFileSync as writeFileSync7
|
|
3240
3619
|
} from "node:fs";
|
|
3241
|
-
import { dirname as
|
|
3620
|
+
import { dirname as dirname6 } from "node:path";
|
|
3242
3621
|
async function readCapabilities(run2) {
|
|
3243
3622
|
const answers = {};
|
|
3244
3623
|
const checks = Object.entries(CAPABILITY_CHECKS);
|
|
@@ -3297,28 +3676,28 @@ var runProvisionOperation = async (operation) => {
|
|
|
3297
3676
|
}
|
|
3298
3677
|
if (operation.kind === "install-runtime") {
|
|
3299
3678
|
const release = `/opt/forgezero/agent/versions/${operation.version}`;
|
|
3300
|
-
|
|
3301
|
-
|
|
3679
|
+
mkdirSync8(`${release}/dist`, { recursive: true, mode: 493 });
|
|
3680
|
+
mkdirSync8(dirname6(operation.binary), { recursive: true, mode: 493 });
|
|
3302
3681
|
copyFileSync3(operation.source, `${release}/dist/fz-agent.js`);
|
|
3303
3682
|
chmodSync6(`${release}/dist/fz-agent.js`, 493);
|
|
3304
|
-
const gitSshSource = `${
|
|
3305
|
-
if (!
|
|
3683
|
+
const gitSshSource = `${dirname6(operation.source)}/fz-git-ssh.js`;
|
|
3684
|
+
if (!existsSync8(gitSshSource))
|
|
3306
3685
|
return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
|
|
3307
3686
|
copyFileSync3(gitSshSource, `${release}/dist/fz-git-ssh.js`);
|
|
3308
3687
|
chmodSync6(`${release}/dist/fz-git-ssh.js`, 493);
|
|
3309
3688
|
const pending = "/opt/forgezero/agent/current.next";
|
|
3310
|
-
|
|
3689
|
+
rmSync7(pending, { force: true });
|
|
3311
3690
|
symlinkSync4(`versions/${operation.version}`, pending);
|
|
3312
|
-
|
|
3313
|
-
|
|
3691
|
+
renameSync7(pending, "/opt/forgezero/agent/current");
|
|
3692
|
+
rmSync7(operation.binary, { force: true });
|
|
3314
3693
|
symlinkSync4("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
|
|
3315
3694
|
const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
|
|
3316
|
-
|
|
3695
|
+
rmSync7(gitSshBinary, { force: true });
|
|
3317
3696
|
symlinkSync4("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
|
|
3318
3697
|
return { stdout: "", exitCode: 0 };
|
|
3319
3698
|
}
|
|
3320
3699
|
if (operation.kind === "ensure-seed") {
|
|
3321
|
-
if (
|
|
3700
|
+
if (existsSync8(operation.credential) && lstatSync2(operation.credential).size > 0)
|
|
3322
3701
|
return { stdout: "", exitCode: 0 };
|
|
3323
3702
|
const seed = randomBytes(32).toString("base64url");
|
|
3324
3703
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
|
|
@@ -3330,9 +3709,9 @@ var runProvisionOperation = async (operation) => {
|
|
|
3330
3709
|
const key = "/run/forgezero-git-deploy-key";
|
|
3331
3710
|
const publicKey = `${key}.pub`;
|
|
3332
3711
|
try {
|
|
3333
|
-
if (!
|
|
3334
|
-
|
|
3335
|
-
|
|
3712
|
+
if (!existsSync8(operation.credential) || lstatSync2(operation.credential).size < 1) {
|
|
3713
|
+
rmSync7(key, { force: true });
|
|
3714
|
+
rmSync7(publicKey, { force: true });
|
|
3336
3715
|
let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
|
|
3337
3716
|
if (result.exitCode !== 0)
|
|
3338
3717
|
return result;
|
|
@@ -3341,8 +3720,8 @@ var runProvisionOperation = async (operation) => {
|
|
|
3341
3720
|
return result;
|
|
3342
3721
|
chmodSync6(operation.credential, 256);
|
|
3343
3722
|
}
|
|
3344
|
-
if (!
|
|
3345
|
-
if (!
|
|
3723
|
+
if (!existsSync8(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
|
|
3724
|
+
if (!existsSync8(key)) {
|
|
3346
3725
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
|
|
3347
3726
|
if (decrypted.exitCode !== 0)
|
|
3348
3727
|
return decrypted;
|
|
@@ -3350,25 +3729,25 @@ var runProvisionOperation = async (operation) => {
|
|
|
3350
3729
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
3351
3730
|
if (derived.exitCode !== 0)
|
|
3352
3731
|
return derived;
|
|
3353
|
-
|
|
3732
|
+
writeFileSync7(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
|
|
3354
3733
|
`, { mode: 292 });
|
|
3355
3734
|
}
|
|
3356
3735
|
return { stdout: "", exitCode: 0 };
|
|
3357
3736
|
} finally {
|
|
3358
|
-
|
|
3359
|
-
|
|
3737
|
+
rmSync7(key, { force: true });
|
|
3738
|
+
rmSync7(publicKey, { force: true });
|
|
3360
3739
|
}
|
|
3361
3740
|
}
|
|
3362
3741
|
if (operation.kind === "ensure-bootstrap-ssh-identity") {
|
|
3363
3742
|
const key = "/run/forgezero-bootstrap-ssh-key";
|
|
3364
3743
|
const generatedPublicKey = `${key}.pub`;
|
|
3365
3744
|
try {
|
|
3366
|
-
if (!
|
|
3367
|
-
|
|
3368
|
-
|
|
3745
|
+
if (!existsSync8(operation.credential) || lstatSync2(operation.credential).size < 1) {
|
|
3746
|
+
rmSync7(key, { force: true });
|
|
3747
|
+
rmSync7(generatedPublicKey, { force: true });
|
|
3369
3748
|
let result;
|
|
3370
3749
|
if (operation.source) {
|
|
3371
|
-
const source =
|
|
3750
|
+
const source = existsSync8(operation.source) ? lstatSync2(operation.source) : undefined;
|
|
3372
3751
|
if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
|
|
3373
3752
|
return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
|
|
3374
3753
|
}
|
|
@@ -3388,8 +3767,8 @@ var runProvisionOperation = async (operation) => {
|
|
|
3388
3767
|
return result;
|
|
3389
3768
|
chmodSync6(operation.credential, 256);
|
|
3390
3769
|
}
|
|
3391
|
-
if (!
|
|
3392
|
-
if (!
|
|
3770
|
+
if (!existsSync8(operation.publicKey) || lstatSync2(operation.publicKey).size < 1) {
|
|
3771
|
+
if (!existsSync8(key)) {
|
|
3393
3772
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
|
|
3394
3773
|
if (decrypted.exitCode !== 0)
|
|
3395
3774
|
return decrypted;
|
|
@@ -3399,28 +3778,28 @@ var runProvisionOperation = async (operation) => {
|
|
|
3399
3778
|
if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
|
|
3400
3779
|
return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
|
|
3401
3780
|
}
|
|
3402
|
-
|
|
3403
|
-
|
|
3781
|
+
mkdirSync8(dirname6(operation.publicKey), { recursive: true, mode: 493 });
|
|
3782
|
+
writeFileSync7(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
|
|
3404
3783
|
`, { mode: 292 });
|
|
3405
3784
|
chmodSync6(operation.publicKey, 292);
|
|
3406
3785
|
}
|
|
3407
3786
|
if (operation.source)
|
|
3408
|
-
|
|
3787
|
+
rmSync7(operation.source, { force: true });
|
|
3409
3788
|
return { stdout: "", exitCode: 0 };
|
|
3410
3789
|
} finally {
|
|
3411
|
-
|
|
3412
|
-
|
|
3790
|
+
rmSync7(key, { force: true });
|
|
3791
|
+
rmSync7(generatedPublicKey, { force: true });
|
|
3413
3792
|
}
|
|
3414
3793
|
}
|
|
3415
3794
|
if (operation.kind === "ensure-enrolment") {
|
|
3416
|
-
if (
|
|
3795
|
+
if (existsSync8(operation.state) && lstatSync2(operation.state).size > 0 || existsSync8(operation.credential) && lstatSync2(operation.credential).size > 0)
|
|
3417
3796
|
return { stdout: "", exitCode: 0 };
|
|
3418
|
-
if (!
|
|
3797
|
+
if (!existsSync8(operation.source))
|
|
3419
3798
|
return { stdout: "enrolment source is missing", exitCode: 1 };
|
|
3420
3799
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
|
|
3421
3800
|
if (result.exitCode === 0) {
|
|
3422
3801
|
chmodSync6(operation.credential, 256);
|
|
3423
|
-
|
|
3802
|
+
rmSync7(operation.source, { force: true });
|
|
3424
3803
|
}
|
|
3425
3804
|
return result;
|
|
3426
3805
|
}
|
|
@@ -3435,7 +3814,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
3435
3814
|
return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
|
|
3436
3815
|
}
|
|
3437
3816
|
if (operation.kind === "verify-file")
|
|
3438
|
-
return
|
|
3817
|
+
return existsSync8(operation.path) && lstatSync2(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
|
|
3439
3818
|
if (operation.kind === "verify-egress") {
|
|
3440
3819
|
const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
|
|
3441
3820
|
if (active.exitCode !== 0)
|
|
@@ -3451,31 +3830,31 @@ var runProvisionOperation = async (operation) => {
|
|
|
3451
3830
|
if (operation.kind === "verify-resolved-stub") {
|
|
3452
3831
|
try {
|
|
3453
3832
|
const expected = "/run/systemd/resolve/stub-resolv.conf";
|
|
3454
|
-
return
|
|
3833
|
+
return realpathSync5("/etc/resolv.conf") === expected && realpathSync5(expected) === expected ? { stdout: expected, exitCode: 0 } : { stdout: "resolver stub mismatch", exitCode: 1 };
|
|
3455
3834
|
} catch {
|
|
3456
3835
|
return { stdout: "resolver stub missing", exitCode: 1 };
|
|
3457
3836
|
}
|
|
3458
3837
|
}
|
|
3459
3838
|
if (operation.kind === "install-warp") {
|
|
3460
|
-
const os =
|
|
3839
|
+
const os = readFileSync7("/etc/os-release", "utf8");
|
|
3461
3840
|
if (!/^ID=ubuntu$/m.test(os) || !/^VERSION_ID="?26\.04"?$/m.test(os))
|
|
3462
3841
|
return { stdout: "unsupported WARP host OS", exitCode: 1 };
|
|
3463
3842
|
const response = await fetch("https://pkg.cloudflareclient.com/pubkey.gpg", { signal: AbortSignal.timeout(30000) });
|
|
3464
3843
|
if (!response.ok)
|
|
3465
3844
|
return { stdout: `WARP key HTTP ${response.status}`, exitCode: 1 };
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3845
|
+
mkdirSync8("/usr/share/keyrings", { recursive: true, mode: 493 });
|
|
3846
|
+
mkdirSync8("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
|
|
3847
|
+
mkdirSync8("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
|
|
3469
3848
|
const key = "/run/cloudflare-warp-key.gpg";
|
|
3470
|
-
|
|
3849
|
+
writeFileSync7(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
|
|
3471
3850
|
let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
|
|
3472
|
-
|
|
3851
|
+
rmSync7(key, { force: true });
|
|
3473
3852
|
if (result.exitCode !== 0)
|
|
3474
3853
|
return result;
|
|
3475
3854
|
const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
|
|
3476
3855
|
if (!codename)
|
|
3477
3856
|
return { stdout: "Ubuntu codename missing", exitCode: 1 };
|
|
3478
|
-
|
|
3857
|
+
writeFileSync7("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ ${codename} main
|
|
3479
3858
|
`, { mode: 420 });
|
|
3480
3859
|
result = await fixed(["/usr/bin/apt-get", "update", "-qq"]);
|
|
3481
3860
|
return result.exitCode === 0 ? fixed(["/usr/bin/apt-get", "install", "-y", "cloudflare-warp"]) : result;
|
|
@@ -3520,7 +3899,7 @@ async function applyPlan(plan, run2) {
|
|
|
3520
3899
|
import { constants } from "node:fs";
|
|
3521
3900
|
import { randomBytes as randomBytes2, randomUUID as randomUUID3 } from "node:crypto";
|
|
3522
3901
|
import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "node:fs/promises";
|
|
3523
|
-
import { dirname as
|
|
3902
|
+
import { dirname as dirname7, join as join6, resolve as resolve5 } from "node:path";
|
|
3524
3903
|
import { isIP as isIP3 } from "node:net";
|
|
3525
3904
|
|
|
3526
3905
|
// src/cloudflare-edge.ts
|
|
@@ -3692,9 +4071,9 @@ async function bootstrapStatus(host = localBootstrapHost()) {
|
|
|
3692
4071
|
if (health.exitCode !== 0)
|
|
3693
4072
|
problems.push("platform API health check failed");
|
|
3694
4073
|
}
|
|
3695
|
-
const
|
|
3696
|
-
services["nginx-config"] =
|
|
3697
|
-
if (
|
|
4074
|
+
const nginx3 = await host.exec(["nginx", "-t"]);
|
|
4075
|
+
services["nginx-config"] = nginx3.exitCode === 0;
|
|
4076
|
+
if (nginx3.exitCode !== 0)
|
|
3698
4077
|
problems.push("nginx configuration is invalid");
|
|
3699
4078
|
if (state.databaseRole !== "none") {
|
|
3700
4079
|
const unitPath = "/etc/systemd/system/forgezero-db.service";
|
|
@@ -3739,17 +4118,17 @@ function localBootstrapHost() {
|
|
|
3739
4118
|
};
|
|
3740
4119
|
return {
|
|
3741
4120
|
uid: () => process.getuid?.() ?? -1,
|
|
3742
|
-
exists:
|
|
3743
|
-
read: (path2) =>
|
|
4121
|
+
exists: existsSync9,
|
|
4122
|
+
read: (path2) => readFileSync8(path2, "utf8"),
|
|
3744
4123
|
write(path2, content, mode) {
|
|
3745
|
-
|
|
4124
|
+
mkdirSync9(dirname8(path2), { recursive: true, mode: 493 });
|
|
3746
4125
|
const temporary = `${path2}.next.${process.pid}`;
|
|
3747
|
-
|
|
4126
|
+
writeFileSync8(temporary, content, { mode });
|
|
3748
4127
|
chmodSync7(temporary, mode);
|
|
3749
|
-
|
|
4128
|
+
renameSync8(temporary, path2);
|
|
3750
4129
|
},
|
|
3751
|
-
mkdir: (path2, mode) =>
|
|
3752
|
-
remove: (path2) =>
|
|
4130
|
+
mkdir: (path2, mode) => mkdirSync9(path2, { recursive: true, mode }),
|
|
4131
|
+
remove: (path2) => rmSync8(path2, { force: true }),
|
|
3753
4132
|
inspect(path2) {
|
|
3754
4133
|
const value = lstatSync3(path2);
|
|
3755
4134
|
return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
|
|
@@ -3773,7 +4152,7 @@ function localBootstrapHost() {
|
|
|
3773
4152
|
async installAgent(config, enrolTokenSourcePath) {
|
|
3774
4153
|
const capabilities = await readCapabilities(localRunner);
|
|
3775
4154
|
const deployRoot = config.deployRoot ?? "/opt/forgezero";
|
|
3776
|
-
const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) ||
|
|
4155
|
+
const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) || existsSync9("/var/lib/forgezero/enrolment.json");
|
|
3777
4156
|
if (config.kind === "platform") {
|
|
3778
4157
|
const lifecycle = config.database.role === "none" ? {
|
|
3779
4158
|
apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
|
|
@@ -3785,8 +4164,8 @@ function localBootstrapHost() {
|
|
|
3785
4164
|
databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
|
|
3786
4165
|
databasePorts: [8529]
|
|
3787
4166
|
};
|
|
3788
|
-
|
|
3789
|
-
|
|
4167
|
+
mkdirSync9(dirname8(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
|
|
4168
|
+
writeFileSync8(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
|
|
3790
4169
|
`, { mode: 256 });
|
|
3791
4170
|
}
|
|
3792
4171
|
const plan = planInstall({
|
|
@@ -3827,8 +4206,8 @@ function localBootstrapHost() {
|
|
|
3827
4206
|
} : {}
|
|
3828
4207
|
});
|
|
3829
4208
|
for (const unit2 of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
|
|
3830
|
-
|
|
3831
|
-
|
|
4209
|
+
mkdirSync9(dirname8(unit2.path), { recursive: true, mode: 493 });
|
|
4210
|
+
writeFileSync8(unit2.path, unit2.unit, { mode: 420 });
|
|
3832
4211
|
}
|
|
3833
4212
|
await applyPlan(plan, localRunner);
|
|
3834
4213
|
return plan;
|