@forgezero/agent 0.1.73 → 0.1.75
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 +565 -61
- package/dist/agent-heartbeat.d.ts +25 -1
- package/dist/agent-heartbeat.js +45 -5
- package/dist/bootstrap.d.ts +2 -0
- package/dist/bootstrap.js +22 -4
- package/dist/community-rehearsal-host.js +208 -9
- package/dist/credential-schema.d.ts +1 -1
- package/dist/credential-schema.js +1 -1
- package/dist/definition.js +206 -7
- package/dist/deploy-actions.d.ts +7 -0
- package/dist/deploy-compiler.js +191 -10
- package/dist/deploy-file.js +206 -7
- package/dist/deploy-plan-runner.d.ts +3 -0
- package/dist/deploy-plan-runner.js +190 -9
- package/dist/deploy-plan.js +187 -8
- package/dist/deploy-providers.d.ts +19 -0
- package/dist/deploy.d.ts +89 -3
- package/dist/deploy.js +49 -2
- package/dist/deployment-connectivity.d.ts +63 -0
- package/dist/deployment-connectivity.js +148 -0
- package/dist/deployment-pull.d.ts +2 -0
- package/dist/deployment-targets.d.ts +9 -0
- package/dist/deployment-targets.js +141 -0
- package/dist/deployment.d.ts +69 -0
- package/dist/fz-agent.js +1112 -302
- package/dist/fz.js +246 -18
- package/dist/index.d.ts +1 -0
- package/dist/metal-bootstrap.js +1 -1
- package/dist/metal-helper-socket.js +210 -11
- package/dist/metal-provision.js +210 -11
- package/dist/operator-bootstrap.js +22 -4
- package/dist/platform-bootstrap-runtime.d.ts +1 -0
- package/dist/platform-bootstrap-runtime.js +216 -10
- package/dist/platform-fleet-verification.js +523 -150
- package/dist/platform-genesis.js +206 -7
- package/dist/provision.js +465 -99
- package/dist/service-supervisor.d.ts +6 -0
- package/dist/software-helper.d.ts +3 -0
- package/dist/software-helper.js +465 -98
- package/dist/software.d.ts +4 -1
- package/dist/software.js +210 -8
- package/dist/ubuntu.js +206 -7
- package/dist/version.d.ts +1 -1
- package/package.json +12 -4
package/dist/provision.js
CHANGED
|
@@ -682,22 +682,30 @@ import {
|
|
|
682
682
|
accessSync,
|
|
683
683
|
chmodSync as chmodSync3,
|
|
684
684
|
copyFileSync,
|
|
685
|
+
createReadStream,
|
|
685
686
|
existsSync as existsSync3,
|
|
686
687
|
mkdtempSync,
|
|
687
688
|
mkdirSync as mkdirSync3,
|
|
688
689
|
readFileSync as readFileSync3,
|
|
689
690
|
renameSync as renameSync3,
|
|
690
691
|
rmSync as rmSync3,
|
|
692
|
+
statSync,
|
|
691
693
|
symlinkSync as symlinkSync2,
|
|
692
694
|
unlinkSync as unlinkSync2,
|
|
693
695
|
writeFileSync as writeFileSync3
|
|
694
696
|
} from "node:fs";
|
|
695
697
|
import { tmpdir } from "node:os";
|
|
696
|
-
import { join as join3 } from "node:path";
|
|
698
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
697
699
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
698
700
|
var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
|
|
699
701
|
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
700
702
|
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
703
|
+
var NERDCTL_VERSION = "2.3.5";
|
|
704
|
+
var BUILDKIT_VERSION = "0.32.2";
|
|
705
|
+
var KATA_CONTAINERS_VERSION = "4.0.0";
|
|
706
|
+
var NERDCTL_SHA256 = "de3206aeb7cbd5f20f5fb1f55c1e3bf2db1be567812a8a3f5e65eba2488347ee";
|
|
707
|
+
var BUILDKIT_SHA256 = "2975d0f651ad96ba8b80b9992ae1f9a964f4408569af5b6dc36544165c3926af";
|
|
708
|
+
var KATA_SHA256 = "2c3b9dfeba355582b40aee462b12916c9740654d0230f696adf719d67b063a8c";
|
|
701
709
|
var OS_CATALOG = [
|
|
702
710
|
{
|
|
703
711
|
id: "ubuntu",
|
|
@@ -727,6 +735,8 @@ var OS_CATALOG = [
|
|
|
727
735
|
var SOFTWARE_CATALOG = [
|
|
728
736
|
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
729
737
|
{ id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
738
|
+
{ id: "containerd", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
739
|
+
{ id: "kata-containers", version: "4.0.0", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
730
740
|
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
731
741
|
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
732
742
|
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -735,6 +745,7 @@ var SOFTWARE_CATALOG = [
|
|
|
735
745
|
{ id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
736
746
|
{ id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
737
747
|
{ id: "docker", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
748
|
+
{ id: "containerd", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
738
749
|
{ id: "nginx", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
739
750
|
{ id: "ufw", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
740
751
|
{ id: "openssh-client", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -743,6 +754,8 @@ var SOFTWARE_CATALOG = [
|
|
|
743
754
|
var UBUNTU_2604_X64 = [
|
|
744
755
|
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
745
756
|
{ requirement: { id: "docker", version: "ubuntu-26.04" } },
|
|
757
|
+
{ requirement: { id: "containerd", version: "ubuntu-26.04" } },
|
|
758
|
+
{ requirement: { id: "kata-containers", version: "4.0.0" } },
|
|
746
759
|
{ requirement: { id: "nginx", version: "ubuntu-26.04" } },
|
|
747
760
|
{ requirement: { id: "arangodb", version: "3.11.14" } },
|
|
748
761
|
{ requirement: { id: "cloudflared", version: "2026.7.3" } },
|
|
@@ -754,6 +767,7 @@ var UBUNTU_2604_X64 = [
|
|
|
754
767
|
var UBUNTU_2404_X64 = [
|
|
755
768
|
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
756
769
|
{ requirement: { id: "docker", version: "ubuntu-24.04" } },
|
|
770
|
+
{ requirement: { id: "containerd", version: "ubuntu-24.04" } },
|
|
757
771
|
{ requirement: { id: "nginx", version: "ubuntu-24.04" } },
|
|
758
772
|
{ requirement: { id: "ufw", version: "ubuntu-24.04" } },
|
|
759
773
|
{ requirement: { id: "openssh-client", version: "ubuntu-24.04" } },
|
|
@@ -770,6 +784,46 @@ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
|
770
784
|
"log-opts": { "max-size": "10m", "max-file": "3" }
|
|
771
785
|
}, null, 2)}
|
|
772
786
|
`;
|
|
787
|
+
var CONTAINERD_CONFIG_PATH = "/etc/containerd/config.toml";
|
|
788
|
+
var KATA_SNP_CONFIG_PATH = "/opt/kata/share/defaults/kata-containers/configuration-qemu-snp.toml";
|
|
789
|
+
var KATA_ACTIVE_CONFIG_PATH = "/etc/kata-containers/configuration.toml";
|
|
790
|
+
var CONTAINERD_CONFIG = (kata) => `version = 2
|
|
791
|
+
root = "/var/lib/forgezero/containerd"
|
|
792
|
+
state = "/run/forgezero/containerd"
|
|
793
|
+
|
|
794
|
+
[grpc]
|
|
795
|
+
address = "/run/containerd/containerd.sock"
|
|
796
|
+
|
|
797
|
+
[plugins."io.containerd.grpc.v1.cri".containerd]
|
|
798
|
+
snapshotter = "overlayfs"
|
|
799
|
+
default_runtime_name = "runc"
|
|
800
|
+
|
|
801
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
|
|
802
|
+
runtime_type = "io.containerd.runc.v2"
|
|
803
|
+
${kata ? `
|
|
804
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-qemu-snp]
|
|
805
|
+
runtime_type = "io.containerd.kata.v2"
|
|
806
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-qemu-snp.options]
|
|
807
|
+
ConfigPath = "${KATA_ACTIVE_CONFIG_PATH}"
|
|
808
|
+
` : ""}`;
|
|
809
|
+
var BUILDKIT_UNIT = `[Unit]
|
|
810
|
+
Description=ForgeZero BuildKit daemon
|
|
811
|
+
After=network-online.target containerd.service
|
|
812
|
+
Wants=network-online.target
|
|
813
|
+
Requires=containerd.service
|
|
814
|
+
|
|
815
|
+
[Service]
|
|
816
|
+
Type=notify
|
|
817
|
+
ExecStart=/usr/local/bin/buildkitd --addr unix:///run/buildkit/buildkitd.sock --root /var/lib/forgezero/buildkit
|
|
818
|
+
Restart=on-failure
|
|
819
|
+
RestartSec=2
|
|
820
|
+
NoNewPrivileges=yes
|
|
821
|
+
ProtectSystem=strict
|
|
822
|
+
ReadWritePaths=/var/lib/forgezero/buildkit /run/buildkit
|
|
823
|
+
|
|
824
|
+
[Install]
|
|
825
|
+
WantedBy=multi-user.target
|
|
826
|
+
`;
|
|
773
827
|
var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
|
|
774
828
|
var runSoftwareCommand = async (argv, env = {}) => {
|
|
775
829
|
let child;
|
|
@@ -789,14 +843,28 @@ var runSoftwareCommand = async (argv, env = {}) => {
|
|
|
789
843
|
return { exitCode, output: `${stdout}${stderr}` };
|
|
790
844
|
};
|
|
791
845
|
var run = runSoftwareCommand;
|
|
792
|
-
var download = async (url, destination, sha256) => {
|
|
793
|
-
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(
|
|
846
|
+
var download = async (url, destination, sha256, maximumBytes = 512 * 1024 * 1024) => {
|
|
847
|
+
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30 * 60000) });
|
|
794
848
|
if (!response.ok)
|
|
795
849
|
throw new Error(`download failed with HTTP ${response.status}`);
|
|
796
|
-
const
|
|
797
|
-
if (
|
|
850
|
+
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
851
|
+
if (declared > maximumBytes)
|
|
852
|
+
throw new Error("download exceeds reviewed size bound");
|
|
853
|
+
if (existsSync3(destination))
|
|
854
|
+
throw new Error("download destination already exists");
|
|
855
|
+
await Bun.write(destination, response);
|
|
856
|
+
chmodSync3(destination, 384);
|
|
857
|
+
if (statSync(destination).size > maximumBytes) {
|
|
858
|
+
rmSync3(destination, { force: true });
|
|
859
|
+
throw new Error("download exceeds reviewed size bound");
|
|
860
|
+
}
|
|
861
|
+
const digest = createHash2("sha256");
|
|
862
|
+
for await (const chunk of createReadStream(destination))
|
|
863
|
+
digest.update(chunk);
|
|
864
|
+
if (digest.digest("hex") !== sha256) {
|
|
865
|
+
rmSync3(destination, { force: true });
|
|
798
866
|
throw new Error("download checksum mismatch");
|
|
799
|
-
|
|
867
|
+
}
|
|
800
868
|
};
|
|
801
869
|
var successful = (result, pattern) => result.exitCode === 0 && (!pattern || pattern.test(result.output));
|
|
802
870
|
var aptInstall = async (name) => {
|
|
@@ -804,6 +872,106 @@ var aptInstall = async (name) => {
|
|
|
804
872
|
const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
|
|
805
873
|
return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", name], environment) : update;
|
|
806
874
|
};
|
|
875
|
+
var aptInstallMany = async (names) => {
|
|
876
|
+
const environment = { DEBIAN_FRONTEND: "noninteractive" };
|
|
877
|
+
const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
|
|
878
|
+
return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", ...names], environment) : update;
|
|
879
|
+
};
|
|
880
|
+
var writeOwnedPolicy = (pathValue, content, mode = 420) => {
|
|
881
|
+
if (existsSync3(pathValue)) {
|
|
882
|
+
if (readFileSync3(pathValue, "utf8") !== content)
|
|
883
|
+
throw new Error(`refusing to overwrite non-ForgeZero policy at ${pathValue}`);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
mkdirSync3(dirname3(pathValue), { recursive: true, mode: 493 });
|
|
887
|
+
writeFileSync3(pathValue, content, { mode, flag: "wx" });
|
|
888
|
+
};
|
|
889
|
+
var installContainerdRuntime = async (directory, osVersion) => {
|
|
890
|
+
const installed = await aptInstallMany(["containerd", "runc", "containernetworking-plugins"]);
|
|
891
|
+
if (installed.exitCode !== 0)
|
|
892
|
+
return installed;
|
|
893
|
+
const nerdctl = join3(directory, "nerdctl.tar.gz");
|
|
894
|
+
await download(`https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz`, nerdctl, NERDCTL_SHA256);
|
|
895
|
+
const nerdctlExtract = await run(["/usr/bin/tar", "-xzf", nerdctl, "-C", "/usr/local/bin", "nerdctl"]);
|
|
896
|
+
if (nerdctlExtract.exitCode !== 0)
|
|
897
|
+
return nerdctlExtract;
|
|
898
|
+
chmodSync3("/usr/local/bin/nerdctl", 493);
|
|
899
|
+
const buildkit = join3(directory, "buildkit.tar.gz");
|
|
900
|
+
await download(`https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-amd64.tar.gz`, buildkit, BUILDKIT_SHA256);
|
|
901
|
+
const unpacked = join3(directory, "buildkit");
|
|
902
|
+
mkdirSync3(unpacked, { mode: 448 });
|
|
903
|
+
const buildkitExtract = await run(["/usr/bin/tar", "-xzf", buildkit, "-C", unpacked]);
|
|
904
|
+
if (buildkitExtract.exitCode !== 0)
|
|
905
|
+
return buildkitExtract;
|
|
906
|
+
for (const binary of ["buildctl", "buildkitd"]) {
|
|
907
|
+
copyFileSync(join3(unpacked, "bin", binary), `/usr/local/bin/${binary}`);
|
|
908
|
+
chmodSync3(`/usr/local/bin/${binary}`, 493);
|
|
909
|
+
}
|
|
910
|
+
mkdirSync3("/etc/containerd", { recursive: true, mode: 493 });
|
|
911
|
+
const expected = CONTAINERD_CONFIG(false);
|
|
912
|
+
const kataExpected = CONTAINERD_CONFIG(true);
|
|
913
|
+
if (existsSync3(CONTAINERD_CONFIG_PATH) && ![expected, kataExpected].includes(readFileSync3(CONTAINERD_CONFIG_PATH, "utf8"))) {
|
|
914
|
+
return { exitCode: 2, output: "refusing to overwrite an existing containerd configuration that differs from the reviewed ForgeZero policy" };
|
|
915
|
+
}
|
|
916
|
+
if (!existsSync3(CONTAINERD_CONFIG_PATH))
|
|
917
|
+
writeFileSync3(CONTAINERD_CONFIG_PATH, expected, { mode: 420, flag: "wx" });
|
|
918
|
+
writeOwnedPolicy("/etc/systemd/system/forgezero-buildkit.service", BUILDKIT_UNIT);
|
|
919
|
+
mkdirSync3("/var/lib/forgezero/containerd", { recursive: true, mode: 448 });
|
|
920
|
+
mkdirSync3("/var/lib/forgezero/buildkit", { recursive: true, mode: 448 });
|
|
921
|
+
const reload = await run(["/usr/bin/systemctl", "daemon-reload"]);
|
|
922
|
+
if (reload.exitCode !== 0)
|
|
923
|
+
return reload;
|
|
924
|
+
const enabled = await run(["/usr/bin/systemctl", "enable", "--now", "containerd.service", "forgezero-buildkit.service"]);
|
|
925
|
+
return enabled.exitCode === 0 ? { exitCode: 0, output: `containerd ${osVersion} with nerdctl ${NERDCTL_VERSION} and BuildKit ${BUILDKIT_VERSION}` } : enabled;
|
|
926
|
+
};
|
|
927
|
+
var installKataRuntime = async (directory) => {
|
|
928
|
+
for (const pathValue of ["/dev/kvm", "/dev/sev"])
|
|
929
|
+
if (!existsSync3(pathValue))
|
|
930
|
+
return { exitCode: 2, output: `${pathValue} is required for Kata SEV-SNP` };
|
|
931
|
+
for (const parameter of ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"]) {
|
|
932
|
+
if (!existsSync3(parameter) || !/^(1|Y)$/i.test(readFileSync3(parameter, "utf8").trim()))
|
|
933
|
+
return { exitCode: 2, output: `${parameter} does not enable SEV-SNP` };
|
|
934
|
+
}
|
|
935
|
+
const dependencies = await aptInstallMany(["zstd"]);
|
|
936
|
+
if (dependencies.exitCode !== 0)
|
|
937
|
+
return dependencies;
|
|
938
|
+
const archive = join3(directory, "kata.tar.zst");
|
|
939
|
+
await download(`https://github.com/kata-containers/kata-containers/releases/download/${KATA_CONTAINERS_VERSION}/kata-static-${KATA_CONTAINERS_VERSION}-amd64.tar.zst`, archive, KATA_SHA256, 2200000000);
|
|
940
|
+
const tar = join3(directory, "kata.tar");
|
|
941
|
+
const expanded = await run(["/usr/bin/unzstd", "--quiet", archive, "--output", tar]);
|
|
942
|
+
if (expanded.exitCode !== 0)
|
|
943
|
+
return expanded;
|
|
944
|
+
const extracted = await run(["/usr/bin/tar", "-xf", tar, "-C", "/"]);
|
|
945
|
+
if (extracted.exitCode !== 0)
|
|
946
|
+
return extracted;
|
|
947
|
+
if (!existsSync3("/opt/kata/bin/containerd-shim-kata-v2") || !existsSync3(KATA_SNP_CONFIG_PATH)) {
|
|
948
|
+
return { exitCode: 2, output: "Kata archive does not contain the reviewed QEMU SNP runtime and configuration" };
|
|
949
|
+
}
|
|
950
|
+
const snpConfig = readFileSync3(KATA_SNP_CONFIG_PATH, "utf8");
|
|
951
|
+
if (!/^\s*confidential_guest\s*=\s*true\s*$/m.test(snpConfig) || !/^\s*sev_snp_guest\s*=\s*true\s*$/m.test(snpConfig)) {
|
|
952
|
+
return { exitCode: 2, output: "Kata QEMU configuration does not enable confidential SEV-SNP guests" };
|
|
953
|
+
}
|
|
954
|
+
mkdirSync3("/etc/kata-containers", { recursive: true, mode: 493 });
|
|
955
|
+
if (existsSync3(KATA_ACTIVE_CONFIG_PATH) && readFileSync3(KATA_ACTIVE_CONFIG_PATH, "utf8") !== snpConfig) {
|
|
956
|
+
return { exitCode: 2, output: "refusing to overwrite a non-ForgeZero Kata runtime configuration" };
|
|
957
|
+
}
|
|
958
|
+
if (!existsSync3(KATA_ACTIVE_CONFIG_PATH))
|
|
959
|
+
copyFileSync(KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH);
|
|
960
|
+
chmodSync3(KATA_ACTIVE_CONFIG_PATH, 420);
|
|
961
|
+
try {
|
|
962
|
+
unlinkSync2("/usr/local/bin/containerd-shim-kata-v2");
|
|
963
|
+
} catch {}
|
|
964
|
+
symlinkSync2("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
|
|
965
|
+
const base = CONTAINERD_CONFIG(false);
|
|
966
|
+
const kata = CONTAINERD_CONFIG(true);
|
|
967
|
+
if (!existsSync3(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync3(CONTAINERD_CONFIG_PATH, "utf8"))) {
|
|
968
|
+
return { exitCode: 2, output: "containerd must use the reviewed ForgeZero policy before Kata is installed" };
|
|
969
|
+
}
|
|
970
|
+
if (readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") !== kata)
|
|
971
|
+
writeFileSync3(CONTAINERD_CONFIG_PATH, kata, { mode: 420 });
|
|
972
|
+
const restarted = await run(["/usr/bin/systemctl", "restart", "containerd.service"]);
|
|
973
|
+
return restarted.exitCode === 0 ? { exitCode: 0, output: `Kata Containers ${KATA_CONTAINERS_VERSION} QEMU SNP` } : restarted;
|
|
974
|
+
};
|
|
807
975
|
async function executeSoftwareOperation(operation) {
|
|
808
976
|
const { software, version } = operation;
|
|
809
977
|
if (![...UBUNTU_2604_X64, ...UBUNTU_2404_X64].some(({ requirement }) => requirement.id === software && requirement.version === version)) {
|
|
@@ -821,6 +989,26 @@ async function executeSoftwareOperation(operation) {
|
|
|
821
989
|
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
|
|
822
990
|
return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
|
|
823
991
|
}
|
|
992
|
+
if (software === "containerd") {
|
|
993
|
+
const [containerd, nerdctl, buildkit, active, builder] = await Promise.all([
|
|
994
|
+
run(["/usr/bin/containerd", "--version"]),
|
|
995
|
+
run(["/usr/local/bin/nerdctl", "--version"]),
|
|
996
|
+
run(["/usr/local/bin/buildkitd", "--version"]),
|
|
997
|
+
run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]),
|
|
998
|
+
run(["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-buildkit.service"])
|
|
999
|
+
]);
|
|
1000
|
+
const policy = existsSync3(CONTAINERD_CONFIG_PATH) ? readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") : "";
|
|
1001
|
+
const validPolicy = policy === CONTAINERD_CONFIG(false) || policy === CONTAINERD_CONFIG(true);
|
|
1002
|
+
return successful(containerd, /containerd/) && successful(nerdctl, new RegExp(NERDCTL_VERSION.replaceAll(".", "\\."))) && successful(buildkit, new RegExp(BUILDKIT_VERSION.replaceAll(".", "\\."))) && active.exitCode === 0 && builder.exitCode === 0 && validPolicy ? { exitCode: 0, output: `${containerd.output}${nerdctl.output}${buildkit.output}` } : { exitCode: 1, output: "containerd, nerdctl, BuildKit, or ForgeZero storage/runtime policy is unavailable" };
|
|
1003
|
+
}
|
|
1004
|
+
if (software === "kata-containers") {
|
|
1005
|
+
const shim = await run(["/opt/kata/bin/containerd-shim-kata-v2", "--version"]);
|
|
1006
|
+
const policy = existsSync3(CONTAINERD_CONFIG_PATH) ? readFileSync3(CONTAINERD_CONFIG_PATH, "utf8") : "";
|
|
1007
|
+
const activeConfig = existsSync3(KATA_ACTIVE_CONFIG_PATH) ? readFileSync3(KATA_ACTIVE_CONFIG_PATH, "utf8") : "";
|
|
1008
|
+
const host = ["/dev/kvm", "/dev/sev", KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH].every(existsSync3) && ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"].every((parameter) => existsSync3(parameter) && /^(1|Y)$/i.test(readFileSync3(parameter, "utf8").trim()));
|
|
1009
|
+
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]);
|
|
1010
|
+
return successful(shim, /kata.*4\.0\.0/i) && policy === CONTAINERD_CONFIG(true) && activeConfig === (existsSync3(KATA_SNP_CONFIG_PATH) ? readFileSync3(KATA_SNP_CONFIG_PATH, "utf8") : "") && host && active.exitCode === 0 ? { exitCode: 0, output: shim.output } : { exitCode: 1, output: "Kata QEMU SNP runtime, host capability, or containerd policy is unavailable" };
|
|
1011
|
+
}
|
|
824
1012
|
if (software === "nginx") {
|
|
825
1013
|
const binary = await run(["/usr/sbin/nginx", "-v"]);
|
|
826
1014
|
return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
|
|
@@ -872,6 +1060,17 @@ async function executeSoftwareOperation(operation) {
|
|
|
872
1060
|
}
|
|
873
1061
|
const directory = mkdtempSync(join3(tmpdir(), "forgezero-software-"));
|
|
874
1062
|
try {
|
|
1063
|
+
if (software === "containerd")
|
|
1064
|
+
return installContainerdRuntime(directory, version === "ubuntu-24.04" ? "24.04" : "26.04");
|
|
1065
|
+
if (software === "kata-containers") {
|
|
1066
|
+
const containerd = await executeSoftwareOperation({ kind: "check", software: "containerd", version: "ubuntu-26.04" });
|
|
1067
|
+
if (containerd.exitCode !== 0) {
|
|
1068
|
+
const installed2 = await installContainerdRuntime(directory, "26.04");
|
|
1069
|
+
if (installed2.exitCode !== 0)
|
|
1070
|
+
return installed2;
|
|
1071
|
+
}
|
|
1072
|
+
return installKataRuntime(directory);
|
|
1073
|
+
}
|
|
875
1074
|
if (software === "cloudflare-warp") {
|
|
876
1075
|
const key = join3(directory, "cloudflare-warp-key.gpg");
|
|
877
1076
|
await download("https://pkg.cloudflareclient.com/pubkey.gpg", key, "0f37fc298c98e88ee3c0ee68c95b69f1dba9eb477abe3167e13982105911264d");
|
|
@@ -953,7 +1152,7 @@ function validateSoftwareRequirements(value, _options = {}) {
|
|
|
953
1152
|
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
954
1153
|
throw new Error("software requirement contains an unknown field");
|
|
955
1154
|
}
|
|
956
|
-
if (!["bun", "docker", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client", "git"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
1155
|
+
if (!["bun", "docker", "containerd", "kata-containers", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client", "git"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
957
1156
|
throw new Error("software requirement coordinate is invalid");
|
|
958
1157
|
}
|
|
959
1158
|
const requirement = { id: row.id, version: row.version };
|
|
@@ -1425,34 +1624,180 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
|
|
|
1425
1624
|
};
|
|
1426
1625
|
}
|
|
1427
1626
|
|
|
1627
|
+
// src/deployment-connectivity.ts
|
|
1628
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1629
|
+
import { mkdirSync as mkdirSync4, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1630
|
+
import { dirname as dirname4 } from "node:path";
|
|
1631
|
+
var TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
|
|
1632
|
+
var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
|
|
1633
|
+
var HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
1634
|
+
var NAME2 = /^[a-z][a-z0-9._-]{0,99}$/;
|
|
1635
|
+
var idFor = (key) => createHash3("sha256").update(key).digest("hex").slice(0, 20);
|
|
1636
|
+
var checked2 = async (host, argv2, label) => {
|
|
1637
|
+
const result = await host.exec(argv2);
|
|
1638
|
+
if (result.exitCode !== 0)
|
|
1639
|
+
throw new Error(`${label} failed: ${result.output.slice(0, 512)}`);
|
|
1640
|
+
return result.output.trim();
|
|
1641
|
+
};
|
|
1642
|
+
async function sealWithSystemd(name, path2, value) {
|
|
1643
|
+
mkdirSync4(dirname4(path2), { recursive: true, mode: 448 });
|
|
1644
|
+
const next = `${path2}.next`;
|
|
1645
|
+
const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
|
|
1646
|
+
stdin: "pipe",
|
|
1647
|
+
stdout: "pipe",
|
|
1648
|
+
stderr: "pipe",
|
|
1649
|
+
env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" }
|
|
1650
|
+
});
|
|
1651
|
+
child.stdin.write(value);
|
|
1652
|
+
child.stdin.end();
|
|
1653
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
1654
|
+
new Response(child.stdout).text(),
|
|
1655
|
+
new Response(child.stderr).text(),
|
|
1656
|
+
child.exited
|
|
1657
|
+
]);
|
|
1658
|
+
if (exitCode !== 0)
|
|
1659
|
+
throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
|
|
1660
|
+
renameSync4(next, path2);
|
|
1661
|
+
}
|
|
1662
|
+
var defaultHost = {
|
|
1663
|
+
seal: sealWithSystemd,
|
|
1664
|
+
write(path2, content, mode) {
|
|
1665
|
+
mkdirSync4(dirname4(path2), { recursive: true, mode: 493 });
|
|
1666
|
+
const next = `${path2}.next`;
|
|
1667
|
+
writeFileSync4(next, content, { mode });
|
|
1668
|
+
renameSync4(next, path2);
|
|
1669
|
+
},
|
|
1670
|
+
async exec(argv2) {
|
|
1671
|
+
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" } });
|
|
1672
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
1673
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
1674
|
+
},
|
|
1675
|
+
sleep: (ms) => Bun.sleep(ms)
|
|
1676
|
+
};
|
|
1677
|
+
function validate(request) {
|
|
1678
|
+
if (!request.key || request.key.length > 256)
|
|
1679
|
+
throw new Error("deployment connectivity key is invalid");
|
|
1680
|
+
const publicIntent = request.intent.public;
|
|
1681
|
+
const privateIntent = request.intent.private;
|
|
1682
|
+
if (publicIntent?.mode === "cloudflare-tunnel") {
|
|
1683
|
+
if (!NAME2.test(publicIntent.credential) || !HOSTNAME.test(publicIntent.hostname) || !NAME2.test(publicIntent.tunnelKey) || publicIntent.service.protocol !== "http" || !Number.isInteger(publicIntent.service.port) || publicIntent.service.port < 1 || publicIntent.service.port > 65535 || !request.capabilities.public || !UUID.test(request.capabilities.public.tunnelId) || !TOKEN.test(request.capabilities.public.connectorToken)) {
|
|
1684
|
+
throw new Error("public deployment connectivity is malformed");
|
|
1685
|
+
}
|
|
1686
|
+
} else if (request.capabilities.public)
|
|
1687
|
+
throw new Error("unexpected public deployment capability");
|
|
1688
|
+
if (privateIntent?.mode === "cloudflare-warp") {
|
|
1689
|
+
if (!NAME2.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !TOKEN.test(request.capabilities.private.connectorToken)) {
|
|
1690
|
+
throw new Error("private deployment connectivity is malformed");
|
|
1691
|
+
}
|
|
1692
|
+
} else if (request.capabilities.private)
|
|
1693
|
+
throw new Error("unexpected private deployment capability");
|
|
1694
|
+
}
|
|
1695
|
+
async function applyDeploymentConnectivity(request, host = defaultHost) {
|
|
1696
|
+
validate(request);
|
|
1697
|
+
const id = idFor(request.key);
|
|
1698
|
+
const evidence = { key: request.key };
|
|
1699
|
+
if (request.intent.public?.mode === "cloudflare-tunnel") {
|
|
1700
|
+
const capability = request.capabilities.public;
|
|
1701
|
+
const credentialName = `FZ_TUNNEL_${id.toUpperCase()}`;
|
|
1702
|
+
const credentialPath = `/etc/forgezero/creds/deployment-tunnel-${id}.cred`;
|
|
1703
|
+
const unit = `forgezero-cloudflared-${id}.service`;
|
|
1704
|
+
const unitPath = `/etc/systemd/system/${unit}`;
|
|
1705
|
+
const metricsPort = 20000 + Number.parseInt(id.slice(0, 4), 16) % 20000;
|
|
1706
|
+
await host.seal(credentialName, credentialPath, capability.connectorToken);
|
|
1707
|
+
host.write(unitPath, `[Unit]
|
|
1708
|
+
Description=ForgeZero deployment Tunnel ${id}
|
|
1709
|
+
After=network-online.target
|
|
1710
|
+
Wants=network-online.target
|
|
1711
|
+
|
|
1712
|
+
[Service]
|
|
1713
|
+
Type=simple
|
|
1714
|
+
DynamicUser=yes
|
|
1715
|
+
LoadCredentialEncrypted=${credentialName}:${credentialPath}
|
|
1716
|
+
ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --metrics 127.0.0.1:${metricsPort} run --token-file %d/${credentialName}
|
|
1717
|
+
Restart=always
|
|
1718
|
+
RestartSec=5
|
|
1719
|
+
NoNewPrivileges=true
|
|
1720
|
+
PrivateTmp=true
|
|
1721
|
+
ProtectSystem=strict
|
|
1722
|
+
ProtectHome=true
|
|
1723
|
+
|
|
1724
|
+
[Install]
|
|
1725
|
+
WantedBy=multi-user.target
|
|
1726
|
+
`, 420);
|
|
1727
|
+
await checked2(host, ["/usr/bin/systemctl", "daemon-reload"], "cloudflared daemon reload");
|
|
1728
|
+
await checked2(host, ["/usr/bin/systemctl", "enable", "--now", unit], "cloudflared start");
|
|
1729
|
+
await checked2(host, ["/usr/bin/systemctl", "is-active", "--quiet", unit], "cloudflared readiness");
|
|
1730
|
+
evidence.public = { hostname: request.intent.public.hostname, tunnelId: capability.tunnelId, unit, active: true };
|
|
1731
|
+
}
|
|
1732
|
+
if (request.intent.private?.mode === "cloudflare-warp") {
|
|
1733
|
+
const capability = request.capabilities.private;
|
|
1734
|
+
const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
|
|
1735
|
+
const unit = "forgezero-deployment-mesh.service";
|
|
1736
|
+
await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
|
|
1737
|
+
host.write(`/etc/systemd/system/${unit}`, `[Unit]
|
|
1738
|
+
Description=ForgeZero deployment Mesh/WARP connector
|
|
1739
|
+
Requires=warp-svc.service
|
|
1740
|
+
After=network-online.target warp-svc.service
|
|
1741
|
+
Wants=network-online.target
|
|
1742
|
+
|
|
1743
|
+
[Service]
|
|
1744
|
+
Type=oneshot
|
|
1745
|
+
RemainAfterExit=yes
|
|
1746
|
+
LoadCredentialEncrypted=CF_WARP_CONNECTOR_TOKEN:${credentialPath}
|
|
1747
|
+
ExecStart=/usr/local/bin/fz-agent mesh-config
|
|
1748
|
+
NoNewPrivileges=true
|
|
1749
|
+
PrivateTmp=true
|
|
1750
|
+
ProtectSystem=strict
|
|
1751
|
+
ProtectHome=true
|
|
1752
|
+
ReadWritePaths=/var/lib/cloudflare-warp
|
|
1753
|
+
|
|
1754
|
+
[Install]
|
|
1755
|
+
WantedBy=multi-user.target
|
|
1756
|
+
`, 420);
|
|
1757
|
+
await checked2(host, ["/usr/bin/systemctl", "daemon-reload"], "WARP daemon reload");
|
|
1758
|
+
await checked2(host, ["/usr/bin/systemctl", "enable", "--now", "warp-svc.service", unit], "WARP connector start");
|
|
1759
|
+
for (let attempt = 0;attempt < 10; attempt += 1) {
|
|
1760
|
+
const status = await host.exec(["/usr/bin/warp-cli", "--accept-tos", "status"]);
|
|
1761
|
+
if (status.exitCode === 0 && /\bconnected\b/i.test(status.output) && !/\bdisconnected\b/i.test(status.output)) {
|
|
1762
|
+
evidence.private = { network: request.intent.private.network, connectorId: capability.connectorId, unit, active: true };
|
|
1763
|
+
break;
|
|
1764
|
+
}
|
|
1765
|
+
await host.sleep(1000);
|
|
1766
|
+
}
|
|
1767
|
+
if (!evidence.private)
|
|
1768
|
+
throw new Error("WARP connector readiness failed");
|
|
1769
|
+
}
|
|
1770
|
+
return evidence;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1428
1773
|
// src/software-helper.ts
|
|
1429
|
-
import { chmodSync as chmodSync4, existsSync as existsSync6, mkdirSync as
|
|
1774
|
+
import { chmodSync as chmodSync4, existsSync as existsSync6, mkdirSync as mkdirSync7, unlinkSync as unlinkSync3 } from "node:fs";
|
|
1430
1775
|
import { connect as connect2, createServer as createServer2 } from "node:net";
|
|
1431
|
-
import { dirname as
|
|
1776
|
+
import { dirname as dirname7 } from "node:path";
|
|
1432
1777
|
|
|
1433
1778
|
// src/service-supervisor.ts
|
|
1434
|
-
import { createHash as
|
|
1435
|
-
import { existsSync as existsSync4, mkdirSync as
|
|
1436
|
-
import { dirname as
|
|
1779
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1780
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, realpathSync, renameSync as renameSync5, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1781
|
+
import { dirname as dirname5, join as join4, resolve as resolve3, sep } from "node:path";
|
|
1437
1782
|
var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
|
|
1438
1783
|
var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
|
|
1439
1784
|
var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
|
|
1440
1785
|
var SERVICE_NGINX_DIRECTORY = "/etc/nginx/conf.d";
|
|
1441
|
-
var
|
|
1786
|
+
var idFor2 = (key) => createHash4("sha256").update(key).digest("hex").slice(0, 24);
|
|
1442
1787
|
var statePath = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
|
|
1443
1788
|
var within = (root, path2) => path2 === root || path2.startsWith(`${root}${sep}`);
|
|
1444
|
-
var
|
|
1789
|
+
var defaultHost2 = {
|
|
1445
1790
|
write(path2, content, mode) {
|
|
1446
|
-
|
|
1791
|
+
mkdirSync5(dirname5(path2), { recursive: true, mode: 493 });
|
|
1447
1792
|
const next = `${path2}.next`;
|
|
1448
|
-
|
|
1449
|
-
|
|
1793
|
+
writeFileSync5(next, content, { mode });
|
|
1794
|
+
renameSync5(next, path2);
|
|
1450
1795
|
},
|
|
1451
1796
|
read: (path2) => readFileSync4(path2, "utf8"),
|
|
1452
1797
|
exists: existsSync4,
|
|
1453
1798
|
list: (path2) => existsSync4(path2) ? readdirSync(path2) : [],
|
|
1454
1799
|
realpath: realpathSync,
|
|
1455
|
-
mkdir: (path2, mode) =>
|
|
1800
|
+
mkdir: (path2, mode) => mkdirSync5(path2, { recursive: true, mode }),
|
|
1456
1801
|
remove: (path2) => rmSync4(path2, { force: true }),
|
|
1457
1802
|
async exec(argv2) {
|
|
1458
1803
|
const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: {
|
|
@@ -1511,7 +1856,7 @@ function allocatedPorts(request, id, previous, states) {
|
|
|
1511
1856
|
if (previous && previous.applicationPorts.length === required && previous.applicationPorts.every((port) => port >= allocation.from && port <= allocation.to && !occupied.has(port)))
|
|
1512
1857
|
return [...previous.applicationPorts];
|
|
1513
1858
|
const width = allocation.to - allocation.from + 1;
|
|
1514
|
-
const start = Number.parseInt(
|
|
1859
|
+
const start = Number.parseInt(createHash4("sha256").update(request.key).digest("hex").slice(0, 8), 16) % width;
|
|
1515
1860
|
for (let offset = 0;offset < width; offset += 1) {
|
|
1516
1861
|
const first = allocation.from + (start + offset) % width;
|
|
1517
1862
|
const candidate = Array.from({ length: required }, (_, index) => first + index);
|
|
@@ -1569,12 +1914,12 @@ ${request.service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
|
|
|
1569
1914
|
}
|
|
1570
1915
|
}
|
|
1571
1916
|
`;
|
|
1572
|
-
async function
|
|
1917
|
+
async function checked3(host, argv2, code) {
|
|
1573
1918
|
const result = await host.exec(argv2);
|
|
1574
1919
|
if (result.exitCode !== 0)
|
|
1575
1920
|
throw new Error(`${code}: ${result.output.slice(0, 512)}`);
|
|
1576
1921
|
}
|
|
1577
|
-
async function activateSupervisedService(request, host =
|
|
1922
|
+
async function activateSupervisedService(request, host = defaultHost2) {
|
|
1578
1923
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/.test(request.key) || !/^[a-f0-9]{40}$/.test(request.revision)) {
|
|
1579
1924
|
throw new Error("SERVICE_IDENTITY_INVALID");
|
|
1580
1925
|
}
|
|
@@ -1583,7 +1928,7 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
1583
1928
|
if (!within(root, release))
|
|
1584
1929
|
throw new Error("SERVICE_RELEASE_OUTSIDE_ROOT");
|
|
1585
1930
|
request = { ...request, root, release, service: validateDeploymentService(request.service) };
|
|
1586
|
-
const id =
|
|
1931
|
+
const id = idFor2(request.key);
|
|
1587
1932
|
host.mkdir(SERVICE_STATE_DIRECTORY, 448);
|
|
1588
1933
|
host.mkdir(SERVICE_CONFIG_DIRECTORY, 457);
|
|
1589
1934
|
const previous = readState(host, statePath(id));
|
|
@@ -1594,11 +1939,11 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
1594
1939
|
const unitName = `forgezero-app-${id}-slot${nextSlot}.service`;
|
|
1595
1940
|
host.write(configPath, appConfig({ ...request, release }, port), 292);
|
|
1596
1941
|
host.write(`${SERVICE_UNIT_DIRECTORY}/${unitName}`, unit(id, nextSlot), 420);
|
|
1597
|
-
await
|
|
1942
|
+
await checked3(host, ["/usr/bin/systemctl", "daemon-reload"], "SERVICE_UNIT_RELOAD_FAILED");
|
|
1598
1943
|
if (request.service.strategy === "direct" && previous) {
|
|
1599
1944
|
await host.exec(["/usr/bin/systemctl", "stop", `forgezero-app-${id}-slot${previous.activeSlot}.service`]);
|
|
1600
1945
|
}
|
|
1601
|
-
await
|
|
1946
|
+
await checked3(host, ["/usr/bin/systemctl", "restart", unitName], "SERVICE_START_FAILED");
|
|
1602
1947
|
let healthy = false;
|
|
1603
1948
|
for (let attempt = 0;attempt < 30; attempt += 1) {
|
|
1604
1949
|
const active = await host.exec(["/usr/bin/systemctl", "is-active", "--quiet", unitName]);
|
|
@@ -1616,8 +1961,8 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
1616
1961
|
const oldNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
|
|
1617
1962
|
try {
|
|
1618
1963
|
host.write(nginxPath, nginx(request, id, port), 420);
|
|
1619
|
-
await
|
|
1620
|
-
await
|
|
1964
|
+
await checked3(host, ["/usr/sbin/nginx", "-t"], "SERVICE_NGINX_INVALID");
|
|
1965
|
+
await checked3(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "SERVICE_NGINX_RELOAD_FAILED");
|
|
1621
1966
|
} catch (cause) {
|
|
1622
1967
|
if (oldNginx === null)
|
|
1623
1968
|
host.remove(nginxPath);
|
|
@@ -1648,22 +1993,22 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
1648
1993
|
}
|
|
1649
1994
|
|
|
1650
1995
|
// src/container-supervisor.ts
|
|
1651
|
-
import { createHash as
|
|
1652
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
1653
|
-
import { dirname as
|
|
1654
|
-
var
|
|
1996
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
1997
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync5, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync6, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1998
|
+
import { dirname as dirname6, resolve as resolve4, sep as sep2 } from "node:path";
|
|
1999
|
+
var defaultHost3 = {
|
|
1655
2000
|
realpath: realpathSync2,
|
|
1656
2001
|
exists: existsSync5,
|
|
1657
2002
|
read: (path2) => readFileSync5(path2, "utf8"),
|
|
1658
2003
|
write(path2, content, mode) {
|
|
1659
|
-
|
|
2004
|
+
mkdirSync6(dirname6(path2), { recursive: true, mode: 493 });
|
|
1660
2005
|
const next = `${path2}.next`;
|
|
1661
|
-
|
|
1662
|
-
|
|
2006
|
+
writeFileSync6(next, content, { mode });
|
|
2007
|
+
renameSync6(next, path2);
|
|
1663
2008
|
},
|
|
1664
2009
|
remove: (path2) => rmSync5(path2, { force: true }),
|
|
1665
2010
|
list: (path2) => existsSync5(path2) ? readdirSync2(path2) : [],
|
|
1666
|
-
mkdir: (path2, mode) =>
|
|
2011
|
+
mkdir: (path2, mode) => mkdirSync6(path2, { recursive: true, mode }),
|
|
1667
2012
|
async exec(argv2) {
|
|
1668
2013
|
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" } });
|
|
1669
2014
|
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
@@ -1681,9 +2026,41 @@ var defaultHost2 = {
|
|
|
1681
2026
|
now: Date.now
|
|
1682
2027
|
};
|
|
1683
2028
|
var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
|
|
1684
|
-
var
|
|
2029
|
+
var idFor3 = (key) => createHash5("sha256").update(key).digest("hex").slice(0, 24);
|
|
1685
2030
|
var statePath2 = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
|
|
1686
2031
|
var nameFor = (id, slot) => `forgezero-${id}-slot${slot}`;
|
|
2032
|
+
function backendFor(application) {
|
|
2033
|
+
const runtime = application.runtime;
|
|
2034
|
+
if (runtime.kind !== "container")
|
|
2035
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
2036
|
+
if (runtime.provider === "forgezero.docker") {
|
|
2037
|
+
if (runtime.runtimeClass && runtime.runtimeClass !== "runc")
|
|
2038
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
2039
|
+
return { engine: "docker", runtimeClass: "runc", prefix: ["/usr/bin/docker"] };
|
|
2040
|
+
}
|
|
2041
|
+
if (runtime.provider === "forgezero.containerd") {
|
|
2042
|
+
if ((runtime.runtimeClass ?? "runc") !== "runc")
|
|
2043
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
2044
|
+
return {
|
|
2045
|
+
engine: "containerd",
|
|
2046
|
+
runtimeClass: "runc",
|
|
2047
|
+
prefix: ["/usr/local/bin/nerdctl", "--namespace", "forgezero"],
|
|
2048
|
+
runtimeName: "io.containerd.runc.v2"
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
if (runtime.provider === "forgezero.kata") {
|
|
2052
|
+
if (runtime.runtimeClass !== "kata-qemu-snp")
|
|
2053
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
2054
|
+
return {
|
|
2055
|
+
engine: "containerd",
|
|
2056
|
+
runtimeClass: "kata-qemu-snp",
|
|
2057
|
+
prefix: ["/usr/local/bin/nerdctl", "--namespace", "forgezero"],
|
|
2058
|
+
runtimeName: "io.containerd.kata.v2"
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_UNSUPPORTED");
|
|
2062
|
+
}
|
|
2063
|
+
var command2 = (backend, ...argv2) => [...backend.prefix, ...argv2];
|
|
1687
2064
|
function validateRequest(request, host) {
|
|
1688
2065
|
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))
|
|
1689
2066
|
throw new Error("CONTAINER_IDENTITY_INVALID");
|
|
@@ -1694,6 +2071,7 @@ function validateRequest(request, host) {
|
|
|
1694
2071
|
const application = request.application;
|
|
1695
2072
|
if (application.kind !== "application" || application.runtime.kind !== "container")
|
|
1696
2073
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
2074
|
+
backendFor(application);
|
|
1697
2075
|
if (!application.resources.cpu || !application.resources.memory || !application.resources.pids)
|
|
1698
2076
|
throw new Error("CONTAINER_LIMITS_REQUIRED");
|
|
1699
2077
|
if (application.runtime.security.privileged !== false || application.runtime.security.noNewPrivileges !== true || !application.runtime.security.dropCapabilities.includes("ALL"))
|
|
@@ -1704,23 +2082,24 @@ function validateRequest(request, host) {
|
|
|
1704
2082
|
throw new Error("CONTAINER_STABLE_PORT_REQUIRED");
|
|
1705
2083
|
return { ...request, root, release };
|
|
1706
2084
|
}
|
|
1707
|
-
async function
|
|
2085
|
+
async function checked4(host, argv2, code) {
|
|
1708
2086
|
const result = await host.exec(argv2);
|
|
1709
2087
|
if (result.exitCode !== 0)
|
|
1710
2088
|
throw new Error(`${code}: ${result.output.slice(0, 512)}`);
|
|
1711
2089
|
return result.output.trim();
|
|
1712
2090
|
}
|
|
1713
|
-
async function buildContainerImage(requestValue, host =
|
|
2091
|
+
async function buildContainerImage(requestValue, host = defaultHost3) {
|
|
1714
2092
|
const request = validateRequest(requestValue, host);
|
|
1715
2093
|
const runtime = request.application.runtime;
|
|
1716
2094
|
if (runtime.kind !== "container")
|
|
1717
2095
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
2096
|
+
const backend = backendFor(request.application);
|
|
1718
2097
|
const source = runtime.image.source;
|
|
1719
2098
|
if (source.kind === "registry") {
|
|
1720
2099
|
if (!source.reference.includes("@sha256:"))
|
|
1721
2100
|
throw new Error("CONTAINER_IMAGE_DIGEST_REQUIRED");
|
|
1722
|
-
await
|
|
1723
|
-
const digest2 = await
|
|
2101
|
+
await checked4(host, command2(backend, "pull", "--quiet", source.reference), "CONTAINER_PULL_FAILED");
|
|
2102
|
+
const digest2 = await checked4(host, command2(backend, "image", "inspect", "--format={{.Id}}", source.reference), "CONTAINER_INSPECT_FAILED");
|
|
1724
2103
|
if (!/^sha256:[a-f0-9]{64}$/.test(digest2))
|
|
1725
2104
|
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
1726
2105
|
return { image: source.reference, digest: digest2 };
|
|
@@ -1729,23 +2108,9 @@ async function buildContainerImage(requestValue, host = defaultHost2) {
|
|
|
1729
2108
|
const dockerfile = host.realpath(resolve4(request.release, source.dockerfile));
|
|
1730
2109
|
if (!within2(request.release, context) || !within2(context, dockerfile))
|
|
1731
2110
|
throw new Error("CONTAINER_BUILD_PATH_INVALID");
|
|
1732
|
-
const tag = `forgezero/${
|
|
1733
|
-
await
|
|
1734
|
-
|
|
1735
|
-
"build",
|
|
1736
|
-
"--pull",
|
|
1737
|
-
...request.noCache ? ["--no-cache"] : [],
|
|
1738
|
-
"--file",
|
|
1739
|
-
dockerfile,
|
|
1740
|
-
"--tag",
|
|
1741
|
-
tag,
|
|
1742
|
-
"--label",
|
|
1743
|
-
`net.forgezero.revision=${request.revision}`,
|
|
1744
|
-
"--label",
|
|
1745
|
-
`net.forgezero.component=${request.component}`,
|
|
1746
|
-
context
|
|
1747
|
-
], "CONTAINER_BUILD_FAILED");
|
|
1748
|
-
const digest = await checked3(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", tag], "CONTAINER_INSPECT_FAILED");
|
|
2111
|
+
const tag = `forgezero/${idFor3(request.key)}:${request.revision}`;
|
|
2112
|
+
await checked4(host, command2(backend, "build", "--pull", ...request.noCache ? ["--no-cache"] : [], "--file", dockerfile, "--tag", tag, "--label", `net.forgezero.revision=${request.revision}`, "--label", `net.forgezero.component=${request.component}`, context), "CONTAINER_BUILD_FAILED");
|
|
2113
|
+
const digest = await checked4(host, command2(backend, "image", "inspect", "--format={{.Id}}", tag), "CONTAINER_INSPECT_FAILED");
|
|
1749
2114
|
if (!/^sha256:[a-f0-9]{64}$/.test(digest))
|
|
1750
2115
|
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
1751
2116
|
return { image: tag, digest };
|
|
@@ -1813,7 +2178,7 @@ ${service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
|
|
|
1813
2178
|
}
|
|
1814
2179
|
`;
|
|
1815
2180
|
}
|
|
1816
|
-
function runArgv(request, id, slot, port, networkName) {
|
|
2181
|
+
function runArgv(request, backend, id, slot, port, networkName) {
|
|
1817
2182
|
const { application } = request;
|
|
1818
2183
|
const resources = application.resources;
|
|
1819
2184
|
const runtime = application.runtime;
|
|
@@ -1821,35 +2186,7 @@ function runArgv(request, id, slot, port, networkName) {
|
|
|
1821
2186
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1822
2187
|
const memory = resources.memory;
|
|
1823
2188
|
const network = application.network.container;
|
|
1824
|
-
const argv2 = [
|
|
1825
|
-
"/usr/bin/docker",
|
|
1826
|
-
"run",
|
|
1827
|
-
"--detach",
|
|
1828
|
-
"--name",
|
|
1829
|
-
nameFor(id, slot),
|
|
1830
|
-
"--restart",
|
|
1831
|
-
"unless-stopped",
|
|
1832
|
-
"--label",
|
|
1833
|
-
`net.forgezero.key=${request.key}`,
|
|
1834
|
-
"--label",
|
|
1835
|
-
`net.forgezero.revision=${request.revision}`,
|
|
1836
|
-
"--cpus",
|
|
1837
|
-
String(resources.cpu.limit),
|
|
1838
|
-
"--cpu-shares",
|
|
1839
|
-
String(resources.cpu.weight ?? 100),
|
|
1840
|
-
"--memory",
|
|
1841
|
-
`${memory.limitMiB}m`,
|
|
1842
|
-
...memory.reservationMiB ? ["--memory-reservation", `${memory.reservationMiB}m`] : [],
|
|
1843
|
-
...memory.swap === "disabled" ? ["--memory-swap", `${memory.limitMiB}m`] : [],
|
|
1844
|
-
"--pids-limit",
|
|
1845
|
-
String(resources.pids.limit),
|
|
1846
|
-
"--security-opt",
|
|
1847
|
-
"no-new-privileges=true",
|
|
1848
|
-
"--cap-drop",
|
|
1849
|
-
"ALL",
|
|
1850
|
-
...runtime.security.root === "read-only" ? ["--read-only"] : [],
|
|
1851
|
-
...network.mode === "host" ? ["--network", "host", "--env", `FZ_APP_PORT=${port}`] : ["--network", networkName, "--publish", `127.0.0.1:${port}:${application.service.port}`]
|
|
1852
|
-
];
|
|
2189
|
+
const argv2 = command2(backend, "run", "--detach", "--name", nameFor(id, slot), "--restart", "unless-stopped", "--label", `net.forgezero.key=${request.key}`, "--label", `net.forgezero.revision=${request.revision}`, ...backend.runtimeName ? ["--runtime", backend.runtimeName] : [], "--cpus", String(resources.cpu.limit), "--cpu-shares", String(resources.cpu.weight ?? 100), "--memory", `${memory.limitMiB}m`, ...memory.reservationMiB ? ["--memory-reservation", `${memory.reservationMiB}m`] : [], ...memory.swap === "disabled" ? ["--memory-swap", `${memory.limitMiB}m`] : [], "--pids-limit", String(resources.pids.limit), "--security-opt", "no-new-privileges=true", "--cap-drop", "ALL", ...runtime.security.root === "read-only" ? ["--read-only"] : [], ...network.mode === "host" ? ["--network", "host", "--env", `FZ_APP_PORT=${port}`] : ["--network", networkName, "--publish", `127.0.0.1:${port}:${application.service.port}`]);
|
|
1853
2190
|
for (const mount of application.storage ?? []) {
|
|
1854
2191
|
if (mount.class === "ephemeral")
|
|
1855
2192
|
argv2.push("--tmpfs", `${mount.path}:rw,noexec,nosuid,size=${mount.sizeMiB}m`);
|
|
@@ -1859,11 +2196,12 @@ function runArgv(request, id, slot, port, networkName) {
|
|
|
1859
2196
|
argv2.push(request.image, ...runtime.entrypoint ?? []);
|
|
1860
2197
|
return argv2;
|
|
1861
2198
|
}
|
|
1862
|
-
async function activateContainer(requestValue, host =
|
|
2199
|
+
async function activateContainer(requestValue, host = defaultHost3) {
|
|
1863
2200
|
const request = validateRequest(requestValue, host);
|
|
1864
2201
|
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:"))
|
|
1865
2202
|
throw new Error("CONTAINER_IMAGE_INVALID");
|
|
1866
|
-
const
|
|
2203
|
+
const backend = backendFor(request.application);
|
|
2204
|
+
const id = idFor3(request.key);
|
|
1867
2205
|
host.mkdir(SERVICE_STATE_DIRECTORY, 448);
|
|
1868
2206
|
host.mkdir(SERVICE_NGINX_DIRECTORY, 493);
|
|
1869
2207
|
const previous = readState2(host, id);
|
|
@@ -1875,9 +2213,9 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1875
2213
|
if (containerNetwork.mode === "bridge") {
|
|
1876
2214
|
networkName = containerNetwork.network ? `forgezero-${id}-${containerNetwork.network}` : "bridge";
|
|
1877
2215
|
if (networkName !== "bridge") {
|
|
1878
|
-
const inspected = await host.exec(
|
|
2216
|
+
const inspected = await host.exec(command2(backend, "network", "inspect", '--format={{index .Labels "net.forgezero.owner"}}', networkName));
|
|
1879
2217
|
if (inspected.exitCode !== 0) {
|
|
1880
|
-
await
|
|
2218
|
+
await checked4(host, command2(backend, "network", "create", "--driver", "bridge", "--label", `net.forgezero.owner=${id}`, networkName), "CONTAINER_NETWORK_CREATE_FAILED");
|
|
1881
2219
|
} else if (inspected.output.trim() !== id)
|
|
1882
2220
|
throw new Error("CONTAINER_NETWORK_OWNERSHIP_INVALID");
|
|
1883
2221
|
}
|
|
@@ -1886,8 +2224,8 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1886
2224
|
const nextSlot = slots === 2 && previous?.activeSlot === 0 ? 1 : 0;
|
|
1887
2225
|
const port = ports[nextSlot];
|
|
1888
2226
|
const name = nameFor(id, nextSlot);
|
|
1889
|
-
await host.exec(
|
|
1890
|
-
await
|
|
2227
|
+
await host.exec(command2(backend, "rm", "--force", name));
|
|
2228
|
+
await checked4(host, runArgv(request, backend, id, nextSlot, port, networkName), "CONTAINER_START_FAILED");
|
|
1891
2229
|
let healthy = false;
|
|
1892
2230
|
const health = request.application.service.health;
|
|
1893
2231
|
for (let attempt = 0;attempt < (health.attempts ?? 30); attempt += 1) {
|
|
@@ -1898,15 +2236,28 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1898
2236
|
await host.sleep(health.intervalMs ?? 1000);
|
|
1899
2237
|
}
|
|
1900
2238
|
if (!healthy) {
|
|
1901
|
-
await host.exec(
|
|
2239
|
+
await host.exec(command2(backend, "rm", "--force", name));
|
|
1902
2240
|
throw new Error("CONTAINER_HEALTH_FAILED");
|
|
1903
2241
|
}
|
|
2242
|
+
let runtimeEvidence;
|
|
2243
|
+
if (backend.engine === "containerd") {
|
|
2244
|
+
const raw = await checked4(host, ["/usr/bin/ctr", "--namespace", "forgezero", "containers", "info", name], "CONTAINER_RUNTIME_EVIDENCE_FAILED");
|
|
2245
|
+
let observed = "";
|
|
2246
|
+
try {
|
|
2247
|
+
observed = String(JSON.parse(raw).Runtime?.Name ?? "");
|
|
2248
|
+
} catch {
|
|
2249
|
+
throw new Error("CONTAINER_RUNTIME_EVIDENCE_INVALID");
|
|
2250
|
+
}
|
|
2251
|
+
if (observed !== backend.runtimeName)
|
|
2252
|
+
throw new Error("CONTAINER_RUNTIME_DOWNGRADE");
|
|
2253
|
+
runtimeEvidence = observed;
|
|
2254
|
+
}
|
|
1904
2255
|
const nginxPath = `${SERVICE_NGINX_DIRECTORY}/forgezero-${id}.conf`;
|
|
1905
2256
|
const previousNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
|
|
1906
2257
|
host.write(nginxPath, nginx2(request, id, port), 420);
|
|
1907
2258
|
try {
|
|
1908
|
-
await
|
|
1909
|
-
await
|
|
2259
|
+
await checked4(host, ["/usr/sbin/nginx", "-t"], "CONTAINER_NGINX_INVALID");
|
|
2260
|
+
await checked4(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "CONTAINER_NGINX_RELOAD_FAILED");
|
|
1910
2261
|
} catch (cause) {
|
|
1911
2262
|
if (previousNginx === null)
|
|
1912
2263
|
host.remove(nginxPath);
|
|
@@ -1914,7 +2265,7 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1914
2265
|
host.write(nginxPath, previousNginx, 420);
|
|
1915
2266
|
await host.exec(["/usr/sbin/nginx", "-t"]);
|
|
1916
2267
|
await host.exec(["/usr/bin/systemctl", "reload", "nginx.service"]);
|
|
1917
|
-
await host.exec(
|
|
2268
|
+
await host.exec(command2(backend, "rm", "--force", name));
|
|
1918
2269
|
throw cause;
|
|
1919
2270
|
}
|
|
1920
2271
|
const state = {
|
|
@@ -1927,13 +2278,18 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1927
2278
|
applicationPorts: ports,
|
|
1928
2279
|
activeSlot: nextSlot,
|
|
1929
2280
|
healthPath: health.path,
|
|
2281
|
+
containerRuntime: {
|
|
2282
|
+
engine: backend.engine,
|
|
2283
|
+
runtimeClass: backend.runtimeClass,
|
|
2284
|
+
...backend.engine === "containerd" ? { namespace: "forgezero", evidence: runtimeEvidence } : {}
|
|
2285
|
+
},
|
|
1930
2286
|
updatedAtTs: host.now()
|
|
1931
2287
|
};
|
|
1932
2288
|
host.write(statePath2(id), `${JSON.stringify(state, null, 2)}
|
|
1933
2289
|
`, 384);
|
|
1934
2290
|
if (previous && previous.activeSlot !== nextSlot) {
|
|
1935
2291
|
await host.sleep(request.application.rollout.drainMs ?? 30000);
|
|
1936
|
-
await host.exec(
|
|
2292
|
+
await host.exec(command2(backend, "rm", "--force", nameFor(id, previous.activeSlot)));
|
|
1937
2293
|
}
|
|
1938
2294
|
return state;
|
|
1939
2295
|
}
|
|
@@ -1948,11 +2304,12 @@ function startSoftwareHelper(options = {}) {
|
|
|
1948
2304
|
const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
|
|
1949
2305
|
if (existsSync6(socketPath))
|
|
1950
2306
|
unlinkSync3(socketPath);
|
|
1951
|
-
|
|
2307
|
+
mkdirSync7(dirname7(socketPath), { recursive: true, mode: 488 });
|
|
1952
2308
|
const ensure = options.ensure ?? ensureSoftwareRequirements;
|
|
1953
2309
|
const activate = options.activate ?? activateSupervisedService;
|
|
1954
2310
|
const buildContainer = options.buildContainer ?? buildContainerImage;
|
|
1955
2311
|
const activateTypedContainer = options.activateContainer ?? activateContainer;
|
|
2312
|
+
const applyConnectivity = options.applyConnectivity ?? applyDeploymentConnectivity;
|
|
1956
2313
|
let tail = Promise.resolve();
|
|
1957
2314
|
let pending = 0;
|
|
1958
2315
|
const server = createServer2((socket) => {
|
|
@@ -2009,6 +2366,12 @@ function startSoftwareHelper(options = {}) {
|
|
|
2009
2366
|
throw new Error("container deployment root is not owned by this helper");
|
|
2010
2367
|
const state = await activateTypedContainer(containerRequest);
|
|
2011
2368
|
socket.end(`${JSON.stringify({ ok: true, state })}
|
|
2369
|
+
`);
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
if (request.op === "apply-connectivity" && request.request) {
|
|
2373
|
+
const result = await applyConnectivity(request.request);
|
|
2374
|
+
socket.end(`${JSON.stringify({ ok: true, result })}
|
|
2012
2375
|
`);
|
|
2013
2376
|
return;
|
|
2014
2377
|
}
|
|
@@ -2066,6 +2429,9 @@ function requestContainerBuild(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOC
|
|
|
2066
2429
|
function requestContainerActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
2067
2430
|
return requestContainer("activate-container", request, socketPath, timeoutMs);
|
|
2068
2431
|
}
|
|
2432
|
+
function requestDeploymentConnectivity(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
2433
|
+
return requestContainer("apply-connectivity", request, socketPath, timeoutMs);
|
|
2434
|
+
}
|
|
2069
2435
|
function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
|
|
2070
2436
|
return new Promise((resolve5, reject) => {
|
|
2071
2437
|
const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
|
|
@@ -2125,7 +2491,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
2125
2491
|
}
|
|
2126
2492
|
|
|
2127
2493
|
// src/version.ts
|
|
2128
|
-
var VERSION3 = "0.1.
|
|
2494
|
+
var VERSION3 = "0.1.75";
|
|
2129
2495
|
|
|
2130
2496
|
// src/egress-policy.ts
|
|
2131
2497
|
import { realpathSync as realpathSync3 } from "node:fs";
|