@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/fz-agent.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
6
|
-
import { readFileSync as readFileSync20, writeFileSync as
|
|
7
|
-
import { cpus, totalmem } from "os";
|
|
8
|
-
import { dirname as
|
|
6
|
+
import { readFileSync as readFileSync20, writeFileSync as writeFileSync18, existsSync as existsSync22, mkdirSync as mkdirSync18, chmodSync as chmodSync18, lstatSync as lstatSync9, statfsSync as statfsSync3 } from "fs";
|
|
7
|
+
import { cpus as cpus2, totalmem as totalmem2 } from "os";
|
|
8
|
+
import { dirname as dirname14, join as join12 } from "path";
|
|
9
9
|
|
|
10
10
|
// ../access/dist/security.js
|
|
11
11
|
var HEX = Array.from({ length: 256 }, (_, index) => index.toString(16).padStart(2, "0"));
|
|
@@ -4643,7 +4643,7 @@ function safeOp(line) {
|
|
|
4643
4643
|
// src/deployment.ts
|
|
4644
4644
|
import { chmodSync as chmodSync4, existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync3, statfsSync, writeFileSync as writeFileSync3 } from "fs";
|
|
4645
4645
|
import { createHash as createHash5, randomUUID } from "crypto";
|
|
4646
|
-
import { dirname, isAbsolute as isAbsolute2, join as join2 } from "path";
|
|
4646
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2 } from "path";
|
|
4647
4647
|
|
|
4648
4648
|
// ../runtime/dist/queue.js
|
|
4649
4649
|
class QueueStoppedError extends Error {
|
|
@@ -4953,22 +4953,30 @@ import {
|
|
|
4953
4953
|
accessSync,
|
|
4954
4954
|
chmodSync as chmodSync2,
|
|
4955
4955
|
copyFileSync,
|
|
4956
|
+
createReadStream,
|
|
4956
4957
|
existsSync as existsSync2,
|
|
4957
4958
|
mkdtempSync,
|
|
4958
4959
|
mkdirSync,
|
|
4959
4960
|
readFileSync,
|
|
4960
4961
|
renameSync,
|
|
4961
4962
|
rmSync,
|
|
4963
|
+
statSync,
|
|
4962
4964
|
symlinkSync,
|
|
4963
4965
|
unlinkSync as unlinkSync2,
|
|
4964
4966
|
writeFileSync
|
|
4965
4967
|
} from "fs";
|
|
4966
4968
|
import { tmpdir } from "os";
|
|
4967
|
-
import { join } from "path";
|
|
4969
|
+
import { dirname, join } from "path";
|
|
4968
4970
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
4969
4971
|
var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
|
|
4970
4972
|
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
4971
4973
|
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
4974
|
+
var NERDCTL_VERSION = "2.3.5";
|
|
4975
|
+
var BUILDKIT_VERSION = "0.32.2";
|
|
4976
|
+
var KATA_CONTAINERS_VERSION = "4.0.0";
|
|
4977
|
+
var NERDCTL_SHA256 = "de3206aeb7cbd5f20f5fb1f55c1e3bf2db1be567812a8a3f5e65eba2488347ee";
|
|
4978
|
+
var BUILDKIT_SHA256 = "2975d0f651ad96ba8b80b9992ae1f9a964f4408569af5b6dc36544165c3926af";
|
|
4979
|
+
var KATA_SHA256 = "2c3b9dfeba355582b40aee462b12916c9740654d0230f696adf719d67b063a8c";
|
|
4972
4980
|
var OS_CATALOG = [
|
|
4973
4981
|
{
|
|
4974
4982
|
id: "ubuntu",
|
|
@@ -4998,6 +5006,8 @@ var OS_CATALOG = [
|
|
|
4998
5006
|
var SOFTWARE_CATALOG = [
|
|
4999
5007
|
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
5000
5008
|
{ id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
5009
|
+
{ id: "containerd", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
5010
|
+
{ id: "kata-containers", version: "4.0.0", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
5001
5011
|
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
5002
5012
|
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
5003
5013
|
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -5006,6 +5016,7 @@ var SOFTWARE_CATALOG = [
|
|
|
5006
5016
|
{ id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
5007
5017
|
{ id: "git", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
5008
5018
|
{ id: "docker", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
5019
|
+
{ id: "containerd", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
5009
5020
|
{ id: "nginx", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
5010
5021
|
{ id: "ufw", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
5011
5022
|
{ id: "openssh-client", version: "ubuntu-24.04", status: "active", os: "ubuntu", osVersion: "24.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -5014,6 +5025,8 @@ var SOFTWARE_CATALOG = [
|
|
|
5014
5025
|
var UBUNTU_2604_X64 = [
|
|
5015
5026
|
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
5016
5027
|
{ requirement: { id: "docker", version: "ubuntu-26.04" } },
|
|
5028
|
+
{ requirement: { id: "containerd", version: "ubuntu-26.04" } },
|
|
5029
|
+
{ requirement: { id: "kata-containers", version: "4.0.0" } },
|
|
5017
5030
|
{ requirement: { id: "nginx", version: "ubuntu-26.04" } },
|
|
5018
5031
|
{ requirement: { id: "arangodb", version: "3.11.14" } },
|
|
5019
5032
|
{ requirement: { id: "cloudflared", version: "2026.7.3" } },
|
|
@@ -5025,6 +5038,7 @@ var UBUNTU_2604_X64 = [
|
|
|
5025
5038
|
var UBUNTU_2404_X64 = [
|
|
5026
5039
|
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
5027
5040
|
{ requirement: { id: "docker", version: "ubuntu-24.04" } },
|
|
5041
|
+
{ requirement: { id: "containerd", version: "ubuntu-24.04" } },
|
|
5028
5042
|
{ requirement: { id: "nginx", version: "ubuntu-24.04" } },
|
|
5029
5043
|
{ requirement: { id: "ufw", version: "ubuntu-24.04" } },
|
|
5030
5044
|
{ requirement: { id: "openssh-client", version: "ubuntu-24.04" } },
|
|
@@ -5041,6 +5055,46 @@ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
|
5041
5055
|
"log-opts": { "max-size": "10m", "max-file": "3" }
|
|
5042
5056
|
}, null, 2)}
|
|
5043
5057
|
`;
|
|
5058
|
+
var CONTAINERD_CONFIG_PATH = "/etc/containerd/config.toml";
|
|
5059
|
+
var KATA_SNP_CONFIG_PATH = "/opt/kata/share/defaults/kata-containers/configuration-qemu-snp.toml";
|
|
5060
|
+
var KATA_ACTIVE_CONFIG_PATH = "/etc/kata-containers/configuration.toml";
|
|
5061
|
+
var CONTAINERD_CONFIG = (kata) => `version = 2
|
|
5062
|
+
root = "/var/lib/forgezero/containerd"
|
|
5063
|
+
state = "/run/forgezero/containerd"
|
|
5064
|
+
|
|
5065
|
+
[grpc]
|
|
5066
|
+
address = "/run/containerd/containerd.sock"
|
|
5067
|
+
|
|
5068
|
+
[plugins."io.containerd.grpc.v1.cri".containerd]
|
|
5069
|
+
snapshotter = "overlayfs"
|
|
5070
|
+
default_runtime_name = "runc"
|
|
5071
|
+
|
|
5072
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
|
|
5073
|
+
runtime_type = "io.containerd.runc.v2"
|
|
5074
|
+
${kata ? `
|
|
5075
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-qemu-snp]
|
|
5076
|
+
runtime_type = "io.containerd.kata.v2"
|
|
5077
|
+
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-qemu-snp.options]
|
|
5078
|
+
ConfigPath = "${KATA_ACTIVE_CONFIG_PATH}"
|
|
5079
|
+
` : ""}`;
|
|
5080
|
+
var BUILDKIT_UNIT = `[Unit]
|
|
5081
|
+
Description=ForgeZero BuildKit daemon
|
|
5082
|
+
After=network-online.target containerd.service
|
|
5083
|
+
Wants=network-online.target
|
|
5084
|
+
Requires=containerd.service
|
|
5085
|
+
|
|
5086
|
+
[Service]
|
|
5087
|
+
Type=notify
|
|
5088
|
+
ExecStart=/usr/local/bin/buildkitd --addr unix:///run/buildkit/buildkitd.sock --root /var/lib/forgezero/buildkit
|
|
5089
|
+
Restart=on-failure
|
|
5090
|
+
RestartSec=2
|
|
5091
|
+
NoNewPrivileges=yes
|
|
5092
|
+
ProtectSystem=strict
|
|
5093
|
+
ReadWritePaths=/var/lib/forgezero/buildkit /run/buildkit
|
|
5094
|
+
|
|
5095
|
+
[Install]
|
|
5096
|
+
WantedBy=multi-user.target
|
|
5097
|
+
`;
|
|
5044
5098
|
var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
|
|
5045
5099
|
var runSoftwareCommand = async (argv, env = {}) => {
|
|
5046
5100
|
let child;
|
|
@@ -5060,14 +5114,28 @@ var runSoftwareCommand = async (argv, env = {}) => {
|
|
|
5060
5114
|
return { exitCode, output: `${stdout}${stderr}` };
|
|
5061
5115
|
};
|
|
5062
5116
|
var run = runSoftwareCommand;
|
|
5063
|
-
var download = async (url, destination, sha2562) => {
|
|
5064
|
-
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(
|
|
5117
|
+
var download = async (url, destination, sha2562, maximumBytes = 512 * 1024 * 1024) => {
|
|
5118
|
+
const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30 * 60000) });
|
|
5065
5119
|
if (!response.ok)
|
|
5066
5120
|
throw new Error(`download failed with HTTP ${response.status}`);
|
|
5067
|
-
const
|
|
5068
|
-
if (
|
|
5121
|
+
const declared = Number(response.headers.get("content-length") ?? 0);
|
|
5122
|
+
if (declared > maximumBytes)
|
|
5123
|
+
throw new Error("download exceeds reviewed size bound");
|
|
5124
|
+
if (existsSync2(destination))
|
|
5125
|
+
throw new Error("download destination already exists");
|
|
5126
|
+
await Bun.write(destination, response);
|
|
5127
|
+
chmodSync2(destination, 384);
|
|
5128
|
+
if (statSync(destination).size > maximumBytes) {
|
|
5129
|
+
rmSync(destination, { force: true });
|
|
5130
|
+
throw new Error("download exceeds reviewed size bound");
|
|
5131
|
+
}
|
|
5132
|
+
const digest = createHash("sha256");
|
|
5133
|
+
for await (const chunk of createReadStream(destination))
|
|
5134
|
+
digest.update(chunk);
|
|
5135
|
+
if (digest.digest("hex") !== sha2562) {
|
|
5136
|
+
rmSync(destination, { force: true });
|
|
5069
5137
|
throw new Error("download checksum mismatch");
|
|
5070
|
-
|
|
5138
|
+
}
|
|
5071
5139
|
};
|
|
5072
5140
|
var successful = (result, pattern) => result.exitCode === 0 && (!pattern || pattern.test(result.output));
|
|
5073
5141
|
var aptInstall = async (name) => {
|
|
@@ -5075,6 +5143,106 @@ var aptInstall = async (name) => {
|
|
|
5075
5143
|
const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
|
|
5076
5144
|
return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", name], environment) : update;
|
|
5077
5145
|
};
|
|
5146
|
+
var aptInstallMany = async (names) => {
|
|
5147
|
+
const environment = { DEBIAN_FRONTEND: "noninteractive" };
|
|
5148
|
+
const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
|
|
5149
|
+
return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", ...names], environment) : update;
|
|
5150
|
+
};
|
|
5151
|
+
var writeOwnedPolicy = (pathValue, content, mode = 420) => {
|
|
5152
|
+
if (existsSync2(pathValue)) {
|
|
5153
|
+
if (readFileSync(pathValue, "utf8") !== content)
|
|
5154
|
+
throw new Error(`refusing to overwrite non-ForgeZero policy at ${pathValue}`);
|
|
5155
|
+
return;
|
|
5156
|
+
}
|
|
5157
|
+
mkdirSync(dirname(pathValue), { recursive: true, mode: 493 });
|
|
5158
|
+
writeFileSync(pathValue, content, { mode, flag: "wx" });
|
|
5159
|
+
};
|
|
5160
|
+
var installContainerdRuntime = async (directory, osVersion) => {
|
|
5161
|
+
const installed = await aptInstallMany(["containerd", "runc", "containernetworking-plugins"]);
|
|
5162
|
+
if (installed.exitCode !== 0)
|
|
5163
|
+
return installed;
|
|
5164
|
+
const nerdctl = join(directory, "nerdctl.tar.gz");
|
|
5165
|
+
await download(`https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz`, nerdctl, NERDCTL_SHA256);
|
|
5166
|
+
const nerdctlExtract = await run(["/usr/bin/tar", "-xzf", nerdctl, "-C", "/usr/local/bin", "nerdctl"]);
|
|
5167
|
+
if (nerdctlExtract.exitCode !== 0)
|
|
5168
|
+
return nerdctlExtract;
|
|
5169
|
+
chmodSync2("/usr/local/bin/nerdctl", 493);
|
|
5170
|
+
const buildkit = join(directory, "buildkit.tar.gz");
|
|
5171
|
+
await download(`https://github.com/moby/buildkit/releases/download/v${BUILDKIT_VERSION}/buildkit-v${BUILDKIT_VERSION}.linux-amd64.tar.gz`, buildkit, BUILDKIT_SHA256);
|
|
5172
|
+
const unpacked = join(directory, "buildkit");
|
|
5173
|
+
mkdirSync(unpacked, { mode: 448 });
|
|
5174
|
+
const buildkitExtract = await run(["/usr/bin/tar", "-xzf", buildkit, "-C", unpacked]);
|
|
5175
|
+
if (buildkitExtract.exitCode !== 0)
|
|
5176
|
+
return buildkitExtract;
|
|
5177
|
+
for (const binary of ["buildctl", "buildkitd"]) {
|
|
5178
|
+
copyFileSync(join(unpacked, "bin", binary), `/usr/local/bin/${binary}`);
|
|
5179
|
+
chmodSync2(`/usr/local/bin/${binary}`, 493);
|
|
5180
|
+
}
|
|
5181
|
+
mkdirSync("/etc/containerd", { recursive: true, mode: 493 });
|
|
5182
|
+
const expected = CONTAINERD_CONFIG(false);
|
|
5183
|
+
const kataExpected = CONTAINERD_CONFIG(true);
|
|
5184
|
+
if (existsSync2(CONTAINERD_CONFIG_PATH) && ![expected, kataExpected].includes(readFileSync(CONTAINERD_CONFIG_PATH, "utf8"))) {
|
|
5185
|
+
return { exitCode: 2, output: "refusing to overwrite an existing containerd configuration that differs from the reviewed ForgeZero policy" };
|
|
5186
|
+
}
|
|
5187
|
+
if (!existsSync2(CONTAINERD_CONFIG_PATH))
|
|
5188
|
+
writeFileSync(CONTAINERD_CONFIG_PATH, expected, { mode: 420, flag: "wx" });
|
|
5189
|
+
writeOwnedPolicy("/etc/systemd/system/forgezero-buildkit.service", BUILDKIT_UNIT);
|
|
5190
|
+
mkdirSync("/var/lib/forgezero/containerd", { recursive: true, mode: 448 });
|
|
5191
|
+
mkdirSync("/var/lib/forgezero/buildkit", { recursive: true, mode: 448 });
|
|
5192
|
+
const reload = await run(["/usr/bin/systemctl", "daemon-reload"]);
|
|
5193
|
+
if (reload.exitCode !== 0)
|
|
5194
|
+
return reload;
|
|
5195
|
+
const enabled = await run(["/usr/bin/systemctl", "enable", "--now", "containerd.service", "forgezero-buildkit.service"]);
|
|
5196
|
+
return enabled.exitCode === 0 ? { exitCode: 0, output: `containerd ${osVersion} with nerdctl ${NERDCTL_VERSION} and BuildKit ${BUILDKIT_VERSION}` } : enabled;
|
|
5197
|
+
};
|
|
5198
|
+
var installKataRuntime = async (directory) => {
|
|
5199
|
+
for (const pathValue of ["/dev/kvm", "/dev/sev"])
|
|
5200
|
+
if (!existsSync2(pathValue))
|
|
5201
|
+
return { exitCode: 2, output: `${pathValue} is required for Kata SEV-SNP` };
|
|
5202
|
+
for (const parameter of ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"]) {
|
|
5203
|
+
if (!existsSync2(parameter) || !/^(1|Y)$/i.test(readFileSync(parameter, "utf8").trim()))
|
|
5204
|
+
return { exitCode: 2, output: `${parameter} does not enable SEV-SNP` };
|
|
5205
|
+
}
|
|
5206
|
+
const dependencies = await aptInstallMany(["zstd"]);
|
|
5207
|
+
if (dependencies.exitCode !== 0)
|
|
5208
|
+
return dependencies;
|
|
5209
|
+
const archive = join(directory, "kata.tar.zst");
|
|
5210
|
+
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);
|
|
5211
|
+
const tar = join(directory, "kata.tar");
|
|
5212
|
+
const expanded = await run(["/usr/bin/unzstd", "--quiet", archive, "--output", tar]);
|
|
5213
|
+
if (expanded.exitCode !== 0)
|
|
5214
|
+
return expanded;
|
|
5215
|
+
const extracted = await run(["/usr/bin/tar", "-xf", tar, "-C", "/"]);
|
|
5216
|
+
if (extracted.exitCode !== 0)
|
|
5217
|
+
return extracted;
|
|
5218
|
+
if (!existsSync2("/opt/kata/bin/containerd-shim-kata-v2") || !existsSync2(KATA_SNP_CONFIG_PATH)) {
|
|
5219
|
+
return { exitCode: 2, output: "Kata archive does not contain the reviewed QEMU SNP runtime and configuration" };
|
|
5220
|
+
}
|
|
5221
|
+
const snpConfig = readFileSync(KATA_SNP_CONFIG_PATH, "utf8");
|
|
5222
|
+
if (!/^\s*confidential_guest\s*=\s*true\s*$/m.test(snpConfig) || !/^\s*sev_snp_guest\s*=\s*true\s*$/m.test(snpConfig)) {
|
|
5223
|
+
return { exitCode: 2, output: "Kata QEMU configuration does not enable confidential SEV-SNP guests" };
|
|
5224
|
+
}
|
|
5225
|
+
mkdirSync("/etc/kata-containers", { recursive: true, mode: 493 });
|
|
5226
|
+
if (existsSync2(KATA_ACTIVE_CONFIG_PATH) && readFileSync(KATA_ACTIVE_CONFIG_PATH, "utf8") !== snpConfig) {
|
|
5227
|
+
return { exitCode: 2, output: "refusing to overwrite a non-ForgeZero Kata runtime configuration" };
|
|
5228
|
+
}
|
|
5229
|
+
if (!existsSync2(KATA_ACTIVE_CONFIG_PATH))
|
|
5230
|
+
copyFileSync(KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH);
|
|
5231
|
+
chmodSync2(KATA_ACTIVE_CONFIG_PATH, 420);
|
|
5232
|
+
try {
|
|
5233
|
+
unlinkSync2("/usr/local/bin/containerd-shim-kata-v2");
|
|
5234
|
+
} catch {}
|
|
5235
|
+
symlinkSync("/opt/kata/bin/containerd-shim-kata-v2", "/usr/local/bin/containerd-shim-kata-v2");
|
|
5236
|
+
const base = CONTAINERD_CONFIG(false);
|
|
5237
|
+
const kata = CONTAINERD_CONFIG(true);
|
|
5238
|
+
if (!existsSync2(CONTAINERD_CONFIG_PATH) || ![base, kata].includes(readFileSync(CONTAINERD_CONFIG_PATH, "utf8"))) {
|
|
5239
|
+
return { exitCode: 2, output: "containerd must use the reviewed ForgeZero policy before Kata is installed" };
|
|
5240
|
+
}
|
|
5241
|
+
if (readFileSync(CONTAINERD_CONFIG_PATH, "utf8") !== kata)
|
|
5242
|
+
writeFileSync(CONTAINERD_CONFIG_PATH, kata, { mode: 420 });
|
|
5243
|
+
const restarted = await run(["/usr/bin/systemctl", "restart", "containerd.service"]);
|
|
5244
|
+
return restarted.exitCode === 0 ? { exitCode: 0, output: `Kata Containers ${KATA_CONTAINERS_VERSION} QEMU SNP` } : restarted;
|
|
5245
|
+
};
|
|
5078
5246
|
async function executeSoftwareOperation(operation) {
|
|
5079
5247
|
const { software, version } = operation;
|
|
5080
5248
|
if (![...UBUNTU_2604_X64, ...UBUNTU_2404_X64].some(({ requirement }) => requirement.id === software && requirement.version === version)) {
|
|
@@ -5092,6 +5260,26 @@ async function executeSoftwareOperation(operation) {
|
|
|
5092
5260
|
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
|
|
5093
5261
|
return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
|
|
5094
5262
|
}
|
|
5263
|
+
if (software === "containerd") {
|
|
5264
|
+
const [containerd, nerdctl, buildkit, active, builder] = await Promise.all([
|
|
5265
|
+
run(["/usr/bin/containerd", "--version"]),
|
|
5266
|
+
run(["/usr/local/bin/nerdctl", "--version"]),
|
|
5267
|
+
run(["/usr/local/bin/buildkitd", "--version"]),
|
|
5268
|
+
run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]),
|
|
5269
|
+
run(["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-buildkit.service"])
|
|
5270
|
+
]);
|
|
5271
|
+
const policy = existsSync2(CONTAINERD_CONFIG_PATH) ? readFileSync(CONTAINERD_CONFIG_PATH, "utf8") : "";
|
|
5272
|
+
const validPolicy = policy === CONTAINERD_CONFIG(false) || policy === CONTAINERD_CONFIG(true);
|
|
5273
|
+
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" };
|
|
5274
|
+
}
|
|
5275
|
+
if (software === "kata-containers") {
|
|
5276
|
+
const shim = await run(["/opt/kata/bin/containerd-shim-kata-v2", "--version"]);
|
|
5277
|
+
const policy = existsSync2(CONTAINERD_CONFIG_PATH) ? readFileSync(CONTAINERD_CONFIG_PATH, "utf8") : "";
|
|
5278
|
+
const activeConfig = existsSync2(KATA_ACTIVE_CONFIG_PATH) ? readFileSync(KATA_ACTIVE_CONFIG_PATH, "utf8") : "";
|
|
5279
|
+
const host = ["/dev/kvm", "/dev/sev", KATA_SNP_CONFIG_PATH, KATA_ACTIVE_CONFIG_PATH].every(existsSync2) && ["/sys/module/kvm_amd/parameters/sev", "/sys/module/kvm_amd/parameters/sev_snp"].every((parameter) => existsSync2(parameter) && /^(1|Y)$/i.test(readFileSync(parameter, "utf8").trim()));
|
|
5280
|
+
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "containerd.service"]);
|
|
5281
|
+
return successful(shim, /kata.*4\.0\.0/i) && policy === CONTAINERD_CONFIG(true) && activeConfig === (existsSync2(KATA_SNP_CONFIG_PATH) ? readFileSync(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" };
|
|
5282
|
+
}
|
|
5095
5283
|
if (software === "nginx") {
|
|
5096
5284
|
const binary = await run(["/usr/sbin/nginx", "-v"]);
|
|
5097
5285
|
return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
|
|
@@ -5143,6 +5331,17 @@ async function executeSoftwareOperation(operation) {
|
|
|
5143
5331
|
}
|
|
5144
5332
|
const directory = mkdtempSync(join(tmpdir(), "forgezero-software-"));
|
|
5145
5333
|
try {
|
|
5334
|
+
if (software === "containerd")
|
|
5335
|
+
return installContainerdRuntime(directory, version === "ubuntu-24.04" ? "24.04" : "26.04");
|
|
5336
|
+
if (software === "kata-containers") {
|
|
5337
|
+
const containerd = await executeSoftwareOperation({ kind: "check", software: "containerd", version: "ubuntu-26.04" });
|
|
5338
|
+
if (containerd.exitCode !== 0) {
|
|
5339
|
+
const installed2 = await installContainerdRuntime(directory, "26.04");
|
|
5340
|
+
if (installed2.exitCode !== 0)
|
|
5341
|
+
return installed2;
|
|
5342
|
+
}
|
|
5343
|
+
return installKataRuntime(directory);
|
|
5344
|
+
}
|
|
5146
5345
|
if (software === "cloudflare-warp") {
|
|
5147
5346
|
const key = join(directory, "cloudflare-warp-key.gpg");
|
|
5148
5347
|
await download("https://pkg.cloudflareclient.com/pubkey.gpg", key, "0f37fc298c98e88ee3c0ee68c95b69f1dba9eb477abe3167e13982105911264d");
|
|
@@ -5224,7 +5423,7 @@ function validateSoftwareRequirements(value, _options = {}) {
|
|
|
5224
5423
|
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
5225
5424
|
throw new Error("software requirement contains an unknown field");
|
|
5226
5425
|
}
|
|
5227
|
-
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)) {
|
|
5426
|
+
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)) {
|
|
5228
5427
|
throw new Error("software requirement coordinate is invalid");
|
|
5229
5428
|
}
|
|
5230
5429
|
const requirement = { id: row.id, version: row.version };
|
|
@@ -5998,6 +6197,13 @@ function validateBuiltInAction(definition, context, where) {
|
|
|
5998
6197
|
throw new Error(`${where}.with.requirements must name existing requirements`);
|
|
5999
6198
|
return;
|
|
6000
6199
|
}
|
|
6200
|
+
if (definition.uses === "forgezero.network/ensure@1") {
|
|
6201
|
+
exact(withValue, ["target"], `${where}.with`);
|
|
6202
|
+
if (typeof withValue.target !== "string" || !Object.values(context.components).some((entry) => entry.target === withValue.target)) {
|
|
6203
|
+
throw new Error(`${where}.with.target must name a target used by a component`);
|
|
6204
|
+
}
|
|
6205
|
+
return;
|
|
6206
|
+
}
|
|
6001
6207
|
if (definition.uses === "forgezero.exec/argv@1") {
|
|
6002
6208
|
const allowed = ["component", "argv", "credentials"];
|
|
6003
6209
|
const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
|
|
@@ -6053,6 +6259,8 @@ function validateBuiltInProvider(requirement2, where) {
|
|
|
6053
6259
|
const expected = {
|
|
6054
6260
|
"forgezero.bun": { capability: "runtime.javascript", versions: ["1.3.14"], keys: ["installScope"] },
|
|
6055
6261
|
"forgezero.docker": { capability: "runtime.container", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "storageDriver", "dataRoot", "liveRestore", "logDriver", "defaultNetwork", "rootless"] },
|
|
6262
|
+
"forgezero.containerd": { capability: "runtime.container", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "runtimeClass", "namespace", "snapshotter"] },
|
|
6263
|
+
"forgezero.kata": { capability: "runtime.container", versions: ["4.0.0"], keys: ["installScope", "runtimeClass", "namespace", "hypervisor", "confidentialGuest"] },
|
|
6056
6264
|
"forgezero.nginx": { capability: "proxy.http", versions: ["ubuntu-24.04", "ubuntu-26.04"], keys: ["installScope", "websocket", "maximumBodyMiB", "workerProcesses", "workerConnections"] },
|
|
6057
6265
|
"forgezero.arangodb": { capability: "database.arangodb", versions: ["3.11.14"], keys: ["installScope", "runtime", "dataPath", "bind"] },
|
|
6058
6266
|
"forgezero.cloudflared": { capability: "network.public-tunnel", versions: ["2026.7.3"], keys: ["installScope", "mode"] },
|
|
@@ -6073,6 +6281,10 @@ function validateBuiltInProvider(requirement2, where) {
|
|
|
6073
6281
|
if (row2.storageDriver !== "overlay2" || row2.dataRoot !== "/var/lib/docker" || row2.liveRestore !== true || row2.logDriver !== "local" || row2.defaultNetwork !== "bridge" || row2.rootless !== false)
|
|
6074
6282
|
throw new Error(`${where} Docker config is unsupported by the fixed host strategy`);
|
|
6075
6283
|
}
|
|
6284
|
+
if (requirement2.provider === "forgezero.containerd" && (row2.runtimeClass !== "runc" || row2.namespace !== "forgezero" || row2.snapshotter !== "overlayfs"))
|
|
6285
|
+
throw new Error(`${where} containerd config is unsupported by the fixed host strategy`);
|
|
6286
|
+
if (requirement2.provider === "forgezero.kata" && (row2.runtimeClass !== "kata-qemu-snp" || row2.namespace !== "forgezero" || row2.hypervisor !== "qemu" || row2.confidentialGuest !== "sev-snp"))
|
|
6287
|
+
throw new Error(`${where} Kata config is unsupported by the fixed host strategy`);
|
|
6076
6288
|
if (requirement2.provider === "forgezero.nginx" && (typeof row2.websocket !== "boolean" || !Number.isInteger(row2.maximumBodyMiB) || Number(row2.maximumBodyMiB) < 1 || Number(row2.maximumBodyMiB) > 1024 || !(row2.workerProcesses === "auto" || Number.isInteger(row2.workerProcesses)) || !Number.isInteger(row2.workerConnections)))
|
|
6077
6289
|
throw new Error(`${where} Nginx config is invalid`);
|
|
6078
6290
|
if (requirement2.provider === "forgezero.arangodb" && (row2.runtime !== "native" || !absolute(row2.dataPath) || row2.bind !== "private"))
|
|
@@ -6150,7 +6362,7 @@ function reference(value, where) {
|
|
|
6150
6362
|
const row2 = object(value, where);
|
|
6151
6363
|
exact3(row2, ["$ref"], where);
|
|
6152
6364
|
const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
|
|
6153
|
-
if (!/^(inputs\.[a-z][A-Za-z0-9-]{0,62}|steps\.[a-z][A-Za-z0-9-]{0,62}\.(status|outputs\.[a-z][A-Za-z0-9-]{0,62})|components\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|targets\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|deployment\.[a-z][A-Za-z0-9-]{0,62})$/.test(coordinate)) {
|
|
6365
|
+
if (!/^(inputs\.[a-z][A-Za-z0-9-]{0,62}|steps\.[a-z][A-Za-z0-9-]{0,62}\.(status|outputs\.[a-z][A-Za-z0-9-]{0,62})|components\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|targets\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|deployment\.[a-z][A-Za-z0-9-]{0,62}|execution\.(targetName|slot|releaseExecutor|runtime|cpuCores|memoryMiB|storageGiB))$/.test(coordinate)) {
|
|
6154
6366
|
fail(where, "contains an unsupported reference.");
|
|
6155
6367
|
}
|
|
6156
6368
|
return { $ref: coordinate };
|
|
@@ -6390,10 +6602,9 @@ function validateNetwork(value, where) {
|
|
|
6390
6602
|
named(container.network, `${where}.container.network`);
|
|
6391
6603
|
}
|
|
6392
6604
|
}
|
|
6393
|
-
function validateComponent(value, where,
|
|
6605
|
+
function validateComponent(value, where, targets, requirements) {
|
|
6394
6606
|
const row2 = object(value, where);
|
|
6395
|
-
|
|
6396
|
-
fail(`${where}.target`, "must name an existing target.");
|
|
6607
|
+
const selectedTarget = targets.get(String(row2.target)) ?? fail(`${where}.target`, "must name an existing target.");
|
|
6397
6608
|
if (row2.kind === "application") {
|
|
6398
6609
|
exact3(row2, ["kind", "target", "runtime", "service", "resources", "storage", "network", "rollout"], where);
|
|
6399
6610
|
const runtime = object(row2.runtime, `${where}.runtime`);
|
|
@@ -6404,7 +6615,9 @@ function validateComponent(value, where, targetNames, requirements) {
|
|
|
6404
6615
|
exact3(runtime, ["kind", "provider", "requirement", "argv"], `${where}.runtime`);
|
|
6405
6616
|
validateArgv(runtime.argv, `${where}.runtime.argv`);
|
|
6406
6617
|
} else if (runtime.kind === "container") {
|
|
6407
|
-
exact3(runtime, ["kind", "provider", "requirement", "image", "entrypoint", "security"], `${where}.runtime`);
|
|
6618
|
+
exact3(runtime, ["kind", "provider", "requirement", "runtimeClass", "image", "entrypoint", "security"], `${where}.runtime`);
|
|
6619
|
+
if (runtime.runtimeClass !== undefined && !["runc", "kata-qemu-snp"].includes(String(runtime.runtimeClass)))
|
|
6620
|
+
fail(`${where}.runtime.runtimeClass`, "is unsupported.");
|
|
6408
6621
|
const image = object(runtime.image, `${where}.runtime.image`);
|
|
6409
6622
|
exact3(image, ["source"], `${where}.runtime.image`);
|
|
6410
6623
|
const source = object(image.source, `${where}.runtime.image.source`);
|
|
@@ -6438,6 +6651,23 @@ function validateComponent(value, where, targetNames, requirements) {
|
|
|
6438
6651
|
fail(`${where}.runtime.requirement`, "must provide runtime.container.");
|
|
6439
6652
|
if (runtimeRequirement && runtime.kind === "native" && !runtimeRequirement.capability.startsWith("runtime."))
|
|
6440
6653
|
fail(`${where}.runtime.requirement`, "must provide a runtime capability.");
|
|
6654
|
+
const execution = selectedTarget.allocation.execution;
|
|
6655
|
+
if (execution === "native" && runtime.kind !== "native")
|
|
6656
|
+
fail(`${where}.runtime.kind`, "must be native for the selected target.");
|
|
6657
|
+
if (execution !== "native" && runtime.kind !== "container")
|
|
6658
|
+
fail(`${where}.runtime.kind`, "must be container for the selected target.");
|
|
6659
|
+
if (runtime.kind === "container" && execution === "oci-runc") {
|
|
6660
|
+
if (!["forgezero.containerd", "forgezero.docker"].includes(String(runtime.provider)))
|
|
6661
|
+
fail(`${where}.runtime.provider`, "must be forgezero.containerd for an OCI runc target.");
|
|
6662
|
+
if (runtime.runtimeClass !== undefined && runtime.runtimeClass !== "runc")
|
|
6663
|
+
fail(`${where}.runtime.runtimeClass`, "must be runc for the selected target.");
|
|
6664
|
+
}
|
|
6665
|
+
if (runtime.kind === "container" && execution === "oci-kata-qemu-snp") {
|
|
6666
|
+
if (runtime.provider !== "forgezero.kata")
|
|
6667
|
+
fail(`${where}.runtime.provider`, "must be forgezero.kata for a Kata QEMU SNP target.");
|
|
6668
|
+
if (runtime.runtimeClass !== "kata-qemu-snp")
|
|
6669
|
+
fail(`${where}.runtime.runtimeClass`, "must be kata-qemu-snp for the selected target.");
|
|
6670
|
+
}
|
|
6441
6671
|
const service = object(row2.service, `${where}.service`);
|
|
6442
6672
|
exact3(service, ["protocol", "port", "health", "websocket", "maximumConnections"], `${where}.service`);
|
|
6443
6673
|
if (service.protocol !== "http")
|
|
@@ -6469,6 +6699,18 @@ function validateComponent(value, where, targetNames, requirements) {
|
|
|
6469
6699
|
const network = object(row2.network, `${where}.network`);
|
|
6470
6700
|
if (network.container === undefined)
|
|
6471
6701
|
fail(`${where}.network.container`, "must select bridge or reviewed host networking.");
|
|
6702
|
+
if (selectedTarget.allocation.sharing === "shared") {
|
|
6703
|
+
if (object(runtime.security, `${where}.runtime.security`).root !== "read-only")
|
|
6704
|
+
fail(`${where}.runtime.security.root`, "must be read-only on a shared compute.");
|
|
6705
|
+
if (network.container.mode !== "bridge")
|
|
6706
|
+
fail(`${where}.network.container.mode`, "must be bridge on a shared compute.");
|
|
6707
|
+
if (Array.isArray(row2.storage) && row2.storage.some((mount) => mount.class !== "ephemeral")) {
|
|
6708
|
+
fail(`${where}.storage`, "may contain only size-bounded tmpfs mounts on a shared compute.");
|
|
6709
|
+
}
|
|
6710
|
+
}
|
|
6711
|
+
}
|
|
6712
|
+
if (runtime.kind === "native" && selectedTarget.allocation.sharing !== "exclusive") {
|
|
6713
|
+
fail(`${where}.target`, "native applications require an exclusive compute.");
|
|
6472
6714
|
}
|
|
6473
6715
|
if (row2.storage !== undefined) {
|
|
6474
6716
|
if (!Array.isArray(row2.storage) || row2.storage.length > 32)
|
|
@@ -6505,6 +6747,8 @@ function validateComponent(value, where, targetNames, requirements) {
|
|
|
6505
6747
|
fail(`${where}.runtime.mode`, "must match the provider runtime mode.");
|
|
6506
6748
|
if (runtime.mode !== "native")
|
|
6507
6749
|
fail(`${where}.runtime.mode`, "must be native in deployment plan v1.");
|
|
6750
|
+
if (selectedTarget.allocation.execution !== "native")
|
|
6751
|
+
fail(`${where}.target`, "must select a native execution target for database components.");
|
|
6508
6752
|
const topology = object(row2.topology, `${where}.topology`);
|
|
6509
6753
|
if (topology.mode === "standalone")
|
|
6510
6754
|
exact3(topology, ["mode"], `${where}.topology`);
|
|
@@ -6701,15 +6945,20 @@ function compileDeployment(sourceValue) {
|
|
|
6701
6945
|
if (targetEntries.length < 1 || targetEntries.length > 64)
|
|
6702
6946
|
fail("deployment.spec.targets", "must contain 1 to 64 targets.");
|
|
6703
6947
|
const targetNames = new Set(targetEntries.map(([targetName]) => named(targetName, "deployment.spec.targets key")));
|
|
6948
|
+
const targetDefinitions = new Map;
|
|
6704
6949
|
for (const [targetName, value] of targetEntries) {
|
|
6705
6950
|
const target = object(value, `deployment.spec.targets.${targetName}`);
|
|
6706
|
-
exact3(target, ["kind", "selector", "cardinality", "placement"], `deployment.spec.targets.${targetName}`);
|
|
6951
|
+
exact3(target, ["kind", "selector", "cardinality", "allocation", "provisioning", "connectivity", "placement"], `deployment.spec.targets.${targetName}`);
|
|
6707
6952
|
if (target.kind !== "compute")
|
|
6708
6953
|
fail(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
|
|
6709
6954
|
const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
|
|
6710
|
-
exact3(selector, ["profiles", "confidentialCompute", "labels"], `deployment.spec.targets.${targetName}.selector`);
|
|
6955
|
+
exact3(selector, ["profiles", "operatingSystem", "confidentialCompute", "labels"], `deployment.spec.targets.${targetName}.selector`);
|
|
6711
6956
|
if (!Array.isArray(selector.profiles) || selector.profiles.length < 1 || selector.profiles.length > 32 || selector.profiles.some((profile) => typeof profile !== "string" || !NAME2.test(profile)) || new Set(selector.profiles).size !== selector.profiles.length)
|
|
6712
6957
|
fail(`deployment.spec.targets.${targetName}.selector.profiles`, "is invalid.");
|
|
6958
|
+
const operatingSystem = object(selector.operatingSystem, `deployment.spec.targets.${targetName}.selector.operatingSystem`);
|
|
6959
|
+
exact3(operatingSystem, ["id", "version", "architecture"], `deployment.spec.targets.${targetName}.selector.operatingSystem`);
|
|
6960
|
+
if (operatingSystem.id !== "ubuntu" || !["24.04", "26.04"].includes(String(operatingSystem.version)) || operatingSystem.architecture !== "x64")
|
|
6961
|
+
fail(`deployment.spec.targets.${targetName}.selector.operatingSystem`, "is unsupported.");
|
|
6713
6962
|
if (selector.confidentialCompute !== undefined && !["required", "preferred", "disabled"].includes(String(selector.confidentialCompute)))
|
|
6714
6963
|
fail(`deployment.spec.targets.${targetName}.selector.confidentialCompute`, "is unsupported.");
|
|
6715
6964
|
if (selector.labels !== undefined)
|
|
@@ -6722,8 +6971,108 @@ function compileDeployment(sourceValue) {
|
|
|
6722
6971
|
integer(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`, minimum, maximum);
|
|
6723
6972
|
else
|
|
6724
6973
|
reference(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`);
|
|
6974
|
+
const allocation = object(target.allocation, `deployment.spec.targets.${targetName}.allocation`);
|
|
6975
|
+
exact3(allocation, ["execution", "sharing", "resources", "existing"], `deployment.spec.targets.${targetName}.allocation`);
|
|
6976
|
+
if (!["native", "oci-runc", "oci-kata-qemu-snp"].includes(String(allocation.execution)))
|
|
6977
|
+
fail(`deployment.spec.targets.${targetName}.allocation.execution`, "is unsupported.");
|
|
6978
|
+
if (!["exclusive", "shared"].includes(String(allocation.sharing)))
|
|
6979
|
+
fail(`deployment.spec.targets.${targetName}.allocation.sharing`, "is unsupported.");
|
|
6980
|
+
if (!["prefer", "require"].includes(String(allocation.existing)))
|
|
6981
|
+
fail(`deployment.spec.targets.${targetName}.allocation.existing`, "is unsupported.");
|
|
6982
|
+
const capacity = object(allocation.resources, `deployment.spec.targets.${targetName}.allocation.resources`);
|
|
6983
|
+
exact3(capacity, ["cpuCores", "memoryMiB", "storageGiB"], `deployment.spec.targets.${targetName}.allocation.resources`);
|
|
6984
|
+
const capacityValue = (value2, where, minimumValue, maximumValue) => {
|
|
6985
|
+
if (typeof value2 === "number")
|
|
6986
|
+
integer(value2, where, minimumValue, maximumValue);
|
|
6987
|
+
else
|
|
6988
|
+
reference(value2, where);
|
|
6989
|
+
};
|
|
6990
|
+
capacityValue(capacity.cpuCores, `deployment.spec.targets.${targetName}.allocation.resources.cpuCores`, 1, 1024);
|
|
6991
|
+
capacityValue(capacity.memoryMiB, `deployment.spec.targets.${targetName}.allocation.resources.memoryMiB`, 128, 4194304);
|
|
6992
|
+
capacityValue(capacity.storageGiB, `deployment.spec.targets.${targetName}.allocation.resources.storageGiB`, 1, 1048576);
|
|
6993
|
+
if (allocation.execution === "oci-kata-qemu-snp" && selector.confidentialCompute !== "required")
|
|
6994
|
+
fail(`deployment.spec.targets.${targetName}.selector.confidentialCompute`, "must be required for Kata QEMU SNP.");
|
|
6995
|
+
if ((allocation.execution === "oci-kata-qemu-snp" || selector.confidentialCompute === "required") && operatingSystem.version !== "26.04")
|
|
6996
|
+
fail(`deployment.spec.targets.${targetName}.selector.operatingSystem.version`, "must be Ubuntu 26.04 for SEV-SNP.");
|
|
6997
|
+
if (allocation.execution !== "oci-kata-qemu-snp" && selector.confidentialCompute === "required" && allocation.execution !== "native")
|
|
6998
|
+
fail(`deployment.spec.targets.${targetName}.allocation.execution`, "cannot satisfy required confidential compute.");
|
|
6999
|
+
if (target.provisioning !== undefined) {
|
|
7000
|
+
const provisioning = object(target.provisioning, `deployment.spec.targets.${targetName}.provisioning`);
|
|
7001
|
+
exact3(provisioning, ["planKey", "regionKey", "imageKey", "environmentKey", "ownership", "metalHostname", "maxMonthlySpendMinor"], `deployment.spec.targets.${targetName}.provisioning`);
|
|
7002
|
+
for (const key of ["planKey", "regionKey", "imageKey", "environmentKey"])
|
|
7003
|
+
named(provisioning[key], `deployment.spec.targets.${targetName}.provisioning.${key}`);
|
|
7004
|
+
if (!["platform", "tenant-metal"].includes(String(provisioning.ownership)) || provisioning.ownership === "tenant-metal" !== Boolean(provisioning.metalHostname))
|
|
7005
|
+
fail(`deployment.spec.targets.${targetName}.provisioning.ownership`, "must bind platform capacity or one tenant metal hostname.");
|
|
7006
|
+
if (provisioning.metalHostname !== undefined && !/^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/.test(String(provisioning.metalHostname)))
|
|
7007
|
+
fail(`deployment.spec.targets.${targetName}.provisioning.metalHostname`, "is invalid.");
|
|
7008
|
+
if (typeof provisioning.maxMonthlySpendMinor !== "string" || !/^\d{1,18}$/.test(provisioning.maxMonthlySpendMinor))
|
|
7009
|
+
fail(`deployment.spec.targets.${targetName}.provisioning.maxMonthlySpendMinor`, "must be a bounded decimal minor-unit amount.");
|
|
7010
|
+
}
|
|
7011
|
+
if (allocation.existing === "prefer" && target.provisioning === undefined)
|
|
7012
|
+
fail(`deployment.spec.targets.${targetName}.provisioning`, "is required when placement may provision after reuse.");
|
|
7013
|
+
if (allocation.existing === "require" && target.provisioning !== undefined)
|
|
7014
|
+
fail(`deployment.spec.targets.${targetName}.provisioning`, "must be omitted for reuse-only placement.");
|
|
7015
|
+
if (target.connectivity !== undefined) {
|
|
7016
|
+
const connectivity = object(target.connectivity, `deployment.spec.targets.${targetName}.connectivity`);
|
|
7017
|
+
exact3(connectivity, ["private", "public"], `deployment.spec.targets.${targetName}.connectivity`);
|
|
7018
|
+
const declaredPrivate = connectivity.private === undefined ? undefined : object(connectivity.private, `deployment.spec.targets.${targetName}.connectivity.private`);
|
|
7019
|
+
if (declaredPrivate?.mode === "cloudflare-warp" && allocation.sharing !== "exclusive")
|
|
7020
|
+
fail(`deployment.spec.targets.${targetName}.allocation.sharing`, "must be exclusive for a Cloudflare WARP connector.");
|
|
7021
|
+
if (connectivity.private !== undefined) {
|
|
7022
|
+
const privateNetwork = declaredPrivate;
|
|
7023
|
+
if (privateNetwork.mode === "cloudflare-warp") {
|
|
7024
|
+
exact3(privateNetwork, ["mode", "credential", "network"], `deployment.spec.targets.${targetName}.connectivity.private`);
|
|
7025
|
+
if (!credentials[named(privateNetwork.credential, `deployment.spec.targets.${targetName}.connectivity.private.credential`)])
|
|
7026
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.private.credential`, "must name a declared credential.");
|
|
7027
|
+
if (typeof privateNetwork.network === "string")
|
|
7028
|
+
named(privateNetwork.network, `deployment.spec.targets.${targetName}.connectivity.private.network`);
|
|
7029
|
+
else
|
|
7030
|
+
reference(privateNetwork.network, `deployment.spec.targets.${targetName}.connectivity.private.network`);
|
|
7031
|
+
} else {
|
|
7032
|
+
exact3(privateNetwork, ["mode"], `deployment.spec.targets.${targetName}.connectivity.private`);
|
|
7033
|
+
if (!["disabled", "private-lan"].includes(String(privateNetwork.mode)))
|
|
7034
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.private.mode`, "is unsupported.");
|
|
7035
|
+
}
|
|
7036
|
+
}
|
|
7037
|
+
if (connectivity.public !== undefined) {
|
|
7038
|
+
const publicNetwork = object(connectivity.public, `deployment.spec.targets.${targetName}.connectivity.public`);
|
|
7039
|
+
if (publicNetwork.mode === "cloudflare-tunnel") {
|
|
7040
|
+
exact3(publicNetwork, ["mode", "credential", "zone", "hostname"], `deployment.spec.targets.${targetName}.connectivity.public`);
|
|
7041
|
+
if (!credentials[named(publicNetwork.credential, `deployment.spec.targets.${targetName}.connectivity.public.credential`)])
|
|
7042
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.public.credential`, "must name a declared credential.");
|
|
7043
|
+
if (typeof publicNetwork.zone === "string") {
|
|
7044
|
+
if (!/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(publicNetwork.zone))
|
|
7045
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.public.zone`, "is invalid.");
|
|
7046
|
+
} else
|
|
7047
|
+
reference(publicNetwork.zone, `deployment.spec.targets.${targetName}.connectivity.public.zone`);
|
|
7048
|
+
const hostname = object(publicNetwork.hostname, `deployment.spec.targets.${targetName}.connectivity.public.hostname`);
|
|
7049
|
+
const value2 = (coordinate, where, pattern) => {
|
|
7050
|
+
if (typeof coordinate === "string") {
|
|
7051
|
+
if (!pattern.test(coordinate))
|
|
7052
|
+
fail(where, "is invalid.");
|
|
7053
|
+
} else
|
|
7054
|
+
reference(coordinate, where);
|
|
7055
|
+
};
|
|
7056
|
+
if (hostname.mode === "static") {
|
|
7057
|
+
exact3(hostname, ["mode", "label"], `deployment.spec.targets.${targetName}.connectivity.public.hostname`);
|
|
7058
|
+
value2(hostname.label, `deployment.spec.targets.${targetName}.connectivity.public.hostname.label`, /^[a-z0-9](?:[a-z0-9-]{0,48}[a-z0-9])?$/);
|
|
7059
|
+
} else if (hostname.mode === "indexed") {
|
|
7060
|
+
exact3(hostname, ["mode", "prefix", "startAt"], `deployment.spec.targets.${targetName}.connectivity.public.hostname`);
|
|
7061
|
+
value2(hostname.prefix, `deployment.spec.targets.${targetName}.connectivity.public.hostname.prefix`, /^[a-z0-9](?:[a-z0-9-]{0,48}[a-z0-9])?$/);
|
|
7062
|
+
if (hostname.startAt !== undefined)
|
|
7063
|
+
integer(hostname.startAt, `deployment.spec.targets.${targetName}.connectivity.public.hostname.startAt`, 0, 1e6);
|
|
7064
|
+
} else
|
|
7065
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.public.hostname.mode`, "is unsupported.");
|
|
7066
|
+
} else {
|
|
7067
|
+
exact3(publicNetwork, ["mode"], `deployment.spec.targets.${targetName}.connectivity.public`);
|
|
7068
|
+
if (publicNetwork.mode !== "disabled")
|
|
7069
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.public.mode`, "is unsupported.");
|
|
7070
|
+
}
|
|
7071
|
+
}
|
|
7072
|
+
}
|
|
6725
7073
|
if (target.placement !== undefined)
|
|
6726
7074
|
jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
|
|
7075
|
+
targetDefinitions.set(targetName, target);
|
|
6727
7076
|
}
|
|
6728
7077
|
const requirements = object(spec.requirements ?? {}, "deployment.spec.requirements");
|
|
6729
7078
|
if (Object.keys(requirements).length > 64)
|
|
@@ -6737,13 +7086,34 @@ function compileDeployment(sourceValue) {
|
|
|
6737
7086
|
const key = `${requirementValue.provider}@${requirementValue.contract}:${requirementValue.capability}`;
|
|
6738
7087
|
requiredProviders.set(key, { provider: requirementValue.provider, contract: requirementValue.contract, capability: requirementValue.capability });
|
|
6739
7088
|
}
|
|
7089
|
+
for (const [targetName, target] of targetDefinitions) {
|
|
7090
|
+
if (target.connectivity?.public?.mode === "cloudflare-tunnel" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.public-tunnel")) {
|
|
7091
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.public`, "requires one network.public-tunnel provider requirement.");
|
|
7092
|
+
}
|
|
7093
|
+
if (target.connectivity?.private?.mode === "cloudflare-warp" && ![...requirementDefinitions.values()].some(({ capability }) => capability === "network.private")) {
|
|
7094
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.private`, "requires one network.private provider requirement.");
|
|
7095
|
+
}
|
|
7096
|
+
}
|
|
6740
7097
|
const components = object(spec.components, "deployment.spec.components");
|
|
6741
7098
|
const componentEntries = Object.entries(components);
|
|
6742
7099
|
if (componentEntries.length < 1 || componentEntries.length > 128)
|
|
6743
7100
|
fail("deployment.spec.components", "must contain 1 to 128 components.");
|
|
6744
7101
|
for (const [componentName, value] of componentEntries) {
|
|
6745
7102
|
named(componentName, "deployment.spec.components key");
|
|
6746
|
-
validateComponent(value, `deployment.spec.components.${componentName}`,
|
|
7103
|
+
validateComponent(value, `deployment.spec.components.${componentName}`, targetDefinitions, requirementDefinitions);
|
|
7104
|
+
}
|
|
7105
|
+
for (const [targetName, target] of targetDefinitions) {
|
|
7106
|
+
if (target.connectivity?.public?.mode !== "cloudflare-tunnel")
|
|
7107
|
+
continue;
|
|
7108
|
+
const publicApplications = componentEntries.filter(([, component2]) => {
|
|
7109
|
+
const candidate = component2;
|
|
7110
|
+
return candidate.kind === "application" && candidate.target === targetName && candidate.network?.public?.mode === "cloudflare-tunnel";
|
|
7111
|
+
});
|
|
7112
|
+
if (publicApplications.length !== 1)
|
|
7113
|
+
fail(`deployment.spec.targets.${targetName}.connectivity.public`, "requires exactly one application on the target with network.public cloudflare-tunnel.");
|
|
7114
|
+
const application = publicApplications[0][1];
|
|
7115
|
+
if (application.kind !== "application" || !application.network?.ingress?.stablePort)
|
|
7116
|
+
fail(`deployment.spec.components.${publicApplications[0][0]}.network.ingress`, "requires one stablePort for Tunnel ingress.");
|
|
6747
7117
|
}
|
|
6748
7118
|
const actionContext = {
|
|
6749
7119
|
components,
|
|
@@ -6785,6 +7155,14 @@ function compileDeployment(sourceValue) {
|
|
|
6785
7155
|
});
|
|
6786
7156
|
plannedWorkflows.push({ id: workflowName, ...workflowValue.concurrency ? { concurrency: workflowValue.concurrency } : {}, stages: plannedStages });
|
|
6787
7157
|
}
|
|
7158
|
+
const deployWorkflow = plannedWorkflows.find(({ id: id2 }) => id2 === "deploy");
|
|
7159
|
+
for (const [targetName, target] of targetDefinitions) {
|
|
7160
|
+
if (!target.connectivity || target.connectivity.public?.mode !== "cloudflare-tunnel" && target.connectivity.private?.mode !== "cloudflare-warp")
|
|
7161
|
+
continue;
|
|
7162
|
+
const ensures = deployWorkflow?.stages.flatMap(({ steps }) => steps).filter((step2) => step2.uses === "forgezero.network/ensure@1" && step2.with.target === targetName && (step2.scope.kind === "each-target" || step2.scope.kind === "target-batches") && step2.scope.target === targetName) ?? [];
|
|
7163
|
+
if (ensures.length !== 1)
|
|
7164
|
+
fail(`deployment.spec.workflows.deploy`, `must contain exactly one network.ensure step scoped to connected target ${targetName}.`);
|
|
7165
|
+
}
|
|
6788
7166
|
const typedSource = source;
|
|
6789
7167
|
return {
|
|
6790
7168
|
format: DEPLOY_PLAN_FORMAT,
|
|
@@ -6871,6 +7249,143 @@ function parseDeploymentPlan(value) {
|
|
|
6871
7249
|
return { ...compiled, sourceDigest: row2.sourceDigest };
|
|
6872
7250
|
}
|
|
6873
7251
|
|
|
7252
|
+
// src/deployment-targets.ts
|
|
7253
|
+
class DeploymentInputError extends Error {
|
|
7254
|
+
constructor(message) {
|
|
7255
|
+
super(message);
|
|
7256
|
+
this.name = "DeploymentInputError";
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
7259
|
+
var HOSTNAME = /^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?$/;
|
|
7260
|
+
function inputValue(name, definition, supplied) {
|
|
7261
|
+
let value = supplied ?? definition.default;
|
|
7262
|
+
if (value === undefined) {
|
|
7263
|
+
if (definition.required)
|
|
7264
|
+
throw new DeploymentInputError(`deployment input ${name} is required`);
|
|
7265
|
+
return;
|
|
7266
|
+
}
|
|
7267
|
+
if (typeof value === "string") {
|
|
7268
|
+
const text2 = value;
|
|
7269
|
+
if (definition.type === "boolean" && (text2 === "true" || text2 === "false"))
|
|
7270
|
+
value = text2 === "true";
|
|
7271
|
+
if ((definition.type === "integer" || definition.type === "number") && text2.trim() !== "")
|
|
7272
|
+
value = Number(text2);
|
|
7273
|
+
}
|
|
7274
|
+
if (definition.type === "boolean" && typeof value !== "boolean")
|
|
7275
|
+
throw new DeploymentInputError(`deployment input ${name} must be boolean`);
|
|
7276
|
+
if (definition.type === "integer" && (!Number.isSafeInteger(value) || Number(value) < (definition.minimum ?? Number.MIN_SAFE_INTEGER) || Number(value) > (definition.maximum ?? Number.MAX_SAFE_INTEGER)))
|
|
7277
|
+
throw new DeploymentInputError(`deployment input ${name} must be a bounded integer`);
|
|
7278
|
+
if (definition.type === "number" && (typeof value !== "number" || !Number.isFinite(value) || value < (definition.minimum ?? -Number.MAX_VALUE) || value > (definition.maximum ?? Number.MAX_VALUE)))
|
|
7279
|
+
throw new DeploymentInputError(`deployment input ${name} must be a bounded number`);
|
|
7280
|
+
if (definition.type === "enum" && (typeof value !== "string" || !definition.values.includes(value)))
|
|
7281
|
+
throw new DeploymentInputError(`deployment input ${name} must be one of its declared values`);
|
|
7282
|
+
if (definition.type === "hostname" && (typeof value !== "string" || !HOSTNAME.test(value)))
|
|
7283
|
+
throw new DeploymentInputError(`deployment input ${name} must be a hostname`);
|
|
7284
|
+
if (definition.type === "string") {
|
|
7285
|
+
if (typeof value !== "string" || value.length < (definition.minimumLength ?? 0) || value.length > (definition.maximumLength ?? 4096))
|
|
7286
|
+
throw new DeploymentInputError(`deployment input ${name} must be a bounded string`);
|
|
7287
|
+
if (definition.pattern !== undefined && !new RegExp(definition.pattern).test(value))
|
|
7288
|
+
throw new DeploymentInputError(`deployment input ${name} does not match its pattern`);
|
|
7289
|
+
}
|
|
7290
|
+
return value;
|
|
7291
|
+
}
|
|
7292
|
+
function resolveDeploymentTargets(plan, supplied = {}) {
|
|
7293
|
+
const inputs = resolveDeploymentInputs(plan, supplied);
|
|
7294
|
+
const stringCoordinate = (value, label) => {
|
|
7295
|
+
const resolved = typeof value === "string" ? value : value.$ref.startsWith("inputs.") ? inputs[value.$ref.slice("inputs.".length)] : undefined;
|
|
7296
|
+
if (typeof resolved !== "string" || resolved.length < 1 || resolved.length > 253) {
|
|
7297
|
+
throw new DeploymentInputError(`${label} did not resolve to a bounded string`);
|
|
7298
|
+
}
|
|
7299
|
+
return resolved.toLowerCase();
|
|
7300
|
+
};
|
|
7301
|
+
const integerCoordinate = (value, label, minimum, maximum) => {
|
|
7302
|
+
const resolved = typeof value === "number" ? value : value.$ref.startsWith("inputs.") ? inputs[value.$ref.slice("inputs.".length)] : undefined;
|
|
7303
|
+
if (!Number.isSafeInteger(resolved) || Number(resolved) < minimum || Number(resolved) > maximum) {
|
|
7304
|
+
throw new DeploymentInputError(`${label} did not resolve to a bounded integer`);
|
|
7305
|
+
}
|
|
7306
|
+
return Number(resolved);
|
|
7307
|
+
};
|
|
7308
|
+
const credentialSource = (alias, label) => {
|
|
7309
|
+
const source = plan.spec.credentials?.[alias]?.source.name;
|
|
7310
|
+
if (!source)
|
|
7311
|
+
throw new DeploymentInputError(`${label} did not resolve to a declared Vault credential`);
|
|
7312
|
+
return source;
|
|
7313
|
+
};
|
|
7314
|
+
return Object.entries(plan.spec.targets).map(([name, target]) => {
|
|
7315
|
+
const desired = typeof target.cardinality.desired === "number" ? target.cardinality.desired : target.cardinality.desired.$ref.startsWith("inputs.") ? inputs[target.cardinality.desired.$ref.slice("inputs.".length)] : undefined;
|
|
7316
|
+
if (!Number.isSafeInteger(desired) || Number(desired) < target.cardinality.minimum || Number(desired) > target.cardinality.maximum) {
|
|
7317
|
+
throw new DeploymentInputError(`target ${name} desired cardinality did not resolve to a bounded integer`);
|
|
7318
|
+
}
|
|
7319
|
+
let connectivity;
|
|
7320
|
+
if (target.connectivity) {
|
|
7321
|
+
const privateNetwork = target.connectivity.private;
|
|
7322
|
+
const publicNetwork = target.connectivity.public;
|
|
7323
|
+
connectivity = {
|
|
7324
|
+
...privateNetwork ? { private: privateNetwork.mode === "cloudflare-warp" ? {
|
|
7325
|
+
mode: "cloudflare-warp",
|
|
7326
|
+
credential: credentialSource(privateNetwork.credential, `target ${name} WARP credential`),
|
|
7327
|
+
network: stringCoordinate(privateNetwork.network, `target ${name} WARP network`)
|
|
7328
|
+
} : { mode: privateNetwork.mode } } : {},
|
|
7329
|
+
...publicNetwork ? { public: publicNetwork.mode === "cloudflare-tunnel" ? {
|
|
7330
|
+
mode: "cloudflare-tunnel",
|
|
7331
|
+
credential: credentialSource(publicNetwork.credential, `target ${name} Tunnel credential`),
|
|
7332
|
+
zone: stringCoordinate(publicNetwork.zone, `target ${name} Tunnel zone`),
|
|
7333
|
+
hostname: publicNetwork.hostname.mode === "static" ? {
|
|
7334
|
+
mode: "static",
|
|
7335
|
+
label: stringCoordinate(publicNetwork.hostname.label, `target ${name} Tunnel label`)
|
|
7336
|
+
} : {
|
|
7337
|
+
mode: "indexed",
|
|
7338
|
+
prefix: stringCoordinate(publicNetwork.hostname.prefix, `target ${name} Tunnel prefix`),
|
|
7339
|
+
startAt: publicNetwork.hostname.startAt ?? 1
|
|
7340
|
+
},
|
|
7341
|
+
service: (() => {
|
|
7342
|
+
const applications = Object.values(plan.spec.components).filter((component2) => component2.target === name && component2.kind === "application" && component2.network?.public?.mode === "cloudflare-tunnel");
|
|
7343
|
+
if (applications.length !== 1)
|
|
7344
|
+
throw new DeploymentInputError(`target ${name} Tunnel requires exactly one public application`);
|
|
7345
|
+
const port = applications[0].network.ingress?.stablePort;
|
|
7346
|
+
if (!Number.isInteger(port))
|
|
7347
|
+
throw new DeploymentInputError(`target ${name} Tunnel application requires a stable ingress port`);
|
|
7348
|
+
return { protocol: "http", port: Number(port) };
|
|
7349
|
+
})()
|
|
7350
|
+
} : { mode: "disabled" } } : {}
|
|
7351
|
+
};
|
|
7352
|
+
}
|
|
7353
|
+
return {
|
|
7354
|
+
name,
|
|
7355
|
+
profiles: [...target.selector.profiles],
|
|
7356
|
+
operatingSystem: { ...target.selector.operatingSystem },
|
|
7357
|
+
confidentialCompute: target.selector.confidentialCompute ?? "disabled",
|
|
7358
|
+
labels: { ...target.selector.labels ?? {} },
|
|
7359
|
+
minimum: target.cardinality.minimum,
|
|
7360
|
+
desired: Number(desired),
|
|
7361
|
+
maximum: target.cardinality.maximum,
|
|
7362
|
+
allocation: {
|
|
7363
|
+
...target.allocation,
|
|
7364
|
+
resources: {
|
|
7365
|
+
cpuCores: integerCoordinate(target.allocation.resources.cpuCores, `target ${name} cpuCores`, 1, 1024),
|
|
7366
|
+
memoryMiB: integerCoordinate(target.allocation.resources.memoryMiB, `target ${name} memoryMiB`, 128, 4194304),
|
|
7367
|
+
storageGiB: integerCoordinate(target.allocation.resources.storageGiB, `target ${name} storageGiB`, 1, 1048576)
|
|
7368
|
+
}
|
|
7369
|
+
},
|
|
7370
|
+
...target.provisioning ? { provisioning: { ...target.provisioning } } : {},
|
|
7371
|
+
...connectivity ? { connectivity } : {}
|
|
7372
|
+
};
|
|
7373
|
+
});
|
|
7374
|
+
}
|
|
7375
|
+
function resolveDeploymentInputs(plan, supplied = {}) {
|
|
7376
|
+
for (const name of Object.keys(supplied))
|
|
7377
|
+
if (!(name in (plan.spec.inputs ?? {}))) {
|
|
7378
|
+
throw new DeploymentInputError(`deployment input ${name} is not declared`);
|
|
7379
|
+
}
|
|
7380
|
+
const inputs = {};
|
|
7381
|
+
for (const [name, definition] of Object.entries(plan.spec.inputs ?? {})) {
|
|
7382
|
+
const value = inputValue(name, definition, supplied[name]);
|
|
7383
|
+
if (value !== undefined)
|
|
7384
|
+
inputs[name] = value;
|
|
7385
|
+
}
|
|
7386
|
+
return inputs;
|
|
7387
|
+
}
|
|
7388
|
+
|
|
6874
7389
|
// src/deploy-plan-runner.ts
|
|
6875
7390
|
class DeploymentActionError extends Error {
|
|
6876
7391
|
kind;
|
|
@@ -6906,6 +7421,8 @@ function resolveReference(reference2, state) {
|
|
|
6906
7421
|
return nested(state.plan.spec.targets[parts[1]], parts.slice(2));
|
|
6907
7422
|
if (parts[0] === "deployment")
|
|
6908
7423
|
return nested(state.plan, parts.slice(1));
|
|
7424
|
+
if (parts[0] === "execution")
|
|
7425
|
+
return nested(state.execution, parts.slice(1));
|
|
6909
7426
|
return;
|
|
6910
7427
|
}
|
|
6911
7428
|
function resolveOperand(value, state) {
|
|
@@ -7108,7 +7625,7 @@ async function runDeploymentPlan(options) {
|
|
|
7108
7625
|
for (const [name, definition] of Object.entries(inputDefinitions))
|
|
7109
7626
|
inputValues[name] = validateInputValue(definition, supplied[name], name);
|
|
7110
7627
|
const execute = async () => {
|
|
7111
|
-
const state = { inputs: inputValues, steps: new Map, plan };
|
|
7628
|
+
const state = { inputs: inputValues, steps: new Map, plan, execution: options.execution };
|
|
7112
7629
|
const records = [];
|
|
7113
7630
|
const successful2 = [];
|
|
7114
7631
|
for (const stage2 of workflow.stages) {
|
|
@@ -7163,7 +7680,7 @@ var DEPLOY_PLAN_FILE = ".fz/deploy.plan.json";
|
|
|
7163
7680
|
import { createHash as createHash4, randomBytes as randomBytes5 } from "crypto";
|
|
7164
7681
|
import {
|
|
7165
7682
|
chmodSync as chmodSync3,
|
|
7166
|
-
createReadStream,
|
|
7683
|
+
createReadStream as createReadStream2,
|
|
7167
7684
|
existsSync as existsSync3,
|
|
7168
7685
|
lstatSync,
|
|
7169
7686
|
mkdirSync as mkdirSync2,
|
|
@@ -7178,7 +7695,7 @@ var MAX_BOOTSTRAP_BUNDLE_BYTES = 512 * 1024 * 1024;
|
|
|
7178
7695
|
async function sha256File(path2) {
|
|
7179
7696
|
const hash = createHash4("sha256");
|
|
7180
7697
|
await new Promise((resolveDone, reject) => {
|
|
7181
|
-
const stream =
|
|
7698
|
+
const stream = createReadStream2(path2);
|
|
7182
7699
|
stream.on("data", (chunk) => hash.update(chunk));
|
|
7183
7700
|
stream.on("error", reject);
|
|
7184
7701
|
stream.on("end", resolveDone);
|
|
@@ -7264,7 +7781,7 @@ function persistCapacityCalibration(args) {
|
|
|
7264
7781
|
throw new DeploymentError("PIPELINE_FAILED", "capacity evidence directory must be an absolute Agent-owned path");
|
|
7265
7782
|
}
|
|
7266
7783
|
if (!existsSync4(args.directory)) {
|
|
7267
|
-
const parent = lstatSync2(
|
|
7784
|
+
const parent = lstatSync2(dirname2(args.directory));
|
|
7268
7785
|
if (!parent.isDirectory() || parent.isSymbolicLink()) {
|
|
7269
7786
|
throw new DeploymentError("PIPELINE_FAILED", "capacity evidence parent must be a real Agent-owned directory");
|
|
7270
7787
|
}
|
|
@@ -7328,7 +7845,7 @@ function createDeploymentManager(options) {
|
|
|
7328
7845
|
}
|
|
7329
7846
|
if (existsSync4(knownHostsPath) && readFileSync3(knownHostsPath, "utf8") === content)
|
|
7330
7847
|
return;
|
|
7331
|
-
mkdirSync3(
|
|
7848
|
+
mkdirSync3(dirname2(knownHostsPath), { recursive: true, mode: 448 });
|
|
7332
7849
|
const next = `${knownHostsPath}.${process.pid}.${randomUUID()}.next`;
|
|
7333
7850
|
writeFileSync3(next, content, { mode: 384, flag: "wx" });
|
|
7334
7851
|
renameSync3(next, knownHostsPath);
|
|
@@ -7515,8 +8032,8 @@ function createDeploymentManager(options) {
|
|
|
7515
8032
|
throw new DeploymentError("PIPELINE_FAILED", "rolling/canary plans require a fenced multi-target rollout lease that this control protocol does not yet supply");
|
|
7516
8033
|
}
|
|
7517
8034
|
}
|
|
7518
|
-
const
|
|
7519
|
-
if (
|
|
8035
|
+
const matchingTargetNames = Object.entries(plan.spec.targets).filter(([, target]) => target.selector.profiles.includes(options.profile)).map(([name]) => name);
|
|
8036
|
+
if (matchingTargetNames.length === 0)
|
|
7520
8037
|
throw new DeploymentError("PIPELINE_FAILED", `deployment plan has no target for profile ${options.profile}`);
|
|
7521
8038
|
const assurance = plan.spec.security?.attestation ?? "disabled";
|
|
7522
8039
|
if (assurance !== "disabled") {
|
|
@@ -7539,35 +8056,66 @@ function createDeploymentManager(options) {
|
|
|
7539
8056
|
FZ_DEPLOY_PROFILE: options.profile,
|
|
7540
8057
|
...options.publicApiUrl ? { PUBLIC_API_URL: options.publicApiUrl } : {}
|
|
7541
8058
|
};
|
|
7542
|
-
const
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
8059
|
+
const resolvedTargets = resolveDeploymentTargets(plan, options.environment ?? {}).filter((target) => target.profiles.includes(options.profile));
|
|
8060
|
+
const assigned = request.assignment;
|
|
8061
|
+
let targetNames;
|
|
8062
|
+
if (assigned) {
|
|
8063
|
+
const selected = resolvedTargets.find(({ name }) => name === assigned.targetName);
|
|
8064
|
+
const mismatches = [
|
|
8065
|
+
!selected ? "target" : null,
|
|
8066
|
+
assigned.definitionDigest !== plan.sourceDigest ? "digest" : null,
|
|
8067
|
+
!Number.isSafeInteger(assigned.slot) || assigned.slot < 0 || selected && assigned.slot >= selected.maximum ? "slot" : null,
|
|
8068
|
+
selected && (assigned.operatingSystem.id !== selected.operatingSystem.id || assigned.operatingSystem.version !== selected.operatingSystem.version || assigned.operatingSystem.architecture !== selected.operatingSystem.architecture) ? "operating-system" : null,
|
|
8069
|
+
selected && assigned.confidentialCompute !== selected.confidentialCompute ? "confidential-compute" : null,
|
|
8070
|
+
selected && assigned.execution !== selected.allocation.execution ? "execution" : null,
|
|
8071
|
+
selected && assigned.sharing !== selected.allocation.sharing ? "sharing" : null,
|
|
8072
|
+
selected && (assigned.resources.cpuCores !== selected.allocation.resources.cpuCores || assigned.resources.memoryMiB !== selected.allocation.resources.memoryMiB || assigned.resources.storageGiB !== selected.allocation.resources.storageGiB) ? "resources" : null
|
|
8073
|
+
].filter(Boolean);
|
|
8074
|
+
if (!selected || mismatches.length > 0) {
|
|
8075
|
+
throw new DeploymentError("PIPELINE_FAILED", `the control-plane target assignment does not match this exact deployment plan (${mismatches.join(", ")})`);
|
|
8076
|
+
}
|
|
8077
|
+
const expectedPrivate = selected.connectivity?.private;
|
|
8078
|
+
const expectedPublic = selected.connectivity?.public;
|
|
8079
|
+
const assignedPrivate = assigned.connectivity?.private;
|
|
8080
|
+
const assignedPublic = assigned.connectivity?.public;
|
|
8081
|
+
const publicMatches = expectedPublic?.mode === "cloudflare-tunnel" ? assignedPublic?.mode === "cloudflare-tunnel" && assignedPublic.credential === expectedPublic.credential && assignedPublic.service.protocol === expectedPublic.service.protocol && assignedPublic.service.port === expectedPublic.service.port && assignedPublic.hostname === (expectedPublic.hostname.mode === "static" ? `${expectedPublic.hostname.label}.${expectedPublic.zone}` : `${expectedPublic.hostname.prefix}${expectedPublic.hostname.startAt + assigned.slot}.${expectedPublic.zone}`) : expectedPublic?.mode === "disabled" ? assignedPublic?.mode === "disabled" : assignedPublic === undefined;
|
|
8082
|
+
if (JSON.stringify(assignedPrivate) !== JSON.stringify(expectedPrivate) || !publicMatches) {
|
|
8083
|
+
throw new DeploymentError("PIPELINE_FAILED", "the control-plane connectivity assignment does not match this target slot");
|
|
8084
|
+
}
|
|
8085
|
+
targetNames = new Set([assigned.targetName]);
|
|
8086
|
+
} else {
|
|
8087
|
+
if (matchingTargetNames.length !== 1) {
|
|
8088
|
+
throw new DeploymentError("PIPELINE_FAILED", "an unassigned bootstrap claim must resolve to exactly one target for its profile");
|
|
7549
8089
|
}
|
|
7550
|
-
|
|
8090
|
+
targetNames = new Set(matchingTargetNames);
|
|
7551
8091
|
}
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
8092
|
+
if (assigned)
|
|
8093
|
+
Object.assign(phaseEnvironment2, {
|
|
8094
|
+
FZ_TARGET_NAME: assigned.targetName,
|
|
8095
|
+
FZ_TARGET_SLOT: String(assigned.slot),
|
|
8096
|
+
FZ_TARGET_CPU_CORES: String(assigned.resources.cpuCores),
|
|
8097
|
+
FZ_TARGET_MEMORY_MIB: String(assigned.resources.memoryMiB),
|
|
8098
|
+
FZ_TARGET_STORAGE_GIB: String(assigned.resources.storageGiB),
|
|
8099
|
+
FZ_TARGET_EXECUTION: assigned.execution
|
|
8100
|
+
});
|
|
8101
|
+
for (const targetName of targetNames) {
|
|
8102
|
+
const capacity2 = resolvedTargets.find(({ name }) => name === targetName)?.allocation.resources;
|
|
8103
|
+
if (!capacity2)
|
|
8104
|
+
throw new DeploymentError("PIPELINE_FAILED", `target ${targetName} has no resolved capacity`);
|
|
8105
|
+
const components = Object.values(plan.spec.components).filter(({ target }) => target === targetName);
|
|
8106
|
+
const cpu = components.reduce((sum, component2) => sum + (component2.resources?.cpu?.limit ?? 0), 0);
|
|
8107
|
+
const memoryMiB = components.reduce((sum, component2) => sum + (component2.resources?.memory?.limitMiB ?? 0), 0);
|
|
8108
|
+
const ephemeralMiB = components.reduce((sum, component2) => sum + (component2.kind === "application" ? (component2.storage ?? []).reduce((storage, mount) => storage + (mount.class === "ephemeral" ? mount.sizeMiB : 0), 0) : 0), 0);
|
|
8109
|
+
if (cpu > capacity2.cpuCores || memoryMiB > capacity2.memoryMiB || ephemeralMiB > capacity2.storageGiB * 1024) {
|
|
8110
|
+
throw new DeploymentError("PIPELINE_FAILED", `component limits exceed the reserved capacity of target ${targetName}`);
|
|
7556
8111
|
}
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
profiles: [...target.selector.profiles],
|
|
7560
|
-
confidentialCompute: target.selector.confidentialCompute ?? "disabled",
|
|
7561
|
-
labels: { ...target.selector.labels ?? {} },
|
|
7562
|
-
minimum: target.cardinality.minimum,
|
|
7563
|
-
desired: Number(desired),
|
|
7564
|
-
maximum: target.cardinality.maximum
|
|
7565
|
-
};
|
|
7566
|
-
});
|
|
8112
|
+
}
|
|
8113
|
+
const inputValues = resolveDeploymentInputs(plan, options.environment ?? {});
|
|
7567
8114
|
const projectExec2 = options.projectExec ?? spawnExact;
|
|
7568
8115
|
const materializePlanArgv = (argv2) => argv2.map((argument) => {
|
|
7569
|
-
|
|
7570
|
-
|
|
8116
|
+
const coordinate = argument.match(/^\$\{(FZ_[A-Z0-9_]+)\}$/)?.[1];
|
|
8117
|
+
if (coordinate && phaseEnvironment2[coordinate] !== undefined)
|
|
8118
|
+
return phaseEnvironment2[coordinate];
|
|
7571
8119
|
if (argument.includes("${")) {
|
|
7572
8120
|
throw new DeploymentActionError("refused", `unsupported deployment argv coordinate ${argument}`);
|
|
7573
8121
|
}
|
|
@@ -7576,6 +8124,8 @@ function createDeploymentManager(options) {
|
|
|
7576
8124
|
const providerSoftware = {
|
|
7577
8125
|
"forgezero.bun": "bun",
|
|
7578
8126
|
"forgezero.docker": "docker",
|
|
8127
|
+
"forgezero.containerd": "containerd",
|
|
8128
|
+
"forgezero.kata": "kata-containers",
|
|
7579
8129
|
"forgezero.nginx": "nginx",
|
|
7580
8130
|
"forgezero.arangodb": "arangodb",
|
|
7581
8131
|
"forgezero.cloudflared": "cloudflared",
|
|
@@ -7601,6 +8151,29 @@ function createDeploymentManager(options) {
|
|
|
7601
8151
|
await options.ensureSoftware(requirements);
|
|
7602
8152
|
return { changed: true };
|
|
7603
8153
|
},
|
|
8154
|
+
"forgezero.network/ensure@1": async ({ resolved }) => {
|
|
8155
|
+
if (!assigned || typeof resolved.target !== "string" || resolved.target !== assigned.targetName) {
|
|
8156
|
+
throw new DeploymentActionError("refused", "network.ensure must target this exact control-plane assignment");
|
|
8157
|
+
}
|
|
8158
|
+
if (!assigned.connectivity)
|
|
8159
|
+
return { changed: false, evidence: { target: assigned.targetName } };
|
|
8160
|
+
if (!options.ensureSoftware || !options.applyConnectivity) {
|
|
8161
|
+
throw new DeploymentActionError("provider-unavailable", "the supervised connectivity helper is unavailable");
|
|
8162
|
+
}
|
|
8163
|
+
const requirements = [];
|
|
8164
|
+
if (assigned.connectivity.public?.mode === "cloudflare-tunnel")
|
|
8165
|
+
requirements.push({ id: "cloudflared", version: "latest" });
|
|
8166
|
+
if (assigned.connectivity.private?.mode === "cloudflare-warp")
|
|
8167
|
+
requirements.push({ id: "cloudflare-warp", version: "latest" });
|
|
8168
|
+
if (requirements.length > 0)
|
|
8169
|
+
await options.ensureSoftware(requirements);
|
|
8170
|
+
const evidence = await options.applyConnectivity({
|
|
8171
|
+
key: `${options.key}:${assigned.targetName}:${assigned.slot}`,
|
|
8172
|
+
intent: assigned.connectivity,
|
|
8173
|
+
capabilities: request.connectivityCapabilities ?? {}
|
|
8174
|
+
});
|
|
8175
|
+
return { changed: true, evidence: structuredClone(evidence) };
|
|
8176
|
+
},
|
|
7604
8177
|
"forgezero.exec/argv@1": async ({ resolved, step: step2 }) => {
|
|
7605
8178
|
if (!Array.isArray(resolved.argv) || resolved.argv.some((value) => typeof value !== "string"))
|
|
7606
8179
|
throw new DeploymentActionError("refused", "exec.argv requires an exact argv array");
|
|
@@ -7642,7 +8215,7 @@ function createDeploymentManager(options) {
|
|
|
7642
8215
|
const component2 = plan.spec.components[String(resolved.component ?? "")];
|
|
7643
8216
|
const mounts = component2?.kind === "database" ? [component2.storage] : component2?.storage ?? [];
|
|
7644
8217
|
for (const mount of mounts) {
|
|
7645
|
-
const candidate = existsSync4(mount.path) ? mount.path :
|
|
8218
|
+
const candidate = existsSync4(mount.path) ? mount.path : dirname2(mount.path);
|
|
7646
8219
|
const stats = statfsSync(candidate);
|
|
7647
8220
|
const freePercent = Number(stats.bavail) / Number(stats.blocks) * 100;
|
|
7648
8221
|
const minimum = mount.class === "ephemeral" ? 0 : mount.minimumFreePercent ?? 0;
|
|
@@ -7699,6 +8272,23 @@ function createDeploymentManager(options) {
|
|
|
7699
8272
|
workflow: "deploy",
|
|
7700
8273
|
inputs: inputValues,
|
|
7701
8274
|
actions: actions2,
|
|
8275
|
+
execution: assigned ? {
|
|
8276
|
+
targetName: assigned.targetName,
|
|
8277
|
+
slot: assigned.slot,
|
|
8278
|
+
releaseExecutor: request.releaseExecutor === true,
|
|
8279
|
+
runtime: assigned.execution,
|
|
8280
|
+
cpuCores: assigned.resources.cpuCores,
|
|
8281
|
+
memoryMiB: assigned.resources.memoryMiB,
|
|
8282
|
+
storageGiB: assigned.resources.storageGiB
|
|
8283
|
+
} : {
|
|
8284
|
+
targetName: matchingTargetNames[0],
|
|
8285
|
+
slot: 0,
|
|
8286
|
+
releaseExecutor: request.releaseExecutor === true,
|
|
8287
|
+
runtime: resolvedTargets[0].allocation.execution,
|
|
8288
|
+
cpuCores: resolvedTargets[0].allocation.resources.cpuCores,
|
|
8289
|
+
memoryMiB: resolvedTargets[0].allocation.resources.memoryMiB,
|
|
8290
|
+
storageGiB: resolvedTargets[0].allocation.resources.storageGiB
|
|
8291
|
+
},
|
|
7702
8292
|
shouldRun: (step2) => step2.scope.kind === "release-executor" || step2.scope.kind === "elected-one" ? request.releaseExecutor === true : targetNames.has(step2.scope.target)
|
|
7703
8293
|
});
|
|
7704
8294
|
const phase = {
|
|
@@ -8060,7 +8650,12 @@ async function deployClaim(options, claim) {
|
|
|
8060
8650
|
schedule();
|
|
8061
8651
|
let result;
|
|
8062
8652
|
try {
|
|
8063
|
-
const run2 = () => manager.deploy({
|
|
8653
|
+
const run2 = () => manager.deploy({
|
|
8654
|
+
revision: claim.revision,
|
|
8655
|
+
releaseExecutor: claim.releaseExecutor,
|
|
8656
|
+
...claim.assignment ? { assignment: claim.assignment } : {},
|
|
8657
|
+
...claim.connectivityCapabilities ? { connectivityCapabilities: claim.connectivityCapabilities } : {}
|
|
8658
|
+
}).result;
|
|
8064
8659
|
result = options.telemetry ? await options.telemetry.observe("deployment.run", run2) : await run2();
|
|
8065
8660
|
} finally {
|
|
8066
8661
|
stopped = true;
|
|
@@ -8130,7 +8725,11 @@ async function pullDeploymentOnce(options) {
|
|
|
8130
8725
|
...result.resolvedTargets ? { resolvedTargets: result.resolvedTargets.map((target) => ({
|
|
8131
8726
|
...target,
|
|
8132
8727
|
profiles: [...target.profiles],
|
|
8133
|
-
|
|
8728
|
+
operatingSystem: { ...target.operatingSystem },
|
|
8729
|
+
labels: { ...target.labels },
|
|
8730
|
+
allocation: { ...target.allocation, resources: { ...target.allocation.resources } },
|
|
8731
|
+
...target.provisioning ? { provisioning: { ...target.provisioning } } : {},
|
|
8732
|
+
...target.connectivity ? { connectivity: structuredClone(target.connectivity) } : {}
|
|
8134
8733
|
})) } : {},
|
|
8135
8734
|
...result.capacity ? { capacity: {
|
|
8136
8735
|
recommendedConcurrency: result.capacity.recommendedConcurrency,
|
|
@@ -8186,11 +8785,11 @@ import {
|
|
|
8186
8785
|
mkdirSync as mkdirSync4,
|
|
8187
8786
|
readFileSync as readFileSync4,
|
|
8188
8787
|
renameSync as renameSync4,
|
|
8189
|
-
statSync,
|
|
8788
|
+
statSync as statSync2,
|
|
8190
8789
|
unlinkSync as unlinkSync4,
|
|
8191
8790
|
writeFileSync as writeFileSync4
|
|
8192
8791
|
} from "fs";
|
|
8193
|
-
import { dirname as
|
|
8792
|
+
import { dirname as dirname3 } from "path";
|
|
8194
8793
|
function privateNetworkAttachmentFromEnvironment(env = process.env) {
|
|
8195
8794
|
const values = {
|
|
8196
8795
|
accountId: env.FZ_CF_ACCOUNT_ID?.trim() ?? "",
|
|
@@ -8220,7 +8819,7 @@ var validBinding = (value, expectedNodeKey) => {
|
|
|
8220
8819
|
function loadGuestBinding(path2, expectedNodeKey) {
|
|
8221
8820
|
if (!existsSync6(path2))
|
|
8222
8821
|
return null;
|
|
8223
|
-
const mode =
|
|
8822
|
+
const mode = statSync2(path2).mode & 511;
|
|
8224
8823
|
if ((mode & 63) !== 0) {
|
|
8225
8824
|
throw new Error(`guest enrolment state at ${path2} is not private`);
|
|
8226
8825
|
}
|
|
@@ -8236,7 +8835,7 @@ function loadGuestBinding(path2, expectedNodeKey) {
|
|
|
8236
8835
|
return parsed;
|
|
8237
8836
|
}
|
|
8238
8837
|
function persistGuestBinding(path2, binding) {
|
|
8239
|
-
mkdirSync4(
|
|
8838
|
+
mkdirSync4(dirname3(path2), { recursive: true, mode: 448 });
|
|
8240
8839
|
const temporary = `${path2}.next`;
|
|
8241
8840
|
writeFileSync4(temporary, `${JSON.stringify(binding)}
|
|
8242
8841
|
`, { mode: 384 });
|
|
@@ -8588,7 +9187,7 @@ import { join as join3 } from "path";
|
|
|
8588
9187
|
import { fileURLToPath } from "url";
|
|
8589
9188
|
|
|
8590
9189
|
// src/version.ts
|
|
8591
|
-
var VERSION2 = "0.1.
|
|
9190
|
+
var VERSION2 = "0.1.75";
|
|
8592
9191
|
|
|
8593
9192
|
// src/ssh-bootstrap.ts
|
|
8594
9193
|
class SshBootstrapError extends Error {
|
|
@@ -9304,11 +9903,11 @@ import {
|
|
|
9304
9903
|
readFileSync as readFileSync6,
|
|
9305
9904
|
readdirSync,
|
|
9306
9905
|
rmSync as rmSync4,
|
|
9307
|
-
statSync as
|
|
9906
|
+
statSync as statSync3,
|
|
9308
9907
|
unlinkSync as unlinkSync5,
|
|
9309
9908
|
writeFileSync as writeFileSync6
|
|
9310
9909
|
} from "fs";
|
|
9311
|
-
import { dirname as
|
|
9910
|
+
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join4 } from "path";
|
|
9312
9911
|
import { isIP as isIP2 } from "net";
|
|
9313
9912
|
|
|
9314
9913
|
// src/compute.ts
|
|
@@ -9733,7 +10332,7 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
9733
10332
|
if (!image || !isAbsolute3(image.path) || !SHA256.test(image.sha256)) {
|
|
9734
10333
|
throw new MetalProvisionError(`image ${claim.spec.imageKey} is not configured locally`);
|
|
9735
10334
|
}
|
|
9736
|
-
if (!
|
|
10335
|
+
if (!statSync3(image.path).isFile())
|
|
9737
10336
|
throw new MetalProvisionError("configured image is not a file");
|
|
9738
10337
|
const digest = (await checked2(exec, ["sha256sum", image.path])).stdout.trim().split(/\s+/)[0];
|
|
9739
10338
|
if (digest !== image.sha256)
|
|
@@ -9818,7 +10417,7 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
9818
10417
|
}
|
|
9819
10418
|
const service = `forgezero-guest@${name}.service`;
|
|
9820
10419
|
const unitPath = join4(profile.unitDir, service);
|
|
9821
|
-
mkdirSync5(
|
|
10420
|
+
mkdirSync5(dirname4(unitPath), { recursive: true });
|
|
9822
10421
|
writeFileSync6(unitPath, guestUnit(spec), { mode: 420 });
|
|
9823
10422
|
await checked2(exec, ["systemctl", "daemon-reload"]);
|
|
9824
10423
|
if (prior?.phase === "running") {
|
|
@@ -10882,7 +11481,7 @@ import {
|
|
|
10882
11481
|
symlinkSync as symlinkSync3,
|
|
10883
11482
|
writeFileSync as writeFileSync9
|
|
10884
11483
|
} from "fs";
|
|
10885
|
-
import { dirname as
|
|
11484
|
+
import { dirname as dirname5, join as join7, resolve as resolve2 } from "path";
|
|
10886
11485
|
var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
|
|
10887
11486
|
var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
|
|
10888
11487
|
var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
|
|
@@ -10904,7 +11503,7 @@ var syncReleaseDirectory = (directory) => {
|
|
|
10904
11503
|
join7(directory, "dist", "fz-git-ssh.js"),
|
|
10905
11504
|
join7(directory, "dist"),
|
|
10906
11505
|
directory,
|
|
10907
|
-
|
|
11506
|
+
dirname5(directory)
|
|
10908
11507
|
])
|
|
10909
11508
|
syncPath(path2);
|
|
10910
11509
|
};
|
|
@@ -11026,7 +11625,7 @@ async function stageAgentRelease(releaseInput, options) {
|
|
|
11026
11625
|
args: ["-xOzf", archive, member]
|
|
11027
11626
|
}, `agent update extraction of ${member}`);
|
|
11028
11627
|
const destination = join7(unpacked, relative);
|
|
11029
|
-
mkdirSync8(
|
|
11628
|
+
mkdirSync8(dirname5(destination), { recursive: true, mode: 448 });
|
|
11030
11629
|
writeFileSync9(destination, extracted.output, { mode: 384, flag: "wx" });
|
|
11031
11630
|
}
|
|
11032
11631
|
await validateReleaseDirectory(unpacked, release, run2);
|
|
@@ -11058,21 +11657,21 @@ async function stageAgentRelease(releaseInput, options) {
|
|
|
11058
11657
|
}
|
|
11059
11658
|
}
|
|
11060
11659
|
function selectAgentRelease(staged) {
|
|
11061
|
-
const next = join7(
|
|
11660
|
+
const next = join7(dirname5(staged.currentLink), `.current.${randomUUID2()}.next`);
|
|
11062
11661
|
try {
|
|
11063
11662
|
symlinkSync3(staged.nextTarget, next);
|
|
11064
11663
|
renameSync6(next, staged.currentLink);
|
|
11065
|
-
syncPath(
|
|
11664
|
+
syncPath(dirname5(staged.currentLink));
|
|
11066
11665
|
} finally {
|
|
11067
11666
|
rmSync5(next, { force: true });
|
|
11068
11667
|
}
|
|
11069
11668
|
}
|
|
11070
11669
|
function restoreAgentRelease(staged) {
|
|
11071
|
-
const next = join7(
|
|
11670
|
+
const next = join7(dirname5(staged.currentLink), `.current.${randomUUID2()}.rollback`);
|
|
11072
11671
|
try {
|
|
11073
11672
|
symlinkSync3(staged.previousTarget, next);
|
|
11074
11673
|
renameSync6(next, staged.currentLink);
|
|
11075
|
-
syncPath(
|
|
11674
|
+
syncPath(dirname5(staged.currentLink));
|
|
11076
11675
|
} finally {
|
|
11077
11676
|
rmSync5(next, { force: true });
|
|
11078
11677
|
}
|
|
@@ -11094,7 +11693,7 @@ import {
|
|
|
11094
11693
|
writeFileSync as writeFileSync10
|
|
11095
11694
|
} from "fs";
|
|
11096
11695
|
import { connect as connect5, createServer as createServer6 } from "net";
|
|
11097
|
-
import { dirname as
|
|
11696
|
+
import { dirname as dirname6, join as join8, resolve as resolve3 } from "path";
|
|
11098
11697
|
var AGENT_UPDATE_GROUP = "forgezero-update";
|
|
11099
11698
|
var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-helper.service";
|
|
11100
11699
|
var AGENT_UPDATE_JOURNAL = "/var/lib/forgezero/agent-update.json";
|
|
@@ -11194,7 +11793,7 @@ function readJournal(path2, root) {
|
|
|
11194
11793
|
return validateJournal(JSON.parse(readFileSync10(path2, "utf8")), root);
|
|
11195
11794
|
}
|
|
11196
11795
|
function writeAtomic(path2, value, mode) {
|
|
11197
|
-
mkdirSync9(
|
|
11796
|
+
mkdirSync9(dirname6(path2), { recursive: true, mode: 493 });
|
|
11198
11797
|
const next = `${path2}.${randomUUID3()}.next`;
|
|
11199
11798
|
let file;
|
|
11200
11799
|
try {
|
|
@@ -11205,7 +11804,7 @@ function writeAtomic(path2, value, mode) {
|
|
|
11205
11804
|
closeSync3(file);
|
|
11206
11805
|
file = undefined;
|
|
11207
11806
|
renameSync7(next, path2);
|
|
11208
|
-
const directory = openSync3(
|
|
11807
|
+
const directory = openSync3(dirname6(path2), "r");
|
|
11209
11808
|
try {
|
|
11210
11809
|
fsyncSync2(directory);
|
|
11211
11810
|
} finally {
|
|
@@ -11327,7 +11926,7 @@ async function activateAgentRelease(staged, options = {}) {
|
|
|
11327
11926
|
const now = options.now ?? Date.now;
|
|
11328
11927
|
const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
|
|
11329
11928
|
const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
|
|
11330
|
-
const previous = readJournal(journalPath,
|
|
11929
|
+
const previous = readJournal(journalPath, dirname6(staged.currentLink));
|
|
11331
11930
|
const attemptId = options.attemptId ?? randomUUID3();
|
|
11332
11931
|
if (!ATTEMPT_ID.test(attemptId))
|
|
11333
11932
|
throw new Error("Agent update attempt ID is invalid");
|
|
@@ -11435,7 +12034,7 @@ function startAgentUpdateHelper(options = {}) {
|
|
|
11435
12034
|
const socketPath = options.socketPath ?? DEFAULT_AGENT_UPDATE_SOCKET;
|
|
11436
12035
|
if (existsSync13(socketPath))
|
|
11437
12036
|
unlinkSync10(socketPath);
|
|
11438
|
-
mkdirSync9(
|
|
12037
|
+
mkdirSync9(dirname6(socketPath), { recursive: true, mode: 488 });
|
|
11439
12038
|
const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
|
|
11440
12039
|
const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
|
|
11441
12040
|
const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
|
|
@@ -11544,23 +12143,63 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
|
|
|
11544
12143
|
}
|
|
11545
12144
|
|
|
11546
12145
|
// src/agent-heartbeat.ts
|
|
11547
|
-
import { readFileSync as readFileSync11 } from "fs";
|
|
12146
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, statfsSync as statfsSync2 } from "fs";
|
|
12147
|
+
import { cpus, freemem, loadavg, totalmem } from "os";
|
|
12148
|
+
function readAgentHostMetrics() {
|
|
12149
|
+
const filesystem = statfsSync2("/");
|
|
12150
|
+
return {
|
|
12151
|
+
logicalCpu: cpus().length,
|
|
12152
|
+
memoryBytes: totalmem(),
|
|
12153
|
+
memoryFreeBytes: freemem(),
|
|
12154
|
+
storageBytes: Number(filesystem.blocks) * Number(filesystem.bsize),
|
|
12155
|
+
storageFreeBytes: Number(filesystem.bavail) * Number(filesystem.bsize),
|
|
12156
|
+
load1m: loadavg()[0]
|
|
12157
|
+
};
|
|
12158
|
+
}
|
|
11548
12159
|
var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
|
|
11549
12160
|
var remoteOutcome4 = (cause) => cause instanceof SignedNodeHttpError && cause.status < 500 ? "refused" : cause instanceof SignedNodeHttpError ? "retryable" : "failed";
|
|
11550
12161
|
|
|
11551
12162
|
class AgentUpdateRefusedError extends Error {
|
|
11552
12163
|
}
|
|
11553
|
-
function observeAgentHost(version = VERSION2, mode = "enrolled", osRelease = readFileSync11("/etc/os-release", "utf8"), architecture = process.arch) {
|
|
12164
|
+
function observeAgentHost(version = VERSION2, mode = "enrolled", osRelease = readFileSync11("/etc/os-release", "utf8"), architecture = process.arch, metrics = readAgentHostMetrics()) {
|
|
11554
12165
|
const values = Object.fromEntries(osRelease.split(`
|
|
11555
12166
|
`).flatMap((line) => {
|
|
11556
12167
|
const separator = line.indexOf("=");
|
|
11557
12168
|
return separator > 0 ? [[line.slice(0, separator), unquote(line.slice(separator + 1))]] : [];
|
|
11558
12169
|
}));
|
|
12170
|
+
const osId = (values.ID ?? "unknown").toLowerCase();
|
|
12171
|
+
const osVersion = values.VERSION_ID ?? "unknown";
|
|
12172
|
+
const ubuntuX64 = osId === "ubuntu" && ["24.04", "26.04"].includes(osVersion) && architecture === "x64";
|
|
12173
|
+
const enabled = (path2) => {
|
|
12174
|
+
try {
|
|
12175
|
+
return /^(?:1|y)$/i.test(readFileSync11(path2, "utf8").trim());
|
|
12176
|
+
} catch {
|
|
12177
|
+
return false;
|
|
12178
|
+
}
|
|
12179
|
+
};
|
|
12180
|
+
const gib = 1024 ** 3;
|
|
12181
|
+
const mib = 1024 ** 2;
|
|
12182
|
+
const storageGiB = Math.max(1, Math.floor(metrics.storageBytes / gib));
|
|
11559
12183
|
return {
|
|
11560
12184
|
version,
|
|
11561
|
-
os: { id:
|
|
12185
|
+
os: { id: osId, versionId: osVersion },
|
|
11562
12186
|
architecture,
|
|
11563
|
-
mode
|
|
12187
|
+
mode,
|
|
12188
|
+
runtimeCapabilities: {
|
|
12189
|
+
native: true,
|
|
12190
|
+
ociRunc: ubuntuX64,
|
|
12191
|
+
kataQemuSnp: ubuntuX64 && osVersion === "26.04" && existsSync14("/dev/kvm") && existsSync14("/dev/sev") && enabled("/sys/module/kvm_amd/parameters/sev") && enabled("/sys/module/kvm_amd/parameters/sev_snp")
|
|
12192
|
+
},
|
|
12193
|
+
capacity: {
|
|
12194
|
+
logicalCpu: Math.max(1, metrics.logicalCpu),
|
|
12195
|
+
memoryMiB: Math.max(1, Math.floor(metrics.memoryBytes / mib)),
|
|
12196
|
+
storageGiB
|
|
12197
|
+
},
|
|
12198
|
+
utilization: {
|
|
12199
|
+
load1mMilli: Math.max(0, Math.round(metrics.load1m * 1000)),
|
|
12200
|
+
memoryUsedMiB: Math.max(0, Math.floor((metrics.memoryBytes - metrics.memoryFreeBytes) / mib)),
|
|
12201
|
+
storageUsedGiB: Math.max(0, Math.min(storageGiB, Math.ceil((metrics.storageBytes - metrics.storageFreeBytes) / gib)))
|
|
12202
|
+
}
|
|
11564
12203
|
};
|
|
11565
12204
|
}
|
|
11566
12205
|
async function heartbeatAgentOnce(options) {
|
|
@@ -11668,14 +12307,14 @@ function startAgentHeartbeat(options) {
|
|
|
11668
12307
|
}
|
|
11669
12308
|
|
|
11670
12309
|
// src/software-helper.ts
|
|
11671
|
-
import { chmodSync as chmodSync14, existsSync as
|
|
12310
|
+
import { chmodSync as chmodSync14, existsSync as existsSync17, mkdirSync as mkdirSync13, unlinkSync as unlinkSync11 } from "fs";
|
|
11672
12311
|
import { connect as connect6, createServer as createServer7 } from "net";
|
|
11673
|
-
import { dirname as
|
|
12312
|
+
import { dirname as dirname10 } from "path";
|
|
11674
12313
|
|
|
11675
12314
|
// src/service-supervisor.ts
|
|
11676
12315
|
import { createHash as createHash9 } from "crypto";
|
|
11677
|
-
import { existsSync as
|
|
11678
|
-
import { dirname as
|
|
12316
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync12, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync8, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "fs";
|
|
12317
|
+
import { dirname as dirname7, join as join9, resolve as resolve4, sep as sep2 } from "path";
|
|
11679
12318
|
var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
|
|
11680
12319
|
var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
|
|
11681
12320
|
var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
|
|
@@ -11685,14 +12324,14 @@ var statePath = (id2) => `${SERVICE_STATE_DIRECTORY}/${id2}.json`;
|
|
|
11685
12324
|
var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
|
|
11686
12325
|
var defaultHost = {
|
|
11687
12326
|
write(path2, content, mode) {
|
|
11688
|
-
mkdirSync10(
|
|
12327
|
+
mkdirSync10(dirname7(path2), { recursive: true, mode: 493 });
|
|
11689
12328
|
const next = `${path2}.next`;
|
|
11690
12329
|
writeFileSync11(next, content, { mode });
|
|
11691
12330
|
renameSync8(next, path2);
|
|
11692
12331
|
},
|
|
11693
12332
|
read: (path2) => readFileSync12(path2, "utf8"),
|
|
11694
|
-
exists:
|
|
11695
|
-
list: (path2) =>
|
|
12333
|
+
exists: existsSync15,
|
|
12334
|
+
list: (path2) => existsSync15(path2) ? readdirSync2(path2) : [],
|
|
11696
12335
|
realpath: realpathSync2,
|
|
11697
12336
|
mkdir: (path2, mode) => mkdirSync10(path2, { recursive: true, mode }),
|
|
11698
12337
|
remove: (path2) => rmSync7(path2, { force: true }),
|
|
@@ -11891,20 +12530,20 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
11891
12530
|
|
|
11892
12531
|
// src/container-supervisor.ts
|
|
11893
12532
|
import { createHash as createHash10 } from "crypto";
|
|
11894
|
-
import { existsSync as
|
|
11895
|
-
import { dirname as
|
|
12533
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync13, readdirSync as readdirSync3, realpathSync as realpathSync3, renameSync as renameSync9, rmSync as rmSync8, writeFileSync as writeFileSync12 } from "fs";
|
|
12534
|
+
import { dirname as dirname8, resolve as resolve5, sep as sep3 } from "path";
|
|
11896
12535
|
var defaultHost2 = {
|
|
11897
12536
|
realpath: realpathSync3,
|
|
11898
|
-
exists:
|
|
12537
|
+
exists: existsSync16,
|
|
11899
12538
|
read: (path2) => readFileSync13(path2, "utf8"),
|
|
11900
12539
|
write(path2, content, mode) {
|
|
11901
|
-
mkdirSync11(
|
|
12540
|
+
mkdirSync11(dirname8(path2), { recursive: true, mode: 493 });
|
|
11902
12541
|
const next = `${path2}.next`;
|
|
11903
12542
|
writeFileSync12(next, content, { mode });
|
|
11904
12543
|
renameSync9(next, path2);
|
|
11905
12544
|
},
|
|
11906
12545
|
remove: (path2) => rmSync8(path2, { force: true }),
|
|
11907
|
-
list: (path2) =>
|
|
12546
|
+
list: (path2) => existsSync16(path2) ? readdirSync3(path2) : [],
|
|
11908
12547
|
mkdir: (path2, mode) => mkdirSync11(path2, { recursive: true, mode }),
|
|
11909
12548
|
async exec(argv2) {
|
|
11910
12549
|
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" } });
|
|
@@ -11926,6 +12565,38 @@ var within3 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep3
|
|
|
11926
12565
|
var idFor2 = (key) => createHash10("sha256").update(key).digest("hex").slice(0, 24);
|
|
11927
12566
|
var statePath2 = (id2) => `${SERVICE_STATE_DIRECTORY}/${id2}.json`;
|
|
11928
12567
|
var nameFor = (id2, slot) => `forgezero-${id2}-slot${slot}`;
|
|
12568
|
+
function backendFor(application) {
|
|
12569
|
+
const runtime = application.runtime;
|
|
12570
|
+
if (runtime.kind !== "container")
|
|
12571
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
12572
|
+
if (runtime.provider === "forgezero.docker") {
|
|
12573
|
+
if (runtime.runtimeClass && runtime.runtimeClass !== "runc")
|
|
12574
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
12575
|
+
return { engine: "docker", runtimeClass: "runc", prefix: ["/usr/bin/docker"] };
|
|
12576
|
+
}
|
|
12577
|
+
if (runtime.provider === "forgezero.containerd") {
|
|
12578
|
+
if ((runtime.runtimeClass ?? "runc") !== "runc")
|
|
12579
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
12580
|
+
return {
|
|
12581
|
+
engine: "containerd",
|
|
12582
|
+
runtimeClass: "runc",
|
|
12583
|
+
prefix: ["/usr/local/bin/nerdctl", "--namespace", "forgezero"],
|
|
12584
|
+
runtimeName: "io.containerd.runc.v2"
|
|
12585
|
+
};
|
|
12586
|
+
}
|
|
12587
|
+
if (runtime.provider === "forgezero.kata") {
|
|
12588
|
+
if (runtime.runtimeClass !== "kata-qemu-snp")
|
|
12589
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
12590
|
+
return {
|
|
12591
|
+
engine: "containerd",
|
|
12592
|
+
runtimeClass: "kata-qemu-snp",
|
|
12593
|
+
prefix: ["/usr/local/bin/nerdctl", "--namespace", "forgezero"],
|
|
12594
|
+
runtimeName: "io.containerd.kata.v2"
|
|
12595
|
+
};
|
|
12596
|
+
}
|
|
12597
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_UNSUPPORTED");
|
|
12598
|
+
}
|
|
12599
|
+
var command2 = (backend, ...argv2) => [...backend.prefix, ...argv2];
|
|
11929
12600
|
function validateRequest(request, host) {
|
|
11930
12601
|
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))
|
|
11931
12602
|
throw new Error("CONTAINER_IDENTITY_INVALID");
|
|
@@ -11936,6 +12607,7 @@ function validateRequest(request, host) {
|
|
|
11936
12607
|
const application = request.application;
|
|
11937
12608
|
if (application.kind !== "application" || application.runtime.kind !== "container")
|
|
11938
12609
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
12610
|
+
backendFor(application);
|
|
11939
12611
|
if (!application.resources.cpu || !application.resources.memory || !application.resources.pids)
|
|
11940
12612
|
throw new Error("CONTAINER_LIMITS_REQUIRED");
|
|
11941
12613
|
if (application.runtime.security.privileged !== false || application.runtime.security.noNewPrivileges !== true || !application.runtime.security.dropCapabilities.includes("ALL"))
|
|
@@ -11957,12 +12629,13 @@ async function buildContainerImage(requestValue, host = defaultHost2) {
|
|
|
11957
12629
|
const runtime = request.application.runtime;
|
|
11958
12630
|
if (runtime.kind !== "container")
|
|
11959
12631
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
12632
|
+
const backend = backendFor(request.application);
|
|
11960
12633
|
const source = runtime.image.source;
|
|
11961
12634
|
if (source.kind === "registry") {
|
|
11962
12635
|
if (!source.reference.includes("@sha256:"))
|
|
11963
12636
|
throw new Error("CONTAINER_IMAGE_DIGEST_REQUIRED");
|
|
11964
|
-
await checked6(host,
|
|
11965
|
-
const digest2 = await checked6(host,
|
|
12637
|
+
await checked6(host, command2(backend, "pull", "--quiet", source.reference), "CONTAINER_PULL_FAILED");
|
|
12638
|
+
const digest2 = await checked6(host, command2(backend, "image", "inspect", "--format={{.Id}}", source.reference), "CONTAINER_INSPECT_FAILED");
|
|
11966
12639
|
if (!/^sha256:[a-f0-9]{64}$/.test(digest2))
|
|
11967
12640
|
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
11968
12641
|
return { image: source.reference, digest: digest2 };
|
|
@@ -11972,22 +12645,8 @@ async function buildContainerImage(requestValue, host = defaultHost2) {
|
|
|
11972
12645
|
if (!within3(request.release, context) || !within3(context, dockerfile))
|
|
11973
12646
|
throw new Error("CONTAINER_BUILD_PATH_INVALID");
|
|
11974
12647
|
const tag = `forgezero/${idFor2(request.key)}:${request.revision}`;
|
|
11975
|
-
await checked6(host, [
|
|
11976
|
-
|
|
11977
|
-
"build",
|
|
11978
|
-
"--pull",
|
|
11979
|
-
...request.noCache ? ["--no-cache"] : [],
|
|
11980
|
-
"--file",
|
|
11981
|
-
dockerfile,
|
|
11982
|
-
"--tag",
|
|
11983
|
-
tag,
|
|
11984
|
-
"--label",
|
|
11985
|
-
`net.forgezero.revision=${request.revision}`,
|
|
11986
|
-
"--label",
|
|
11987
|
-
`net.forgezero.component=${request.component}`,
|
|
11988
|
-
context
|
|
11989
|
-
], "CONTAINER_BUILD_FAILED");
|
|
11990
|
-
const digest = await checked6(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", tag], "CONTAINER_INSPECT_FAILED");
|
|
12648
|
+
await checked6(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");
|
|
12649
|
+
const digest = await checked6(host, command2(backend, "image", "inspect", "--format={{.Id}}", tag), "CONTAINER_INSPECT_FAILED");
|
|
11991
12650
|
if (!/^sha256:[a-f0-9]{64}$/.test(digest))
|
|
11992
12651
|
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
11993
12652
|
return { image: tag, digest };
|
|
@@ -12055,7 +12714,7 @@ ${service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
|
|
|
12055
12714
|
}
|
|
12056
12715
|
`;
|
|
12057
12716
|
}
|
|
12058
|
-
function runArgv(request, id2, slot, port, networkName) {
|
|
12717
|
+
function runArgv(request, backend, id2, slot, port, networkName) {
|
|
12059
12718
|
const { application } = request;
|
|
12060
12719
|
const resources = application.resources;
|
|
12061
12720
|
const runtime = application.runtime;
|
|
@@ -12063,35 +12722,7 @@ function runArgv(request, id2, slot, port, networkName) {
|
|
|
12063
12722
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
12064
12723
|
const memory = resources.memory;
|
|
12065
12724
|
const network = application.network.container;
|
|
12066
|
-
const argv2 = [
|
|
12067
|
-
"/usr/bin/docker",
|
|
12068
|
-
"run",
|
|
12069
|
-
"--detach",
|
|
12070
|
-
"--name",
|
|
12071
|
-
nameFor(id2, slot),
|
|
12072
|
-
"--restart",
|
|
12073
|
-
"unless-stopped",
|
|
12074
|
-
"--label",
|
|
12075
|
-
`net.forgezero.key=${request.key}`,
|
|
12076
|
-
"--label",
|
|
12077
|
-
`net.forgezero.revision=${request.revision}`,
|
|
12078
|
-
"--cpus",
|
|
12079
|
-
String(resources.cpu.limit),
|
|
12080
|
-
"--cpu-shares",
|
|
12081
|
-
String(resources.cpu.weight ?? 100),
|
|
12082
|
-
"--memory",
|
|
12083
|
-
`${memory.limitMiB}m`,
|
|
12084
|
-
...memory.reservationMiB ? ["--memory-reservation", `${memory.reservationMiB}m`] : [],
|
|
12085
|
-
...memory.swap === "disabled" ? ["--memory-swap", `${memory.limitMiB}m`] : [],
|
|
12086
|
-
"--pids-limit",
|
|
12087
|
-
String(resources.pids.limit),
|
|
12088
|
-
"--security-opt",
|
|
12089
|
-
"no-new-privileges=true",
|
|
12090
|
-
"--cap-drop",
|
|
12091
|
-
"ALL",
|
|
12092
|
-
...runtime.security.root === "read-only" ? ["--read-only"] : [],
|
|
12093
|
-
...network.mode === "host" ? ["--network", "host", "--env", `FZ_APP_PORT=${port}`] : ["--network", networkName, "--publish", `127.0.0.1:${port}:${application.service.port}`]
|
|
12094
|
-
];
|
|
12725
|
+
const argv2 = command2(backend, "run", "--detach", "--name", nameFor(id2, 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}`]);
|
|
12095
12726
|
for (const mount of application.storage ?? []) {
|
|
12096
12727
|
if (mount.class === "ephemeral")
|
|
12097
12728
|
argv2.push("--tmpfs", `${mount.path}:rw,noexec,nosuid,size=${mount.sizeMiB}m`);
|
|
@@ -12105,6 +12736,7 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
12105
12736
|
const request = validateRequest(requestValue, host);
|
|
12106
12737
|
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:"))
|
|
12107
12738
|
throw new Error("CONTAINER_IMAGE_INVALID");
|
|
12739
|
+
const backend = backendFor(request.application);
|
|
12108
12740
|
const id2 = idFor2(request.key);
|
|
12109
12741
|
host.mkdir(SERVICE_STATE_DIRECTORY, 448);
|
|
12110
12742
|
host.mkdir(SERVICE_NGINX_DIRECTORY, 493);
|
|
@@ -12117,9 +12749,9 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
12117
12749
|
if (containerNetwork.mode === "bridge") {
|
|
12118
12750
|
networkName = containerNetwork.network ? `forgezero-${id2}-${containerNetwork.network}` : "bridge";
|
|
12119
12751
|
if (networkName !== "bridge") {
|
|
12120
|
-
const inspected = await host.exec(
|
|
12752
|
+
const inspected = await host.exec(command2(backend, "network", "inspect", '--format={{index .Labels "net.forgezero.owner"}}', networkName));
|
|
12121
12753
|
if (inspected.exitCode !== 0) {
|
|
12122
|
-
await checked6(host,
|
|
12754
|
+
await checked6(host, command2(backend, "network", "create", "--driver", "bridge", "--label", `net.forgezero.owner=${id2}`, networkName), "CONTAINER_NETWORK_CREATE_FAILED");
|
|
12123
12755
|
} else if (inspected.output.trim() !== id2)
|
|
12124
12756
|
throw new Error("CONTAINER_NETWORK_OWNERSHIP_INVALID");
|
|
12125
12757
|
}
|
|
@@ -12128,8 +12760,8 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
12128
12760
|
const nextSlot = slots === 2 && previous?.activeSlot === 0 ? 1 : 0;
|
|
12129
12761
|
const port = ports[nextSlot];
|
|
12130
12762
|
const name = nameFor(id2, nextSlot);
|
|
12131
|
-
await host.exec(
|
|
12132
|
-
await checked6(host, runArgv(request, id2, nextSlot, port, networkName), "CONTAINER_START_FAILED");
|
|
12763
|
+
await host.exec(command2(backend, "rm", "--force", name));
|
|
12764
|
+
await checked6(host, runArgv(request, backend, id2, nextSlot, port, networkName), "CONTAINER_START_FAILED");
|
|
12133
12765
|
let healthy = false;
|
|
12134
12766
|
const health = request.application.service.health;
|
|
12135
12767
|
for (let attempt = 0;attempt < (health.attempts ?? 30); attempt += 1) {
|
|
@@ -12140,9 +12772,22 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
12140
12772
|
await host.sleep(health.intervalMs ?? 1000);
|
|
12141
12773
|
}
|
|
12142
12774
|
if (!healthy) {
|
|
12143
|
-
await host.exec(
|
|
12775
|
+
await host.exec(command2(backend, "rm", "--force", name));
|
|
12144
12776
|
throw new Error("CONTAINER_HEALTH_FAILED");
|
|
12145
12777
|
}
|
|
12778
|
+
let runtimeEvidence;
|
|
12779
|
+
if (backend.engine === "containerd") {
|
|
12780
|
+
const raw = await checked6(host, ["/usr/bin/ctr", "--namespace", "forgezero", "containers", "info", name], "CONTAINER_RUNTIME_EVIDENCE_FAILED");
|
|
12781
|
+
let observed = "";
|
|
12782
|
+
try {
|
|
12783
|
+
observed = String(JSON.parse(raw).Runtime?.Name ?? "");
|
|
12784
|
+
} catch {
|
|
12785
|
+
throw new Error("CONTAINER_RUNTIME_EVIDENCE_INVALID");
|
|
12786
|
+
}
|
|
12787
|
+
if (observed !== backend.runtimeName)
|
|
12788
|
+
throw new Error("CONTAINER_RUNTIME_DOWNGRADE");
|
|
12789
|
+
runtimeEvidence = observed;
|
|
12790
|
+
}
|
|
12146
12791
|
const nginxPath = `${SERVICE_NGINX_DIRECTORY}/forgezero-${id2}.conf`;
|
|
12147
12792
|
const previousNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
|
|
12148
12793
|
host.write(nginxPath, nginx2(request, id2, port), 420);
|
|
@@ -12156,7 +12801,7 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
12156
12801
|
host.write(nginxPath, previousNginx, 420);
|
|
12157
12802
|
await host.exec(["/usr/sbin/nginx", "-t"]);
|
|
12158
12803
|
await host.exec(["/usr/bin/systemctl", "reload", "nginx.service"]);
|
|
12159
|
-
await host.exec(
|
|
12804
|
+
await host.exec(command2(backend, "rm", "--force", name));
|
|
12160
12805
|
throw cause;
|
|
12161
12806
|
}
|
|
12162
12807
|
const state = {
|
|
@@ -12169,17 +12814,168 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
12169
12814
|
applicationPorts: ports,
|
|
12170
12815
|
activeSlot: nextSlot,
|
|
12171
12816
|
healthPath: health.path,
|
|
12817
|
+
containerRuntime: {
|
|
12818
|
+
engine: backend.engine,
|
|
12819
|
+
runtimeClass: backend.runtimeClass,
|
|
12820
|
+
...backend.engine === "containerd" ? { namespace: "forgezero", evidence: runtimeEvidence } : {}
|
|
12821
|
+
},
|
|
12172
12822
|
updatedAtTs: host.now()
|
|
12173
12823
|
};
|
|
12174
12824
|
host.write(statePath2(id2), `${JSON.stringify(state, null, 2)}
|
|
12175
12825
|
`, 384);
|
|
12176
12826
|
if (previous && previous.activeSlot !== nextSlot) {
|
|
12177
12827
|
await host.sleep(request.application.rollout.drainMs ?? 30000);
|
|
12178
|
-
await host.exec(
|
|
12828
|
+
await host.exec(command2(backend, "rm", "--force", nameFor(id2, previous.activeSlot)));
|
|
12179
12829
|
}
|
|
12180
12830
|
return state;
|
|
12181
12831
|
}
|
|
12182
12832
|
|
|
12833
|
+
// src/deployment-connectivity.ts
|
|
12834
|
+
import { createHash as createHash11 } from "crypto";
|
|
12835
|
+
import { mkdirSync as mkdirSync12, renameSync as renameSync10, writeFileSync as writeFileSync13 } from "fs";
|
|
12836
|
+
import { dirname as dirname9 } from "path";
|
|
12837
|
+
var TOKEN2 = /^[A-Za-z0-9._-]{40,16384}$/;
|
|
12838
|
+
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;
|
|
12839
|
+
var HOSTNAME2 = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
12840
|
+
var NAME3 = /^[a-z][a-z0-9._-]{0,99}$/;
|
|
12841
|
+
var idFor3 = (key) => createHash11("sha256").update(key).digest("hex").slice(0, 20);
|
|
12842
|
+
var checked7 = async (host, argv2, label) => {
|
|
12843
|
+
const result = await host.exec(argv2);
|
|
12844
|
+
if (result.exitCode !== 0)
|
|
12845
|
+
throw new Error(`${label} failed: ${result.output.slice(0, 512)}`);
|
|
12846
|
+
return result.output.trim();
|
|
12847
|
+
};
|
|
12848
|
+
async function sealWithSystemd(name, path2, value) {
|
|
12849
|
+
mkdirSync12(dirname9(path2), { recursive: true, mode: 448 });
|
|
12850
|
+
const next = `${path2}.next`;
|
|
12851
|
+
const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
|
|
12852
|
+
stdin: "pipe",
|
|
12853
|
+
stdout: "pipe",
|
|
12854
|
+
stderr: "pipe",
|
|
12855
|
+
env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" }
|
|
12856
|
+
});
|
|
12857
|
+
child.stdin.write(value);
|
|
12858
|
+
child.stdin.end();
|
|
12859
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
12860
|
+
new Response(child.stdout).text(),
|
|
12861
|
+
new Response(child.stderr).text(),
|
|
12862
|
+
child.exited
|
|
12863
|
+
]);
|
|
12864
|
+
if (exitCode !== 0)
|
|
12865
|
+
throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
|
|
12866
|
+
renameSync10(next, path2);
|
|
12867
|
+
}
|
|
12868
|
+
var defaultHost3 = {
|
|
12869
|
+
seal: sealWithSystemd,
|
|
12870
|
+
write(path2, content, mode) {
|
|
12871
|
+
mkdirSync12(dirname9(path2), { recursive: true, mode: 493 });
|
|
12872
|
+
const next = `${path2}.next`;
|
|
12873
|
+
writeFileSync13(next, content, { mode });
|
|
12874
|
+
renameSync10(next, path2);
|
|
12875
|
+
},
|
|
12876
|
+
async exec(argv2) {
|
|
12877
|
+
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" } });
|
|
12878
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
12879
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
12880
|
+
},
|
|
12881
|
+
sleep: (ms) => Bun.sleep(ms)
|
|
12882
|
+
};
|
|
12883
|
+
function validate2(request) {
|
|
12884
|
+
if (!request.key || request.key.length > 256)
|
|
12885
|
+
throw new Error("deployment connectivity key is invalid");
|
|
12886
|
+
const publicIntent = request.intent.public;
|
|
12887
|
+
const privateIntent = request.intent.private;
|
|
12888
|
+
if (publicIntent?.mode === "cloudflare-tunnel") {
|
|
12889
|
+
if (!NAME3.test(publicIntent.credential) || !HOSTNAME2.test(publicIntent.hostname) || !NAME3.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) || !TOKEN2.test(request.capabilities.public.connectorToken)) {
|
|
12890
|
+
throw new Error("public deployment connectivity is malformed");
|
|
12891
|
+
}
|
|
12892
|
+
} else if (request.capabilities.public)
|
|
12893
|
+
throw new Error("unexpected public deployment capability");
|
|
12894
|
+
if (privateIntent?.mode === "cloudflare-warp") {
|
|
12895
|
+
if (!NAME3.test(privateIntent.credential) || privateIntent.network.length < 1 || privateIntent.network.length > 128 || !request.capabilities.private || !UUID.test(request.capabilities.private.connectorId) || !TOKEN2.test(request.capabilities.private.connectorToken)) {
|
|
12896
|
+
throw new Error("private deployment connectivity is malformed");
|
|
12897
|
+
}
|
|
12898
|
+
} else if (request.capabilities.private)
|
|
12899
|
+
throw new Error("unexpected private deployment capability");
|
|
12900
|
+
}
|
|
12901
|
+
async function applyDeploymentConnectivity(request, host = defaultHost3) {
|
|
12902
|
+
validate2(request);
|
|
12903
|
+
const id2 = idFor3(request.key);
|
|
12904
|
+
const evidence = { key: request.key };
|
|
12905
|
+
if (request.intent.public?.mode === "cloudflare-tunnel") {
|
|
12906
|
+
const capability = request.capabilities.public;
|
|
12907
|
+
const credentialName = `FZ_TUNNEL_${id2.toUpperCase()}`;
|
|
12908
|
+
const credentialPath = `/etc/forgezero/creds/deployment-tunnel-${id2}.cred`;
|
|
12909
|
+
const unit2 = `forgezero-cloudflared-${id2}.service`;
|
|
12910
|
+
const unitPath = `/etc/systemd/system/${unit2}`;
|
|
12911
|
+
const metricsPort = 20000 + Number.parseInt(id2.slice(0, 4), 16) % 20000;
|
|
12912
|
+
await host.seal(credentialName, credentialPath, capability.connectorToken);
|
|
12913
|
+
host.write(unitPath, `[Unit]
|
|
12914
|
+
Description=ForgeZero deployment Tunnel ${id2}
|
|
12915
|
+
After=network-online.target
|
|
12916
|
+
Wants=network-online.target
|
|
12917
|
+
|
|
12918
|
+
[Service]
|
|
12919
|
+
Type=simple
|
|
12920
|
+
DynamicUser=yes
|
|
12921
|
+
LoadCredentialEncrypted=${credentialName}:${credentialPath}
|
|
12922
|
+
ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --metrics 127.0.0.1:${metricsPort} run --token-file %d/${credentialName}
|
|
12923
|
+
Restart=always
|
|
12924
|
+
RestartSec=5
|
|
12925
|
+
NoNewPrivileges=true
|
|
12926
|
+
PrivateTmp=true
|
|
12927
|
+
ProtectSystem=strict
|
|
12928
|
+
ProtectHome=true
|
|
12929
|
+
|
|
12930
|
+
[Install]
|
|
12931
|
+
WantedBy=multi-user.target
|
|
12932
|
+
`, 420);
|
|
12933
|
+
await checked7(host, ["/usr/bin/systemctl", "daemon-reload"], "cloudflared daemon reload");
|
|
12934
|
+
await checked7(host, ["/usr/bin/systemctl", "enable", "--now", unit2], "cloudflared start");
|
|
12935
|
+
await checked7(host, ["/usr/bin/systemctl", "is-active", "--quiet", unit2], "cloudflared readiness");
|
|
12936
|
+
evidence.public = { hostname: request.intent.public.hostname, tunnelId: capability.tunnelId, unit: unit2, active: true };
|
|
12937
|
+
}
|
|
12938
|
+
if (request.intent.private?.mode === "cloudflare-warp") {
|
|
12939
|
+
const capability = request.capabilities.private;
|
|
12940
|
+
const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
|
|
12941
|
+
const unit2 = "forgezero-deployment-mesh.service";
|
|
12942
|
+
await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
|
|
12943
|
+
host.write(`/etc/systemd/system/${unit2}`, `[Unit]
|
|
12944
|
+
Description=ForgeZero deployment Mesh/WARP connector
|
|
12945
|
+
Requires=warp-svc.service
|
|
12946
|
+
After=network-online.target warp-svc.service
|
|
12947
|
+
Wants=network-online.target
|
|
12948
|
+
|
|
12949
|
+
[Service]
|
|
12950
|
+
Type=oneshot
|
|
12951
|
+
RemainAfterExit=yes
|
|
12952
|
+
LoadCredentialEncrypted=CF_WARP_CONNECTOR_TOKEN:${credentialPath}
|
|
12953
|
+
ExecStart=/usr/local/bin/fz-agent mesh-config
|
|
12954
|
+
NoNewPrivileges=true
|
|
12955
|
+
PrivateTmp=true
|
|
12956
|
+
ProtectSystem=strict
|
|
12957
|
+
ProtectHome=true
|
|
12958
|
+
ReadWritePaths=/var/lib/cloudflare-warp
|
|
12959
|
+
|
|
12960
|
+
[Install]
|
|
12961
|
+
WantedBy=multi-user.target
|
|
12962
|
+
`, 420);
|
|
12963
|
+
await checked7(host, ["/usr/bin/systemctl", "daemon-reload"], "WARP daemon reload");
|
|
12964
|
+
await checked7(host, ["/usr/bin/systemctl", "enable", "--now", "warp-svc.service", unit2], "WARP connector start");
|
|
12965
|
+
for (let attempt = 0;attempt < 10; attempt += 1) {
|
|
12966
|
+
const status = await host.exec(["/usr/bin/warp-cli", "--accept-tos", "status"]);
|
|
12967
|
+
if (status.exitCode === 0 && /\bconnected\b/i.test(status.output) && !/\bdisconnected\b/i.test(status.output)) {
|
|
12968
|
+
evidence.private = { network: request.intent.private.network, connectorId: capability.connectorId, unit: unit2, active: true };
|
|
12969
|
+
break;
|
|
12970
|
+
}
|
|
12971
|
+
await host.sleep(1000);
|
|
12972
|
+
}
|
|
12973
|
+
if (!evidence.private)
|
|
12974
|
+
throw new Error("WARP connector readiness failed");
|
|
12975
|
+
}
|
|
12976
|
+
return evidence;
|
|
12977
|
+
}
|
|
12978
|
+
|
|
12183
12979
|
// src/software-helper.ts
|
|
12184
12980
|
var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
|
|
12185
12981
|
var SOFTWARE_HELPER_GROUP = "forgezero-software";
|
|
@@ -12188,13 +12984,14 @@ var MAX_REQUEST_BYTES6 = 128 * 1024;
|
|
|
12188
12984
|
var MAX_PENDING_REQUESTS = 128;
|
|
12189
12985
|
function startSoftwareHelper(options = {}) {
|
|
12190
12986
|
const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
|
|
12191
|
-
if (
|
|
12987
|
+
if (existsSync17(socketPath))
|
|
12192
12988
|
unlinkSync11(socketPath);
|
|
12193
|
-
|
|
12989
|
+
mkdirSync13(dirname10(socketPath), { recursive: true, mode: 488 });
|
|
12194
12990
|
const ensure = options.ensure ?? ensureSoftwareRequirements;
|
|
12195
12991
|
const activate = options.activate ?? activateSupervisedService;
|
|
12196
12992
|
const buildContainer = options.buildContainer ?? buildContainerImage;
|
|
12197
12993
|
const activateTypedContainer = options.activateContainer ?? activateContainer;
|
|
12994
|
+
const applyConnectivity = options.applyConnectivity ?? applyDeploymentConnectivity;
|
|
12198
12995
|
let tail = Promise.resolve();
|
|
12199
12996
|
let pending = 0;
|
|
12200
12997
|
const server = createServer7((socket) => {
|
|
@@ -12251,6 +13048,12 @@ function startSoftwareHelper(options = {}) {
|
|
|
12251
13048
|
throw new Error("container deployment root is not owned by this helper");
|
|
12252
13049
|
const state = await activateTypedContainer(containerRequest);
|
|
12253
13050
|
socket.end(`${JSON.stringify({ ok: true, state })}
|
|
13051
|
+
`);
|
|
13052
|
+
return;
|
|
13053
|
+
}
|
|
13054
|
+
if (request.op === "apply-connectivity" && request.request) {
|
|
13055
|
+
const result = await applyConnectivity(request.request);
|
|
13056
|
+
socket.end(`${JSON.stringify({ ok: true, result })}
|
|
12254
13057
|
`);
|
|
12255
13058
|
return;
|
|
12256
13059
|
}
|
|
@@ -12308,6 +13111,9 @@ function requestContainerBuild(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOC
|
|
|
12308
13111
|
function requestContainerActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
12309
13112
|
return requestContainer("activate-container", request, socketPath, timeoutMs);
|
|
12310
13113
|
}
|
|
13114
|
+
function requestDeploymentConnectivity(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
13115
|
+
return requestContainer("apply-connectivity", request, socketPath, timeoutMs);
|
|
13116
|
+
}
|
|
12311
13117
|
function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
|
|
12312
13118
|
return new Promise((resolve6, reject) => {
|
|
12313
13119
|
const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
|
|
@@ -12521,9 +13327,9 @@ var systemCommand = (argv2, stdin) => {
|
|
|
12521
13327
|
stderr: result.stderr.toString()
|
|
12522
13328
|
};
|
|
12523
13329
|
};
|
|
12524
|
-
function resolveServiceUid(user,
|
|
13330
|
+
function resolveServiceUid(user, command3 = systemCommand) {
|
|
12525
13331
|
assertServiceUser(user);
|
|
12526
|
-
const result =
|
|
13332
|
+
const result = command3(["/usr/bin/id", "-u", user]);
|
|
12527
13333
|
const uid = Number(result.stdout.trim());
|
|
12528
13334
|
if (result.exitCode !== 0 || !/^\d+$/.test(result.stdout.trim())) {
|
|
12529
13335
|
throw new Error(`Agent egress service account ${user} does not exist.`);
|
|
@@ -12548,14 +13354,14 @@ function assertResolvedStub(realpath = realpathSync4) {
|
|
|
12548
13354
|
throw new Error("Agent egress requires the active systemd-resolved stub.");
|
|
12549
13355
|
}
|
|
12550
13356
|
}
|
|
12551
|
-
var listPolicy = (
|
|
12552
|
-
function verifyAgentEgressPolicy(uids,
|
|
13357
|
+
var listPolicy = (command3) => command3(["/usr/sbin/nft", "--numeric", "list", "table", "inet", AGENT_EGRESS_TABLE]);
|
|
13358
|
+
function verifyAgentEgressPolicy(uids, command3 = systemCommand, loopback) {
|
|
12553
13359
|
if (uids.length < 1)
|
|
12554
13360
|
return false;
|
|
12555
13361
|
for (const uid of uids)
|
|
12556
13362
|
assertUid(uid);
|
|
12557
13363
|
const uniqueUids = [...new Set(uids)].sort((left, right) => left - right);
|
|
12558
|
-
const result = listPolicy(
|
|
13364
|
+
const result = listPolicy(command3);
|
|
12559
13365
|
if (result.exitCode !== 0)
|
|
12560
13366
|
return false;
|
|
12561
13367
|
let loopbackComment = "";
|
|
@@ -12601,44 +13407,44 @@ function applyAgentEgressPolicy(options) {
|
|
|
12601
13407
|
if (typeof getuid !== "function" || getuid() !== 0) {
|
|
12602
13408
|
throw new Error("Agent egress policy must run as root.");
|
|
12603
13409
|
}
|
|
12604
|
-
const
|
|
13410
|
+
const command3 = options.command ?? systemCommand;
|
|
12605
13411
|
assertResolvedStub(options.realpath);
|
|
12606
13412
|
if (options.users.length < 1)
|
|
12607
13413
|
throw new Error("Agent egress policy needs at least one service user.");
|
|
12608
|
-
const uids = [...new Set(options.users.map((user) => resolveServiceUid(user,
|
|
13414
|
+
const uids = [...new Set(options.users.map((user) => resolveServiceUid(user, command3)))];
|
|
12609
13415
|
const loopback = options.loopback ? {
|
|
12610
|
-
uid: resolveServiceUid(options.loopback.user,
|
|
13416
|
+
uid: resolveServiceUid(options.loopback.user, command3),
|
|
12611
13417
|
tcpPorts: normalizeEgressTcpPorts(options.loopback.tcpPorts),
|
|
12612
13418
|
publicTcpPorts: options.loopback.publicTcpPorts === undefined ? undefined : normalizeEgressTcpPorts(options.loopback.publicTcpPorts)
|
|
12613
13419
|
} : undefined;
|
|
12614
13420
|
if (loopback && !uids.includes(loopback.uid))
|
|
12615
13421
|
uids.push(loopback.uid);
|
|
12616
|
-
const current2 = listPolicy(
|
|
12617
|
-
const applied =
|
|
13422
|
+
const current2 = listPolicy(command3);
|
|
13423
|
+
const applied = command3(["/usr/sbin/nft", "--file", "-"], renderAgentEgressNft(uids, current2.exitCode === 0, loopback));
|
|
12618
13424
|
if (applied.exitCode !== 0) {
|
|
12619
13425
|
throw new Error(`Could not apply Agent egress policy: ${applied.stderr.trim() || "nft refused it"}`);
|
|
12620
13426
|
}
|
|
12621
|
-
if (!verifyAgentEgressPolicy(uids,
|
|
13427
|
+
if (!verifyAgentEgressPolicy(uids, command3, loopback)) {
|
|
12622
13428
|
throw new Error("Agent egress policy did not verify after installation.");
|
|
12623
13429
|
}
|
|
12624
13430
|
return uids;
|
|
12625
13431
|
}
|
|
12626
13432
|
async function superviseAgentEgressPolicy(options) {
|
|
12627
|
-
const
|
|
13433
|
+
const command3 = options.command ?? systemCommand;
|
|
12628
13434
|
const uids = applyAgentEgressPolicy({
|
|
12629
13435
|
users: options.users,
|
|
12630
13436
|
loopback: options.loopback,
|
|
12631
|
-
command:
|
|
13437
|
+
command: command3,
|
|
12632
13438
|
realpath: options.realpath,
|
|
12633
13439
|
getuid: options.getuid
|
|
12634
13440
|
});
|
|
12635
13441
|
const loopback = options.loopback ? {
|
|
12636
|
-
uid: resolveServiceUid(options.loopback.user,
|
|
13442
|
+
uid: resolveServiceUid(options.loopback.user, command3),
|
|
12637
13443
|
tcpPorts: normalizeEgressTcpPorts(options.loopback.tcpPorts),
|
|
12638
13444
|
publicTcpPorts: options.loopback.publicTcpPorts === undefined ? undefined : normalizeEgressTcpPorts(options.loopback.publicTcpPorts)
|
|
12639
13445
|
} : undefined;
|
|
12640
13446
|
(options.notifyReady ?? (() => {
|
|
12641
|
-
const result =
|
|
13447
|
+
const result = command3(["/usr/bin/systemd-notify", "--ready"]);
|
|
12642
13448
|
if (result.exitCode !== 0)
|
|
12643
13449
|
throw new Error("Could not notify systemd that Agent egress is ready.");
|
|
12644
13450
|
}))();
|
|
@@ -12649,7 +13455,7 @@ async function superviseAgentEgressPolicy(options) {
|
|
|
12649
13455
|
}
|
|
12650
13456
|
for (;; ) {
|
|
12651
13457
|
await wait(intervalMs);
|
|
12652
|
-
if (!verifyAgentEgressPolicy(uids,
|
|
13458
|
+
if (!verifyAgentEgressPolicy(uids, command3, loopback)) {
|
|
12653
13459
|
throw new Error("Agent egress policy disappeared or changed; stopping the bound Agent.");
|
|
12654
13460
|
}
|
|
12655
13461
|
}
|
|
@@ -13260,7 +14066,7 @@ var AGENT_CREDENTIAL_POLICY = {
|
|
|
13260
14066
|
"platform-compute": { vault: true, systemdFallback: true, hiddenInput: false },
|
|
13261
14067
|
"tenant-compute": { vault: true, systemdFallback: true, hiddenInput: false }
|
|
13262
14068
|
};
|
|
13263
|
-
var CREDENTIAL_NAME = /^[A-
|
|
14069
|
+
var CREDENTIAL_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
|
|
13264
14070
|
var SCOPE_PART2 = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
|
|
13265
14071
|
function deploymentCredentialSchema(input) {
|
|
13266
14072
|
if (!SCOPE_PART2.test(input.projectKey) || !SCOPE_PART2.test(input.environmentKey)) {
|
|
@@ -13302,17 +14108,17 @@ import {
|
|
|
13302
14108
|
chmodSync as chmodSync15,
|
|
13303
14109
|
chownSync,
|
|
13304
14110
|
copyFileSync as copyFileSync2,
|
|
13305
|
-
existsSync as
|
|
14111
|
+
existsSync as existsSync18,
|
|
13306
14112
|
lstatSync as lstatSync3,
|
|
13307
|
-
mkdirSync as
|
|
14113
|
+
mkdirSync as mkdirSync14,
|
|
13308
14114
|
readFileSync as readFileSync14,
|
|
13309
14115
|
readdirSync as readdirSync4,
|
|
13310
14116
|
realpathSync as realpathSync5,
|
|
13311
|
-
renameSync as
|
|
14117
|
+
renameSync as renameSync11,
|
|
13312
14118
|
rmSync as rmSync9,
|
|
13313
|
-
statSync as
|
|
14119
|
+
statSync as statSync4,
|
|
13314
14120
|
symlinkSync as symlinkSync4,
|
|
13315
|
-
writeFileSync as
|
|
14121
|
+
writeFileSync as writeFileSync14
|
|
13316
14122
|
} from "fs";
|
|
13317
14123
|
import { join as join10 } from "path";
|
|
13318
14124
|
var boundedInteger2 = (name, value, minimum, maximum) => {
|
|
@@ -13372,7 +14178,7 @@ var replaceLink = (path2, target2) => {
|
|
|
13372
14178
|
return;
|
|
13373
14179
|
}
|
|
13374
14180
|
symlinkSync4(target2, pending);
|
|
13375
|
-
|
|
14181
|
+
renameSync11(pending, path2);
|
|
13376
14182
|
};
|
|
13377
14183
|
var secureRelease = (path2, uid, gid) => {
|
|
13378
14184
|
const visit = (current2) => {
|
|
@@ -13395,7 +14201,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
13395
14201
|
if (!release.startsWith(`${releases}/`) || release === releases)
|
|
13396
14202
|
throw new Error("release is outside configured releases directory");
|
|
13397
14203
|
for (const required of ["forgezero.deploy.ts", ".fz/deploy.plan.json", "src/index.ts", "bun.lock"]) {
|
|
13398
|
-
const metadata =
|
|
14204
|
+
const metadata = statSync4(join10(release, required));
|
|
13399
14205
|
if (!metadata.isFile() || metadata.size < 1)
|
|
13400
14206
|
throw new Error(`release is incomplete: ${required}`);
|
|
13401
14207
|
}
|
|
@@ -13403,9 +14209,9 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
13403
14209
|
const request = options.fetch ?? fetch;
|
|
13404
14210
|
const sleep2 = options.sleep ?? Bun.sleep;
|
|
13405
14211
|
const slots = join10(normalized.root, "slots");
|
|
13406
|
-
|
|
14212
|
+
mkdirSync14(slots, { recursive: true, mode: 493 });
|
|
13407
14213
|
const slotFile = join10(normalized.root, ".forge-slot");
|
|
13408
|
-
const previousSlot =
|
|
14214
|
+
const previousSlot = existsSync18(slotFile) ? readFileSync14(slotFile, "utf8").trim() : undefined;
|
|
13409
14215
|
const target2 = previousSlot === "blue" ? "green" : "blue";
|
|
13410
14216
|
const port = target2 === "blue" ? normalized.bluePort : normalized.greenPort;
|
|
13411
14217
|
const targetLink = join10(slots, target2);
|
|
@@ -13445,17 +14251,17 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
13445
14251
|
}
|
|
13446
14252
|
const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
|
|
13447
14253
|
const backup = `${upstream}.forgezero-backup`;
|
|
13448
|
-
if (
|
|
14254
|
+
if (existsSync18(upstream))
|
|
13449
14255
|
copyFileSync2(upstream, backup);
|
|
13450
14256
|
else
|
|
13451
14257
|
rmSync9(backup, { force: true });
|
|
13452
|
-
|
|
14258
|
+
writeFileSync14(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
|
|
13453
14259
|
`, { mode: 420 });
|
|
13454
14260
|
const test = await exec(["/usr/sbin/nginx", "-t"]);
|
|
13455
14261
|
const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
|
|
13456
14262
|
if (reload.exitCode !== 0) {
|
|
13457
|
-
if (
|
|
13458
|
-
|
|
14263
|
+
if (existsSync18(backup))
|
|
14264
|
+
renameSync11(backup, upstream);
|
|
13459
14265
|
else
|
|
13460
14266
|
rmSync9(upstream, { force: true });
|
|
13461
14267
|
await exec(["/usr/sbin/nginx", "-t"]);
|
|
@@ -13464,13 +14270,13 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
13464
14270
|
throw new Error("nginx refused the promoted upstream");
|
|
13465
14271
|
}
|
|
13466
14272
|
rmSync9(backup, { force: true });
|
|
13467
|
-
|
|
14273
|
+
writeFileSync14(slotFile, `${target2}
|
|
13468
14274
|
`, { mode: 420 });
|
|
13469
14275
|
if (previousSlot && previousSlot !== target2) {
|
|
13470
14276
|
await sleep2(normalized.drainDeadlineMs);
|
|
13471
14277
|
await exec(["/usr/bin/systemctl", "stop", `forgezero@${previousSlot}.service`]);
|
|
13472
14278
|
}
|
|
13473
|
-
const old = readdirSync4(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join10(releases, entry.name)).sort((left, right) =>
|
|
14279
|
+
const old = readdirSync4(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join10(releases, entry.name)).sort((left, right) => statSync4(right).mtimeMs - statSync4(left).mtimeMs).slice(normalized.keepReleases);
|
|
13474
14280
|
for (const path2 of old)
|
|
13475
14281
|
if (path2 !== release)
|
|
13476
14282
|
rmSync9(path2, { recursive: true, force: true });
|
|
@@ -13478,7 +14284,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
13478
14284
|
}
|
|
13479
14285
|
|
|
13480
14286
|
// src/recovery-host.ts
|
|
13481
|
-
import { existsSync as
|
|
14287
|
+
import { existsSync as existsSync19, readFileSync as readFileSync15, statSync as statSync5 } from "fs";
|
|
13482
14288
|
import { hostname } from "os";
|
|
13483
14289
|
import { join as join11 } from "path";
|
|
13484
14290
|
var execute2 = async (argv2) => {
|
|
@@ -13486,7 +14292,7 @@ var execute2 = async (argv2) => {
|
|
|
13486
14292
|
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
13487
14293
|
return { stdout, stderr, exitCode };
|
|
13488
14294
|
};
|
|
13489
|
-
var
|
|
14295
|
+
var checked8 = async (run2, argv2, expected = 0) => {
|
|
13490
14296
|
const result = await run2(argv2);
|
|
13491
14297
|
if (result.exitCode !== expected)
|
|
13492
14298
|
throw new Error(`${argv2[0]} failed (${result.exitCode})`);
|
|
@@ -13504,7 +14310,7 @@ var exactState = (path2, expected) => {
|
|
|
13504
14310
|
throw new Error(`state mismatch: ${expected}`);
|
|
13505
14311
|
};
|
|
13506
14312
|
var nonEmpty = (path2) => {
|
|
13507
|
-
if (!
|
|
14313
|
+
if (!existsSync19(path2) || !statSync5(path2).isFile() || statSync5(path2).size < 1)
|
|
13508
14314
|
throw new Error(`required file missing: ${path2}`);
|
|
13509
14315
|
};
|
|
13510
14316
|
var option = (args, name) => {
|
|
@@ -13548,29 +14354,29 @@ async function runRecoveryHost(args, run2 = execute2) {
|
|
|
13548
14354
|
const slot = activeSlot();
|
|
13549
14355
|
nonEmpty(join11("/opt/forgezero/slots", slot, "src/server/maintenance/restore-backup.ts"));
|
|
13550
14356
|
nonEmpty(join11("/opt/forgezero/slots", slot, "src/server/maintenance/verify-recovery-cluster.ts"));
|
|
13551
|
-
await
|
|
13552
|
-
await
|
|
14357
|
+
await checked8(run2, ["/usr/bin/test", "-x", "/usr/local/bin/fz"]);
|
|
14358
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-db.service"], hasDatabase ? 0 : 3);
|
|
13553
14359
|
if (args.includes("--require-stopped")) {
|
|
13554
|
-
await
|
|
13555
|
-
await
|
|
14360
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@blue.service"], 3);
|
|
14361
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@green.service"], 3);
|
|
13556
14362
|
}
|
|
13557
14363
|
return;
|
|
13558
14364
|
}
|
|
13559
14365
|
if (action === "fence") {
|
|
13560
|
-
await
|
|
13561
|
-
await
|
|
13562
|
-
await
|
|
14366
|
+
await checked8(run2, ["/usr/bin/systemctl", "stop", "forgezero@blue.service", "forgezero@green.service"]);
|
|
14367
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@blue.service"], 3);
|
|
14368
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@green.service"], 3);
|
|
13563
14369
|
const role = option(args, "db-role");
|
|
13564
|
-
await
|
|
14370
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-db.service"], role === "none" ? 3 : 0);
|
|
13565
14371
|
return;
|
|
13566
14372
|
}
|
|
13567
14373
|
if (action === "start-active") {
|
|
13568
14374
|
const slot = activeSlot();
|
|
13569
14375
|
const inactive = slot === "blue" ? "green" : "blue";
|
|
13570
|
-
await
|
|
13571
|
-
await
|
|
13572
|
-
await
|
|
13573
|
-
await
|
|
14376
|
+
await checked8(run2, ["/usr/bin/systemctl", "stop", `forgezero@${inactive}.service`]);
|
|
14377
|
+
await checked8(run2, ["/usr/bin/systemctl", "start", `forgezero@${slot}.service`]);
|
|
14378
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", `forgezero@${slot}.service`]);
|
|
14379
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", `forgezero@${inactive}.service`], 3);
|
|
13574
14380
|
return;
|
|
13575
14381
|
}
|
|
13576
14382
|
if (action === "maintenance") {
|
|
@@ -13593,7 +14399,7 @@ async function runRecoveryHost(args, run2 = execute2) {
|
|
|
13593
14399
|
"--property=LoadCredentialEncrypted=backup-recovery-root:/etc/forgezero/creds/backup-recovery-root.cred"
|
|
13594
14400
|
] : []
|
|
13595
14401
|
];
|
|
13596
|
-
await
|
|
14402
|
+
await checked8(run2, ["/usr/bin/systemd-run", "--quiet", "--wait", "--pipe", "--collect", `--unit=${unit2(option(args, "unit"))}`, ...properties, "/usr/local/bin/fz", ...fzArgs]);
|
|
13597
14403
|
return;
|
|
13598
14404
|
}
|
|
13599
14405
|
throw new Error("unknown recovery-host action");
|
|
@@ -13605,15 +14411,15 @@ import { lstatSync as lstatSync6 } from "fs";
|
|
|
13605
14411
|
// src/bootstrap.ts
|
|
13606
14412
|
import {
|
|
13607
14413
|
chmodSync as chmodSync17,
|
|
13608
|
-
existsSync as
|
|
14414
|
+
existsSync as existsSync21,
|
|
13609
14415
|
lstatSync as lstatSync5,
|
|
13610
|
-
mkdirSync as
|
|
14416
|
+
mkdirSync as mkdirSync16,
|
|
13611
14417
|
readFileSync as readFileSync17,
|
|
13612
|
-
renameSync as
|
|
14418
|
+
renameSync as renameSync13,
|
|
13613
14419
|
rmSync as rmSync11,
|
|
13614
|
-
writeFileSync as
|
|
14420
|
+
writeFileSync as writeFileSync16
|
|
13615
14421
|
} from "fs";
|
|
13616
|
-
import { dirname as
|
|
14422
|
+
import { dirname as dirname12 } from "path";
|
|
13617
14423
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
13618
14424
|
|
|
13619
14425
|
// src/provision.ts
|
|
@@ -14546,17 +15352,17 @@ import { randomBytes as randomBytes6 } from "crypto";
|
|
|
14546
15352
|
import {
|
|
14547
15353
|
chmodSync as chmodSync16,
|
|
14548
15354
|
copyFileSync as copyFileSync3,
|
|
14549
|
-
existsSync as
|
|
15355
|
+
existsSync as existsSync20,
|
|
14550
15356
|
lstatSync as lstatSync4,
|
|
14551
|
-
mkdirSync as
|
|
15357
|
+
mkdirSync as mkdirSync15,
|
|
14552
15358
|
readFileSync as readFileSync16,
|
|
14553
15359
|
realpathSync as realpathSync6,
|
|
14554
|
-
renameSync as
|
|
15360
|
+
renameSync as renameSync12,
|
|
14555
15361
|
rmSync as rmSync10,
|
|
14556
15362
|
symlinkSync as symlinkSync5,
|
|
14557
|
-
writeFileSync as
|
|
15363
|
+
writeFileSync as writeFileSync15
|
|
14558
15364
|
} from "fs";
|
|
14559
|
-
import { dirname as
|
|
15365
|
+
import { dirname as dirname11 } from "path";
|
|
14560
15366
|
async function readCapabilities(run2) {
|
|
14561
15367
|
const answers = {};
|
|
14562
15368
|
const checks = Object.entries(CAPABILITY_CHECKS);
|
|
@@ -14591,10 +15397,10 @@ var fixed = async (argv2, stdin) => {
|
|
|
14591
15397
|
var runProvisionOperation = async (operation) => {
|
|
14592
15398
|
if (operation.kind === "commands") {
|
|
14593
15399
|
let output = "";
|
|
14594
|
-
for (const
|
|
14595
|
-
const result = await fixed(
|
|
15400
|
+
for (const command3 of operation.commands) {
|
|
15401
|
+
const result = await fixed(command3.argv);
|
|
14596
15402
|
output += result.stdout;
|
|
14597
|
-
if (!(
|
|
15403
|
+
if (!(command3.acceptedExitCodes ?? [0]).includes(result.exitCode))
|
|
14598
15404
|
return { stdout: output, exitCode: result.exitCode };
|
|
14599
15405
|
}
|
|
14600
15406
|
return { stdout: output, exitCode: 0 };
|
|
@@ -14615,19 +15421,19 @@ var runProvisionOperation = async (operation) => {
|
|
|
14615
15421
|
}
|
|
14616
15422
|
if (operation.kind === "install-runtime") {
|
|
14617
15423
|
const release = `/opt/forgezero/agent/versions/${operation.version}`;
|
|
14618
|
-
|
|
14619
|
-
|
|
15424
|
+
mkdirSync15(`${release}/dist`, { recursive: true, mode: 493 });
|
|
15425
|
+
mkdirSync15(dirname11(operation.binary), { recursive: true, mode: 493 });
|
|
14620
15426
|
copyFileSync3(operation.source, `${release}/dist/fz-agent.js`);
|
|
14621
15427
|
chmodSync16(`${release}/dist/fz-agent.js`, 493);
|
|
14622
|
-
const gitSshSource = `${
|
|
14623
|
-
if (!
|
|
15428
|
+
const gitSshSource = `${dirname11(operation.source)}/fz-git-ssh.js`;
|
|
15429
|
+
if (!existsSync20(gitSshSource))
|
|
14624
15430
|
return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
|
|
14625
15431
|
copyFileSync3(gitSshSource, `${release}/dist/fz-git-ssh.js`);
|
|
14626
15432
|
chmodSync16(`${release}/dist/fz-git-ssh.js`, 493);
|
|
14627
15433
|
const pending = "/opt/forgezero/agent/current.next";
|
|
14628
15434
|
rmSync10(pending, { force: true });
|
|
14629
15435
|
symlinkSync5(`versions/${operation.version}`, pending);
|
|
14630
|
-
|
|
15436
|
+
renameSync12(pending, "/opt/forgezero/agent/current");
|
|
14631
15437
|
rmSync10(operation.binary, { force: true });
|
|
14632
15438
|
symlinkSync5("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
|
|
14633
15439
|
const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
|
|
@@ -14636,7 +15442,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
14636
15442
|
return { stdout: "", exitCode: 0 };
|
|
14637
15443
|
}
|
|
14638
15444
|
if (operation.kind === "ensure-seed") {
|
|
14639
|
-
if (
|
|
15445
|
+
if (existsSync20(operation.credential) && lstatSync4(operation.credential).size > 0)
|
|
14640
15446
|
return { stdout: "", exitCode: 0 };
|
|
14641
15447
|
const seed = randomBytes6(32).toString("base64url");
|
|
14642
15448
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
|
|
@@ -14648,7 +15454,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
14648
15454
|
const key = "/run/forgezero-git-deploy-key";
|
|
14649
15455
|
const publicKey = `${key}.pub`;
|
|
14650
15456
|
try {
|
|
14651
|
-
if (!
|
|
15457
|
+
if (!existsSync20(operation.credential) || lstatSync4(operation.credential).size < 1) {
|
|
14652
15458
|
rmSync10(key, { force: true });
|
|
14653
15459
|
rmSync10(publicKey, { force: true });
|
|
14654
15460
|
let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
|
|
@@ -14659,8 +15465,8 @@ var runProvisionOperation = async (operation) => {
|
|
|
14659
15465
|
return result;
|
|
14660
15466
|
chmodSync16(operation.credential, 256);
|
|
14661
15467
|
}
|
|
14662
|
-
if (!
|
|
14663
|
-
if (!
|
|
15468
|
+
if (!existsSync20(operation.publicKey) || lstatSync4(operation.publicKey).size < 1) {
|
|
15469
|
+
if (!existsSync20(key)) {
|
|
14664
15470
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
|
|
14665
15471
|
if (decrypted.exitCode !== 0)
|
|
14666
15472
|
return decrypted;
|
|
@@ -14668,7 +15474,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
14668
15474
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
14669
15475
|
if (derived.exitCode !== 0)
|
|
14670
15476
|
return derived;
|
|
14671
|
-
|
|
15477
|
+
writeFileSync15(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
|
|
14672
15478
|
`, { mode: 292 });
|
|
14673
15479
|
}
|
|
14674
15480
|
return { stdout: "", exitCode: 0 };
|
|
@@ -14681,12 +15487,12 @@ var runProvisionOperation = async (operation) => {
|
|
|
14681
15487
|
const key = "/run/forgezero-bootstrap-ssh-key";
|
|
14682
15488
|
const generatedPublicKey = `${key}.pub`;
|
|
14683
15489
|
try {
|
|
14684
|
-
if (!
|
|
15490
|
+
if (!existsSync20(operation.credential) || lstatSync4(operation.credential).size < 1) {
|
|
14685
15491
|
rmSync10(key, { force: true });
|
|
14686
15492
|
rmSync10(generatedPublicKey, { force: true });
|
|
14687
15493
|
let result;
|
|
14688
15494
|
if (operation.source) {
|
|
14689
|
-
const source =
|
|
15495
|
+
const source = existsSync20(operation.source) ? lstatSync4(operation.source) : undefined;
|
|
14690
15496
|
if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
|
|
14691
15497
|
return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
|
|
14692
15498
|
}
|
|
@@ -14706,8 +15512,8 @@ var runProvisionOperation = async (operation) => {
|
|
|
14706
15512
|
return result;
|
|
14707
15513
|
chmodSync16(operation.credential, 256);
|
|
14708
15514
|
}
|
|
14709
|
-
if (!
|
|
14710
|
-
if (!
|
|
15515
|
+
if (!existsSync20(operation.publicKey) || lstatSync4(operation.publicKey).size < 1) {
|
|
15516
|
+
if (!existsSync20(key)) {
|
|
14711
15517
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
|
|
14712
15518
|
if (decrypted.exitCode !== 0)
|
|
14713
15519
|
return decrypted;
|
|
@@ -14717,8 +15523,8 @@ var runProvisionOperation = async (operation) => {
|
|
|
14717
15523
|
if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
|
|
14718
15524
|
return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
|
|
14719
15525
|
}
|
|
14720
|
-
|
|
14721
|
-
|
|
15526
|
+
mkdirSync15(dirname11(operation.publicKey), { recursive: true, mode: 493 });
|
|
15527
|
+
writeFileSync15(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
|
|
14722
15528
|
`, { mode: 292 });
|
|
14723
15529
|
chmodSync16(operation.publicKey, 292);
|
|
14724
15530
|
}
|
|
@@ -14731,9 +15537,9 @@ var runProvisionOperation = async (operation) => {
|
|
|
14731
15537
|
}
|
|
14732
15538
|
}
|
|
14733
15539
|
if (operation.kind === "ensure-enrolment") {
|
|
14734
|
-
if (
|
|
15540
|
+
if (existsSync20(operation.state) && lstatSync4(operation.state).size > 0 || existsSync20(operation.credential) && lstatSync4(operation.credential).size > 0)
|
|
14735
15541
|
return { stdout: "", exitCode: 0 };
|
|
14736
|
-
if (!
|
|
15542
|
+
if (!existsSync20(operation.source))
|
|
14737
15543
|
return { stdout: "enrolment source is missing", exitCode: 1 };
|
|
14738
15544
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
|
|
14739
15545
|
if (result.exitCode === 0) {
|
|
@@ -14753,7 +15559,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
14753
15559
|
return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
|
|
14754
15560
|
}
|
|
14755
15561
|
if (operation.kind === "verify-file")
|
|
14756
|
-
return
|
|
15562
|
+
return existsSync20(operation.path) && lstatSync4(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
|
|
14757
15563
|
if (operation.kind === "verify-egress") {
|
|
14758
15564
|
const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
|
|
14759
15565
|
if (active.exitCode !== 0)
|
|
@@ -14781,11 +15587,11 @@ var runProvisionOperation = async (operation) => {
|
|
|
14781
15587
|
const response = await fetch("https://pkg.cloudflareclient.com/pubkey.gpg", { signal: AbortSignal.timeout(30000) });
|
|
14782
15588
|
if (!response.ok)
|
|
14783
15589
|
return { stdout: `WARP key HTTP ${response.status}`, exitCode: 1 };
|
|
14784
|
-
|
|
14785
|
-
|
|
14786
|
-
|
|
15590
|
+
mkdirSync15("/usr/share/keyrings", { recursive: true, mode: 493 });
|
|
15591
|
+
mkdirSync15("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
|
|
15592
|
+
mkdirSync15("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
|
|
14787
15593
|
const key = "/run/cloudflare-warp-key.gpg";
|
|
14788
|
-
|
|
15594
|
+
writeFileSync15(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
|
|
14789
15595
|
let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
|
|
14790
15596
|
rmSync10(key, { force: true });
|
|
14791
15597
|
if (result.exitCode !== 0)
|
|
@@ -14793,7 +15599,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
14793
15599
|
const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
|
|
14794
15600
|
if (!codename)
|
|
14795
15601
|
return { stdout: "Ubuntu codename missing", exitCode: 1 };
|
|
14796
|
-
|
|
15602
|
+
writeFileSync15("/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
|
|
14797
15603
|
`, { mode: 420 });
|
|
14798
15604
|
result = await fixed(["/usr/bin/apt-get", "update", "-qq"]);
|
|
14799
15605
|
return result.exitCode === 0 ? fixed(["/usr/bin/apt-get", "install", "-y", "cloudflare-warp"]) : result;
|
|
@@ -14913,7 +15719,7 @@ function parseStoredState(raw) {
|
|
|
14913
15719
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
14914
15720
|
throw new Error("bootstrap state is malformed");
|
|
14915
15721
|
const state = value;
|
|
14916
|
-
if (state.format !== 2 || !["platform", "enrolled-compute"].includes(state.kind ?? "") || !/^[a-f0-9]{64}$/.test(state.identityDigest ?? "") || typeof state.profile !== "string" || typeof state.nodeHostname !== "string" || typeof state.apiUrl !== "string" || state.kind === "platform" && !["member", "none"].includes(state.databaseAgency ?? "")) {
|
|
15722
|
+
if (state.format !== 2 || !["platform", "enrolled-compute"].includes(state.kind ?? "") || !/^[a-f0-9]{64}$/.test(state.identityDigest ?? "") || typeof state.profile !== "string" || typeof state.nodeHostname !== "string" || typeof state.apiUrl !== "string" || state.kind === "platform" && (!["member", "none"].includes(state.databaseAgency ?? "") || typeof state.databaseReadPreferred !== "boolean")) {
|
|
14917
15723
|
throw new Error("bootstrap state is legacy or incomplete; refusing an unbound repair");
|
|
14918
15724
|
}
|
|
14919
15725
|
return state;
|
|
@@ -15107,16 +15913,16 @@ function localBootstrapHost() {
|
|
|
15107
15913
|
};
|
|
15108
15914
|
return {
|
|
15109
15915
|
uid: () => process.getuid?.() ?? -1,
|
|
15110
|
-
exists:
|
|
15916
|
+
exists: existsSync21,
|
|
15111
15917
|
read: (path2) => readFileSync17(path2, "utf8"),
|
|
15112
15918
|
write(path2, content, mode) {
|
|
15113
|
-
|
|
15919
|
+
mkdirSync16(dirname12(path2), { recursive: true, mode: 493 });
|
|
15114
15920
|
const temporary = `${path2}.next.${process.pid}`;
|
|
15115
|
-
|
|
15921
|
+
writeFileSync16(temporary, content, { mode });
|
|
15116
15922
|
chmodSync17(temporary, mode);
|
|
15117
|
-
|
|
15923
|
+
renameSync13(temporary, path2);
|
|
15118
15924
|
},
|
|
15119
|
-
mkdir: (path2, mode) =>
|
|
15925
|
+
mkdir: (path2, mode) => mkdirSync16(path2, { recursive: true, mode }),
|
|
15120
15926
|
remove: (path2) => rmSync11(path2, { force: true }),
|
|
15121
15927
|
inspect(path2) {
|
|
15122
15928
|
const value = lstatSync5(path2);
|
|
@@ -15140,9 +15946,9 @@ function localBootstrapHost() {
|
|
|
15140
15946
|
},
|
|
15141
15947
|
async installAgent(config, phase) {
|
|
15142
15948
|
const capabilities = await readCapabilities(localRunner);
|
|
15143
|
-
const hasBinding =
|
|
15144
|
-
const hasEnrolCredential =
|
|
15145
|
-
const initialBundle = config.kind === "platform" && !
|
|
15949
|
+
const hasBinding = existsSync21("/var/lib/forgezero/enrolment.json");
|
|
15950
|
+
const hasEnrolCredential = existsSync21(ENROL_CREDENTIAL);
|
|
15951
|
+
const initialBundle = config.kind === "platform" && !existsSync21(BOOTSTRAP_RELEASE_EVIDENCE) ? {
|
|
15146
15952
|
path: config.bootstrapBundle.bundleFile,
|
|
15147
15953
|
manifestPath: config.bootstrapBundle.manifestFile,
|
|
15148
15954
|
manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync17(config.bootstrapBundle.manifestFile, "utf8")))
|
|
@@ -15158,8 +15964,8 @@ function localBootstrapHost() {
|
|
|
15158
15964
|
databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
|
|
15159
15965
|
databasePorts: [8529]
|
|
15160
15966
|
};
|
|
15161
|
-
|
|
15162
|
-
|
|
15967
|
+
mkdirSync16(dirname12(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
|
|
15968
|
+
writeFileSync16(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
|
|
15163
15969
|
`, { mode: 256 });
|
|
15164
15970
|
}
|
|
15165
15971
|
const plan = planBootstrapAgentInstall(config, phase, {
|
|
@@ -15169,8 +15975,8 @@ function localBootstrapHost() {
|
|
|
15169
15975
|
initialBundle
|
|
15170
15976
|
});
|
|
15171
15977
|
for (const unit3 of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
|
|
15172
|
-
|
|
15173
|
-
|
|
15978
|
+
mkdirSync16(dirname12(unit3.path), { recursive: true, mode: 493 });
|
|
15979
|
+
writeFileSync16(unit3.path, unit3.unit, { mode: 420 });
|
|
15174
15980
|
}
|
|
15175
15981
|
await applyPlan(plan, localRunner);
|
|
15176
15982
|
return plan;
|
|
@@ -15222,10 +16028,10 @@ async function verifyPlatformFleetHost(expectedVersion, runtime = localRuntime()
|
|
|
15222
16028
|
}
|
|
15223
16029
|
|
|
15224
16030
|
// src/community-rehearsal-host.ts
|
|
15225
|
-
import { createHash as
|
|
15226
|
-
import { lstatSync as lstatSync7, mkdirSync as
|
|
16031
|
+
import { createHash as createHash12 } from "crypto";
|
|
16032
|
+
import { lstatSync as lstatSync7, mkdirSync as mkdirSync17, readFileSync as readFileSync18, rmSync as rmSync12, symlinkSync as symlinkSync6, unlinkSync as unlinkSync12, writeFileSync as writeFileSync17 } from "fs";
|
|
15227
16033
|
import { isIP as isIP5 } from "net";
|
|
15228
|
-
import { dirname as
|
|
16034
|
+
import { dirname as dirname13 } from "path";
|
|
15229
16035
|
var COMMUNITY_REHEARSAL_VERSION = "3.11.14";
|
|
15230
16036
|
var COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
|
|
15231
16037
|
var COMMUNITY_DATABASE_ROOT = "/var/lib/forgezero-rehearsal-cluster";
|
|
@@ -15443,8 +16249,8 @@ async function applyOperations(operations) {
|
|
|
15443
16249
|
if (operation.kind === "remove-tree")
|
|
15444
16250
|
rmSync12(operation.path, { recursive: true, force: true });
|
|
15445
16251
|
else if (operation.kind === "write") {
|
|
15446
|
-
|
|
15447
|
-
|
|
16252
|
+
mkdirSync17(dirname13(operation.path), { recursive: true });
|
|
16253
|
+
writeFileSync17(operation.path, operation.content, { mode: operation.mode });
|
|
15448
16254
|
} else if (operation.kind === "unlink") {
|
|
15449
16255
|
try {
|
|
15450
16256
|
unlinkSync12(operation.path);
|
|
@@ -15496,7 +16302,7 @@ async function runCommunityRehearsalHost(request) {
|
|
|
15496
16302
|
}
|
|
15497
16303
|
if (request.action === "prepare") {
|
|
15498
16304
|
const metadata = lstatSync7(request.archivePath);
|
|
15499
|
-
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > 64 * 1024 * 1024 ||
|
|
16305
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > 64 * 1024 * 1024 || createHash12("sha256").update(readFileSync18(request.archivePath)).digest("hex") !== request.archiveSha256) {
|
|
15500
16306
|
throw new Error("community rehearsal archive is not the declared bounded release");
|
|
15501
16307
|
}
|
|
15502
16308
|
await ensureCommunityRehearsalArango();
|
|
@@ -15590,23 +16396,23 @@ async function runSupervisedApp(path2) {
|
|
|
15590
16396
|
}
|
|
15591
16397
|
|
|
15592
16398
|
// src/index.ts
|
|
15593
|
-
function agentCommandArgs(args,
|
|
15594
|
-
const index = args.indexOf(
|
|
16399
|
+
function agentCommandArgs(args, command3) {
|
|
16400
|
+
const index = args.indexOf(command3);
|
|
15595
16401
|
if (index < 0)
|
|
15596
|
-
throw new Error(`missing Agent command ${
|
|
16402
|
+
throw new Error(`missing Agent command ${command3}`);
|
|
15597
16403
|
return args.slice(index + 1);
|
|
15598
16404
|
}
|
|
15599
16405
|
function loadOrCreateSeed(path2) {
|
|
15600
|
-
if (
|
|
16406
|
+
if (existsSync22(path2)) {
|
|
15601
16407
|
const seed2 = new Uint8Array(Buffer.from(readFileSync20(path2, "utf8").trim(), "base64url"));
|
|
15602
16408
|
if (seed2.length < 32) {
|
|
15603
16409
|
throw new Error(`agent: the seed at ${path2} is too short to derive a key from.`);
|
|
15604
16410
|
}
|
|
15605
16411
|
return seed2;
|
|
15606
16412
|
}
|
|
15607
|
-
|
|
16413
|
+
mkdirSync18(dirname14(path2), { recursive: true });
|
|
15608
16414
|
const seed = new Uint8Array(randomBytes7(32));
|
|
15609
|
-
|
|
16415
|
+
writeFileSync18(path2, Buffer.from(seed).toString("base64url"), { mode: 384 });
|
|
15610
16416
|
chmodSync18(path2, 384);
|
|
15611
16417
|
return seed;
|
|
15612
16418
|
}
|
|
@@ -15636,7 +16442,7 @@ function loadSeedCredential(name = DEFAULT_SEED_CREDENTIAL, directory = process.
|
|
|
15636
16442
|
if (!directory)
|
|
15637
16443
|
throw new Error("agent: CREDENTIALS_DIRECTORY is missing; systemd did not load the node identity.");
|
|
15638
16444
|
const path2 = `${directory}/${name}`;
|
|
15639
|
-
if (!
|
|
16445
|
+
if (!existsSync22(path2))
|
|
15640
16446
|
throw new Error(`agent: the systemd credential ${name} is missing at ${path2}.`);
|
|
15641
16447
|
const seed = new Uint8Array(Buffer.from(readFileSync20(path2, "utf8").trim(), "base64url"));
|
|
15642
16448
|
if (seed.length < 32)
|
|
@@ -15658,7 +16464,7 @@ function createSystemdDeploymentSecrets(names, directory = process.env.CREDENTIA
|
|
|
15658
16464
|
if (allowed.size === 0)
|
|
15659
16465
|
return;
|
|
15660
16466
|
for (const name of allowed) {
|
|
15661
|
-
if (!/^[A-
|
|
16467
|
+
if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/.test(name))
|
|
15662
16468
|
throw new Error(`agent: invalid pipeline credential name ${name}.`);
|
|
15663
16469
|
}
|
|
15664
16470
|
return {
|
|
@@ -15764,8 +16570,8 @@ if (import.meta.main) {
|
|
|
15764
16570
|
console.log(VERSION2);
|
|
15765
16571
|
process.exit(0);
|
|
15766
16572
|
}
|
|
15767
|
-
const
|
|
15768
|
-
if (
|
|
16573
|
+
const command3 = args.find((arg) => !arg.startsWith("-"));
|
|
16574
|
+
if (command3 === "platform-activate") {
|
|
15769
16575
|
if (process.getuid?.() !== 0)
|
|
15770
16576
|
throw new Error("platform-activate must run as root");
|
|
15771
16577
|
const configPath = args.find((arg) => arg.startsWith("--config="))?.slice("--config=".length);
|
|
@@ -15778,14 +16584,14 @@ if (import.meta.main) {
|
|
|
15778
16584
|
console.log(`promoted ${result.release} on ${result.slot}`);
|
|
15779
16585
|
process.exit(0);
|
|
15780
16586
|
}
|
|
15781
|
-
if (
|
|
16587
|
+
if (command3 === "recovery-host") {
|
|
15782
16588
|
if (process.getuid?.() !== 0)
|
|
15783
16589
|
throw new Error("recovery-host must run as root");
|
|
15784
16590
|
const commandIndex = args.indexOf("recovery-host");
|
|
15785
16591
|
await runRecoveryHost(args.slice(commandIndex + 1));
|
|
15786
16592
|
process.exit(0);
|
|
15787
16593
|
}
|
|
15788
|
-
if (
|
|
16594
|
+
if (command3 === "pipeline-todo") {
|
|
15789
16595
|
const index = args.indexOf("pipeline-todo");
|
|
15790
16596
|
const message = args[index + 1];
|
|
15791
16597
|
if (!message || args.length !== index + 2 || message.length > 2000 || /[\r\n\0]/.test(message)) {
|
|
@@ -15794,7 +16600,7 @@ if (import.meta.main) {
|
|
|
15794
16600
|
console.error(message);
|
|
15795
16601
|
process.exit(78);
|
|
15796
16602
|
}
|
|
15797
|
-
if (
|
|
16603
|
+
if (command3 === "project-release-check") {
|
|
15798
16604
|
if (args.length !== 1)
|
|
15799
16605
|
throw new Error("project-release-check accepts no coordinates");
|
|
15800
16606
|
for (const relative of ["src/index.ts", "bun.lock", "src/generated/shared/contract-manifest.json"]) {
|
|
@@ -15813,7 +16619,7 @@ if (import.meta.main) {
|
|
|
15813
16619
|
process.exit(0);
|
|
15814
16620
|
}
|
|
15815
16621
|
const allowFileSeed = process.env.FZ_ALLOW_FILE_SEED === "1";
|
|
15816
|
-
if (
|
|
16622
|
+
if (command3 === "identity") {
|
|
15817
16623
|
const seed = configuredAgentSeed({
|
|
15818
16624
|
credential: process.env.FZ_SEED_CREDENTIAL,
|
|
15819
16625
|
path: process.env.FZ_SEED_PATH,
|
|
@@ -15827,7 +16633,7 @@ if (import.meta.main) {
|
|
|
15827
16633
|
}, null, 2));
|
|
15828
16634
|
process.exit(0);
|
|
15829
16635
|
}
|
|
15830
|
-
if (
|
|
16636
|
+
if (command3 === "enrol") {
|
|
15831
16637
|
if (!process.env.FZ_API)
|
|
15832
16638
|
throw new Error("agent enrolment requires FZ_API");
|
|
15833
16639
|
if (!process.env.FZ_ENROL_TOKEN_CREDENTIAL) {
|
|
@@ -15856,7 +16662,7 @@ if (import.meta.main) {
|
|
|
15856
16662
|
console.log(`[agent] enrolled ${binding2.computeReference} in project ${binding2.projectKey}/${binding2.environmentKey}`);
|
|
15857
16663
|
process.exit(0);
|
|
15858
16664
|
}
|
|
15859
|
-
if (
|
|
16665
|
+
if (command3 === "egress-policy" || command3 === "egress-policy-check") {
|
|
15860
16666
|
const users = args.filter((arg) => arg.startsWith("--user=")).map((arg) => arg.slice("--user=".length));
|
|
15861
16667
|
if (users.length < 1)
|
|
15862
16668
|
throw new Error("egress-policy requires --user=<service-user>");
|
|
@@ -15875,7 +16681,7 @@ if (import.meta.main) {
|
|
|
15875
16681
|
tcpPorts: loopbackTcpPorts,
|
|
15876
16682
|
publicTcpPorts: publicTcpPorts.length > 0 ? publicTcpPorts : undefined
|
|
15877
16683
|
} : undefined;
|
|
15878
|
-
if (
|
|
16684
|
+
if (command3 === "egress-policy-check") {
|
|
15879
16685
|
const uids = users.map((user) => resolveServiceUid(user));
|
|
15880
16686
|
const grant = loopback ? {
|
|
15881
16687
|
uid: resolveServiceUid(loopback.user),
|
|
@@ -15888,7 +16694,7 @@ if (import.meta.main) {
|
|
|
15888
16694
|
}
|
|
15889
16695
|
await superviseAgentEgressPolicy({ users, loopback });
|
|
15890
16696
|
}
|
|
15891
|
-
if (
|
|
16697
|
+
if (command3 === "metal-helper") {
|
|
15892
16698
|
const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
15893
16699
|
if (!profilePath)
|
|
15894
16700
|
throw new Error("metal-helper requires --profile=/absolute/path.json");
|
|
@@ -15910,7 +16716,7 @@ if (import.meta.main) {
|
|
|
15910
16716
|
process.on("SIGINT", () => void stop());
|
|
15911
16717
|
await new Promise(() => {});
|
|
15912
16718
|
}
|
|
15913
|
-
if (
|
|
16719
|
+
if (command3 === "lifecycle-helper") {
|
|
15914
16720
|
const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
15915
16721
|
if (!profilePath?.startsWith("/"))
|
|
15916
16722
|
throw new Error("lifecycle-helper requires --profile=/absolute/path.json");
|
|
@@ -15934,7 +16740,7 @@ if (import.meta.main) {
|
|
|
15934
16740
|
process.on("SIGINT", () => void stop());
|
|
15935
16741
|
await new Promise(() => {});
|
|
15936
16742
|
}
|
|
15937
|
-
if (
|
|
16743
|
+
if (command3 === "warp-config") {
|
|
15938
16744
|
const organization = args.find((arg) => arg.startsWith("--organization="))?.slice("--organization=".length);
|
|
15939
16745
|
if (!organization)
|
|
15940
16746
|
throw new Error("warp-config requires --organization=<team-name>");
|
|
@@ -15952,7 +16758,7 @@ if (import.meta.main) {
|
|
|
15952
16758
|
console.log("[warp-config] enrollment materialized in tmpfs");
|
|
15953
16759
|
process.exit(0);
|
|
15954
16760
|
}
|
|
15955
|
-
if (
|
|
16761
|
+
if (command3 === "mesh-config") {
|
|
15956
16762
|
if (typeof process.getuid !== "function" || process.getuid() !== 0)
|
|
15957
16763
|
throw new Error("mesh-config must run as root");
|
|
15958
16764
|
const token = readMeshConnectorCredential();
|
|
@@ -15975,7 +16781,7 @@ if (import.meta.main) {
|
|
|
15975
16781
|
console.log("[mesh-config] Cloudflare Mesh connector is connected");
|
|
15976
16782
|
process.exit(0);
|
|
15977
16783
|
}
|
|
15978
|
-
if (
|
|
16784
|
+
if (command3 === "update-helper") {
|
|
15979
16785
|
if (typeof process.getuid !== "function" || process.getuid() !== 0) {
|
|
15980
16786
|
throw new Error("update-helper must run as root");
|
|
15981
16787
|
}
|
|
@@ -15994,7 +16800,7 @@ if (import.meta.main) {
|
|
|
15994
16800
|
process.on("SIGINT", () => void stop());
|
|
15995
16801
|
await new Promise(() => {});
|
|
15996
16802
|
}
|
|
15997
|
-
if (
|
|
16803
|
+
if (command3 === "software-helper") {
|
|
15998
16804
|
if (typeof process.getuid !== "function" || process.getuid() !== 0) {
|
|
15999
16805
|
throw new Error("software-helper must run as root");
|
|
16000
16806
|
}
|
|
@@ -16016,13 +16822,13 @@ if (import.meta.main) {
|
|
|
16016
16822
|
process.on("SIGINT", () => void stop());
|
|
16017
16823
|
await new Promise(() => {});
|
|
16018
16824
|
}
|
|
16019
|
-
if (
|
|
16825
|
+
if (command3 === "supervised-app") {
|
|
16020
16826
|
const config = args.find((arg) => arg.startsWith("--config="))?.slice("--config=".length);
|
|
16021
16827
|
if (!config)
|
|
16022
16828
|
throw new Error("supervised-app requires --config=<absolute-path>");
|
|
16023
16829
|
process.exit(await runSupervisedApp(config));
|
|
16024
16830
|
}
|
|
16025
|
-
if (
|
|
16831
|
+
if (command3 === "software-ensure") {
|
|
16026
16832
|
const requirements = args.filter((arg) => arg.startsWith("--require=")).map((arg) => {
|
|
16027
16833
|
const coordinate = arg.slice("--require=".length);
|
|
16028
16834
|
const separator = coordinate.indexOf("@");
|
|
@@ -16038,7 +16844,7 @@ if (import.meta.main) {
|
|
|
16038
16844
|
console.log(JSON.stringify({ ok: true, results }));
|
|
16039
16845
|
process.exit(0);
|
|
16040
16846
|
}
|
|
16041
|
-
if (
|
|
16847
|
+
if (command3 === "metal-isolation") {
|
|
16042
16848
|
const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
16043
16849
|
if (!profilePath)
|
|
16044
16850
|
throw new Error("metal-isolation requires --profile=/absolute/path.json");
|
|
@@ -16047,7 +16853,7 @@ if (import.meta.main) {
|
|
|
16047
16853
|
console.log("[metal-isolation] host and guest cgroup boundaries active");
|
|
16048
16854
|
process.exit(0);
|
|
16049
16855
|
}
|
|
16050
|
-
if (
|
|
16856
|
+
if (command3 === "deploy-runner") {
|
|
16051
16857
|
const root2 = args.find((arg) => arg.startsWith("--root="))?.slice("--root=".length);
|
|
16052
16858
|
const home = args.find((arg) => arg.startsWith("--home="))?.slice("--home=".length);
|
|
16053
16859
|
if (!root2 || !home)
|
|
@@ -16077,7 +16883,7 @@ if (import.meta.main) {
|
|
|
16077
16883
|
process.on("SIGINT", () => void stop());
|
|
16078
16884
|
await new Promise(() => {});
|
|
16079
16885
|
}
|
|
16080
|
-
if (
|
|
16886
|
+
if (command3 === "metal-apply") {
|
|
16081
16887
|
const claimArg = args.find((arg) => arg.startsWith("--claim="))?.slice("--claim=".length);
|
|
16082
16888
|
if (!claimArg)
|
|
16083
16889
|
throw new Error("metal-apply requires --claim=- or --claim=/absolute/path.json");
|
|
@@ -16098,8 +16904,8 @@ if (import.meta.main) {
|
|
|
16098
16904
|
console.log(JSON.stringify({ ok: true, computeKey: claim.computeKey, ...result }));
|
|
16099
16905
|
process.exit(0);
|
|
16100
16906
|
}
|
|
16101
|
-
if (
|
|
16102
|
-
const commandArgs = agentCommandArgs(args,
|
|
16907
|
+
if (command3 === "platform-fleet-verify") {
|
|
16908
|
+
const commandArgs = agentCommandArgs(args, command3);
|
|
16103
16909
|
const expectedVersion = commandArgs.find((arg) => arg.startsWith("--expected-version="))?.slice("--expected-version=".length);
|
|
16104
16910
|
if (!expectedVersion || commandArgs.length !== 1) {
|
|
16105
16911
|
throw new Error("platform-fleet-verify requires exactly --expected-version=<semver>");
|
|
@@ -16107,8 +16913,8 @@ if (import.meta.main) {
|
|
|
16107
16913
|
console.log(JSON.stringify(await verifyPlatformFleetHost(expectedVersion)));
|
|
16108
16914
|
process.exit(0);
|
|
16109
16915
|
}
|
|
16110
|
-
if (
|
|
16111
|
-
const commandArgs = agentCommandArgs(args,
|
|
16916
|
+
if (command3 === "community-rehearsal") {
|
|
16917
|
+
const commandArgs = agentCommandArgs(args, command3);
|
|
16112
16918
|
if (commandArgs.length !== 1 || commandArgs[0] !== "--request=-") {
|
|
16113
16919
|
throw new Error("community-rehearsal requires exactly --request=-");
|
|
16114
16920
|
}
|
|
@@ -16119,7 +16925,7 @@ if (import.meta.main) {
|
|
|
16119
16925
|
console.log(JSON.stringify(await runCommunityRehearsalHost(request)));
|
|
16120
16926
|
process.exit(0);
|
|
16121
16927
|
}
|
|
16122
|
-
if (
|
|
16928
|
+
if (command3 === "metal-admission-proof") {
|
|
16123
16929
|
if (args.length !== 1 || args[0] !== "--request=-") {
|
|
16124
16930
|
throw new Error("metal-admission-proof requires exactly --request=-");
|
|
16125
16931
|
}
|
|
@@ -16133,17 +16939,17 @@ if (import.meta.main) {
|
|
|
16133
16939
|
const seed = configuredAgentSeed({ credential: process.env.FZ_SEED_CREDENTIAL, allowFileSeed: false });
|
|
16134
16940
|
const keys2 = deriveKeysFromSeed(seed);
|
|
16135
16941
|
seed.fill(0);
|
|
16136
|
-
const filesystem =
|
|
16942
|
+
const filesystem = statfsSync3("/var/lib/forgezero");
|
|
16137
16943
|
const body = {
|
|
16138
16944
|
...request,
|
|
16139
16945
|
publicKeys: { ed25519: keys2.ed25519.publicKey, mlDsa: keys2.mlDsa.publicKey },
|
|
16140
16946
|
preflight: {
|
|
16141
|
-
vcpu:
|
|
16142
|
-
memoryGib: Math.max(1, Math.floor(
|
|
16947
|
+
vcpu: cpus2().length,
|
|
16948
|
+
memoryGib: Math.max(1, Math.floor(totalmem2() / 1024 ** 3)),
|
|
16143
16949
|
diskGib: Math.max(1, Math.floor(Number(filesystem.blocks) * Number(filesystem.bsize) / 1024 ** 3)),
|
|
16144
|
-
kvm:
|
|
16145
|
-
snpHost:
|
|
16146
|
-
helper:
|
|
16950
|
+
kvm: existsSync22("/dev/kvm"),
|
|
16951
|
+
snpHost: existsSync22("/dev/sev"),
|
|
16952
|
+
helper: existsSync22(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
|
|
16147
16953
|
}
|
|
16148
16954
|
};
|
|
16149
16955
|
const envelope = signRequest(keys2, keys2.ed25519.publicKey, {
|
|
@@ -16178,9 +16984,9 @@ if (import.meta.main) {
|
|
|
16178
16984
|
metalHostname: process.env.FZ_METAL_HOSTNAME,
|
|
16179
16985
|
run: (claim) => requestMetalProvision(claim, process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET),
|
|
16180
16986
|
metalPreflight: () => ({
|
|
16181
|
-
snpHost:
|
|
16182
|
-
kvm:
|
|
16183
|
-
helper:
|
|
16987
|
+
snpHost: existsSync22("/dev/sev"),
|
|
16988
|
+
kvm: existsSync22("/dev/kvm"),
|
|
16989
|
+
helper: existsSync22(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
|
|
16184
16990
|
}),
|
|
16185
16991
|
onEvent: (event, detail) => console.log(`[metal-agent] ${event}${detail ? ` ${JSON.stringify(detail)}` : ""}`)
|
|
16186
16992
|
});
|
|
@@ -16235,22 +17041,22 @@ if (import.meta.main) {
|
|
|
16235
17041
|
process.on("SIGINT", () => void stop("SIGINT"));
|
|
16236
17042
|
await new Promise(() => {});
|
|
16237
17043
|
}
|
|
16238
|
-
if (
|
|
17044
|
+
if (command3 && ["deploy", "status", "pause", "resume", "pause-key", "resume-key", "stop-key", "start-key", "cancel"].includes(command3)) {
|
|
16239
17045
|
const revisionArg = args.find((arg) => arg.startsWith("--revision="));
|
|
16240
17046
|
const socketArg = args.find((arg) => arg.startsWith("--control-socket="));
|
|
16241
17047
|
const keyArg = args.find((arg) => arg.startsWith("--key="));
|
|
16242
17048
|
const idArg = args.find((arg) => arg.startsWith("--id="));
|
|
16243
17049
|
const socketPath = socketArg?.slice("--control-socket=".length) ?? process.env.FZ_CONTROL_SOCKET ?? DEFAULT_CONTROL_SOCKET;
|
|
16244
|
-
const request =
|
|
17050
|
+
const request = command3 === "deploy" ? {
|
|
16245
17051
|
op: "deploy",
|
|
16246
17052
|
request: {
|
|
16247
17053
|
revision: revisionArg?.slice("--revision=".length),
|
|
16248
17054
|
releaseExecutor: args.includes("--release-executor")
|
|
16249
17055
|
}
|
|
16250
|
-
} :
|
|
16251
|
-
op:
|
|
17056
|
+
} : command3 === "cancel" ? { op: "cancel", id: idArg?.slice("--id=".length) ?? "" } : ["pause-key", "resume-key", "stop-key", "start-key"].includes(command3) ? {
|
|
17057
|
+
op: command3,
|
|
16252
17058
|
key: keyArg?.slice("--key=".length) ?? ""
|
|
16253
|
-
} : { op:
|
|
17059
|
+
} : { op: command3 };
|
|
16254
17060
|
try {
|
|
16255
17061
|
const response = await requestControl(request, socketPath);
|
|
16256
17062
|
console.log(JSON.stringify(response, null, 2));
|
|
@@ -16263,7 +17069,7 @@ if (import.meta.main) {
|
|
|
16263
17069
|
const telemetry = new AgentTelemetryRuntime(createAgentTelemetry(resolveAgentTelemetryConfig()));
|
|
16264
17070
|
telemetry.event("agent.started");
|
|
16265
17071
|
telemetry.setDraining(false);
|
|
16266
|
-
const attestationSource =
|
|
17072
|
+
const attestationSource = existsSync22("/dev/sev-guest") ? createSnpAttestationSource() : undefined;
|
|
16267
17073
|
const running = runAgent({
|
|
16268
17074
|
socketPath: process.env.FZ_SOCKET_PATH ?? DEFAULT_SOCKET_PATH,
|
|
16269
17075
|
seedCredential: process.env.FZ_SEED_CREDENTIAL,
|
|
@@ -16278,8 +17084,8 @@ if (import.meta.main) {
|
|
|
16278
17084
|
const enrolmentStatePath = process.env.FZ_ENROL_STATE_FILE ?? DEFAULT_ENROLMENT_STATE_PATH;
|
|
16279
17085
|
let binding = loadGuestBinding(enrolmentStatePath, nodeKey);
|
|
16280
17086
|
const enrolmentCredential = process.env.FZ_ENROL_TOKEN_CREDENTIAL;
|
|
16281
|
-
const enrolmentCredentialAvailable = Boolean(enrolmentCredential && process.env.CREDENTIALS_DIRECTORY &&
|
|
16282
|
-
const legacyEnrolmentFileAvailable = Boolean(process.env.FZ_ENROL_TOKEN_FILE &&
|
|
17087
|
+
const enrolmentCredentialAvailable = Boolean(enrolmentCredential && process.env.CREDENTIALS_DIRECTORY && existsSync22(`${process.env.CREDENTIALS_DIRECTORY}/${enrolmentCredential}`));
|
|
17088
|
+
const legacyEnrolmentFileAvailable = Boolean(process.env.FZ_ENROL_TOKEN_FILE && existsSync22(process.env.FZ_ENROL_TOKEN_FILE));
|
|
16283
17089
|
if (!binding && (enrolmentCredentialAvailable || legacyEnrolmentFileAvailable) && process.env.FZ_API) {
|
|
16284
17090
|
binding = await enrolGuestIdentity({
|
|
16285
17091
|
apiUrl: process.env.FZ_API,
|
|
@@ -16357,7 +17163,7 @@ if (import.meta.main) {
|
|
|
16357
17163
|
const bootstrapEnabled = process.env.FZ_BOOTSTRAP_PULL === "true";
|
|
16358
17164
|
const bootstrapCredential = process.env.FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL;
|
|
16359
17165
|
const bootstrapKeyPath = bootstrapCredential && process.env.CREDENTIALS_DIRECTORY ? `${process.env.CREDENTIALS_DIRECTORY}/${bootstrapCredential}` : undefined;
|
|
16360
|
-
if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !
|
|
17166
|
+
if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !existsSync22(bootstrapKeyPath) || !process.env.FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT)) {
|
|
16361
17167
|
throw new Error("agent: bootstrap runner requires enrolment, API, local SSH credential and target OTLP coordinate");
|
|
16362
17168
|
}
|
|
16363
17169
|
const bootstrapPull = bootstrapEnabled ? startSshBootstrapPull({
|
|
@@ -16433,6 +17239,7 @@ if (import.meta.main) {
|
|
|
16433
17239
|
activateService: (request) => requestServiceActivation(request, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
16434
17240
|
buildContainer: (request) => requestContainerBuild(request, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
16435
17241
|
activateContainer: (request) => requestContainerActivation(request, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
17242
|
+
applyConnectivity: (request) => requestDeploymentConnectivity(request, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
16436
17243
|
projectExec: process.env.FZ_DEPLOY_RUNNER_SOCKET ? (input) => requestDeploymentCommand(input, process.env.FZ_DEPLOY_RUNNER_SOCKET) : undefined
|
|
16437
17244
|
});
|
|
16438
17245
|
const staticManager = repository && branch && profile ? createDeploymentManager(managerOptions({ repository, branch, profile }, process.env.FZ_DEPLOY_KEY ?? `${repository}:${branch}:${profile}`)) : bootstrapBundle && branch && profile ? createDeploymentManager({
|
|
@@ -16650,6 +17457,8 @@ export {
|
|
|
16650
17457
|
selectAgentRelease,
|
|
16651
17458
|
runAgent,
|
|
16652
17459
|
restoreAgentRelease,
|
|
17460
|
+
resolveDeploymentTargets,
|
|
17461
|
+
resolveDeploymentInputs,
|
|
16653
17462
|
resolveAgentTelemetryConfig,
|
|
16654
17463
|
requestSoftware,
|
|
16655
17464
|
requestMetalProvision,
|
|
@@ -16727,6 +17536,7 @@ export {
|
|
|
16727
17536
|
OS_CATALOG,
|
|
16728
17537
|
METAL_SYSTEMD_CREDENTIALS,
|
|
16729
17538
|
MAX_AGENT_TARBALL_BYTES,
|
|
17539
|
+
DeploymentInputError,
|
|
16730
17540
|
DeploymentError,
|
|
16731
17541
|
DEFAULT_SOFTWARE_HELPER_SOCKET,
|
|
16732
17542
|
DEFAULT_SOCKET_PATH,
|