@forgezero/agent 0.1.74 → 0.1.76
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 +126 -17
- package/dist/agent-heartbeat.d.ts +25 -1
- package/dist/agent-heartbeat.js +45 -5
- package/dist/bootstrap.js +10 -7
- package/dist/community-rehearsal-host.js +218 -13
- 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 +155 -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 +1122 -313
- package/dist/fz.js +234 -26
- 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 +11 -9
- package/dist/platform-bootstrap-runtime.js +209 -10
- package/dist/platform-fleet-verification.js +525 -155
- package/dist/platform-genesis.js +206 -7
- package/dist/process-input.d.ts +11 -0
- package/dist/provision.js +472 -99
- package/dist/service-supervisor.d.ts +6 -0
- package/dist/software-helper.d.ts +3 -0
- package/dist/software-helper.js +472 -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
|
@@ -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,187 @@ 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
|
+
|
|
1632
|
+
// src/process-input.ts
|
|
1633
|
+
async function writeAndCloseProcessInput(input, value) {
|
|
1634
|
+
input.write(value);
|
|
1635
|
+
await input.end();
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// src/deployment-connectivity.ts
|
|
1639
|
+
var TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
|
|
1640
|
+
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;
|
|
1641
|
+
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])?$/;
|
|
1642
|
+
var NAME2 = /^[a-z][a-z0-9._-]{0,99}$/;
|
|
1643
|
+
var idFor = (key) => createHash3("sha256").update(key).digest("hex").slice(0, 20);
|
|
1644
|
+
var checked2 = async (host, argv2, label) => {
|
|
1645
|
+
const result = await host.exec(argv2);
|
|
1646
|
+
if (result.exitCode !== 0)
|
|
1647
|
+
throw new Error(`${label} failed: ${result.output.slice(0, 512)}`);
|
|
1648
|
+
return result.output.trim();
|
|
1649
|
+
};
|
|
1650
|
+
async function sealWithSystemd(name, path2, value) {
|
|
1651
|
+
mkdirSync4(dirname4(path2), { recursive: true, mode: 448 });
|
|
1652
|
+
const next = `${path2}.next`;
|
|
1653
|
+
const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
|
|
1654
|
+
stdin: "pipe",
|
|
1655
|
+
stdout: "pipe",
|
|
1656
|
+
stderr: "pipe",
|
|
1657
|
+
env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" }
|
|
1658
|
+
});
|
|
1659
|
+
await writeAndCloseProcessInput(child.stdin, value);
|
|
1660
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
1661
|
+
new Response(child.stdout).text(),
|
|
1662
|
+
new Response(child.stderr).text(),
|
|
1663
|
+
child.exited
|
|
1664
|
+
]);
|
|
1665
|
+
if (exitCode !== 0)
|
|
1666
|
+
throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
|
|
1667
|
+
renameSync4(next, path2);
|
|
1668
|
+
}
|
|
1669
|
+
var defaultHost = {
|
|
1670
|
+
seal: sealWithSystemd,
|
|
1671
|
+
write(path2, content, mode) {
|
|
1672
|
+
mkdirSync4(dirname4(path2), { recursive: true, mode: 493 });
|
|
1673
|
+
const next = `${path2}.next`;
|
|
1674
|
+
writeFileSync4(next, content, { mode });
|
|
1675
|
+
renameSync4(next, path2);
|
|
1676
|
+
},
|
|
1677
|
+
async exec(argv2) {
|
|
1678
|
+
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" } });
|
|
1679
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
1680
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
1681
|
+
},
|
|
1682
|
+
sleep: (ms) => Bun.sleep(ms)
|
|
1683
|
+
};
|
|
1684
|
+
function validate(request) {
|
|
1685
|
+
if (!request.key || request.key.length > 256)
|
|
1686
|
+
throw new Error("deployment connectivity key is invalid");
|
|
1687
|
+
const publicIntent = request.intent.public;
|
|
1688
|
+
const privateIntent = request.intent.private;
|
|
1689
|
+
if (publicIntent?.mode === "cloudflare-tunnel") {
|
|
1690
|
+
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)) {
|
|
1691
|
+
throw new Error("public deployment connectivity is malformed");
|
|
1692
|
+
}
|
|
1693
|
+
} else if (request.capabilities.public)
|
|
1694
|
+
throw new Error("unexpected public deployment capability");
|
|
1695
|
+
if (privateIntent?.mode === "cloudflare-warp") {
|
|
1696
|
+
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)) {
|
|
1697
|
+
throw new Error("private deployment connectivity is malformed");
|
|
1698
|
+
}
|
|
1699
|
+
} else if (request.capabilities.private)
|
|
1700
|
+
throw new Error("unexpected private deployment capability");
|
|
1701
|
+
}
|
|
1702
|
+
async function applyDeploymentConnectivity(request, host = defaultHost) {
|
|
1703
|
+
validate(request);
|
|
1704
|
+
const id = idFor(request.key);
|
|
1705
|
+
const evidence = { key: request.key };
|
|
1706
|
+
if (request.intent.public?.mode === "cloudflare-tunnel") {
|
|
1707
|
+
const capability = request.capabilities.public;
|
|
1708
|
+
const credentialName = `FZ_TUNNEL_${id.toUpperCase()}`;
|
|
1709
|
+
const credentialPath = `/etc/forgezero/creds/deployment-tunnel-${id}.cred`;
|
|
1710
|
+
const unit = `forgezero-cloudflared-${id}.service`;
|
|
1711
|
+
const unitPath = `/etc/systemd/system/${unit}`;
|
|
1712
|
+
const metricsPort = 20000 + Number.parseInt(id.slice(0, 4), 16) % 20000;
|
|
1713
|
+
await host.seal(credentialName, credentialPath, capability.connectorToken);
|
|
1714
|
+
host.write(unitPath, `[Unit]
|
|
1715
|
+
Description=ForgeZero deployment Tunnel ${id}
|
|
1716
|
+
After=network-online.target
|
|
1717
|
+
Wants=network-online.target
|
|
1718
|
+
|
|
1719
|
+
[Service]
|
|
1720
|
+
Type=simple
|
|
1721
|
+
DynamicUser=yes
|
|
1722
|
+
LoadCredentialEncrypted=${credentialName}:${credentialPath}
|
|
1723
|
+
ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --metrics 127.0.0.1:${metricsPort} run --token-file %d/${credentialName}
|
|
1724
|
+
Restart=always
|
|
1725
|
+
RestartSec=5
|
|
1726
|
+
NoNewPrivileges=true
|
|
1727
|
+
PrivateTmp=true
|
|
1728
|
+
ProtectSystem=strict
|
|
1729
|
+
ProtectHome=true
|
|
1730
|
+
|
|
1731
|
+
[Install]
|
|
1732
|
+
WantedBy=multi-user.target
|
|
1733
|
+
`, 420);
|
|
1734
|
+
await checked2(host, ["/usr/bin/systemctl", "daemon-reload"], "cloudflared daemon reload");
|
|
1735
|
+
await checked2(host, ["/usr/bin/systemctl", "enable", "--now", unit], "cloudflared start");
|
|
1736
|
+
await checked2(host, ["/usr/bin/systemctl", "is-active", "--quiet", unit], "cloudflared readiness");
|
|
1737
|
+
evidence.public = { hostname: request.intent.public.hostname, tunnelId: capability.tunnelId, unit, active: true };
|
|
1738
|
+
}
|
|
1739
|
+
if (request.intent.private?.mode === "cloudflare-warp") {
|
|
1740
|
+
const capability = request.capabilities.private;
|
|
1741
|
+
const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
|
|
1742
|
+
const unit = "forgezero-deployment-mesh.service";
|
|
1743
|
+
await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
|
|
1744
|
+
host.write(`/etc/systemd/system/${unit}`, `[Unit]
|
|
1745
|
+
Description=ForgeZero deployment Mesh/WARP connector
|
|
1746
|
+
Requires=warp-svc.service
|
|
1747
|
+
After=network-online.target warp-svc.service
|
|
1748
|
+
Wants=network-online.target
|
|
1749
|
+
|
|
1750
|
+
[Service]
|
|
1751
|
+
Type=oneshot
|
|
1752
|
+
RemainAfterExit=yes
|
|
1753
|
+
LoadCredentialEncrypted=CF_WARP_CONNECTOR_TOKEN:${credentialPath}
|
|
1754
|
+
ExecStart=/usr/local/bin/fz-agent mesh-config
|
|
1755
|
+
NoNewPrivileges=true
|
|
1756
|
+
PrivateTmp=true
|
|
1757
|
+
ProtectSystem=strict
|
|
1758
|
+
ProtectHome=true
|
|
1759
|
+
ReadWritePaths=/var/lib/cloudflare-warp
|
|
1760
|
+
|
|
1761
|
+
[Install]
|
|
1762
|
+
WantedBy=multi-user.target
|
|
1763
|
+
`, 420);
|
|
1764
|
+
await checked2(host, ["/usr/bin/systemctl", "daemon-reload"], "WARP daemon reload");
|
|
1765
|
+
await checked2(host, ["/usr/bin/systemctl", "enable", "--now", "warp-svc.service", unit], "WARP connector start");
|
|
1766
|
+
for (let attempt = 0;attempt < 10; attempt += 1) {
|
|
1767
|
+
const status = await host.exec(["/usr/bin/warp-cli", "--accept-tos", "status"]);
|
|
1768
|
+
if (status.exitCode === 0 && /\bconnected\b/i.test(status.output) && !/\bdisconnected\b/i.test(status.output)) {
|
|
1769
|
+
evidence.private = { network: request.intent.private.network, connectorId: capability.connectorId, unit, active: true };
|
|
1770
|
+
break;
|
|
1771
|
+
}
|
|
1772
|
+
await host.sleep(1000);
|
|
1773
|
+
}
|
|
1774
|
+
if (!evidence.private)
|
|
1775
|
+
throw new Error("WARP connector readiness failed");
|
|
1776
|
+
}
|
|
1777
|
+
return evidence;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1428
1780
|
// src/software-helper.ts
|
|
1429
|
-
import { chmodSync as chmodSync4, existsSync as existsSync6, mkdirSync as
|
|
1781
|
+
import { chmodSync as chmodSync4, existsSync as existsSync6, mkdirSync as mkdirSync7, unlinkSync as unlinkSync3 } from "node:fs";
|
|
1430
1782
|
import { connect as connect2, createServer as createServer2 } from "node:net";
|
|
1431
|
-
import { dirname as
|
|
1783
|
+
import { dirname as dirname7 } from "node:path";
|
|
1432
1784
|
|
|
1433
1785
|
// src/service-supervisor.ts
|
|
1434
|
-
import { createHash as
|
|
1435
|
-
import { existsSync as existsSync4, mkdirSync as
|
|
1436
|
-
import { dirname as
|
|
1786
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1787
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, realpathSync, renameSync as renameSync5, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1788
|
+
import { dirname as dirname5, join as join4, resolve as resolve3, sep } from "node:path";
|
|
1437
1789
|
var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
|
|
1438
1790
|
var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
|
|
1439
1791
|
var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
|
|
1440
1792
|
var SERVICE_NGINX_DIRECTORY = "/etc/nginx/conf.d";
|
|
1441
|
-
var
|
|
1793
|
+
var idFor2 = (key) => createHash4("sha256").update(key).digest("hex").slice(0, 24);
|
|
1442
1794
|
var statePath = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
|
|
1443
1795
|
var within = (root, path2) => path2 === root || path2.startsWith(`${root}${sep}`);
|
|
1444
|
-
var
|
|
1796
|
+
var defaultHost2 = {
|
|
1445
1797
|
write(path2, content, mode) {
|
|
1446
|
-
|
|
1798
|
+
mkdirSync5(dirname5(path2), { recursive: true, mode: 493 });
|
|
1447
1799
|
const next = `${path2}.next`;
|
|
1448
|
-
|
|
1449
|
-
|
|
1800
|
+
writeFileSync5(next, content, { mode });
|
|
1801
|
+
renameSync5(next, path2);
|
|
1450
1802
|
},
|
|
1451
1803
|
read: (path2) => readFileSync4(path2, "utf8"),
|
|
1452
1804
|
exists: existsSync4,
|
|
1453
1805
|
list: (path2) => existsSync4(path2) ? readdirSync(path2) : [],
|
|
1454
1806
|
realpath: realpathSync,
|
|
1455
|
-
mkdir: (path2, mode) =>
|
|
1807
|
+
mkdir: (path2, mode) => mkdirSync5(path2, { recursive: true, mode }),
|
|
1456
1808
|
remove: (path2) => rmSync4(path2, { force: true }),
|
|
1457
1809
|
async exec(argv2) {
|
|
1458
1810
|
const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: {
|
|
@@ -1511,7 +1863,7 @@ function allocatedPorts(request, id, previous, states) {
|
|
|
1511
1863
|
if (previous && previous.applicationPorts.length === required && previous.applicationPorts.every((port) => port >= allocation.from && port <= allocation.to && !occupied.has(port)))
|
|
1512
1864
|
return [...previous.applicationPorts];
|
|
1513
1865
|
const width = allocation.to - allocation.from + 1;
|
|
1514
|
-
const start = Number.parseInt(
|
|
1866
|
+
const start = Number.parseInt(createHash4("sha256").update(request.key).digest("hex").slice(0, 8), 16) % width;
|
|
1515
1867
|
for (let offset = 0;offset < width; offset += 1) {
|
|
1516
1868
|
const first = allocation.from + (start + offset) % width;
|
|
1517
1869
|
const candidate = Array.from({ length: required }, (_, index) => first + index);
|
|
@@ -1569,12 +1921,12 @@ ${request.service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
|
|
|
1569
1921
|
}
|
|
1570
1922
|
}
|
|
1571
1923
|
`;
|
|
1572
|
-
async function
|
|
1924
|
+
async function checked3(host, argv2, code) {
|
|
1573
1925
|
const result = await host.exec(argv2);
|
|
1574
1926
|
if (result.exitCode !== 0)
|
|
1575
1927
|
throw new Error(`${code}: ${result.output.slice(0, 512)}`);
|
|
1576
1928
|
}
|
|
1577
|
-
async function activateSupervisedService(request, host =
|
|
1929
|
+
async function activateSupervisedService(request, host = defaultHost2) {
|
|
1578
1930
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,255}$/.test(request.key) || !/^[a-f0-9]{40}$/.test(request.revision)) {
|
|
1579
1931
|
throw new Error("SERVICE_IDENTITY_INVALID");
|
|
1580
1932
|
}
|
|
@@ -1583,7 +1935,7 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
1583
1935
|
if (!within(root, release))
|
|
1584
1936
|
throw new Error("SERVICE_RELEASE_OUTSIDE_ROOT");
|
|
1585
1937
|
request = { ...request, root, release, service: validateDeploymentService(request.service) };
|
|
1586
|
-
const id =
|
|
1938
|
+
const id = idFor2(request.key);
|
|
1587
1939
|
host.mkdir(SERVICE_STATE_DIRECTORY, 448);
|
|
1588
1940
|
host.mkdir(SERVICE_CONFIG_DIRECTORY, 457);
|
|
1589
1941
|
const previous = readState(host, statePath(id));
|
|
@@ -1594,11 +1946,11 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
1594
1946
|
const unitName = `forgezero-app-${id}-slot${nextSlot}.service`;
|
|
1595
1947
|
host.write(configPath, appConfig({ ...request, release }, port), 292);
|
|
1596
1948
|
host.write(`${SERVICE_UNIT_DIRECTORY}/${unitName}`, unit(id, nextSlot), 420);
|
|
1597
|
-
await
|
|
1949
|
+
await checked3(host, ["/usr/bin/systemctl", "daemon-reload"], "SERVICE_UNIT_RELOAD_FAILED");
|
|
1598
1950
|
if (request.service.strategy === "direct" && previous) {
|
|
1599
1951
|
await host.exec(["/usr/bin/systemctl", "stop", `forgezero-app-${id}-slot${previous.activeSlot}.service`]);
|
|
1600
1952
|
}
|
|
1601
|
-
await
|
|
1953
|
+
await checked3(host, ["/usr/bin/systemctl", "restart", unitName], "SERVICE_START_FAILED");
|
|
1602
1954
|
let healthy = false;
|
|
1603
1955
|
for (let attempt = 0;attempt < 30; attempt += 1) {
|
|
1604
1956
|
const active = await host.exec(["/usr/bin/systemctl", "is-active", "--quiet", unitName]);
|
|
@@ -1616,8 +1968,8 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
1616
1968
|
const oldNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
|
|
1617
1969
|
try {
|
|
1618
1970
|
host.write(nginxPath, nginx(request, id, port), 420);
|
|
1619
|
-
await
|
|
1620
|
-
await
|
|
1971
|
+
await checked3(host, ["/usr/sbin/nginx", "-t"], "SERVICE_NGINX_INVALID");
|
|
1972
|
+
await checked3(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "SERVICE_NGINX_RELOAD_FAILED");
|
|
1621
1973
|
} catch (cause) {
|
|
1622
1974
|
if (oldNginx === null)
|
|
1623
1975
|
host.remove(nginxPath);
|
|
@@ -1648,22 +2000,22 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
1648
2000
|
}
|
|
1649
2001
|
|
|
1650
2002
|
// src/container-supervisor.ts
|
|
1651
|
-
import { createHash as
|
|
1652
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
1653
|
-
import { dirname as
|
|
1654
|
-
var
|
|
2003
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
2004
|
+
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";
|
|
2005
|
+
import { dirname as dirname6, resolve as resolve4, sep as sep2 } from "node:path";
|
|
2006
|
+
var defaultHost3 = {
|
|
1655
2007
|
realpath: realpathSync2,
|
|
1656
2008
|
exists: existsSync5,
|
|
1657
2009
|
read: (path2) => readFileSync5(path2, "utf8"),
|
|
1658
2010
|
write(path2, content, mode) {
|
|
1659
|
-
|
|
2011
|
+
mkdirSync6(dirname6(path2), { recursive: true, mode: 493 });
|
|
1660
2012
|
const next = `${path2}.next`;
|
|
1661
|
-
|
|
1662
|
-
|
|
2013
|
+
writeFileSync6(next, content, { mode });
|
|
2014
|
+
renameSync6(next, path2);
|
|
1663
2015
|
},
|
|
1664
2016
|
remove: (path2) => rmSync5(path2, { force: true }),
|
|
1665
2017
|
list: (path2) => existsSync5(path2) ? readdirSync2(path2) : [],
|
|
1666
|
-
mkdir: (path2, mode) =>
|
|
2018
|
+
mkdir: (path2, mode) => mkdirSync6(path2, { recursive: true, mode }),
|
|
1667
2019
|
async exec(argv2) {
|
|
1668
2020
|
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
2021
|
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
@@ -1681,9 +2033,41 @@ var defaultHost2 = {
|
|
|
1681
2033
|
now: Date.now
|
|
1682
2034
|
};
|
|
1683
2035
|
var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
|
|
1684
|
-
var
|
|
2036
|
+
var idFor3 = (key) => createHash5("sha256").update(key).digest("hex").slice(0, 24);
|
|
1685
2037
|
var statePath2 = (id) => `${SERVICE_STATE_DIRECTORY}/${id}.json`;
|
|
1686
2038
|
var nameFor = (id, slot) => `forgezero-${id}-slot${slot}`;
|
|
2039
|
+
function backendFor(application) {
|
|
2040
|
+
const runtime = application.runtime;
|
|
2041
|
+
if (runtime.kind !== "container")
|
|
2042
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
2043
|
+
if (runtime.provider === "forgezero.docker") {
|
|
2044
|
+
if (runtime.runtimeClass && runtime.runtimeClass !== "runc")
|
|
2045
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
2046
|
+
return { engine: "docker", runtimeClass: "runc", prefix: ["/usr/bin/docker"] };
|
|
2047
|
+
}
|
|
2048
|
+
if (runtime.provider === "forgezero.containerd") {
|
|
2049
|
+
if ((runtime.runtimeClass ?? "runc") !== "runc")
|
|
2050
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
2051
|
+
return {
|
|
2052
|
+
engine: "containerd",
|
|
2053
|
+
runtimeClass: "runc",
|
|
2054
|
+
prefix: ["/usr/local/bin/nerdctl", "--namespace", "forgezero"],
|
|
2055
|
+
runtimeName: "io.containerd.runc.v2"
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
if (runtime.provider === "forgezero.kata") {
|
|
2059
|
+
if (runtime.runtimeClass !== "kata-qemu-snp")
|
|
2060
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
2061
|
+
return {
|
|
2062
|
+
engine: "containerd",
|
|
2063
|
+
runtimeClass: "kata-qemu-snp",
|
|
2064
|
+
prefix: ["/usr/local/bin/nerdctl", "--namespace", "forgezero"],
|
|
2065
|
+
runtimeName: "io.containerd.kata.v2"
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_UNSUPPORTED");
|
|
2069
|
+
}
|
|
2070
|
+
var command2 = (backend, ...argv2) => [...backend.prefix, ...argv2];
|
|
1687
2071
|
function validateRequest(request, host) {
|
|
1688
2072
|
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
2073
|
throw new Error("CONTAINER_IDENTITY_INVALID");
|
|
@@ -1694,6 +2078,7 @@ function validateRequest(request, host) {
|
|
|
1694
2078
|
const application = request.application;
|
|
1695
2079
|
if (application.kind !== "application" || application.runtime.kind !== "container")
|
|
1696
2080
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
2081
|
+
backendFor(application);
|
|
1697
2082
|
if (!application.resources.cpu || !application.resources.memory || !application.resources.pids)
|
|
1698
2083
|
throw new Error("CONTAINER_LIMITS_REQUIRED");
|
|
1699
2084
|
if (application.runtime.security.privileged !== false || application.runtime.security.noNewPrivileges !== true || !application.runtime.security.dropCapabilities.includes("ALL"))
|
|
@@ -1704,23 +2089,24 @@ function validateRequest(request, host) {
|
|
|
1704
2089
|
throw new Error("CONTAINER_STABLE_PORT_REQUIRED");
|
|
1705
2090
|
return { ...request, root, release };
|
|
1706
2091
|
}
|
|
1707
|
-
async function
|
|
2092
|
+
async function checked4(host, argv2, code) {
|
|
1708
2093
|
const result = await host.exec(argv2);
|
|
1709
2094
|
if (result.exitCode !== 0)
|
|
1710
2095
|
throw new Error(`${code}: ${result.output.slice(0, 512)}`);
|
|
1711
2096
|
return result.output.trim();
|
|
1712
2097
|
}
|
|
1713
|
-
async function buildContainerImage(requestValue, host =
|
|
2098
|
+
async function buildContainerImage(requestValue, host = defaultHost3) {
|
|
1714
2099
|
const request = validateRequest(requestValue, host);
|
|
1715
2100
|
const runtime = request.application.runtime;
|
|
1716
2101
|
if (runtime.kind !== "container")
|
|
1717
2102
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
2103
|
+
const backend = backendFor(request.application);
|
|
1718
2104
|
const source = runtime.image.source;
|
|
1719
2105
|
if (source.kind === "registry") {
|
|
1720
2106
|
if (!source.reference.includes("@sha256:"))
|
|
1721
2107
|
throw new Error("CONTAINER_IMAGE_DIGEST_REQUIRED");
|
|
1722
|
-
await
|
|
1723
|
-
const digest2 = await
|
|
2108
|
+
await checked4(host, command2(backend, "pull", "--quiet", source.reference), "CONTAINER_PULL_FAILED");
|
|
2109
|
+
const digest2 = await checked4(host, command2(backend, "image", "inspect", "--format={{.Id}}", source.reference), "CONTAINER_INSPECT_FAILED");
|
|
1724
2110
|
if (!/^sha256:[a-f0-9]{64}$/.test(digest2))
|
|
1725
2111
|
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
1726
2112
|
return { image: source.reference, digest: digest2 };
|
|
@@ -1729,23 +2115,9 @@ async function buildContainerImage(requestValue, host = defaultHost2) {
|
|
|
1729
2115
|
const dockerfile = host.realpath(resolve4(request.release, source.dockerfile));
|
|
1730
2116
|
if (!within2(request.release, context) || !within2(context, dockerfile))
|
|
1731
2117
|
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");
|
|
2118
|
+
const tag = `forgezero/${idFor3(request.key)}:${request.revision}`;
|
|
2119
|
+
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");
|
|
2120
|
+
const digest = await checked4(host, command2(backend, "image", "inspect", "--format={{.Id}}", tag), "CONTAINER_INSPECT_FAILED");
|
|
1749
2121
|
if (!/^sha256:[a-f0-9]{64}$/.test(digest))
|
|
1750
2122
|
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
1751
2123
|
return { image: tag, digest };
|
|
@@ -1813,7 +2185,7 @@ ${service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
|
|
|
1813
2185
|
}
|
|
1814
2186
|
`;
|
|
1815
2187
|
}
|
|
1816
|
-
function runArgv(request, id, slot, port, networkName) {
|
|
2188
|
+
function runArgv(request, backend, id, slot, port, networkName) {
|
|
1817
2189
|
const { application } = request;
|
|
1818
2190
|
const resources = application.resources;
|
|
1819
2191
|
const runtime = application.runtime;
|
|
@@ -1821,35 +2193,7 @@ function runArgv(request, id, slot, port, networkName) {
|
|
|
1821
2193
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
1822
2194
|
const memory = resources.memory;
|
|
1823
2195
|
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
|
-
];
|
|
2196
|
+
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
2197
|
for (const mount of application.storage ?? []) {
|
|
1854
2198
|
if (mount.class === "ephemeral")
|
|
1855
2199
|
argv2.push("--tmpfs", `${mount.path}:rw,noexec,nosuid,size=${mount.sizeMiB}m`);
|
|
@@ -1859,11 +2203,12 @@ function runArgv(request, id, slot, port, networkName) {
|
|
|
1859
2203
|
argv2.push(request.image, ...runtime.entrypoint ?? []);
|
|
1860
2204
|
return argv2;
|
|
1861
2205
|
}
|
|
1862
|
-
async function activateContainer(requestValue, host =
|
|
2206
|
+
async function activateContainer(requestValue, host = defaultHost3) {
|
|
1863
2207
|
const request = validateRequest(requestValue, host);
|
|
1864
2208
|
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
2209
|
throw new Error("CONTAINER_IMAGE_INVALID");
|
|
1866
|
-
const
|
|
2210
|
+
const backend = backendFor(request.application);
|
|
2211
|
+
const id = idFor3(request.key);
|
|
1867
2212
|
host.mkdir(SERVICE_STATE_DIRECTORY, 448);
|
|
1868
2213
|
host.mkdir(SERVICE_NGINX_DIRECTORY, 493);
|
|
1869
2214
|
const previous = readState2(host, id);
|
|
@@ -1875,9 +2220,9 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1875
2220
|
if (containerNetwork.mode === "bridge") {
|
|
1876
2221
|
networkName = containerNetwork.network ? `forgezero-${id}-${containerNetwork.network}` : "bridge";
|
|
1877
2222
|
if (networkName !== "bridge") {
|
|
1878
|
-
const inspected = await host.exec(
|
|
2223
|
+
const inspected = await host.exec(command2(backend, "network", "inspect", '--format={{index .Labels "net.forgezero.owner"}}', networkName));
|
|
1879
2224
|
if (inspected.exitCode !== 0) {
|
|
1880
|
-
await
|
|
2225
|
+
await checked4(host, command2(backend, "network", "create", "--driver", "bridge", "--label", `net.forgezero.owner=${id}`, networkName), "CONTAINER_NETWORK_CREATE_FAILED");
|
|
1881
2226
|
} else if (inspected.output.trim() !== id)
|
|
1882
2227
|
throw new Error("CONTAINER_NETWORK_OWNERSHIP_INVALID");
|
|
1883
2228
|
}
|
|
@@ -1886,8 +2231,8 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1886
2231
|
const nextSlot = slots === 2 && previous?.activeSlot === 0 ? 1 : 0;
|
|
1887
2232
|
const port = ports[nextSlot];
|
|
1888
2233
|
const name = nameFor(id, nextSlot);
|
|
1889
|
-
await host.exec(
|
|
1890
|
-
await
|
|
2234
|
+
await host.exec(command2(backend, "rm", "--force", name));
|
|
2235
|
+
await checked4(host, runArgv(request, backend, id, nextSlot, port, networkName), "CONTAINER_START_FAILED");
|
|
1891
2236
|
let healthy = false;
|
|
1892
2237
|
const health = request.application.service.health;
|
|
1893
2238
|
for (let attempt = 0;attempt < (health.attempts ?? 30); attempt += 1) {
|
|
@@ -1898,15 +2243,28 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1898
2243
|
await host.sleep(health.intervalMs ?? 1000);
|
|
1899
2244
|
}
|
|
1900
2245
|
if (!healthy) {
|
|
1901
|
-
await host.exec(
|
|
2246
|
+
await host.exec(command2(backend, "rm", "--force", name));
|
|
1902
2247
|
throw new Error("CONTAINER_HEALTH_FAILED");
|
|
1903
2248
|
}
|
|
2249
|
+
let runtimeEvidence;
|
|
2250
|
+
if (backend.engine === "containerd") {
|
|
2251
|
+
const raw = await checked4(host, ["/usr/bin/ctr", "--namespace", "forgezero", "containers", "info", name], "CONTAINER_RUNTIME_EVIDENCE_FAILED");
|
|
2252
|
+
let observed = "";
|
|
2253
|
+
try {
|
|
2254
|
+
observed = String(JSON.parse(raw).Runtime?.Name ?? "");
|
|
2255
|
+
} catch {
|
|
2256
|
+
throw new Error("CONTAINER_RUNTIME_EVIDENCE_INVALID");
|
|
2257
|
+
}
|
|
2258
|
+
if (observed !== backend.runtimeName)
|
|
2259
|
+
throw new Error("CONTAINER_RUNTIME_DOWNGRADE");
|
|
2260
|
+
runtimeEvidence = observed;
|
|
2261
|
+
}
|
|
1904
2262
|
const nginxPath = `${SERVICE_NGINX_DIRECTORY}/forgezero-${id}.conf`;
|
|
1905
2263
|
const previousNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
|
|
1906
2264
|
host.write(nginxPath, nginx2(request, id, port), 420);
|
|
1907
2265
|
try {
|
|
1908
|
-
await
|
|
1909
|
-
await
|
|
2266
|
+
await checked4(host, ["/usr/sbin/nginx", "-t"], "CONTAINER_NGINX_INVALID");
|
|
2267
|
+
await checked4(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "CONTAINER_NGINX_RELOAD_FAILED");
|
|
1910
2268
|
} catch (cause) {
|
|
1911
2269
|
if (previousNginx === null)
|
|
1912
2270
|
host.remove(nginxPath);
|
|
@@ -1914,7 +2272,7 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1914
2272
|
host.write(nginxPath, previousNginx, 420);
|
|
1915
2273
|
await host.exec(["/usr/sbin/nginx", "-t"]);
|
|
1916
2274
|
await host.exec(["/usr/bin/systemctl", "reload", "nginx.service"]);
|
|
1917
|
-
await host.exec(
|
|
2275
|
+
await host.exec(command2(backend, "rm", "--force", name));
|
|
1918
2276
|
throw cause;
|
|
1919
2277
|
}
|
|
1920
2278
|
const state = {
|
|
@@ -1927,13 +2285,18 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
1927
2285
|
applicationPorts: ports,
|
|
1928
2286
|
activeSlot: nextSlot,
|
|
1929
2287
|
healthPath: health.path,
|
|
2288
|
+
containerRuntime: {
|
|
2289
|
+
engine: backend.engine,
|
|
2290
|
+
runtimeClass: backend.runtimeClass,
|
|
2291
|
+
...backend.engine === "containerd" ? { namespace: "forgezero", evidence: runtimeEvidence } : {}
|
|
2292
|
+
},
|
|
1930
2293
|
updatedAtTs: host.now()
|
|
1931
2294
|
};
|
|
1932
2295
|
host.write(statePath2(id), `${JSON.stringify(state, null, 2)}
|
|
1933
2296
|
`, 384);
|
|
1934
2297
|
if (previous && previous.activeSlot !== nextSlot) {
|
|
1935
2298
|
await host.sleep(request.application.rollout.drainMs ?? 30000);
|
|
1936
|
-
await host.exec(
|
|
2299
|
+
await host.exec(command2(backend, "rm", "--force", nameFor(id, previous.activeSlot)));
|
|
1937
2300
|
}
|
|
1938
2301
|
return state;
|
|
1939
2302
|
}
|
|
@@ -1948,11 +2311,12 @@ function startSoftwareHelper(options = {}) {
|
|
|
1948
2311
|
const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
|
|
1949
2312
|
if (existsSync6(socketPath))
|
|
1950
2313
|
unlinkSync3(socketPath);
|
|
1951
|
-
|
|
2314
|
+
mkdirSync7(dirname7(socketPath), { recursive: true, mode: 488 });
|
|
1952
2315
|
const ensure = options.ensure ?? ensureSoftwareRequirements;
|
|
1953
2316
|
const activate = options.activate ?? activateSupervisedService;
|
|
1954
2317
|
const buildContainer = options.buildContainer ?? buildContainerImage;
|
|
1955
2318
|
const activateTypedContainer = options.activateContainer ?? activateContainer;
|
|
2319
|
+
const applyConnectivity = options.applyConnectivity ?? applyDeploymentConnectivity;
|
|
1956
2320
|
let tail = Promise.resolve();
|
|
1957
2321
|
let pending = 0;
|
|
1958
2322
|
const server = createServer2((socket) => {
|
|
@@ -2009,6 +2373,12 @@ function startSoftwareHelper(options = {}) {
|
|
|
2009
2373
|
throw new Error("container deployment root is not owned by this helper");
|
|
2010
2374
|
const state = await activateTypedContainer(containerRequest);
|
|
2011
2375
|
socket.end(`${JSON.stringify({ ok: true, state })}
|
|
2376
|
+
`);
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
if (request.op === "apply-connectivity" && request.request) {
|
|
2380
|
+
const result = await applyConnectivity(request.request);
|
|
2381
|
+
socket.end(`${JSON.stringify({ ok: true, result })}
|
|
2012
2382
|
`);
|
|
2013
2383
|
return;
|
|
2014
2384
|
}
|
|
@@ -2066,6 +2436,9 @@ function requestContainerBuild(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOC
|
|
|
2066
2436
|
function requestContainerActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
2067
2437
|
return requestContainer("activate-container", request, socketPath, timeoutMs);
|
|
2068
2438
|
}
|
|
2439
|
+
function requestDeploymentConnectivity(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
2440
|
+
return requestContainer("apply-connectivity", request, socketPath, timeoutMs);
|
|
2441
|
+
}
|
|
2069
2442
|
function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
|
|
2070
2443
|
return new Promise((resolve5, reject) => {
|
|
2071
2444
|
const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
|
|
@@ -2125,7 +2498,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
2125
2498
|
}
|
|
2126
2499
|
|
|
2127
2500
|
// src/version.ts
|
|
2128
|
-
var VERSION3 = "0.1.
|
|
2501
|
+
var VERSION3 = "0.1.76";
|
|
2129
2502
|
|
|
2130
2503
|
// src/egress-policy.ts
|
|
2131
2504
|
import { realpathSync as realpathSync3 } from "node:fs";
|
|
@@ -3259,15 +3632,15 @@ import {
|
|
|
3259
3632
|
copyFileSync as copyFileSync2,
|
|
3260
3633
|
existsSync as existsSync7,
|
|
3261
3634
|
lstatSync,
|
|
3262
|
-
mkdirSync as
|
|
3635
|
+
mkdirSync as mkdirSync8,
|
|
3263
3636
|
readFileSync as readFileSync6,
|
|
3264
3637
|
readdirSync as readdirSync3,
|
|
3265
3638
|
realpathSync as realpathSync4,
|
|
3266
|
-
renameSync as
|
|
3639
|
+
renameSync as renameSync7,
|
|
3267
3640
|
rmSync as rmSync6,
|
|
3268
|
-
statSync,
|
|
3641
|
+
statSync as statSync2,
|
|
3269
3642
|
symlinkSync as symlinkSync3,
|
|
3270
|
-
writeFileSync as
|
|
3643
|
+
writeFileSync as writeFileSync7
|
|
3271
3644
|
} from "node:fs";
|
|
3272
3645
|
import { join as join5 } from "node:path";
|
|
3273
3646
|
var safeAtom = (name, value) => {
|
|
@@ -3686,7 +4059,7 @@ var replaceLink = (path2, target) => {
|
|
|
3686
4059
|
return;
|
|
3687
4060
|
}
|
|
3688
4061
|
symlinkSync3(target, pending);
|
|
3689
|
-
|
|
4062
|
+
renameSync7(pending, path2);
|
|
3690
4063
|
};
|
|
3691
4064
|
var secureRelease = (path2, uid, gid) => {
|
|
3692
4065
|
const visit = (current2) => {
|
|
@@ -3709,7 +4082,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
3709
4082
|
if (!release.startsWith(`${releases}/`) || release === releases)
|
|
3710
4083
|
throw new Error("release is outside configured releases directory");
|
|
3711
4084
|
for (const required of ["forgezero.deploy.ts", ".fz/deploy.plan.json", "src/index.ts", "bun.lock"]) {
|
|
3712
|
-
const metadata =
|
|
4085
|
+
const metadata = statSync2(join5(release, required));
|
|
3713
4086
|
if (!metadata.isFile() || metadata.size < 1)
|
|
3714
4087
|
throw new Error(`release is incomplete: ${required}`);
|
|
3715
4088
|
}
|
|
@@ -3717,7 +4090,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
3717
4090
|
const request = options.fetch ?? fetch;
|
|
3718
4091
|
const sleep = options.sleep ?? Bun.sleep;
|
|
3719
4092
|
const slots = join5(normalized.root, "slots");
|
|
3720
|
-
|
|
4093
|
+
mkdirSync8(slots, { recursive: true, mode: 493 });
|
|
3721
4094
|
const slotFile = join5(normalized.root, ".forge-slot");
|
|
3722
4095
|
const previousSlot = existsSync7(slotFile) ? readFileSync6(slotFile, "utf8").trim() : undefined;
|
|
3723
4096
|
const target = previousSlot === "blue" ? "green" : "blue";
|
|
@@ -3763,13 +4136,13 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
3763
4136
|
copyFileSync2(upstream, backup);
|
|
3764
4137
|
else
|
|
3765
4138
|
rmSync6(backup, { force: true });
|
|
3766
|
-
|
|
4139
|
+
writeFileSync7(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
|
|
3767
4140
|
`, { mode: 420 });
|
|
3768
4141
|
const test = await exec(["/usr/sbin/nginx", "-t"]);
|
|
3769
4142
|
const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
|
|
3770
4143
|
if (reload.exitCode !== 0) {
|
|
3771
4144
|
if (existsSync7(backup))
|
|
3772
|
-
|
|
4145
|
+
renameSync7(backup, upstream);
|
|
3773
4146
|
else
|
|
3774
4147
|
rmSync6(upstream, { force: true });
|
|
3775
4148
|
await exec(["/usr/sbin/nginx", "-t"]);
|
|
@@ -3778,13 +4151,13 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
3778
4151
|
throw new Error("nginx refused the promoted upstream");
|
|
3779
4152
|
}
|
|
3780
4153
|
rmSync6(backup, { force: true });
|
|
3781
|
-
|
|
4154
|
+
writeFileSync7(slotFile, `${target}
|
|
3782
4155
|
`, { mode: 420 });
|
|
3783
4156
|
if (previousSlot && previousSlot !== target) {
|
|
3784
4157
|
await sleep(normalized.drainDeadlineMs);
|
|
3785
4158
|
await exec(["/usr/bin/systemctl", "stop", `forgezero@${previousSlot}.service`]);
|
|
3786
4159
|
}
|
|
3787
|
-
const old = readdirSync3(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join5(releases, entry.name)).sort((left, right) =>
|
|
4160
|
+
const old = readdirSync3(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join5(releases, entry.name)).sort((left, right) => statSync2(right).mtimeMs - statSync2(left).mtimeMs).slice(normalized.keepReleases);
|
|
3788
4161
|
for (const path2 of old)
|
|
3789
4162
|
if (path2 !== release)
|
|
3790
4163
|
rmSync6(path2, { recursive: true, force: true });
|
|
@@ -3831,18 +4204,18 @@ function planLocalOtlpProof(endpoint, collectorUnit) {
|
|
|
3831
4204
|
import { lstatSync as lstatSync5 } from "node:fs";
|
|
3832
4205
|
|
|
3833
4206
|
// src/bootstrap.ts
|
|
3834
|
-
import { createHash as
|
|
4207
|
+
import { createHash as createHash7, createHmac as createHmac2, randomBytes as randomBytes3 } from "node:crypto";
|
|
3835
4208
|
import {
|
|
3836
4209
|
chmodSync as chmodSync8,
|
|
3837
4210
|
existsSync as existsSync10,
|
|
3838
4211
|
lstatSync as lstatSync4,
|
|
3839
|
-
mkdirSync as
|
|
4212
|
+
mkdirSync as mkdirSync11,
|
|
3840
4213
|
readFileSync as readFileSync9,
|
|
3841
|
-
renameSync as
|
|
4214
|
+
renameSync as renameSync10,
|
|
3842
4215
|
rmSync as rmSync9,
|
|
3843
|
-
writeFileSync as
|
|
4216
|
+
writeFileSync as writeFileSync10
|
|
3844
4217
|
} from "node:fs";
|
|
3845
|
-
import { dirname as
|
|
4218
|
+
import { dirname as dirname11 } from "node:path";
|
|
3846
4219
|
import { fileURLToPath } from "node:url";
|
|
3847
4220
|
|
|
3848
4221
|
// src/cli/agent-install.ts
|
|
@@ -3852,15 +4225,15 @@ import {
|
|
|
3852
4225
|
copyFileSync as copyFileSync3,
|
|
3853
4226
|
existsSync as existsSync8,
|
|
3854
4227
|
lstatSync as lstatSync2,
|
|
3855
|
-
mkdirSync as
|
|
4228
|
+
mkdirSync as mkdirSync9,
|
|
3856
4229
|
readFileSync as readFileSync7,
|
|
3857
4230
|
realpathSync as realpathSync5,
|
|
3858
|
-
renameSync as
|
|
4231
|
+
renameSync as renameSync8,
|
|
3859
4232
|
rmSync as rmSync7,
|
|
3860
4233
|
symlinkSync as symlinkSync4,
|
|
3861
|
-
writeFileSync as
|
|
4234
|
+
writeFileSync as writeFileSync8
|
|
3862
4235
|
} from "node:fs";
|
|
3863
|
-
import { dirname as
|
|
4236
|
+
import { dirname as dirname8 } from "node:path";
|
|
3864
4237
|
async function readCapabilities(run2) {
|
|
3865
4238
|
const answers = {};
|
|
3866
4239
|
const checks = Object.entries(CAPABILITY_CHECKS);
|
|
@@ -3881,10 +4254,8 @@ var fixed = async (argv2, stdin) => {
|
|
|
3881
4254
|
stderr: "pipe",
|
|
3882
4255
|
env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C", DEBIAN_FRONTEND: "noninteractive" }
|
|
3883
4256
|
});
|
|
3884
|
-
if (stdin !== undefined && child.stdin && typeof child.stdin !== "number")
|
|
3885
|
-
child.stdin
|
|
3886
|
-
child.stdin.end();
|
|
3887
|
-
}
|
|
4257
|
+
if (stdin !== undefined && child.stdin && typeof child.stdin !== "number")
|
|
4258
|
+
await writeAndCloseProcessInput(child.stdin, stdin);
|
|
3888
4259
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
3889
4260
|
new Response(child.stdout).text(),
|
|
3890
4261
|
new Response(child.stderr).text(),
|
|
@@ -3895,10 +4266,10 @@ var fixed = async (argv2, stdin) => {
|
|
|
3895
4266
|
var runProvisionOperation = async (operation) => {
|
|
3896
4267
|
if (operation.kind === "commands") {
|
|
3897
4268
|
let output = "";
|
|
3898
|
-
for (const
|
|
3899
|
-
const result = await fixed(
|
|
4269
|
+
for (const command3 of operation.commands) {
|
|
4270
|
+
const result = await fixed(command3.argv);
|
|
3900
4271
|
output += result.stdout;
|
|
3901
|
-
if (!(
|
|
4272
|
+
if (!(command3.acceptedExitCodes ?? [0]).includes(result.exitCode))
|
|
3902
4273
|
return { stdout: output, exitCode: result.exitCode };
|
|
3903
4274
|
}
|
|
3904
4275
|
return { stdout: output, exitCode: 0 };
|
|
@@ -3919,11 +4290,11 @@ var runProvisionOperation = async (operation) => {
|
|
|
3919
4290
|
}
|
|
3920
4291
|
if (operation.kind === "install-runtime") {
|
|
3921
4292
|
const release = `/opt/forgezero/agent/versions/${operation.version}`;
|
|
3922
|
-
|
|
3923
|
-
|
|
4293
|
+
mkdirSync9(`${release}/dist`, { recursive: true, mode: 493 });
|
|
4294
|
+
mkdirSync9(dirname8(operation.binary), { recursive: true, mode: 493 });
|
|
3924
4295
|
copyFileSync3(operation.source, `${release}/dist/fz-agent.js`);
|
|
3925
4296
|
chmodSync6(`${release}/dist/fz-agent.js`, 493);
|
|
3926
|
-
const gitSshSource = `${
|
|
4297
|
+
const gitSshSource = `${dirname8(operation.source)}/fz-git-ssh.js`;
|
|
3927
4298
|
if (!existsSync8(gitSshSource))
|
|
3928
4299
|
return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
|
|
3929
4300
|
copyFileSync3(gitSshSource, `${release}/dist/fz-git-ssh.js`);
|
|
@@ -3931,7 +4302,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
3931
4302
|
const pending = "/opt/forgezero/agent/current.next";
|
|
3932
4303
|
rmSync7(pending, { force: true });
|
|
3933
4304
|
symlinkSync4(`versions/${operation.version}`, pending);
|
|
3934
|
-
|
|
4305
|
+
renameSync8(pending, "/opt/forgezero/agent/current");
|
|
3935
4306
|
rmSync7(operation.binary, { force: true });
|
|
3936
4307
|
symlinkSync4("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
|
|
3937
4308
|
const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
|
|
@@ -3972,7 +4343,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
3972
4343
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
3973
4344
|
if (derived.exitCode !== 0)
|
|
3974
4345
|
return derived;
|
|
3975
|
-
|
|
4346
|
+
writeFileSync8(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
|
|
3976
4347
|
`, { mode: 292 });
|
|
3977
4348
|
}
|
|
3978
4349
|
return { stdout: "", exitCode: 0 };
|
|
@@ -4021,8 +4392,8 @@ var runProvisionOperation = async (operation) => {
|
|
|
4021
4392
|
if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
|
|
4022
4393
|
return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
|
|
4023
4394
|
}
|
|
4024
|
-
|
|
4025
|
-
|
|
4395
|
+
mkdirSync9(dirname8(operation.publicKey), { recursive: true, mode: 493 });
|
|
4396
|
+
writeFileSync8(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
|
|
4026
4397
|
`, { mode: 292 });
|
|
4027
4398
|
chmodSync6(operation.publicKey, 292);
|
|
4028
4399
|
}
|
|
@@ -4085,11 +4456,11 @@ var runProvisionOperation = async (operation) => {
|
|
|
4085
4456
|
const response = await fetch("https://pkg.cloudflareclient.com/pubkey.gpg", { signal: AbortSignal.timeout(30000) });
|
|
4086
4457
|
if (!response.ok)
|
|
4087
4458
|
return { stdout: `WARP key HTTP ${response.status}`, exitCode: 1 };
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4459
|
+
mkdirSync9("/usr/share/keyrings", { recursive: true, mode: 493 });
|
|
4460
|
+
mkdirSync9("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
|
|
4461
|
+
mkdirSync9("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
|
|
4091
4462
|
const key = "/run/cloudflare-warp-key.gpg";
|
|
4092
|
-
|
|
4463
|
+
writeFileSync8(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
|
|
4093
4464
|
let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
|
|
4094
4465
|
rmSync7(key, { force: true });
|
|
4095
4466
|
if (result.exitCode !== 0)
|
|
@@ -4097,7 +4468,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
4097
4468
|
const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
|
|
4098
4469
|
if (!codename)
|
|
4099
4470
|
return { stdout: "Ubuntu codename missing", exitCode: 1 };
|
|
4100
|
-
|
|
4471
|
+
writeFileSync8("/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
|
|
4101
4472
|
`, { mode: 420 });
|
|
4102
4473
|
result = await fixed(["/usr/bin/apt-get", "update", "-qq"]);
|
|
4103
4474
|
return result.exitCode === 0 ? fixed(["/usr/bin/apt-get", "install", "-y", "cloudflare-warp"]) : result;
|
|
@@ -4142,7 +4513,7 @@ async function applyPlan(plan, run2) {
|
|
|
4142
4513
|
import { constants } from "node:fs";
|
|
4143
4514
|
import { createHmac, randomUUID as randomUUID3 } from "node:crypto";
|
|
4144
4515
|
import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "node:fs/promises";
|
|
4145
|
-
import { dirname as
|
|
4516
|
+
import { dirname as dirname9, join as join6, resolve as resolve5 } from "node:path";
|
|
4146
4517
|
import { isIP as isIP3 } from "node:net";
|
|
4147
4518
|
|
|
4148
4519
|
// src/cloudflare-edge.ts
|
|
@@ -4156,19 +4527,19 @@ var UNIT = `/etc/systemd/system/${FORGEZERO_OTEL_COLLECTOR_UNIT}`;
|
|
|
4156
4527
|
import { DEFAULT_SOCKET as DEFAULT_SOCKET2 } from "@forgezero/vault";
|
|
4157
4528
|
|
|
4158
4529
|
// src/bootstrap-bundle.ts
|
|
4159
|
-
import { createHash as
|
|
4530
|
+
import { createHash as createHash6, randomBytes as randomBytes2 } from "node:crypto";
|
|
4160
4531
|
import {
|
|
4161
4532
|
chmodSync as chmodSync7,
|
|
4162
|
-
createReadStream,
|
|
4533
|
+
createReadStream as createReadStream2,
|
|
4163
4534
|
existsSync as existsSync9,
|
|
4164
4535
|
lstatSync as lstatSync3,
|
|
4165
|
-
mkdirSync as
|
|
4536
|
+
mkdirSync as mkdirSync10,
|
|
4166
4537
|
readFileSync as readFileSync8,
|
|
4167
|
-
renameSync as
|
|
4538
|
+
renameSync as renameSync9,
|
|
4168
4539
|
rmSync as rmSync8,
|
|
4169
|
-
writeFileSync as
|
|
4540
|
+
writeFileSync as writeFileSync9
|
|
4170
4541
|
} from "node:fs";
|
|
4171
|
-
import { dirname as
|
|
4542
|
+
import { dirname as dirname10, isAbsolute, resolve as resolve6 } from "node:path";
|
|
4172
4543
|
var BOOTSTRAP_BUNDLE_FORMAT = 1;
|
|
4173
4544
|
var BOOTSTRAP_BUNDLE_KIND = "forgezero-api-git-bundle";
|
|
4174
4545
|
var MAX_BOOTSTRAP_BUNDLE_BYTES = 512 * 1024 * 1024;
|
|
@@ -4462,8 +4833,7 @@ function localBootstrapHost() {
|
|
|
4462
4833
|
const execute = async (argv2, options = {}) => {
|
|
4463
4834
|
const child = Bun.spawn([...argv2], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
|
|
4464
4835
|
if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
|
|
4465
|
-
child.stdin
|
|
4466
|
-
child.stdin.end();
|
|
4836
|
+
await writeAndCloseProcessInput(child.stdin, options.stdin);
|
|
4467
4837
|
}
|
|
4468
4838
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
4469
4839
|
new Response(child.stdout).text(),
|
|
@@ -4477,13 +4847,13 @@ function localBootstrapHost() {
|
|
|
4477
4847
|
exists: existsSync10,
|
|
4478
4848
|
read: (path2) => readFileSync9(path2, "utf8"),
|
|
4479
4849
|
write(path2, content, mode) {
|
|
4480
|
-
|
|
4850
|
+
mkdirSync11(dirname11(path2), { recursive: true, mode: 493 });
|
|
4481
4851
|
const temporary = `${path2}.next.${process.pid}`;
|
|
4482
|
-
|
|
4852
|
+
writeFileSync10(temporary, content, { mode });
|
|
4483
4853
|
chmodSync8(temporary, mode);
|
|
4484
|
-
|
|
4854
|
+
renameSync10(temporary, path2);
|
|
4485
4855
|
},
|
|
4486
|
-
mkdir: (path2, mode) =>
|
|
4856
|
+
mkdir: (path2, mode) => mkdirSync11(path2, { recursive: true, mode }),
|
|
4487
4857
|
remove: (path2) => rmSync9(path2, { force: true }),
|
|
4488
4858
|
inspect(path2) {
|
|
4489
4859
|
const value = lstatSync4(path2);
|
|
@@ -4525,8 +4895,8 @@ function localBootstrapHost() {
|
|
|
4525
4895
|
databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
|
|
4526
4896
|
databasePorts: [8529]
|
|
4527
4897
|
};
|
|
4528
|
-
|
|
4529
|
-
|
|
4898
|
+
mkdirSync11(dirname11(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
|
|
4899
|
+
writeFileSync10(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
|
|
4530
4900
|
`, { mode: 256 });
|
|
4531
4901
|
}
|
|
4532
4902
|
const plan = planBootstrapAgentInstall(config, phase, {
|
|
@@ -4536,8 +4906,8 @@ function localBootstrapHost() {
|
|
|
4536
4906
|
initialBundle
|
|
4537
4907
|
});
|
|
4538
4908
|
for (const unit2 of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
|
|
4539
|
-
|
|
4540
|
-
|
|
4909
|
+
mkdirSync11(dirname11(unit2.path), { recursive: true, mode: 493 });
|
|
4910
|
+
writeFileSync10(unit2.path, unit2.unit, { mode: 420 });
|
|
4541
4911
|
}
|
|
4542
4912
|
await applyPlan(plan, localRunner);
|
|
4543
4913
|
return plan;
|