@forgezero/agent 0.1.74 → 0.1.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -17
- package/dist/agent-heartbeat.d.ts +25 -1
- package/dist/agent-heartbeat.js +45 -5
- package/dist/bootstrap.js +10 -7
- package/dist/community-rehearsal-host.js +218 -13
- package/dist/credential-schema.d.ts +1 -1
- package/dist/credential-schema.js +1 -1
- package/dist/definition.js +206 -7
- package/dist/deploy-actions.d.ts +7 -0
- package/dist/deploy-compiler.js +191 -10
- package/dist/deploy-file.js +206 -7
- package/dist/deploy-plan-runner.d.ts +3 -0
- package/dist/deploy-plan-runner.js +190 -9
- package/dist/deploy-plan.js +187 -8
- package/dist/deploy-providers.d.ts +19 -0
- package/dist/deploy.d.ts +89 -3
- package/dist/deploy.js +49 -2
- package/dist/deployment-connectivity.d.ts +63 -0
- package/dist/deployment-connectivity.js +155 -0
- package/dist/deployment-pull.d.ts +2 -0
- package/dist/deployment-targets.d.ts +9 -0
- package/dist/deployment-targets.js +141 -0
- package/dist/deployment.d.ts +69 -0
- package/dist/fz-agent.js +1122 -313
- package/dist/fz.js +234 -26
- package/dist/index.d.ts +1 -0
- package/dist/metal-bootstrap.js +1 -1
- package/dist/metal-helper-socket.js +210 -11
- package/dist/metal-provision.js +210 -11
- package/dist/operator-bootstrap.js +11 -9
- package/dist/platform-bootstrap-runtime.js +209 -10
- package/dist/platform-fleet-verification.js +525 -155
- package/dist/platform-genesis.js +206 -7
- package/dist/process-input.d.ts +11 -0
- package/dist/provision.js +472 -99
- package/dist/service-supervisor.d.ts +6 -0
- package/dist/software-helper.d.ts +3 -0
- package/dist/software-helper.js +472 -98
- package/dist/software.d.ts +4 -1
- package/dist/software.js +210 -8
- package/dist/ubuntu.js +206 -7
- package/dist/version.d.ts +1 -1
- package/package.json +12 -4
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(", ")})`);
|
|
7549
8076
|
}
|
|
7550
|
-
|
|
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");
|
|
8089
|
+
}
|
|
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 });
|
|
@@ -8587,8 +9186,14 @@ import { tmpdir as tmpdir2 } from "os";
|
|
|
8587
9186
|
import { join as join3 } from "path";
|
|
8588
9187
|
import { fileURLToPath } from "url";
|
|
8589
9188
|
|
|
9189
|
+
// src/process-input.ts
|
|
9190
|
+
async function writeAndCloseProcessInput(input, value) {
|
|
9191
|
+
input.write(value);
|
|
9192
|
+
await input.end();
|
|
9193
|
+
}
|
|
9194
|
+
|
|
8590
9195
|
// src/version.ts
|
|
8591
|
-
var VERSION2 = "0.1.
|
|
9196
|
+
var VERSION2 = "0.1.76";
|
|
8592
9197
|
|
|
8593
9198
|
// src/ssh-bootstrap.ts
|
|
8594
9199
|
class SshBootstrapError extends Error {
|
|
@@ -8606,8 +9211,7 @@ var defaultExec = async (argv2, options = {}) => {
|
|
|
8606
9211
|
env: { PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", LANG: "C", LC_ALL: "C" }
|
|
8607
9212
|
});
|
|
8608
9213
|
if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
|
|
8609
|
-
child.stdin
|
|
8610
|
-
child.stdin.end();
|
|
9214
|
+
await writeAndCloseProcessInput(child.stdin, options.stdin);
|
|
8611
9215
|
}
|
|
8612
9216
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
8613
9217
|
new Response(child.stdout).text(),
|
|
@@ -9304,11 +9908,11 @@ import {
|
|
|
9304
9908
|
readFileSync as readFileSync6,
|
|
9305
9909
|
readdirSync,
|
|
9306
9910
|
rmSync as rmSync4,
|
|
9307
|
-
statSync as
|
|
9911
|
+
statSync as statSync3,
|
|
9308
9912
|
unlinkSync as unlinkSync5,
|
|
9309
9913
|
writeFileSync as writeFileSync6
|
|
9310
9914
|
} from "fs";
|
|
9311
|
-
import { dirname as
|
|
9915
|
+
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join4 } from "path";
|
|
9312
9916
|
import { isIP as isIP2 } from "net";
|
|
9313
9917
|
|
|
9314
9918
|
// src/compute.ts
|
|
@@ -9733,7 +10337,7 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
9733
10337
|
if (!image || !isAbsolute3(image.path) || !SHA256.test(image.sha256)) {
|
|
9734
10338
|
throw new MetalProvisionError(`image ${claim.spec.imageKey} is not configured locally`);
|
|
9735
10339
|
}
|
|
9736
|
-
if (!
|
|
10340
|
+
if (!statSync3(image.path).isFile())
|
|
9737
10341
|
throw new MetalProvisionError("configured image is not a file");
|
|
9738
10342
|
const digest = (await checked2(exec, ["sha256sum", image.path])).stdout.trim().split(/\s+/)[0];
|
|
9739
10343
|
if (digest !== image.sha256)
|
|
@@ -9818,7 +10422,7 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
9818
10422
|
}
|
|
9819
10423
|
const service = `forgezero-guest@${name}.service`;
|
|
9820
10424
|
const unitPath = join4(profile.unitDir, service);
|
|
9821
|
-
mkdirSync5(
|
|
10425
|
+
mkdirSync5(dirname4(unitPath), { recursive: true });
|
|
9822
10426
|
writeFileSync6(unitPath, guestUnit(spec), { mode: 420 });
|
|
9823
10427
|
await checked2(exec, ["systemctl", "daemon-reload"]);
|
|
9824
10428
|
if (prior?.phase === "running") {
|
|
@@ -10882,7 +11486,7 @@ import {
|
|
|
10882
11486
|
symlinkSync as symlinkSync3,
|
|
10883
11487
|
writeFileSync as writeFileSync9
|
|
10884
11488
|
} from "fs";
|
|
10885
|
-
import { dirname as
|
|
11489
|
+
import { dirname as dirname5, join as join7, resolve as resolve2 } from "path";
|
|
10886
11490
|
var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
|
|
10887
11491
|
var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
|
|
10888
11492
|
var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
|
|
@@ -10904,7 +11508,7 @@ var syncReleaseDirectory = (directory) => {
|
|
|
10904
11508
|
join7(directory, "dist", "fz-git-ssh.js"),
|
|
10905
11509
|
join7(directory, "dist"),
|
|
10906
11510
|
directory,
|
|
10907
|
-
|
|
11511
|
+
dirname5(directory)
|
|
10908
11512
|
])
|
|
10909
11513
|
syncPath(path2);
|
|
10910
11514
|
};
|
|
@@ -11026,7 +11630,7 @@ async function stageAgentRelease(releaseInput, options) {
|
|
|
11026
11630
|
args: ["-xOzf", archive, member]
|
|
11027
11631
|
}, `agent update extraction of ${member}`);
|
|
11028
11632
|
const destination = join7(unpacked, relative);
|
|
11029
|
-
mkdirSync8(
|
|
11633
|
+
mkdirSync8(dirname5(destination), { recursive: true, mode: 448 });
|
|
11030
11634
|
writeFileSync9(destination, extracted.output, { mode: 384, flag: "wx" });
|
|
11031
11635
|
}
|
|
11032
11636
|
await validateReleaseDirectory(unpacked, release, run2);
|
|
@@ -11058,21 +11662,21 @@ async function stageAgentRelease(releaseInput, options) {
|
|
|
11058
11662
|
}
|
|
11059
11663
|
}
|
|
11060
11664
|
function selectAgentRelease(staged) {
|
|
11061
|
-
const next = join7(
|
|
11665
|
+
const next = join7(dirname5(staged.currentLink), `.current.${randomUUID2()}.next`);
|
|
11062
11666
|
try {
|
|
11063
11667
|
symlinkSync3(staged.nextTarget, next);
|
|
11064
11668
|
renameSync6(next, staged.currentLink);
|
|
11065
|
-
syncPath(
|
|
11669
|
+
syncPath(dirname5(staged.currentLink));
|
|
11066
11670
|
} finally {
|
|
11067
11671
|
rmSync5(next, { force: true });
|
|
11068
11672
|
}
|
|
11069
11673
|
}
|
|
11070
11674
|
function restoreAgentRelease(staged) {
|
|
11071
|
-
const next = join7(
|
|
11675
|
+
const next = join7(dirname5(staged.currentLink), `.current.${randomUUID2()}.rollback`);
|
|
11072
11676
|
try {
|
|
11073
11677
|
symlinkSync3(staged.previousTarget, next);
|
|
11074
11678
|
renameSync6(next, staged.currentLink);
|
|
11075
|
-
syncPath(
|
|
11679
|
+
syncPath(dirname5(staged.currentLink));
|
|
11076
11680
|
} finally {
|
|
11077
11681
|
rmSync5(next, { force: true });
|
|
11078
11682
|
}
|
|
@@ -11094,7 +11698,7 @@ import {
|
|
|
11094
11698
|
writeFileSync as writeFileSync10
|
|
11095
11699
|
} from "fs";
|
|
11096
11700
|
import { connect as connect5, createServer as createServer6 } from "net";
|
|
11097
|
-
import { dirname as
|
|
11701
|
+
import { dirname as dirname6, join as join8, resolve as resolve3 } from "path";
|
|
11098
11702
|
var AGENT_UPDATE_GROUP = "forgezero-update";
|
|
11099
11703
|
var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-helper.service";
|
|
11100
11704
|
var AGENT_UPDATE_JOURNAL = "/var/lib/forgezero/agent-update.json";
|
|
@@ -11194,7 +11798,7 @@ function readJournal(path2, root) {
|
|
|
11194
11798
|
return validateJournal(JSON.parse(readFileSync10(path2, "utf8")), root);
|
|
11195
11799
|
}
|
|
11196
11800
|
function writeAtomic(path2, value, mode) {
|
|
11197
|
-
mkdirSync9(
|
|
11801
|
+
mkdirSync9(dirname6(path2), { recursive: true, mode: 493 });
|
|
11198
11802
|
const next = `${path2}.${randomUUID3()}.next`;
|
|
11199
11803
|
let file;
|
|
11200
11804
|
try {
|
|
@@ -11205,7 +11809,7 @@ function writeAtomic(path2, value, mode) {
|
|
|
11205
11809
|
closeSync3(file);
|
|
11206
11810
|
file = undefined;
|
|
11207
11811
|
renameSync7(next, path2);
|
|
11208
|
-
const directory = openSync3(
|
|
11812
|
+
const directory = openSync3(dirname6(path2), "r");
|
|
11209
11813
|
try {
|
|
11210
11814
|
fsyncSync2(directory);
|
|
11211
11815
|
} finally {
|
|
@@ -11327,7 +11931,7 @@ async function activateAgentRelease(staged, options = {}) {
|
|
|
11327
11931
|
const now = options.now ?? Date.now;
|
|
11328
11932
|
const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
|
|
11329
11933
|
const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
|
|
11330
|
-
const previous = readJournal(journalPath,
|
|
11934
|
+
const previous = readJournal(journalPath, dirname6(staged.currentLink));
|
|
11331
11935
|
const attemptId = options.attemptId ?? randomUUID3();
|
|
11332
11936
|
if (!ATTEMPT_ID.test(attemptId))
|
|
11333
11937
|
throw new Error("Agent update attempt ID is invalid");
|
|
@@ -11435,7 +12039,7 @@ function startAgentUpdateHelper(options = {}) {
|
|
|
11435
12039
|
const socketPath = options.socketPath ?? DEFAULT_AGENT_UPDATE_SOCKET;
|
|
11436
12040
|
if (existsSync13(socketPath))
|
|
11437
12041
|
unlinkSync10(socketPath);
|
|
11438
|
-
mkdirSync9(
|
|
12042
|
+
mkdirSync9(dirname6(socketPath), { recursive: true, mode: 488 });
|
|
11439
12043
|
const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
|
|
11440
12044
|
const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
|
|
11441
12045
|
const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
|
|
@@ -11544,23 +12148,63 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
|
|
|
11544
12148
|
}
|
|
11545
12149
|
|
|
11546
12150
|
// src/agent-heartbeat.ts
|
|
11547
|
-
import { readFileSync as readFileSync11 } from "fs";
|
|
12151
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, statfsSync as statfsSync2 } from "fs";
|
|
12152
|
+
import { cpus, freemem, loadavg, totalmem } from "os";
|
|
12153
|
+
function readAgentHostMetrics() {
|
|
12154
|
+
const filesystem = statfsSync2("/");
|
|
12155
|
+
return {
|
|
12156
|
+
logicalCpu: cpus().length,
|
|
12157
|
+
memoryBytes: totalmem(),
|
|
12158
|
+
memoryFreeBytes: freemem(),
|
|
12159
|
+
storageBytes: Number(filesystem.blocks) * Number(filesystem.bsize),
|
|
12160
|
+
storageFreeBytes: Number(filesystem.bavail) * Number(filesystem.bsize),
|
|
12161
|
+
load1m: loadavg()[0]
|
|
12162
|
+
};
|
|
12163
|
+
}
|
|
11548
12164
|
var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
|
|
11549
12165
|
var remoteOutcome4 = (cause) => cause instanceof SignedNodeHttpError && cause.status < 500 ? "refused" : cause instanceof SignedNodeHttpError ? "retryable" : "failed";
|
|
11550
12166
|
|
|
11551
12167
|
class AgentUpdateRefusedError extends Error {
|
|
11552
12168
|
}
|
|
11553
|
-
function observeAgentHost(version = VERSION2, mode = "enrolled", osRelease = readFileSync11("/etc/os-release", "utf8"), architecture = process.arch) {
|
|
12169
|
+
function observeAgentHost(version = VERSION2, mode = "enrolled", osRelease = readFileSync11("/etc/os-release", "utf8"), architecture = process.arch, metrics = readAgentHostMetrics()) {
|
|
11554
12170
|
const values = Object.fromEntries(osRelease.split(`
|
|
11555
12171
|
`).flatMap((line) => {
|
|
11556
12172
|
const separator = line.indexOf("=");
|
|
11557
12173
|
return separator > 0 ? [[line.slice(0, separator), unquote(line.slice(separator + 1))]] : [];
|
|
11558
12174
|
}));
|
|
12175
|
+
const osId = (values.ID ?? "unknown").toLowerCase();
|
|
12176
|
+
const osVersion = values.VERSION_ID ?? "unknown";
|
|
12177
|
+
const ubuntuX64 = osId === "ubuntu" && ["24.04", "26.04"].includes(osVersion) && architecture === "x64";
|
|
12178
|
+
const enabled = (path2) => {
|
|
12179
|
+
try {
|
|
12180
|
+
return /^(?:1|y)$/i.test(readFileSync11(path2, "utf8").trim());
|
|
12181
|
+
} catch {
|
|
12182
|
+
return false;
|
|
12183
|
+
}
|
|
12184
|
+
};
|
|
12185
|
+
const gib = 1024 ** 3;
|
|
12186
|
+
const mib = 1024 ** 2;
|
|
12187
|
+
const storageGiB = Math.max(1, Math.floor(metrics.storageBytes / gib));
|
|
11559
12188
|
return {
|
|
11560
12189
|
version,
|
|
11561
|
-
os: { id:
|
|
12190
|
+
os: { id: osId, versionId: osVersion },
|
|
11562
12191
|
architecture,
|
|
11563
|
-
mode
|
|
12192
|
+
mode,
|
|
12193
|
+
runtimeCapabilities: {
|
|
12194
|
+
native: true,
|
|
12195
|
+
ociRunc: ubuntuX64,
|
|
12196
|
+
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")
|
|
12197
|
+
},
|
|
12198
|
+
capacity: {
|
|
12199
|
+
logicalCpu: Math.max(1, metrics.logicalCpu),
|
|
12200
|
+
memoryMiB: Math.max(1, Math.floor(metrics.memoryBytes / mib)),
|
|
12201
|
+
storageGiB
|
|
12202
|
+
},
|
|
12203
|
+
utilization: {
|
|
12204
|
+
load1mMilli: Math.max(0, Math.round(metrics.load1m * 1000)),
|
|
12205
|
+
memoryUsedMiB: Math.max(0, Math.floor((metrics.memoryBytes - metrics.memoryFreeBytes) / mib)),
|
|
12206
|
+
storageUsedGiB: Math.max(0, Math.min(storageGiB, Math.ceil((metrics.storageBytes - metrics.storageFreeBytes) / gib)))
|
|
12207
|
+
}
|
|
11564
12208
|
};
|
|
11565
12209
|
}
|
|
11566
12210
|
async function heartbeatAgentOnce(options) {
|
|
@@ -11668,14 +12312,14 @@ function startAgentHeartbeat(options) {
|
|
|
11668
12312
|
}
|
|
11669
12313
|
|
|
11670
12314
|
// src/software-helper.ts
|
|
11671
|
-
import { chmodSync as chmodSync14, existsSync as
|
|
12315
|
+
import { chmodSync as chmodSync14, existsSync as existsSync17, mkdirSync as mkdirSync13, unlinkSync as unlinkSync11 } from "fs";
|
|
11672
12316
|
import { connect as connect6, createServer as createServer7 } from "net";
|
|
11673
|
-
import { dirname as
|
|
12317
|
+
import { dirname as dirname10 } from "path";
|
|
11674
12318
|
|
|
11675
12319
|
// src/service-supervisor.ts
|
|
11676
12320
|
import { createHash as createHash9 } from "crypto";
|
|
11677
|
-
import { existsSync as
|
|
11678
|
-
import { dirname as
|
|
12321
|
+
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";
|
|
12322
|
+
import { dirname as dirname7, join as join9, resolve as resolve4, sep as sep2 } from "path";
|
|
11679
12323
|
var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
|
|
11680
12324
|
var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
|
|
11681
12325
|
var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
|
|
@@ -11685,14 +12329,14 @@ var statePath = (id2) => `${SERVICE_STATE_DIRECTORY}/${id2}.json`;
|
|
|
11685
12329
|
var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
|
|
11686
12330
|
var defaultHost = {
|
|
11687
12331
|
write(path2, content, mode) {
|
|
11688
|
-
mkdirSync10(
|
|
12332
|
+
mkdirSync10(dirname7(path2), { recursive: true, mode: 493 });
|
|
11689
12333
|
const next = `${path2}.next`;
|
|
11690
12334
|
writeFileSync11(next, content, { mode });
|
|
11691
12335
|
renameSync8(next, path2);
|
|
11692
12336
|
},
|
|
11693
12337
|
read: (path2) => readFileSync12(path2, "utf8"),
|
|
11694
|
-
exists:
|
|
11695
|
-
list: (path2) =>
|
|
12338
|
+
exists: existsSync15,
|
|
12339
|
+
list: (path2) => existsSync15(path2) ? readdirSync2(path2) : [],
|
|
11696
12340
|
realpath: realpathSync2,
|
|
11697
12341
|
mkdir: (path2, mode) => mkdirSync10(path2, { recursive: true, mode }),
|
|
11698
12342
|
remove: (path2) => rmSync7(path2, { force: true }),
|
|
@@ -11891,20 +12535,20 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
11891
12535
|
|
|
11892
12536
|
// src/container-supervisor.ts
|
|
11893
12537
|
import { createHash as createHash10 } from "crypto";
|
|
11894
|
-
import { existsSync as
|
|
11895
|
-
import { dirname as
|
|
12538
|
+
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";
|
|
12539
|
+
import { dirname as dirname8, resolve as resolve5, sep as sep3 } from "path";
|
|
11896
12540
|
var defaultHost2 = {
|
|
11897
12541
|
realpath: realpathSync3,
|
|
11898
|
-
exists:
|
|
12542
|
+
exists: existsSync16,
|
|
11899
12543
|
read: (path2) => readFileSync13(path2, "utf8"),
|
|
11900
12544
|
write(path2, content, mode) {
|
|
11901
|
-
mkdirSync11(
|
|
12545
|
+
mkdirSync11(dirname8(path2), { recursive: true, mode: 493 });
|
|
11902
12546
|
const next = `${path2}.next`;
|
|
11903
12547
|
writeFileSync12(next, content, { mode });
|
|
11904
12548
|
renameSync9(next, path2);
|
|
11905
12549
|
},
|
|
11906
12550
|
remove: (path2) => rmSync8(path2, { force: true }),
|
|
11907
|
-
list: (path2) =>
|
|
12551
|
+
list: (path2) => existsSync16(path2) ? readdirSync3(path2) : [],
|
|
11908
12552
|
mkdir: (path2, mode) => mkdirSync11(path2, { recursive: true, mode }),
|
|
11909
12553
|
async exec(argv2) {
|
|
11910
12554
|
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 +12570,38 @@ var within3 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep3
|
|
|
11926
12570
|
var idFor2 = (key) => createHash10("sha256").update(key).digest("hex").slice(0, 24);
|
|
11927
12571
|
var statePath2 = (id2) => `${SERVICE_STATE_DIRECTORY}/${id2}.json`;
|
|
11928
12572
|
var nameFor = (id2, slot) => `forgezero-${id2}-slot${slot}`;
|
|
12573
|
+
function backendFor(application) {
|
|
12574
|
+
const runtime = application.runtime;
|
|
12575
|
+
if (runtime.kind !== "container")
|
|
12576
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
12577
|
+
if (runtime.provider === "forgezero.docker") {
|
|
12578
|
+
if (runtime.runtimeClass && runtime.runtimeClass !== "runc")
|
|
12579
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
12580
|
+
return { engine: "docker", runtimeClass: "runc", prefix: ["/usr/bin/docker"] };
|
|
12581
|
+
}
|
|
12582
|
+
if (runtime.provider === "forgezero.containerd") {
|
|
12583
|
+
if ((runtime.runtimeClass ?? "runc") !== "runc")
|
|
12584
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
12585
|
+
return {
|
|
12586
|
+
engine: "containerd",
|
|
12587
|
+
runtimeClass: "runc",
|
|
12588
|
+
prefix: ["/usr/local/bin/nerdctl", "--namespace", "forgezero"],
|
|
12589
|
+
runtimeName: "io.containerd.runc.v2"
|
|
12590
|
+
};
|
|
12591
|
+
}
|
|
12592
|
+
if (runtime.provider === "forgezero.kata") {
|
|
12593
|
+
if (runtime.runtimeClass !== "kata-qemu-snp")
|
|
12594
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_MISMATCH");
|
|
12595
|
+
return {
|
|
12596
|
+
engine: "containerd",
|
|
12597
|
+
runtimeClass: "kata-qemu-snp",
|
|
12598
|
+
prefix: ["/usr/local/bin/nerdctl", "--namespace", "forgezero"],
|
|
12599
|
+
runtimeName: "io.containerd.kata.v2"
|
|
12600
|
+
};
|
|
12601
|
+
}
|
|
12602
|
+
throw new Error("CONTAINER_RUNTIME_PROVIDER_UNSUPPORTED");
|
|
12603
|
+
}
|
|
12604
|
+
var command2 = (backend, ...argv2) => [...backend.prefix, ...argv2];
|
|
11929
12605
|
function validateRequest(request, host) {
|
|
11930
12606
|
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
12607
|
throw new Error("CONTAINER_IDENTITY_INVALID");
|
|
@@ -11936,6 +12612,7 @@ function validateRequest(request, host) {
|
|
|
11936
12612
|
const application = request.application;
|
|
11937
12613
|
if (application.kind !== "application" || application.runtime.kind !== "container")
|
|
11938
12614
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
12615
|
+
backendFor(application);
|
|
11939
12616
|
if (!application.resources.cpu || !application.resources.memory || !application.resources.pids)
|
|
11940
12617
|
throw new Error("CONTAINER_LIMITS_REQUIRED");
|
|
11941
12618
|
if (application.runtime.security.privileged !== false || application.runtime.security.noNewPrivileges !== true || !application.runtime.security.dropCapabilities.includes("ALL"))
|
|
@@ -11957,12 +12634,13 @@ async function buildContainerImage(requestValue, host = defaultHost2) {
|
|
|
11957
12634
|
const runtime = request.application.runtime;
|
|
11958
12635
|
if (runtime.kind !== "container")
|
|
11959
12636
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
12637
|
+
const backend = backendFor(request.application);
|
|
11960
12638
|
const source = runtime.image.source;
|
|
11961
12639
|
if (source.kind === "registry") {
|
|
11962
12640
|
if (!source.reference.includes("@sha256:"))
|
|
11963
12641
|
throw new Error("CONTAINER_IMAGE_DIGEST_REQUIRED");
|
|
11964
|
-
await checked6(host,
|
|
11965
|
-
const digest2 = await checked6(host,
|
|
12642
|
+
await checked6(host, command2(backend, "pull", "--quiet", source.reference), "CONTAINER_PULL_FAILED");
|
|
12643
|
+
const digest2 = await checked6(host, command2(backend, "image", "inspect", "--format={{.Id}}", source.reference), "CONTAINER_INSPECT_FAILED");
|
|
11966
12644
|
if (!/^sha256:[a-f0-9]{64}$/.test(digest2))
|
|
11967
12645
|
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
11968
12646
|
return { image: source.reference, digest: digest2 };
|
|
@@ -11972,22 +12650,8 @@ async function buildContainerImage(requestValue, host = defaultHost2) {
|
|
|
11972
12650
|
if (!within3(request.release, context) || !within3(context, dockerfile))
|
|
11973
12651
|
throw new Error("CONTAINER_BUILD_PATH_INVALID");
|
|
11974
12652
|
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");
|
|
12653
|
+
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");
|
|
12654
|
+
const digest = await checked6(host, command2(backend, "image", "inspect", "--format={{.Id}}", tag), "CONTAINER_INSPECT_FAILED");
|
|
11991
12655
|
if (!/^sha256:[a-f0-9]{64}$/.test(digest))
|
|
11992
12656
|
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
11993
12657
|
return { image: tag, digest };
|
|
@@ -12055,7 +12719,7 @@ ${service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
|
|
|
12055
12719
|
}
|
|
12056
12720
|
`;
|
|
12057
12721
|
}
|
|
12058
|
-
function runArgv(request, id2, slot, port, networkName) {
|
|
12722
|
+
function runArgv(request, backend, id2, slot, port, networkName) {
|
|
12059
12723
|
const { application } = request;
|
|
12060
12724
|
const resources = application.resources;
|
|
12061
12725
|
const runtime = application.runtime;
|
|
@@ -12063,35 +12727,7 @@ function runArgv(request, id2, slot, port, networkName) {
|
|
|
12063
12727
|
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
12064
12728
|
const memory = resources.memory;
|
|
12065
12729
|
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
|
-
];
|
|
12730
|
+
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
12731
|
for (const mount of application.storage ?? []) {
|
|
12096
12732
|
if (mount.class === "ephemeral")
|
|
12097
12733
|
argv2.push("--tmpfs", `${mount.path}:rw,noexec,nosuid,size=${mount.sizeMiB}m`);
|
|
@@ -12105,6 +12741,7 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
12105
12741
|
const request = validateRequest(requestValue, host);
|
|
12106
12742
|
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
12743
|
throw new Error("CONTAINER_IMAGE_INVALID");
|
|
12744
|
+
const backend = backendFor(request.application);
|
|
12108
12745
|
const id2 = idFor2(request.key);
|
|
12109
12746
|
host.mkdir(SERVICE_STATE_DIRECTORY, 448);
|
|
12110
12747
|
host.mkdir(SERVICE_NGINX_DIRECTORY, 493);
|
|
@@ -12117,9 +12754,9 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
12117
12754
|
if (containerNetwork.mode === "bridge") {
|
|
12118
12755
|
networkName = containerNetwork.network ? `forgezero-${id2}-${containerNetwork.network}` : "bridge";
|
|
12119
12756
|
if (networkName !== "bridge") {
|
|
12120
|
-
const inspected = await host.exec(
|
|
12757
|
+
const inspected = await host.exec(command2(backend, "network", "inspect", '--format={{index .Labels "net.forgezero.owner"}}', networkName));
|
|
12121
12758
|
if (inspected.exitCode !== 0) {
|
|
12122
|
-
await checked6(host,
|
|
12759
|
+
await checked6(host, command2(backend, "network", "create", "--driver", "bridge", "--label", `net.forgezero.owner=${id2}`, networkName), "CONTAINER_NETWORK_CREATE_FAILED");
|
|
12123
12760
|
} else if (inspected.output.trim() !== id2)
|
|
12124
12761
|
throw new Error("CONTAINER_NETWORK_OWNERSHIP_INVALID");
|
|
12125
12762
|
}
|
|
@@ -12128,8 +12765,8 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
12128
12765
|
const nextSlot = slots === 2 && previous?.activeSlot === 0 ? 1 : 0;
|
|
12129
12766
|
const port = ports[nextSlot];
|
|
12130
12767
|
const name = nameFor(id2, nextSlot);
|
|
12131
|
-
await host.exec(
|
|
12132
|
-
await checked6(host, runArgv(request, id2, nextSlot, port, networkName), "CONTAINER_START_FAILED");
|
|
12768
|
+
await host.exec(command2(backend, "rm", "--force", name));
|
|
12769
|
+
await checked6(host, runArgv(request, backend, id2, nextSlot, port, networkName), "CONTAINER_START_FAILED");
|
|
12133
12770
|
let healthy = false;
|
|
12134
12771
|
const health = request.application.service.health;
|
|
12135
12772
|
for (let attempt = 0;attempt < (health.attempts ?? 30); attempt += 1) {
|
|
@@ -12140,9 +12777,22 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
12140
12777
|
await host.sleep(health.intervalMs ?? 1000);
|
|
12141
12778
|
}
|
|
12142
12779
|
if (!healthy) {
|
|
12143
|
-
await host.exec(
|
|
12780
|
+
await host.exec(command2(backend, "rm", "--force", name));
|
|
12144
12781
|
throw new Error("CONTAINER_HEALTH_FAILED");
|
|
12145
12782
|
}
|
|
12783
|
+
let runtimeEvidence;
|
|
12784
|
+
if (backend.engine === "containerd") {
|
|
12785
|
+
const raw = await checked6(host, ["/usr/bin/ctr", "--namespace", "forgezero", "containers", "info", name], "CONTAINER_RUNTIME_EVIDENCE_FAILED");
|
|
12786
|
+
let observed = "";
|
|
12787
|
+
try {
|
|
12788
|
+
observed = String(JSON.parse(raw).Runtime?.Name ?? "");
|
|
12789
|
+
} catch {
|
|
12790
|
+
throw new Error("CONTAINER_RUNTIME_EVIDENCE_INVALID");
|
|
12791
|
+
}
|
|
12792
|
+
if (observed !== backend.runtimeName)
|
|
12793
|
+
throw new Error("CONTAINER_RUNTIME_DOWNGRADE");
|
|
12794
|
+
runtimeEvidence = observed;
|
|
12795
|
+
}
|
|
12146
12796
|
const nginxPath = `${SERVICE_NGINX_DIRECTORY}/forgezero-${id2}.conf`;
|
|
12147
12797
|
const previousNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
|
|
12148
12798
|
host.write(nginxPath, nginx2(request, id2, port), 420);
|
|
@@ -12156,7 +12806,7 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
12156
12806
|
host.write(nginxPath, previousNginx, 420);
|
|
12157
12807
|
await host.exec(["/usr/sbin/nginx", "-t"]);
|
|
12158
12808
|
await host.exec(["/usr/bin/systemctl", "reload", "nginx.service"]);
|
|
12159
|
-
await host.exec(
|
|
12809
|
+
await host.exec(command2(backend, "rm", "--force", name));
|
|
12160
12810
|
throw cause;
|
|
12161
12811
|
}
|
|
12162
12812
|
const state = {
|
|
@@ -12169,17 +12819,167 @@ async function activateContainer(requestValue, host = defaultHost2) {
|
|
|
12169
12819
|
applicationPorts: ports,
|
|
12170
12820
|
activeSlot: nextSlot,
|
|
12171
12821
|
healthPath: health.path,
|
|
12822
|
+
containerRuntime: {
|
|
12823
|
+
engine: backend.engine,
|
|
12824
|
+
runtimeClass: backend.runtimeClass,
|
|
12825
|
+
...backend.engine === "containerd" ? { namespace: "forgezero", evidence: runtimeEvidence } : {}
|
|
12826
|
+
},
|
|
12172
12827
|
updatedAtTs: host.now()
|
|
12173
12828
|
};
|
|
12174
12829
|
host.write(statePath2(id2), `${JSON.stringify(state, null, 2)}
|
|
12175
12830
|
`, 384);
|
|
12176
12831
|
if (previous && previous.activeSlot !== nextSlot) {
|
|
12177
12832
|
await host.sleep(request.application.rollout.drainMs ?? 30000);
|
|
12178
|
-
await host.exec(
|
|
12833
|
+
await host.exec(command2(backend, "rm", "--force", nameFor(id2, previous.activeSlot)));
|
|
12179
12834
|
}
|
|
12180
12835
|
return state;
|
|
12181
12836
|
}
|
|
12182
12837
|
|
|
12838
|
+
// src/deployment-connectivity.ts
|
|
12839
|
+
import { createHash as createHash11 } from "crypto";
|
|
12840
|
+
import { mkdirSync as mkdirSync12, renameSync as renameSync10, writeFileSync as writeFileSync13 } from "fs";
|
|
12841
|
+
import { dirname as dirname9 } from "path";
|
|
12842
|
+
var TOKEN2 = /^[A-Za-z0-9._-]{40,16384}$/;
|
|
12843
|
+
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;
|
|
12844
|
+
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])?$/;
|
|
12845
|
+
var NAME3 = /^[a-z][a-z0-9._-]{0,99}$/;
|
|
12846
|
+
var idFor3 = (key) => createHash11("sha256").update(key).digest("hex").slice(0, 20);
|
|
12847
|
+
var checked7 = async (host, argv2, label) => {
|
|
12848
|
+
const result = await host.exec(argv2);
|
|
12849
|
+
if (result.exitCode !== 0)
|
|
12850
|
+
throw new Error(`${label} failed: ${result.output.slice(0, 512)}`);
|
|
12851
|
+
return result.output.trim();
|
|
12852
|
+
};
|
|
12853
|
+
async function sealWithSystemd(name, path2, value) {
|
|
12854
|
+
mkdirSync12(dirname9(path2), { recursive: true, mode: 448 });
|
|
12855
|
+
const next = `${path2}.next`;
|
|
12856
|
+
const child = Bun.spawn(["/usr/bin/systemd-creds", "encrypt", `--name=${name}`, "-", next], {
|
|
12857
|
+
stdin: "pipe",
|
|
12858
|
+
stdout: "pipe",
|
|
12859
|
+
stderr: "pipe",
|
|
12860
|
+
env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" }
|
|
12861
|
+
});
|
|
12862
|
+
await writeAndCloseProcessInput(child.stdin, value);
|
|
12863
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
12864
|
+
new Response(child.stdout).text(),
|
|
12865
|
+
new Response(child.stderr).text(),
|
|
12866
|
+
child.exited
|
|
12867
|
+
]);
|
|
12868
|
+
if (exitCode !== 0)
|
|
12869
|
+
throw new Error(`systemd credential sealing failed: ${`${stdout}${stderr}`.replaceAll(value, "[REDACTED]").slice(0, 512)}`);
|
|
12870
|
+
renameSync10(next, path2);
|
|
12871
|
+
}
|
|
12872
|
+
var defaultHost3 = {
|
|
12873
|
+
seal: sealWithSystemd,
|
|
12874
|
+
write(path2, content, mode) {
|
|
12875
|
+
mkdirSync12(dirname9(path2), { recursive: true, mode: 493 });
|
|
12876
|
+
const next = `${path2}.next`;
|
|
12877
|
+
writeFileSync13(next, content, { mode });
|
|
12878
|
+
renameSync10(next, path2);
|
|
12879
|
+
},
|
|
12880
|
+
async exec(argv2) {
|
|
12881
|
+
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" } });
|
|
12882
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
12883
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
12884
|
+
},
|
|
12885
|
+
sleep: (ms) => Bun.sleep(ms)
|
|
12886
|
+
};
|
|
12887
|
+
function validate2(request) {
|
|
12888
|
+
if (!request.key || request.key.length > 256)
|
|
12889
|
+
throw new Error("deployment connectivity key is invalid");
|
|
12890
|
+
const publicIntent = request.intent.public;
|
|
12891
|
+
const privateIntent = request.intent.private;
|
|
12892
|
+
if (publicIntent?.mode === "cloudflare-tunnel") {
|
|
12893
|
+
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)) {
|
|
12894
|
+
throw new Error("public deployment connectivity is malformed");
|
|
12895
|
+
}
|
|
12896
|
+
} else if (request.capabilities.public)
|
|
12897
|
+
throw new Error("unexpected public deployment capability");
|
|
12898
|
+
if (privateIntent?.mode === "cloudflare-warp") {
|
|
12899
|
+
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)) {
|
|
12900
|
+
throw new Error("private deployment connectivity is malformed");
|
|
12901
|
+
}
|
|
12902
|
+
} else if (request.capabilities.private)
|
|
12903
|
+
throw new Error("unexpected private deployment capability");
|
|
12904
|
+
}
|
|
12905
|
+
async function applyDeploymentConnectivity(request, host = defaultHost3) {
|
|
12906
|
+
validate2(request);
|
|
12907
|
+
const id2 = idFor3(request.key);
|
|
12908
|
+
const evidence = { key: request.key };
|
|
12909
|
+
if (request.intent.public?.mode === "cloudflare-tunnel") {
|
|
12910
|
+
const capability = request.capabilities.public;
|
|
12911
|
+
const credentialName = `FZ_TUNNEL_${id2.toUpperCase()}`;
|
|
12912
|
+
const credentialPath = `/etc/forgezero/creds/deployment-tunnel-${id2}.cred`;
|
|
12913
|
+
const unit2 = `forgezero-cloudflared-${id2}.service`;
|
|
12914
|
+
const unitPath = `/etc/systemd/system/${unit2}`;
|
|
12915
|
+
const metricsPort = 20000 + Number.parseInt(id2.slice(0, 4), 16) % 20000;
|
|
12916
|
+
await host.seal(credentialName, credentialPath, capability.connectorToken);
|
|
12917
|
+
host.write(unitPath, `[Unit]
|
|
12918
|
+
Description=ForgeZero deployment Tunnel ${id2}
|
|
12919
|
+
After=network-online.target
|
|
12920
|
+
Wants=network-online.target
|
|
12921
|
+
|
|
12922
|
+
[Service]
|
|
12923
|
+
Type=simple
|
|
12924
|
+
DynamicUser=yes
|
|
12925
|
+
LoadCredentialEncrypted=${credentialName}:${credentialPath}
|
|
12926
|
+
ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --metrics 127.0.0.1:${metricsPort} run --token-file %d/${credentialName}
|
|
12927
|
+
Restart=always
|
|
12928
|
+
RestartSec=5
|
|
12929
|
+
NoNewPrivileges=true
|
|
12930
|
+
PrivateTmp=true
|
|
12931
|
+
ProtectSystem=strict
|
|
12932
|
+
ProtectHome=true
|
|
12933
|
+
|
|
12934
|
+
[Install]
|
|
12935
|
+
WantedBy=multi-user.target
|
|
12936
|
+
`, 420);
|
|
12937
|
+
await checked7(host, ["/usr/bin/systemctl", "daemon-reload"], "cloudflared daemon reload");
|
|
12938
|
+
await checked7(host, ["/usr/bin/systemctl", "enable", "--now", unit2], "cloudflared start");
|
|
12939
|
+
await checked7(host, ["/usr/bin/systemctl", "is-active", "--quiet", unit2], "cloudflared readiness");
|
|
12940
|
+
evidence.public = { hostname: request.intent.public.hostname, tunnelId: capability.tunnelId, unit: unit2, active: true };
|
|
12941
|
+
}
|
|
12942
|
+
if (request.intent.private?.mode === "cloudflare-warp") {
|
|
12943
|
+
const capability = request.capabilities.private;
|
|
12944
|
+
const credentialPath = "/etc/forgezero/creds/CF_WARP_CONNECTOR_TOKEN.cred";
|
|
12945
|
+
const unit2 = "forgezero-deployment-mesh.service";
|
|
12946
|
+
await host.seal("CF_WARP_CONNECTOR_TOKEN", credentialPath, capability.connectorToken);
|
|
12947
|
+
host.write(`/etc/systemd/system/${unit2}`, `[Unit]
|
|
12948
|
+
Description=ForgeZero deployment Mesh/WARP connector
|
|
12949
|
+
Requires=warp-svc.service
|
|
12950
|
+
After=network-online.target warp-svc.service
|
|
12951
|
+
Wants=network-online.target
|
|
12952
|
+
|
|
12953
|
+
[Service]
|
|
12954
|
+
Type=oneshot
|
|
12955
|
+
RemainAfterExit=yes
|
|
12956
|
+
LoadCredentialEncrypted=CF_WARP_CONNECTOR_TOKEN:${credentialPath}
|
|
12957
|
+
ExecStart=/usr/local/bin/fz-agent mesh-config
|
|
12958
|
+
NoNewPrivileges=true
|
|
12959
|
+
PrivateTmp=true
|
|
12960
|
+
ProtectSystem=strict
|
|
12961
|
+
ProtectHome=true
|
|
12962
|
+
ReadWritePaths=/var/lib/cloudflare-warp
|
|
12963
|
+
|
|
12964
|
+
[Install]
|
|
12965
|
+
WantedBy=multi-user.target
|
|
12966
|
+
`, 420);
|
|
12967
|
+
await checked7(host, ["/usr/bin/systemctl", "daemon-reload"], "WARP daemon reload");
|
|
12968
|
+
await checked7(host, ["/usr/bin/systemctl", "enable", "--now", "warp-svc.service", unit2], "WARP connector start");
|
|
12969
|
+
for (let attempt = 0;attempt < 10; attempt += 1) {
|
|
12970
|
+
const status = await host.exec(["/usr/bin/warp-cli", "--accept-tos", "status"]);
|
|
12971
|
+
if (status.exitCode === 0 && /\bconnected\b/i.test(status.output) && !/\bdisconnected\b/i.test(status.output)) {
|
|
12972
|
+
evidence.private = { network: request.intent.private.network, connectorId: capability.connectorId, unit: unit2, active: true };
|
|
12973
|
+
break;
|
|
12974
|
+
}
|
|
12975
|
+
await host.sleep(1000);
|
|
12976
|
+
}
|
|
12977
|
+
if (!evidence.private)
|
|
12978
|
+
throw new Error("WARP connector readiness failed");
|
|
12979
|
+
}
|
|
12980
|
+
return evidence;
|
|
12981
|
+
}
|
|
12982
|
+
|
|
12183
12983
|
// src/software-helper.ts
|
|
12184
12984
|
var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
|
|
12185
12985
|
var SOFTWARE_HELPER_GROUP = "forgezero-software";
|
|
@@ -12188,13 +12988,14 @@ var MAX_REQUEST_BYTES6 = 128 * 1024;
|
|
|
12188
12988
|
var MAX_PENDING_REQUESTS = 128;
|
|
12189
12989
|
function startSoftwareHelper(options = {}) {
|
|
12190
12990
|
const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
|
|
12191
|
-
if (
|
|
12991
|
+
if (existsSync17(socketPath))
|
|
12192
12992
|
unlinkSync11(socketPath);
|
|
12193
|
-
|
|
12993
|
+
mkdirSync13(dirname10(socketPath), { recursive: true, mode: 488 });
|
|
12194
12994
|
const ensure = options.ensure ?? ensureSoftwareRequirements;
|
|
12195
12995
|
const activate = options.activate ?? activateSupervisedService;
|
|
12196
12996
|
const buildContainer = options.buildContainer ?? buildContainerImage;
|
|
12197
12997
|
const activateTypedContainer = options.activateContainer ?? activateContainer;
|
|
12998
|
+
const applyConnectivity = options.applyConnectivity ?? applyDeploymentConnectivity;
|
|
12198
12999
|
let tail = Promise.resolve();
|
|
12199
13000
|
let pending = 0;
|
|
12200
13001
|
const server = createServer7((socket) => {
|
|
@@ -12251,6 +13052,12 @@ function startSoftwareHelper(options = {}) {
|
|
|
12251
13052
|
throw new Error("container deployment root is not owned by this helper");
|
|
12252
13053
|
const state = await activateTypedContainer(containerRequest);
|
|
12253
13054
|
socket.end(`${JSON.stringify({ ok: true, state })}
|
|
13055
|
+
`);
|
|
13056
|
+
return;
|
|
13057
|
+
}
|
|
13058
|
+
if (request.op === "apply-connectivity" && request.request) {
|
|
13059
|
+
const result = await applyConnectivity(request.request);
|
|
13060
|
+
socket.end(`${JSON.stringify({ ok: true, result })}
|
|
12254
13061
|
`);
|
|
12255
13062
|
return;
|
|
12256
13063
|
}
|
|
@@ -12308,6 +13115,9 @@ function requestContainerBuild(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOC
|
|
|
12308
13115
|
function requestContainerActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
12309
13116
|
return requestContainer("activate-container", request, socketPath, timeoutMs);
|
|
12310
13117
|
}
|
|
13118
|
+
function requestDeploymentConnectivity(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
13119
|
+
return requestContainer("apply-connectivity", request, socketPath, timeoutMs);
|
|
13120
|
+
}
|
|
12311
13121
|
function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
|
|
12312
13122
|
return new Promise((resolve6, reject) => {
|
|
12313
13123
|
const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
|
|
@@ -12521,9 +13331,9 @@ var systemCommand = (argv2, stdin) => {
|
|
|
12521
13331
|
stderr: result.stderr.toString()
|
|
12522
13332
|
};
|
|
12523
13333
|
};
|
|
12524
|
-
function resolveServiceUid(user,
|
|
13334
|
+
function resolveServiceUid(user, command3 = systemCommand) {
|
|
12525
13335
|
assertServiceUser(user);
|
|
12526
|
-
const result =
|
|
13336
|
+
const result = command3(["/usr/bin/id", "-u", user]);
|
|
12527
13337
|
const uid = Number(result.stdout.trim());
|
|
12528
13338
|
if (result.exitCode !== 0 || !/^\d+$/.test(result.stdout.trim())) {
|
|
12529
13339
|
throw new Error(`Agent egress service account ${user} does not exist.`);
|
|
@@ -12548,14 +13358,14 @@ function assertResolvedStub(realpath = realpathSync4) {
|
|
|
12548
13358
|
throw new Error("Agent egress requires the active systemd-resolved stub.");
|
|
12549
13359
|
}
|
|
12550
13360
|
}
|
|
12551
|
-
var listPolicy = (
|
|
12552
|
-
function verifyAgentEgressPolicy(uids,
|
|
13361
|
+
var listPolicy = (command3) => command3(["/usr/sbin/nft", "--numeric", "list", "table", "inet", AGENT_EGRESS_TABLE]);
|
|
13362
|
+
function verifyAgentEgressPolicy(uids, command3 = systemCommand, loopback) {
|
|
12553
13363
|
if (uids.length < 1)
|
|
12554
13364
|
return false;
|
|
12555
13365
|
for (const uid of uids)
|
|
12556
13366
|
assertUid(uid);
|
|
12557
13367
|
const uniqueUids = [...new Set(uids)].sort((left, right) => left - right);
|
|
12558
|
-
const result = listPolicy(
|
|
13368
|
+
const result = listPolicy(command3);
|
|
12559
13369
|
if (result.exitCode !== 0)
|
|
12560
13370
|
return false;
|
|
12561
13371
|
let loopbackComment = "";
|
|
@@ -12601,44 +13411,44 @@ function applyAgentEgressPolicy(options) {
|
|
|
12601
13411
|
if (typeof getuid !== "function" || getuid() !== 0) {
|
|
12602
13412
|
throw new Error("Agent egress policy must run as root.");
|
|
12603
13413
|
}
|
|
12604
|
-
const
|
|
13414
|
+
const command3 = options.command ?? systemCommand;
|
|
12605
13415
|
assertResolvedStub(options.realpath);
|
|
12606
13416
|
if (options.users.length < 1)
|
|
12607
13417
|
throw new Error("Agent egress policy needs at least one service user.");
|
|
12608
|
-
const uids = [...new Set(options.users.map((user) => resolveServiceUid(user,
|
|
13418
|
+
const uids = [...new Set(options.users.map((user) => resolveServiceUid(user, command3)))];
|
|
12609
13419
|
const loopback = options.loopback ? {
|
|
12610
|
-
uid: resolveServiceUid(options.loopback.user,
|
|
13420
|
+
uid: resolveServiceUid(options.loopback.user, command3),
|
|
12611
13421
|
tcpPorts: normalizeEgressTcpPorts(options.loopback.tcpPorts),
|
|
12612
13422
|
publicTcpPorts: options.loopback.publicTcpPorts === undefined ? undefined : normalizeEgressTcpPorts(options.loopback.publicTcpPorts)
|
|
12613
13423
|
} : undefined;
|
|
12614
13424
|
if (loopback && !uids.includes(loopback.uid))
|
|
12615
13425
|
uids.push(loopback.uid);
|
|
12616
|
-
const current2 = listPolicy(
|
|
12617
|
-
const applied =
|
|
13426
|
+
const current2 = listPolicy(command3);
|
|
13427
|
+
const applied = command3(["/usr/sbin/nft", "--file", "-"], renderAgentEgressNft(uids, current2.exitCode === 0, loopback));
|
|
12618
13428
|
if (applied.exitCode !== 0) {
|
|
12619
13429
|
throw new Error(`Could not apply Agent egress policy: ${applied.stderr.trim() || "nft refused it"}`);
|
|
12620
13430
|
}
|
|
12621
|
-
if (!verifyAgentEgressPolicy(uids,
|
|
13431
|
+
if (!verifyAgentEgressPolicy(uids, command3, loopback)) {
|
|
12622
13432
|
throw new Error("Agent egress policy did not verify after installation.");
|
|
12623
13433
|
}
|
|
12624
13434
|
return uids;
|
|
12625
13435
|
}
|
|
12626
13436
|
async function superviseAgentEgressPolicy(options) {
|
|
12627
|
-
const
|
|
13437
|
+
const command3 = options.command ?? systemCommand;
|
|
12628
13438
|
const uids = applyAgentEgressPolicy({
|
|
12629
13439
|
users: options.users,
|
|
12630
13440
|
loopback: options.loopback,
|
|
12631
|
-
command:
|
|
13441
|
+
command: command3,
|
|
12632
13442
|
realpath: options.realpath,
|
|
12633
13443
|
getuid: options.getuid
|
|
12634
13444
|
});
|
|
12635
13445
|
const loopback = options.loopback ? {
|
|
12636
|
-
uid: resolveServiceUid(options.loopback.user,
|
|
13446
|
+
uid: resolveServiceUid(options.loopback.user, command3),
|
|
12637
13447
|
tcpPorts: normalizeEgressTcpPorts(options.loopback.tcpPorts),
|
|
12638
13448
|
publicTcpPorts: options.loopback.publicTcpPorts === undefined ? undefined : normalizeEgressTcpPorts(options.loopback.publicTcpPorts)
|
|
12639
13449
|
} : undefined;
|
|
12640
13450
|
(options.notifyReady ?? (() => {
|
|
12641
|
-
const result =
|
|
13451
|
+
const result = command3(["/usr/bin/systemd-notify", "--ready"]);
|
|
12642
13452
|
if (result.exitCode !== 0)
|
|
12643
13453
|
throw new Error("Could not notify systemd that Agent egress is ready.");
|
|
12644
13454
|
}))();
|
|
@@ -12649,7 +13459,7 @@ async function superviseAgentEgressPolicy(options) {
|
|
|
12649
13459
|
}
|
|
12650
13460
|
for (;; ) {
|
|
12651
13461
|
await wait(intervalMs);
|
|
12652
|
-
if (!verifyAgentEgressPolicy(uids,
|
|
13462
|
+
if (!verifyAgentEgressPolicy(uids, command3, loopback)) {
|
|
12653
13463
|
throw new Error("Agent egress policy disappeared or changed; stopping the bound Agent.");
|
|
12654
13464
|
}
|
|
12655
13465
|
}
|
|
@@ -13260,7 +14070,7 @@ var AGENT_CREDENTIAL_POLICY = {
|
|
|
13260
14070
|
"platform-compute": { vault: true, systemdFallback: true, hiddenInput: false },
|
|
13261
14071
|
"tenant-compute": { vault: true, systemdFallback: true, hiddenInput: false }
|
|
13262
14072
|
};
|
|
13263
|
-
var CREDENTIAL_NAME = /^[A-
|
|
14073
|
+
var CREDENTIAL_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
|
|
13264
14074
|
var SCOPE_PART2 = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
|
|
13265
14075
|
function deploymentCredentialSchema(input) {
|
|
13266
14076
|
if (!SCOPE_PART2.test(input.projectKey) || !SCOPE_PART2.test(input.environmentKey)) {
|
|
@@ -13302,17 +14112,17 @@ import {
|
|
|
13302
14112
|
chmodSync as chmodSync15,
|
|
13303
14113
|
chownSync,
|
|
13304
14114
|
copyFileSync as copyFileSync2,
|
|
13305
|
-
existsSync as
|
|
14115
|
+
existsSync as existsSync18,
|
|
13306
14116
|
lstatSync as lstatSync3,
|
|
13307
|
-
mkdirSync as
|
|
14117
|
+
mkdirSync as mkdirSync14,
|
|
13308
14118
|
readFileSync as readFileSync14,
|
|
13309
14119
|
readdirSync as readdirSync4,
|
|
13310
14120
|
realpathSync as realpathSync5,
|
|
13311
|
-
renameSync as
|
|
14121
|
+
renameSync as renameSync11,
|
|
13312
14122
|
rmSync as rmSync9,
|
|
13313
|
-
statSync as
|
|
14123
|
+
statSync as statSync4,
|
|
13314
14124
|
symlinkSync as symlinkSync4,
|
|
13315
|
-
writeFileSync as
|
|
14125
|
+
writeFileSync as writeFileSync14
|
|
13316
14126
|
} from "fs";
|
|
13317
14127
|
import { join as join10 } from "path";
|
|
13318
14128
|
var boundedInteger2 = (name, value, minimum, maximum) => {
|
|
@@ -13372,7 +14182,7 @@ var replaceLink = (path2, target2) => {
|
|
|
13372
14182
|
return;
|
|
13373
14183
|
}
|
|
13374
14184
|
symlinkSync4(target2, pending);
|
|
13375
|
-
|
|
14185
|
+
renameSync11(pending, path2);
|
|
13376
14186
|
};
|
|
13377
14187
|
var secureRelease = (path2, uid, gid) => {
|
|
13378
14188
|
const visit = (current2) => {
|
|
@@ -13395,7 +14205,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
13395
14205
|
if (!release.startsWith(`${releases}/`) || release === releases)
|
|
13396
14206
|
throw new Error("release is outside configured releases directory");
|
|
13397
14207
|
for (const required of ["forgezero.deploy.ts", ".fz/deploy.plan.json", "src/index.ts", "bun.lock"]) {
|
|
13398
|
-
const metadata =
|
|
14208
|
+
const metadata = statSync4(join10(release, required));
|
|
13399
14209
|
if (!metadata.isFile() || metadata.size < 1)
|
|
13400
14210
|
throw new Error(`release is incomplete: ${required}`);
|
|
13401
14211
|
}
|
|
@@ -13403,9 +14213,9 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
13403
14213
|
const request = options.fetch ?? fetch;
|
|
13404
14214
|
const sleep2 = options.sleep ?? Bun.sleep;
|
|
13405
14215
|
const slots = join10(normalized.root, "slots");
|
|
13406
|
-
|
|
14216
|
+
mkdirSync14(slots, { recursive: true, mode: 493 });
|
|
13407
14217
|
const slotFile = join10(normalized.root, ".forge-slot");
|
|
13408
|
-
const previousSlot =
|
|
14218
|
+
const previousSlot = existsSync18(slotFile) ? readFileSync14(slotFile, "utf8").trim() : undefined;
|
|
13409
14219
|
const target2 = previousSlot === "blue" ? "green" : "blue";
|
|
13410
14220
|
const port = target2 === "blue" ? normalized.bluePort : normalized.greenPort;
|
|
13411
14221
|
const targetLink = join10(slots, target2);
|
|
@@ -13445,17 +14255,17 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
13445
14255
|
}
|
|
13446
14256
|
const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
|
|
13447
14257
|
const backup = `${upstream}.forgezero-backup`;
|
|
13448
|
-
if (
|
|
14258
|
+
if (existsSync18(upstream))
|
|
13449
14259
|
copyFileSync2(upstream, backup);
|
|
13450
14260
|
else
|
|
13451
14261
|
rmSync9(backup, { force: true });
|
|
13452
|
-
|
|
14262
|
+
writeFileSync14(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
|
|
13453
14263
|
`, { mode: 420 });
|
|
13454
14264
|
const test = await exec(["/usr/sbin/nginx", "-t"]);
|
|
13455
14265
|
const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
|
|
13456
14266
|
if (reload.exitCode !== 0) {
|
|
13457
|
-
if (
|
|
13458
|
-
|
|
14267
|
+
if (existsSync18(backup))
|
|
14268
|
+
renameSync11(backup, upstream);
|
|
13459
14269
|
else
|
|
13460
14270
|
rmSync9(upstream, { force: true });
|
|
13461
14271
|
await exec(["/usr/sbin/nginx", "-t"]);
|
|
@@ -13464,13 +14274,13 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
13464
14274
|
throw new Error("nginx refused the promoted upstream");
|
|
13465
14275
|
}
|
|
13466
14276
|
rmSync9(backup, { force: true });
|
|
13467
|
-
|
|
14277
|
+
writeFileSync14(slotFile, `${target2}
|
|
13468
14278
|
`, { mode: 420 });
|
|
13469
14279
|
if (previousSlot && previousSlot !== target2) {
|
|
13470
14280
|
await sleep2(normalized.drainDeadlineMs);
|
|
13471
14281
|
await exec(["/usr/bin/systemctl", "stop", `forgezero@${previousSlot}.service`]);
|
|
13472
14282
|
}
|
|
13473
|
-
const old = readdirSync4(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join10(releases, entry.name)).sort((left, right) =>
|
|
14283
|
+
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
14284
|
for (const path2 of old)
|
|
13475
14285
|
if (path2 !== release)
|
|
13476
14286
|
rmSync9(path2, { recursive: true, force: true });
|
|
@@ -13478,7 +14288,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
13478
14288
|
}
|
|
13479
14289
|
|
|
13480
14290
|
// src/recovery-host.ts
|
|
13481
|
-
import { existsSync as
|
|
14291
|
+
import { existsSync as existsSync19, readFileSync as readFileSync15, statSync as statSync5 } from "fs";
|
|
13482
14292
|
import { hostname } from "os";
|
|
13483
14293
|
import { join as join11 } from "path";
|
|
13484
14294
|
var execute2 = async (argv2) => {
|
|
@@ -13486,7 +14296,7 @@ var execute2 = async (argv2) => {
|
|
|
13486
14296
|
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
13487
14297
|
return { stdout, stderr, exitCode };
|
|
13488
14298
|
};
|
|
13489
|
-
var
|
|
14299
|
+
var checked8 = async (run2, argv2, expected = 0) => {
|
|
13490
14300
|
const result = await run2(argv2);
|
|
13491
14301
|
if (result.exitCode !== expected)
|
|
13492
14302
|
throw new Error(`${argv2[0]} failed (${result.exitCode})`);
|
|
@@ -13504,7 +14314,7 @@ var exactState = (path2, expected) => {
|
|
|
13504
14314
|
throw new Error(`state mismatch: ${expected}`);
|
|
13505
14315
|
};
|
|
13506
14316
|
var nonEmpty = (path2) => {
|
|
13507
|
-
if (!
|
|
14317
|
+
if (!existsSync19(path2) || !statSync5(path2).isFile() || statSync5(path2).size < 1)
|
|
13508
14318
|
throw new Error(`required file missing: ${path2}`);
|
|
13509
14319
|
};
|
|
13510
14320
|
var option = (args, name) => {
|
|
@@ -13548,29 +14358,29 @@ async function runRecoveryHost(args, run2 = execute2) {
|
|
|
13548
14358
|
const slot = activeSlot();
|
|
13549
14359
|
nonEmpty(join11("/opt/forgezero/slots", slot, "src/server/maintenance/restore-backup.ts"));
|
|
13550
14360
|
nonEmpty(join11("/opt/forgezero/slots", slot, "src/server/maintenance/verify-recovery-cluster.ts"));
|
|
13551
|
-
await
|
|
13552
|
-
await
|
|
14361
|
+
await checked8(run2, ["/usr/bin/test", "-x", "/usr/local/bin/fz"]);
|
|
14362
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-db.service"], hasDatabase ? 0 : 3);
|
|
13553
14363
|
if (args.includes("--require-stopped")) {
|
|
13554
|
-
await
|
|
13555
|
-
await
|
|
14364
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@blue.service"], 3);
|
|
14365
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@green.service"], 3);
|
|
13556
14366
|
}
|
|
13557
14367
|
return;
|
|
13558
14368
|
}
|
|
13559
14369
|
if (action === "fence") {
|
|
13560
|
-
await
|
|
13561
|
-
await
|
|
13562
|
-
await
|
|
14370
|
+
await checked8(run2, ["/usr/bin/systemctl", "stop", "forgezero@blue.service", "forgezero@green.service"]);
|
|
14371
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@blue.service"], 3);
|
|
14372
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@green.service"], 3);
|
|
13563
14373
|
const role = option(args, "db-role");
|
|
13564
|
-
await
|
|
14374
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-db.service"], role === "none" ? 3 : 0);
|
|
13565
14375
|
return;
|
|
13566
14376
|
}
|
|
13567
14377
|
if (action === "start-active") {
|
|
13568
14378
|
const slot = activeSlot();
|
|
13569
14379
|
const inactive = slot === "blue" ? "green" : "blue";
|
|
13570
|
-
await
|
|
13571
|
-
await
|
|
13572
|
-
await
|
|
13573
|
-
await
|
|
14380
|
+
await checked8(run2, ["/usr/bin/systemctl", "stop", `forgezero@${inactive}.service`]);
|
|
14381
|
+
await checked8(run2, ["/usr/bin/systemctl", "start", `forgezero@${slot}.service`]);
|
|
14382
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", `forgezero@${slot}.service`]);
|
|
14383
|
+
await checked8(run2, ["/usr/bin/systemctl", "is-active", "--quiet", `forgezero@${inactive}.service`], 3);
|
|
13574
14384
|
return;
|
|
13575
14385
|
}
|
|
13576
14386
|
if (action === "maintenance") {
|
|
@@ -13593,7 +14403,7 @@ async function runRecoveryHost(args, run2 = execute2) {
|
|
|
13593
14403
|
"--property=LoadCredentialEncrypted=backup-recovery-root:/etc/forgezero/creds/backup-recovery-root.cred"
|
|
13594
14404
|
] : []
|
|
13595
14405
|
];
|
|
13596
|
-
await
|
|
14406
|
+
await checked8(run2, ["/usr/bin/systemd-run", "--quiet", "--wait", "--pipe", "--collect", `--unit=${unit2(option(args, "unit"))}`, ...properties, "/usr/local/bin/fz", ...fzArgs]);
|
|
13597
14407
|
return;
|
|
13598
14408
|
}
|
|
13599
14409
|
throw new Error("unknown recovery-host action");
|
|
@@ -13605,15 +14415,15 @@ import { lstatSync as lstatSync6 } from "fs";
|
|
|
13605
14415
|
// src/bootstrap.ts
|
|
13606
14416
|
import {
|
|
13607
14417
|
chmodSync as chmodSync17,
|
|
13608
|
-
existsSync as
|
|
14418
|
+
existsSync as existsSync21,
|
|
13609
14419
|
lstatSync as lstatSync5,
|
|
13610
|
-
mkdirSync as
|
|
14420
|
+
mkdirSync as mkdirSync16,
|
|
13611
14421
|
readFileSync as readFileSync17,
|
|
13612
|
-
renameSync as
|
|
14422
|
+
renameSync as renameSync13,
|
|
13613
14423
|
rmSync as rmSync11,
|
|
13614
|
-
writeFileSync as
|
|
14424
|
+
writeFileSync as writeFileSync16
|
|
13615
14425
|
} from "fs";
|
|
13616
|
-
import { dirname as
|
|
14426
|
+
import { dirname as dirname12 } from "path";
|
|
13617
14427
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
13618
14428
|
|
|
13619
14429
|
// src/provision.ts
|
|
@@ -14546,17 +15356,17 @@ import { randomBytes as randomBytes6 } from "crypto";
|
|
|
14546
15356
|
import {
|
|
14547
15357
|
chmodSync as chmodSync16,
|
|
14548
15358
|
copyFileSync as copyFileSync3,
|
|
14549
|
-
existsSync as
|
|
15359
|
+
existsSync as existsSync20,
|
|
14550
15360
|
lstatSync as lstatSync4,
|
|
14551
|
-
mkdirSync as
|
|
15361
|
+
mkdirSync as mkdirSync15,
|
|
14552
15362
|
readFileSync as readFileSync16,
|
|
14553
15363
|
realpathSync as realpathSync6,
|
|
14554
|
-
renameSync as
|
|
15364
|
+
renameSync as renameSync12,
|
|
14555
15365
|
rmSync as rmSync10,
|
|
14556
15366
|
symlinkSync as symlinkSync5,
|
|
14557
|
-
writeFileSync as
|
|
15367
|
+
writeFileSync as writeFileSync15
|
|
14558
15368
|
} from "fs";
|
|
14559
|
-
import { dirname as
|
|
15369
|
+
import { dirname as dirname11 } from "path";
|
|
14560
15370
|
async function readCapabilities(run2) {
|
|
14561
15371
|
const answers = {};
|
|
14562
15372
|
const checks = Object.entries(CAPABILITY_CHECKS);
|
|
@@ -14577,10 +15387,8 @@ var fixed = async (argv2, stdin) => {
|
|
|
14577
15387
|
stderr: "pipe",
|
|
14578
15388
|
env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C", DEBIAN_FRONTEND: "noninteractive" }
|
|
14579
15389
|
});
|
|
14580
|
-
if (stdin !== undefined && child.stdin && typeof child.stdin !== "number")
|
|
14581
|
-
child.stdin
|
|
14582
|
-
child.stdin.end();
|
|
14583
|
-
}
|
|
15390
|
+
if (stdin !== undefined && child.stdin && typeof child.stdin !== "number")
|
|
15391
|
+
await writeAndCloseProcessInput(child.stdin, stdin);
|
|
14584
15392
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
14585
15393
|
new Response(child.stdout).text(),
|
|
14586
15394
|
new Response(child.stderr).text(),
|
|
@@ -14591,10 +15399,10 @@ var fixed = async (argv2, stdin) => {
|
|
|
14591
15399
|
var runProvisionOperation = async (operation) => {
|
|
14592
15400
|
if (operation.kind === "commands") {
|
|
14593
15401
|
let output = "";
|
|
14594
|
-
for (const
|
|
14595
|
-
const result = await fixed(
|
|
15402
|
+
for (const command3 of operation.commands) {
|
|
15403
|
+
const result = await fixed(command3.argv);
|
|
14596
15404
|
output += result.stdout;
|
|
14597
|
-
if (!(
|
|
15405
|
+
if (!(command3.acceptedExitCodes ?? [0]).includes(result.exitCode))
|
|
14598
15406
|
return { stdout: output, exitCode: result.exitCode };
|
|
14599
15407
|
}
|
|
14600
15408
|
return { stdout: output, exitCode: 0 };
|
|
@@ -14615,19 +15423,19 @@ var runProvisionOperation = async (operation) => {
|
|
|
14615
15423
|
}
|
|
14616
15424
|
if (operation.kind === "install-runtime") {
|
|
14617
15425
|
const release = `/opt/forgezero/agent/versions/${operation.version}`;
|
|
14618
|
-
|
|
14619
|
-
|
|
15426
|
+
mkdirSync15(`${release}/dist`, { recursive: true, mode: 493 });
|
|
15427
|
+
mkdirSync15(dirname11(operation.binary), { recursive: true, mode: 493 });
|
|
14620
15428
|
copyFileSync3(operation.source, `${release}/dist/fz-agent.js`);
|
|
14621
15429
|
chmodSync16(`${release}/dist/fz-agent.js`, 493);
|
|
14622
|
-
const gitSshSource = `${
|
|
14623
|
-
if (!
|
|
15430
|
+
const gitSshSource = `${dirname11(operation.source)}/fz-git-ssh.js`;
|
|
15431
|
+
if (!existsSync20(gitSshSource))
|
|
14624
15432
|
return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
|
|
14625
15433
|
copyFileSync3(gitSshSource, `${release}/dist/fz-git-ssh.js`);
|
|
14626
15434
|
chmodSync16(`${release}/dist/fz-git-ssh.js`, 493);
|
|
14627
15435
|
const pending = "/opt/forgezero/agent/current.next";
|
|
14628
15436
|
rmSync10(pending, { force: true });
|
|
14629
15437
|
symlinkSync5(`versions/${operation.version}`, pending);
|
|
14630
|
-
|
|
15438
|
+
renameSync12(pending, "/opt/forgezero/agent/current");
|
|
14631
15439
|
rmSync10(operation.binary, { force: true });
|
|
14632
15440
|
symlinkSync5("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
|
|
14633
15441
|
const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
|
|
@@ -14636,7 +15444,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
14636
15444
|
return { stdout: "", exitCode: 0 };
|
|
14637
15445
|
}
|
|
14638
15446
|
if (operation.kind === "ensure-seed") {
|
|
14639
|
-
if (
|
|
15447
|
+
if (existsSync20(operation.credential) && lstatSync4(operation.credential).size > 0)
|
|
14640
15448
|
return { stdout: "", exitCode: 0 };
|
|
14641
15449
|
const seed = randomBytes6(32).toString("base64url");
|
|
14642
15450
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
|
|
@@ -14648,7 +15456,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
14648
15456
|
const key = "/run/forgezero-git-deploy-key";
|
|
14649
15457
|
const publicKey = `${key}.pub`;
|
|
14650
15458
|
try {
|
|
14651
|
-
if (!
|
|
15459
|
+
if (!existsSync20(operation.credential) || lstatSync4(operation.credential).size < 1) {
|
|
14652
15460
|
rmSync10(key, { force: true });
|
|
14653
15461
|
rmSync10(publicKey, { force: true });
|
|
14654
15462
|
let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
|
|
@@ -14659,8 +15467,8 @@ var runProvisionOperation = async (operation) => {
|
|
|
14659
15467
|
return result;
|
|
14660
15468
|
chmodSync16(operation.credential, 256);
|
|
14661
15469
|
}
|
|
14662
|
-
if (!
|
|
14663
|
-
if (!
|
|
15470
|
+
if (!existsSync20(operation.publicKey) || lstatSync4(operation.publicKey).size < 1) {
|
|
15471
|
+
if (!existsSync20(key)) {
|
|
14664
15472
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
|
|
14665
15473
|
if (decrypted.exitCode !== 0)
|
|
14666
15474
|
return decrypted;
|
|
@@ -14668,7 +15476,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
14668
15476
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
14669
15477
|
if (derived.exitCode !== 0)
|
|
14670
15478
|
return derived;
|
|
14671
|
-
|
|
15479
|
+
writeFileSync15(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
|
|
14672
15480
|
`, { mode: 292 });
|
|
14673
15481
|
}
|
|
14674
15482
|
return { stdout: "", exitCode: 0 };
|
|
@@ -14681,12 +15489,12 @@ var runProvisionOperation = async (operation) => {
|
|
|
14681
15489
|
const key = "/run/forgezero-bootstrap-ssh-key";
|
|
14682
15490
|
const generatedPublicKey = `${key}.pub`;
|
|
14683
15491
|
try {
|
|
14684
|
-
if (!
|
|
15492
|
+
if (!existsSync20(operation.credential) || lstatSync4(operation.credential).size < 1) {
|
|
14685
15493
|
rmSync10(key, { force: true });
|
|
14686
15494
|
rmSync10(generatedPublicKey, { force: true });
|
|
14687
15495
|
let result;
|
|
14688
15496
|
if (operation.source) {
|
|
14689
|
-
const source =
|
|
15497
|
+
const source = existsSync20(operation.source) ? lstatSync4(operation.source) : undefined;
|
|
14690
15498
|
if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
|
|
14691
15499
|
return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
|
|
14692
15500
|
}
|
|
@@ -14706,8 +15514,8 @@ var runProvisionOperation = async (operation) => {
|
|
|
14706
15514
|
return result;
|
|
14707
15515
|
chmodSync16(operation.credential, 256);
|
|
14708
15516
|
}
|
|
14709
|
-
if (!
|
|
14710
|
-
if (!
|
|
15517
|
+
if (!existsSync20(operation.publicKey) || lstatSync4(operation.publicKey).size < 1) {
|
|
15518
|
+
if (!existsSync20(key)) {
|
|
14711
15519
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
|
|
14712
15520
|
if (decrypted.exitCode !== 0)
|
|
14713
15521
|
return decrypted;
|
|
@@ -14717,8 +15525,8 @@ var runProvisionOperation = async (operation) => {
|
|
|
14717
15525
|
if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
|
|
14718
15526
|
return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
|
|
14719
15527
|
}
|
|
14720
|
-
|
|
14721
|
-
|
|
15528
|
+
mkdirSync15(dirname11(operation.publicKey), { recursive: true, mode: 493 });
|
|
15529
|
+
writeFileSync15(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
|
|
14722
15530
|
`, { mode: 292 });
|
|
14723
15531
|
chmodSync16(operation.publicKey, 292);
|
|
14724
15532
|
}
|
|
@@ -14731,9 +15539,9 @@ var runProvisionOperation = async (operation) => {
|
|
|
14731
15539
|
}
|
|
14732
15540
|
}
|
|
14733
15541
|
if (operation.kind === "ensure-enrolment") {
|
|
14734
|
-
if (
|
|
15542
|
+
if (existsSync20(operation.state) && lstatSync4(operation.state).size > 0 || existsSync20(operation.credential) && lstatSync4(operation.credential).size > 0)
|
|
14735
15543
|
return { stdout: "", exitCode: 0 };
|
|
14736
|
-
if (!
|
|
15544
|
+
if (!existsSync20(operation.source))
|
|
14737
15545
|
return { stdout: "enrolment source is missing", exitCode: 1 };
|
|
14738
15546
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
|
|
14739
15547
|
if (result.exitCode === 0) {
|
|
@@ -14753,7 +15561,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
14753
15561
|
return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
|
|
14754
15562
|
}
|
|
14755
15563
|
if (operation.kind === "verify-file")
|
|
14756
|
-
return
|
|
15564
|
+
return existsSync20(operation.path) && lstatSync4(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
|
|
14757
15565
|
if (operation.kind === "verify-egress") {
|
|
14758
15566
|
const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
|
|
14759
15567
|
if (active.exitCode !== 0)
|
|
@@ -14781,11 +15589,11 @@ var runProvisionOperation = async (operation) => {
|
|
|
14781
15589
|
const response = await fetch("https://pkg.cloudflareclient.com/pubkey.gpg", { signal: AbortSignal.timeout(30000) });
|
|
14782
15590
|
if (!response.ok)
|
|
14783
15591
|
return { stdout: `WARP key HTTP ${response.status}`, exitCode: 1 };
|
|
14784
|
-
|
|
14785
|
-
|
|
14786
|
-
|
|
15592
|
+
mkdirSync15("/usr/share/keyrings", { recursive: true, mode: 493 });
|
|
15593
|
+
mkdirSync15("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
|
|
15594
|
+
mkdirSync15("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
|
|
14787
15595
|
const key = "/run/cloudflare-warp-key.gpg";
|
|
14788
|
-
|
|
15596
|
+
writeFileSync15(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
|
|
14789
15597
|
let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
|
|
14790
15598
|
rmSync10(key, { force: true });
|
|
14791
15599
|
if (result.exitCode !== 0)
|
|
@@ -14793,7 +15601,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
14793
15601
|
const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
|
|
14794
15602
|
if (!codename)
|
|
14795
15603
|
return { stdout: "Ubuntu codename missing", exitCode: 1 };
|
|
14796
|
-
|
|
15604
|
+
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
15605
|
`, { mode: 420 });
|
|
14798
15606
|
result = await fixed(["/usr/bin/apt-get", "update", "-qq"]);
|
|
14799
15607
|
return result.exitCode === 0 ? fixed(["/usr/bin/apt-get", "install", "-y", "cloudflare-warp"]) : result;
|
|
@@ -15095,8 +15903,7 @@ function localBootstrapHost() {
|
|
|
15095
15903
|
const execute3 = async (argv2, options = {}) => {
|
|
15096
15904
|
const child = Bun.spawn([...argv2], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
|
|
15097
15905
|
if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
|
|
15098
|
-
child.stdin
|
|
15099
|
-
child.stdin.end();
|
|
15906
|
+
await writeAndCloseProcessInput(child.stdin, options.stdin);
|
|
15100
15907
|
}
|
|
15101
15908
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
15102
15909
|
new Response(child.stdout).text(),
|
|
@@ -15107,16 +15914,16 @@ function localBootstrapHost() {
|
|
|
15107
15914
|
};
|
|
15108
15915
|
return {
|
|
15109
15916
|
uid: () => process.getuid?.() ?? -1,
|
|
15110
|
-
exists:
|
|
15917
|
+
exists: existsSync21,
|
|
15111
15918
|
read: (path2) => readFileSync17(path2, "utf8"),
|
|
15112
15919
|
write(path2, content, mode) {
|
|
15113
|
-
|
|
15920
|
+
mkdirSync16(dirname12(path2), { recursive: true, mode: 493 });
|
|
15114
15921
|
const temporary = `${path2}.next.${process.pid}`;
|
|
15115
|
-
|
|
15922
|
+
writeFileSync16(temporary, content, { mode });
|
|
15116
15923
|
chmodSync17(temporary, mode);
|
|
15117
|
-
|
|
15924
|
+
renameSync13(temporary, path2);
|
|
15118
15925
|
},
|
|
15119
|
-
mkdir: (path2, mode) =>
|
|
15926
|
+
mkdir: (path2, mode) => mkdirSync16(path2, { recursive: true, mode }),
|
|
15120
15927
|
remove: (path2) => rmSync11(path2, { force: true }),
|
|
15121
15928
|
inspect(path2) {
|
|
15122
15929
|
const value = lstatSync5(path2);
|
|
@@ -15140,9 +15947,9 @@ function localBootstrapHost() {
|
|
|
15140
15947
|
},
|
|
15141
15948
|
async installAgent(config, phase) {
|
|
15142
15949
|
const capabilities = await readCapabilities(localRunner);
|
|
15143
|
-
const hasBinding =
|
|
15144
|
-
const hasEnrolCredential =
|
|
15145
|
-
const initialBundle = config.kind === "platform" && !
|
|
15950
|
+
const hasBinding = existsSync21("/var/lib/forgezero/enrolment.json");
|
|
15951
|
+
const hasEnrolCredential = existsSync21(ENROL_CREDENTIAL);
|
|
15952
|
+
const initialBundle = config.kind === "platform" && !existsSync21(BOOTSTRAP_RELEASE_EVIDENCE) ? {
|
|
15146
15953
|
path: config.bootstrapBundle.bundleFile,
|
|
15147
15954
|
manifestPath: config.bootstrapBundle.manifestFile,
|
|
15148
15955
|
manifest: parseBootstrapBundleManifest(JSON.parse(readFileSync17(config.bootstrapBundle.manifestFile, "utf8")))
|
|
@@ -15158,8 +15965,8 @@ function localBootstrapHost() {
|
|
|
15158
15965
|
databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
|
|
15159
15966
|
databasePorts: [8529]
|
|
15160
15967
|
};
|
|
15161
|
-
|
|
15162
|
-
|
|
15968
|
+
mkdirSync16(dirname12(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
|
|
15969
|
+
writeFileSync16(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
|
|
15163
15970
|
`, { mode: 256 });
|
|
15164
15971
|
}
|
|
15165
15972
|
const plan = planBootstrapAgentInstall(config, phase, {
|
|
@@ -15169,8 +15976,8 @@ function localBootstrapHost() {
|
|
|
15169
15976
|
initialBundle
|
|
15170
15977
|
});
|
|
15171
15978
|
for (const unit3 of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
|
|
15172
|
-
|
|
15173
|
-
|
|
15979
|
+
mkdirSync16(dirname12(unit3.path), { recursive: true, mode: 493 });
|
|
15980
|
+
writeFileSync16(unit3.path, unit3.unit, { mode: 420 });
|
|
15174
15981
|
}
|
|
15175
15982
|
await applyPlan(plan, localRunner);
|
|
15176
15983
|
return plan;
|
|
@@ -15222,10 +16029,10 @@ async function verifyPlatformFleetHost(expectedVersion, runtime = localRuntime()
|
|
|
15222
16029
|
}
|
|
15223
16030
|
|
|
15224
16031
|
// src/community-rehearsal-host.ts
|
|
15225
|
-
import { createHash as
|
|
15226
|
-
import { lstatSync as lstatSync7, mkdirSync as
|
|
16032
|
+
import { createHash as createHash12 } from "crypto";
|
|
16033
|
+
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
16034
|
import { isIP as isIP5 } from "net";
|
|
15228
|
-
import { dirname as
|
|
16035
|
+
import { dirname as dirname13 } from "path";
|
|
15229
16036
|
var COMMUNITY_REHEARSAL_VERSION = "3.11.14";
|
|
15230
16037
|
var COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
|
|
15231
16038
|
var COMMUNITY_DATABASE_ROOT = "/var/lib/forgezero-rehearsal-cluster";
|
|
@@ -15427,10 +16234,8 @@ function planCommunityRehearsalCleanup() {
|
|
|
15427
16234
|
}
|
|
15428
16235
|
async function exec(argv2, stdin) {
|
|
15429
16236
|
const child = Bun.spawn([...argv2], { stdin: stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
|
|
15430
|
-
if (stdin !== undefined && child.stdin && typeof child.stdin !== "number")
|
|
15431
|
-
child.stdin
|
|
15432
|
-
child.stdin.end();
|
|
15433
|
-
}
|
|
16237
|
+
if (stdin !== undefined && child.stdin && typeof child.stdin !== "number")
|
|
16238
|
+
await writeAndCloseProcessInput(child.stdin, stdin);
|
|
15434
16239
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
15435
16240
|
new Response(child.stdout).text(),
|
|
15436
16241
|
new Response(child.stderr).text(),
|
|
@@ -15443,8 +16248,8 @@ async function applyOperations(operations) {
|
|
|
15443
16248
|
if (operation.kind === "remove-tree")
|
|
15444
16249
|
rmSync12(operation.path, { recursive: true, force: true });
|
|
15445
16250
|
else if (operation.kind === "write") {
|
|
15446
|
-
|
|
15447
|
-
|
|
16251
|
+
mkdirSync17(dirname13(operation.path), { recursive: true });
|
|
16252
|
+
writeFileSync17(operation.path, operation.content, { mode: operation.mode });
|
|
15448
16253
|
} else if (operation.kind === "unlink") {
|
|
15449
16254
|
try {
|
|
15450
16255
|
unlinkSync12(operation.path);
|
|
@@ -15496,7 +16301,7 @@ async function runCommunityRehearsalHost(request) {
|
|
|
15496
16301
|
}
|
|
15497
16302
|
if (request.action === "prepare") {
|
|
15498
16303
|
const metadata = lstatSync7(request.archivePath);
|
|
15499
|
-
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > 64 * 1024 * 1024 ||
|
|
16304
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > 64 * 1024 * 1024 || createHash12("sha256").update(readFileSync18(request.archivePath)).digest("hex") !== request.archiveSha256) {
|
|
15500
16305
|
throw new Error("community rehearsal archive is not the declared bounded release");
|
|
15501
16306
|
}
|
|
15502
16307
|
await ensureCommunityRehearsalArango();
|
|
@@ -15590,23 +16395,23 @@ async function runSupervisedApp(path2) {
|
|
|
15590
16395
|
}
|
|
15591
16396
|
|
|
15592
16397
|
// src/index.ts
|
|
15593
|
-
function agentCommandArgs(args,
|
|
15594
|
-
const index = args.indexOf(
|
|
16398
|
+
function agentCommandArgs(args, command3) {
|
|
16399
|
+
const index = args.indexOf(command3);
|
|
15595
16400
|
if (index < 0)
|
|
15596
|
-
throw new Error(`missing Agent command ${
|
|
16401
|
+
throw new Error(`missing Agent command ${command3}`);
|
|
15597
16402
|
return args.slice(index + 1);
|
|
15598
16403
|
}
|
|
15599
16404
|
function loadOrCreateSeed(path2) {
|
|
15600
|
-
if (
|
|
16405
|
+
if (existsSync22(path2)) {
|
|
15601
16406
|
const seed2 = new Uint8Array(Buffer.from(readFileSync20(path2, "utf8").trim(), "base64url"));
|
|
15602
16407
|
if (seed2.length < 32) {
|
|
15603
16408
|
throw new Error(`agent: the seed at ${path2} is too short to derive a key from.`);
|
|
15604
16409
|
}
|
|
15605
16410
|
return seed2;
|
|
15606
16411
|
}
|
|
15607
|
-
|
|
16412
|
+
mkdirSync18(dirname14(path2), { recursive: true });
|
|
15608
16413
|
const seed = new Uint8Array(randomBytes7(32));
|
|
15609
|
-
|
|
16414
|
+
writeFileSync18(path2, Buffer.from(seed).toString("base64url"), { mode: 384 });
|
|
15610
16415
|
chmodSync18(path2, 384);
|
|
15611
16416
|
return seed;
|
|
15612
16417
|
}
|
|
@@ -15636,7 +16441,7 @@ function loadSeedCredential(name = DEFAULT_SEED_CREDENTIAL, directory = process.
|
|
|
15636
16441
|
if (!directory)
|
|
15637
16442
|
throw new Error("agent: CREDENTIALS_DIRECTORY is missing; systemd did not load the node identity.");
|
|
15638
16443
|
const path2 = `${directory}/${name}`;
|
|
15639
|
-
if (!
|
|
16444
|
+
if (!existsSync22(path2))
|
|
15640
16445
|
throw new Error(`agent: the systemd credential ${name} is missing at ${path2}.`);
|
|
15641
16446
|
const seed = new Uint8Array(Buffer.from(readFileSync20(path2, "utf8").trim(), "base64url"));
|
|
15642
16447
|
if (seed.length < 32)
|
|
@@ -15658,7 +16463,7 @@ function createSystemdDeploymentSecrets(names, directory = process.env.CREDENTIA
|
|
|
15658
16463
|
if (allowed.size === 0)
|
|
15659
16464
|
return;
|
|
15660
16465
|
for (const name of allowed) {
|
|
15661
|
-
if (!/^[A-
|
|
16466
|
+
if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/.test(name))
|
|
15662
16467
|
throw new Error(`agent: invalid pipeline credential name ${name}.`);
|
|
15663
16468
|
}
|
|
15664
16469
|
return {
|
|
@@ -15764,8 +16569,8 @@ if (import.meta.main) {
|
|
|
15764
16569
|
console.log(VERSION2);
|
|
15765
16570
|
process.exit(0);
|
|
15766
16571
|
}
|
|
15767
|
-
const
|
|
15768
|
-
if (
|
|
16572
|
+
const command3 = args.find((arg) => !arg.startsWith("-"));
|
|
16573
|
+
if (command3 === "platform-activate") {
|
|
15769
16574
|
if (process.getuid?.() !== 0)
|
|
15770
16575
|
throw new Error("platform-activate must run as root");
|
|
15771
16576
|
const configPath = args.find((arg) => arg.startsWith("--config="))?.slice("--config=".length);
|
|
@@ -15778,14 +16583,14 @@ if (import.meta.main) {
|
|
|
15778
16583
|
console.log(`promoted ${result.release} on ${result.slot}`);
|
|
15779
16584
|
process.exit(0);
|
|
15780
16585
|
}
|
|
15781
|
-
if (
|
|
16586
|
+
if (command3 === "recovery-host") {
|
|
15782
16587
|
if (process.getuid?.() !== 0)
|
|
15783
16588
|
throw new Error("recovery-host must run as root");
|
|
15784
16589
|
const commandIndex = args.indexOf("recovery-host");
|
|
15785
16590
|
await runRecoveryHost(args.slice(commandIndex + 1));
|
|
15786
16591
|
process.exit(0);
|
|
15787
16592
|
}
|
|
15788
|
-
if (
|
|
16593
|
+
if (command3 === "pipeline-todo") {
|
|
15789
16594
|
const index = args.indexOf("pipeline-todo");
|
|
15790
16595
|
const message = args[index + 1];
|
|
15791
16596
|
if (!message || args.length !== index + 2 || message.length > 2000 || /[\r\n\0]/.test(message)) {
|
|
@@ -15794,7 +16599,7 @@ if (import.meta.main) {
|
|
|
15794
16599
|
console.error(message);
|
|
15795
16600
|
process.exit(78);
|
|
15796
16601
|
}
|
|
15797
|
-
if (
|
|
16602
|
+
if (command3 === "project-release-check") {
|
|
15798
16603
|
if (args.length !== 1)
|
|
15799
16604
|
throw new Error("project-release-check accepts no coordinates");
|
|
15800
16605
|
for (const relative of ["src/index.ts", "bun.lock", "src/generated/shared/contract-manifest.json"]) {
|
|
@@ -15813,7 +16618,7 @@ if (import.meta.main) {
|
|
|
15813
16618
|
process.exit(0);
|
|
15814
16619
|
}
|
|
15815
16620
|
const allowFileSeed = process.env.FZ_ALLOW_FILE_SEED === "1";
|
|
15816
|
-
if (
|
|
16621
|
+
if (command3 === "identity") {
|
|
15817
16622
|
const seed = configuredAgentSeed({
|
|
15818
16623
|
credential: process.env.FZ_SEED_CREDENTIAL,
|
|
15819
16624
|
path: process.env.FZ_SEED_PATH,
|
|
@@ -15827,7 +16632,7 @@ if (import.meta.main) {
|
|
|
15827
16632
|
}, null, 2));
|
|
15828
16633
|
process.exit(0);
|
|
15829
16634
|
}
|
|
15830
|
-
if (
|
|
16635
|
+
if (command3 === "enrol") {
|
|
15831
16636
|
if (!process.env.FZ_API)
|
|
15832
16637
|
throw new Error("agent enrolment requires FZ_API");
|
|
15833
16638
|
if (!process.env.FZ_ENROL_TOKEN_CREDENTIAL) {
|
|
@@ -15856,7 +16661,7 @@ if (import.meta.main) {
|
|
|
15856
16661
|
console.log(`[agent] enrolled ${binding2.computeReference} in project ${binding2.projectKey}/${binding2.environmentKey}`);
|
|
15857
16662
|
process.exit(0);
|
|
15858
16663
|
}
|
|
15859
|
-
if (
|
|
16664
|
+
if (command3 === "egress-policy" || command3 === "egress-policy-check") {
|
|
15860
16665
|
const users = args.filter((arg) => arg.startsWith("--user=")).map((arg) => arg.slice("--user=".length));
|
|
15861
16666
|
if (users.length < 1)
|
|
15862
16667
|
throw new Error("egress-policy requires --user=<service-user>");
|
|
@@ -15875,7 +16680,7 @@ if (import.meta.main) {
|
|
|
15875
16680
|
tcpPorts: loopbackTcpPorts,
|
|
15876
16681
|
publicTcpPorts: publicTcpPorts.length > 0 ? publicTcpPorts : undefined
|
|
15877
16682
|
} : undefined;
|
|
15878
|
-
if (
|
|
16683
|
+
if (command3 === "egress-policy-check") {
|
|
15879
16684
|
const uids = users.map((user) => resolveServiceUid(user));
|
|
15880
16685
|
const grant = loopback ? {
|
|
15881
16686
|
uid: resolveServiceUid(loopback.user),
|
|
@@ -15888,7 +16693,7 @@ if (import.meta.main) {
|
|
|
15888
16693
|
}
|
|
15889
16694
|
await superviseAgentEgressPolicy({ users, loopback });
|
|
15890
16695
|
}
|
|
15891
|
-
if (
|
|
16696
|
+
if (command3 === "metal-helper") {
|
|
15892
16697
|
const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
15893
16698
|
if (!profilePath)
|
|
15894
16699
|
throw new Error("metal-helper requires --profile=/absolute/path.json");
|
|
@@ -15910,7 +16715,7 @@ if (import.meta.main) {
|
|
|
15910
16715
|
process.on("SIGINT", () => void stop());
|
|
15911
16716
|
await new Promise(() => {});
|
|
15912
16717
|
}
|
|
15913
|
-
if (
|
|
16718
|
+
if (command3 === "lifecycle-helper") {
|
|
15914
16719
|
const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
15915
16720
|
if (!profilePath?.startsWith("/"))
|
|
15916
16721
|
throw new Error("lifecycle-helper requires --profile=/absolute/path.json");
|
|
@@ -15934,7 +16739,7 @@ if (import.meta.main) {
|
|
|
15934
16739
|
process.on("SIGINT", () => void stop());
|
|
15935
16740
|
await new Promise(() => {});
|
|
15936
16741
|
}
|
|
15937
|
-
if (
|
|
16742
|
+
if (command3 === "warp-config") {
|
|
15938
16743
|
const organization = args.find((arg) => arg.startsWith("--organization="))?.slice("--organization=".length);
|
|
15939
16744
|
if (!organization)
|
|
15940
16745
|
throw new Error("warp-config requires --organization=<team-name>");
|
|
@@ -15952,7 +16757,7 @@ if (import.meta.main) {
|
|
|
15952
16757
|
console.log("[warp-config] enrollment materialized in tmpfs");
|
|
15953
16758
|
process.exit(0);
|
|
15954
16759
|
}
|
|
15955
|
-
if (
|
|
16760
|
+
if (command3 === "mesh-config") {
|
|
15956
16761
|
if (typeof process.getuid !== "function" || process.getuid() !== 0)
|
|
15957
16762
|
throw new Error("mesh-config must run as root");
|
|
15958
16763
|
const token = readMeshConnectorCredential();
|
|
@@ -15975,7 +16780,7 @@ if (import.meta.main) {
|
|
|
15975
16780
|
console.log("[mesh-config] Cloudflare Mesh connector is connected");
|
|
15976
16781
|
process.exit(0);
|
|
15977
16782
|
}
|
|
15978
|
-
if (
|
|
16783
|
+
if (command3 === "update-helper") {
|
|
15979
16784
|
if (typeof process.getuid !== "function" || process.getuid() !== 0) {
|
|
15980
16785
|
throw new Error("update-helper must run as root");
|
|
15981
16786
|
}
|
|
@@ -15994,7 +16799,7 @@ if (import.meta.main) {
|
|
|
15994
16799
|
process.on("SIGINT", () => void stop());
|
|
15995
16800
|
await new Promise(() => {});
|
|
15996
16801
|
}
|
|
15997
|
-
if (
|
|
16802
|
+
if (command3 === "software-helper") {
|
|
15998
16803
|
if (typeof process.getuid !== "function" || process.getuid() !== 0) {
|
|
15999
16804
|
throw new Error("software-helper must run as root");
|
|
16000
16805
|
}
|
|
@@ -16016,13 +16821,13 @@ if (import.meta.main) {
|
|
|
16016
16821
|
process.on("SIGINT", () => void stop());
|
|
16017
16822
|
await new Promise(() => {});
|
|
16018
16823
|
}
|
|
16019
|
-
if (
|
|
16824
|
+
if (command3 === "supervised-app") {
|
|
16020
16825
|
const config = args.find((arg) => arg.startsWith("--config="))?.slice("--config=".length);
|
|
16021
16826
|
if (!config)
|
|
16022
16827
|
throw new Error("supervised-app requires --config=<absolute-path>");
|
|
16023
16828
|
process.exit(await runSupervisedApp(config));
|
|
16024
16829
|
}
|
|
16025
|
-
if (
|
|
16830
|
+
if (command3 === "software-ensure") {
|
|
16026
16831
|
const requirements = args.filter((arg) => arg.startsWith("--require=")).map((arg) => {
|
|
16027
16832
|
const coordinate = arg.slice("--require=".length);
|
|
16028
16833
|
const separator = coordinate.indexOf("@");
|
|
@@ -16038,7 +16843,7 @@ if (import.meta.main) {
|
|
|
16038
16843
|
console.log(JSON.stringify({ ok: true, results }));
|
|
16039
16844
|
process.exit(0);
|
|
16040
16845
|
}
|
|
16041
|
-
if (
|
|
16846
|
+
if (command3 === "metal-isolation") {
|
|
16042
16847
|
const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
16043
16848
|
if (!profilePath)
|
|
16044
16849
|
throw new Error("metal-isolation requires --profile=/absolute/path.json");
|
|
@@ -16047,7 +16852,7 @@ if (import.meta.main) {
|
|
|
16047
16852
|
console.log("[metal-isolation] host and guest cgroup boundaries active");
|
|
16048
16853
|
process.exit(0);
|
|
16049
16854
|
}
|
|
16050
|
-
if (
|
|
16855
|
+
if (command3 === "deploy-runner") {
|
|
16051
16856
|
const root2 = args.find((arg) => arg.startsWith("--root="))?.slice("--root=".length);
|
|
16052
16857
|
const home = args.find((arg) => arg.startsWith("--home="))?.slice("--home=".length);
|
|
16053
16858
|
if (!root2 || !home)
|
|
@@ -16077,7 +16882,7 @@ if (import.meta.main) {
|
|
|
16077
16882
|
process.on("SIGINT", () => void stop());
|
|
16078
16883
|
await new Promise(() => {});
|
|
16079
16884
|
}
|
|
16080
|
-
if (
|
|
16885
|
+
if (command3 === "metal-apply") {
|
|
16081
16886
|
const claimArg = args.find((arg) => arg.startsWith("--claim="))?.slice("--claim=".length);
|
|
16082
16887
|
if (!claimArg)
|
|
16083
16888
|
throw new Error("metal-apply requires --claim=- or --claim=/absolute/path.json");
|
|
@@ -16098,8 +16903,8 @@ if (import.meta.main) {
|
|
|
16098
16903
|
console.log(JSON.stringify({ ok: true, computeKey: claim.computeKey, ...result }));
|
|
16099
16904
|
process.exit(0);
|
|
16100
16905
|
}
|
|
16101
|
-
if (
|
|
16102
|
-
const commandArgs = agentCommandArgs(args,
|
|
16906
|
+
if (command3 === "platform-fleet-verify") {
|
|
16907
|
+
const commandArgs = agentCommandArgs(args, command3);
|
|
16103
16908
|
const expectedVersion = commandArgs.find((arg) => arg.startsWith("--expected-version="))?.slice("--expected-version=".length);
|
|
16104
16909
|
if (!expectedVersion || commandArgs.length !== 1) {
|
|
16105
16910
|
throw new Error("platform-fleet-verify requires exactly --expected-version=<semver>");
|
|
@@ -16107,8 +16912,8 @@ if (import.meta.main) {
|
|
|
16107
16912
|
console.log(JSON.stringify(await verifyPlatformFleetHost(expectedVersion)));
|
|
16108
16913
|
process.exit(0);
|
|
16109
16914
|
}
|
|
16110
|
-
if (
|
|
16111
|
-
const commandArgs = agentCommandArgs(args,
|
|
16915
|
+
if (command3 === "community-rehearsal") {
|
|
16916
|
+
const commandArgs = agentCommandArgs(args, command3);
|
|
16112
16917
|
if (commandArgs.length !== 1 || commandArgs[0] !== "--request=-") {
|
|
16113
16918
|
throw new Error("community-rehearsal requires exactly --request=-");
|
|
16114
16919
|
}
|
|
@@ -16119,7 +16924,7 @@ if (import.meta.main) {
|
|
|
16119
16924
|
console.log(JSON.stringify(await runCommunityRehearsalHost(request)));
|
|
16120
16925
|
process.exit(0);
|
|
16121
16926
|
}
|
|
16122
|
-
if (
|
|
16927
|
+
if (command3 === "metal-admission-proof") {
|
|
16123
16928
|
if (args.length !== 1 || args[0] !== "--request=-") {
|
|
16124
16929
|
throw new Error("metal-admission-proof requires exactly --request=-");
|
|
16125
16930
|
}
|
|
@@ -16133,17 +16938,17 @@ if (import.meta.main) {
|
|
|
16133
16938
|
const seed = configuredAgentSeed({ credential: process.env.FZ_SEED_CREDENTIAL, allowFileSeed: false });
|
|
16134
16939
|
const keys2 = deriveKeysFromSeed(seed);
|
|
16135
16940
|
seed.fill(0);
|
|
16136
|
-
const filesystem =
|
|
16941
|
+
const filesystem = statfsSync3("/var/lib/forgezero");
|
|
16137
16942
|
const body = {
|
|
16138
16943
|
...request,
|
|
16139
16944
|
publicKeys: { ed25519: keys2.ed25519.publicKey, mlDsa: keys2.mlDsa.publicKey },
|
|
16140
16945
|
preflight: {
|
|
16141
|
-
vcpu:
|
|
16142
|
-
memoryGib: Math.max(1, Math.floor(
|
|
16946
|
+
vcpu: cpus2().length,
|
|
16947
|
+
memoryGib: Math.max(1, Math.floor(totalmem2() / 1024 ** 3)),
|
|
16143
16948
|
diskGib: Math.max(1, Math.floor(Number(filesystem.blocks) * Number(filesystem.bsize) / 1024 ** 3)),
|
|
16144
|
-
kvm:
|
|
16145
|
-
snpHost:
|
|
16146
|
-
helper:
|
|
16949
|
+
kvm: existsSync22("/dev/kvm"),
|
|
16950
|
+
snpHost: existsSync22("/dev/sev"),
|
|
16951
|
+
helper: existsSync22(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
|
|
16147
16952
|
}
|
|
16148
16953
|
};
|
|
16149
16954
|
const envelope = signRequest(keys2, keys2.ed25519.publicKey, {
|
|
@@ -16178,9 +16983,9 @@ if (import.meta.main) {
|
|
|
16178
16983
|
metalHostname: process.env.FZ_METAL_HOSTNAME,
|
|
16179
16984
|
run: (claim) => requestMetalProvision(claim, process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET),
|
|
16180
16985
|
metalPreflight: () => ({
|
|
16181
|
-
snpHost:
|
|
16182
|
-
kvm:
|
|
16183
|
-
helper:
|
|
16986
|
+
snpHost: existsSync22("/dev/sev"),
|
|
16987
|
+
kvm: existsSync22("/dev/kvm"),
|
|
16988
|
+
helper: existsSync22(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
|
|
16184
16989
|
}),
|
|
16185
16990
|
onEvent: (event, detail) => console.log(`[metal-agent] ${event}${detail ? ` ${JSON.stringify(detail)}` : ""}`)
|
|
16186
16991
|
});
|
|
@@ -16235,22 +17040,22 @@ if (import.meta.main) {
|
|
|
16235
17040
|
process.on("SIGINT", () => void stop("SIGINT"));
|
|
16236
17041
|
await new Promise(() => {});
|
|
16237
17042
|
}
|
|
16238
|
-
if (
|
|
17043
|
+
if (command3 && ["deploy", "status", "pause", "resume", "pause-key", "resume-key", "stop-key", "start-key", "cancel"].includes(command3)) {
|
|
16239
17044
|
const revisionArg = args.find((arg) => arg.startsWith("--revision="));
|
|
16240
17045
|
const socketArg = args.find((arg) => arg.startsWith("--control-socket="));
|
|
16241
17046
|
const keyArg = args.find((arg) => arg.startsWith("--key="));
|
|
16242
17047
|
const idArg = args.find((arg) => arg.startsWith("--id="));
|
|
16243
17048
|
const socketPath = socketArg?.slice("--control-socket=".length) ?? process.env.FZ_CONTROL_SOCKET ?? DEFAULT_CONTROL_SOCKET;
|
|
16244
|
-
const request =
|
|
17049
|
+
const request = command3 === "deploy" ? {
|
|
16245
17050
|
op: "deploy",
|
|
16246
17051
|
request: {
|
|
16247
17052
|
revision: revisionArg?.slice("--revision=".length),
|
|
16248
17053
|
releaseExecutor: args.includes("--release-executor")
|
|
16249
17054
|
}
|
|
16250
|
-
} :
|
|
16251
|
-
op:
|
|
17055
|
+
} : command3 === "cancel" ? { op: "cancel", id: idArg?.slice("--id=".length) ?? "" } : ["pause-key", "resume-key", "stop-key", "start-key"].includes(command3) ? {
|
|
17056
|
+
op: command3,
|
|
16252
17057
|
key: keyArg?.slice("--key=".length) ?? ""
|
|
16253
|
-
} : { op:
|
|
17058
|
+
} : { op: command3 };
|
|
16254
17059
|
try {
|
|
16255
17060
|
const response = await requestControl(request, socketPath);
|
|
16256
17061
|
console.log(JSON.stringify(response, null, 2));
|
|
@@ -16263,7 +17068,7 @@ if (import.meta.main) {
|
|
|
16263
17068
|
const telemetry = new AgentTelemetryRuntime(createAgentTelemetry(resolveAgentTelemetryConfig()));
|
|
16264
17069
|
telemetry.event("agent.started");
|
|
16265
17070
|
telemetry.setDraining(false);
|
|
16266
|
-
const attestationSource =
|
|
17071
|
+
const attestationSource = existsSync22("/dev/sev-guest") ? createSnpAttestationSource() : undefined;
|
|
16267
17072
|
const running = runAgent({
|
|
16268
17073
|
socketPath: process.env.FZ_SOCKET_PATH ?? DEFAULT_SOCKET_PATH,
|
|
16269
17074
|
seedCredential: process.env.FZ_SEED_CREDENTIAL,
|
|
@@ -16278,8 +17083,8 @@ if (import.meta.main) {
|
|
|
16278
17083
|
const enrolmentStatePath = process.env.FZ_ENROL_STATE_FILE ?? DEFAULT_ENROLMENT_STATE_PATH;
|
|
16279
17084
|
let binding = loadGuestBinding(enrolmentStatePath, nodeKey);
|
|
16280
17085
|
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 &&
|
|
17086
|
+
const enrolmentCredentialAvailable = Boolean(enrolmentCredential && process.env.CREDENTIALS_DIRECTORY && existsSync22(`${process.env.CREDENTIALS_DIRECTORY}/${enrolmentCredential}`));
|
|
17087
|
+
const legacyEnrolmentFileAvailable = Boolean(process.env.FZ_ENROL_TOKEN_FILE && existsSync22(process.env.FZ_ENROL_TOKEN_FILE));
|
|
16283
17088
|
if (!binding && (enrolmentCredentialAvailable || legacyEnrolmentFileAvailable) && process.env.FZ_API) {
|
|
16284
17089
|
binding = await enrolGuestIdentity({
|
|
16285
17090
|
apiUrl: process.env.FZ_API,
|
|
@@ -16357,7 +17162,7 @@ if (import.meta.main) {
|
|
|
16357
17162
|
const bootstrapEnabled = process.env.FZ_BOOTSTRAP_PULL === "true";
|
|
16358
17163
|
const bootstrapCredential = process.env.FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL;
|
|
16359
17164
|
const bootstrapKeyPath = bootstrapCredential && process.env.CREDENTIALS_DIRECTORY ? `${process.env.CREDENTIALS_DIRECTORY}/${bootstrapCredential}` : undefined;
|
|
16360
|
-
if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !
|
|
17165
|
+
if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !existsSync22(bootstrapKeyPath) || !process.env.FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT)) {
|
|
16361
17166
|
throw new Error("agent: bootstrap runner requires enrolment, API, local SSH credential and target OTLP coordinate");
|
|
16362
17167
|
}
|
|
16363
17168
|
const bootstrapPull = bootstrapEnabled ? startSshBootstrapPull({
|
|
@@ -16433,6 +17238,7 @@ if (import.meta.main) {
|
|
|
16433
17238
|
activateService: (request) => requestServiceActivation(request, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
16434
17239
|
buildContainer: (request) => requestContainerBuild(request, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
16435
17240
|
activateContainer: (request) => requestContainerActivation(request, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
17241
|
+
applyConnectivity: (request) => requestDeploymentConnectivity(request, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
16436
17242
|
projectExec: process.env.FZ_DEPLOY_RUNNER_SOCKET ? (input) => requestDeploymentCommand(input, process.env.FZ_DEPLOY_RUNNER_SOCKET) : undefined
|
|
16437
17243
|
});
|
|
16438
17244
|
const staticManager = repository && branch && profile ? createDeploymentManager(managerOptions({ repository, branch, profile }, process.env.FZ_DEPLOY_KEY ?? `${repository}:${branch}:${profile}`)) : bootstrapBundle && branch && profile ? createDeploymentManager({
|
|
@@ -16650,6 +17456,8 @@ export {
|
|
|
16650
17456
|
selectAgentRelease,
|
|
16651
17457
|
runAgent,
|
|
16652
17458
|
restoreAgentRelease,
|
|
17459
|
+
resolveDeploymentTargets,
|
|
17460
|
+
resolveDeploymentInputs,
|
|
16653
17461
|
resolveAgentTelemetryConfig,
|
|
16654
17462
|
requestSoftware,
|
|
16655
17463
|
requestMetalProvision,
|
|
@@ -16727,6 +17535,7 @@ export {
|
|
|
16727
17535
|
OS_CATALOG,
|
|
16728
17536
|
METAL_SYSTEMD_CREDENTIALS,
|
|
16729
17537
|
MAX_AGENT_TARBALL_BYTES,
|
|
17538
|
+
DeploymentInputError,
|
|
16730
17539
|
DeploymentError,
|
|
16731
17540
|
DEFAULT_SOFTWARE_HELPER_SOCKET,
|
|
16732
17541
|
DEFAULT_SOCKET_PATH,
|