@forgezero/agent 0.1.53 → 0.1.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -9
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.js +110 -69
- package/dist/community-rehearsal-host.d.ts +7 -1
- package/dist/community-rehearsal-host.js +64 -6
- package/dist/container-supervisor.d.ts +36 -0
- package/dist/definition.js +37 -5
- package/dist/deploy-actions.d.ts +87 -0
- package/dist/deploy-compiler.d.ts +26 -0
- package/dist/deploy-compiler.js +1050 -0
- package/dist/deploy-file.js +44 -12
- package/dist/deploy-plan-runner.d.ts +65 -0
- package/dist/deploy-plan-runner.js +1205 -0
- package/dist/deploy-plan.d.ts +41 -0
- package/dist/deploy-plan.js +925 -0
- package/dist/deploy-providers.d.ts +85 -0
- package/dist/deploy.d.ts +439 -0
- package/dist/deploy.js +169 -0
- package/dist/deployment.d.ts +5 -0
- package/dist/fz-agent.js +2137 -326
- package/dist/fz.js +1492 -320
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.d.ts +1 -1
- package/dist/operator-bootstrap.js +212 -130
- package/dist/platform-bootstrap-runtime.js +1 -1
- package/dist/platform-fleet-verification.js +499 -120
- package/dist/platform-genesis.d.ts +9 -0
- package/dist/provision.js +399 -20
- package/dist/software-helper.d.ts +5 -0
- package/dist/software-helper.js +399 -18
- package/dist/software.d.ts +1 -1
- package/dist/software.js +37 -5
- package/dist/version.d.ts +1 -1
- package/package.json +17 -1
package/dist/fz-agent.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
import { randomBytes as randomBytes6 } from "crypto";
|
|
6
|
-
import { readFileSync as
|
|
6
|
+
import { readFileSync as readFileSync19, writeFileSync as writeFileSync16, existsSync as existsSync20, mkdirSync as mkdirSync16, chmodSync as chmodSync17, lstatSync as lstatSync8, statfsSync as statfsSync2 } from "fs";
|
|
7
7
|
import { cpus, totalmem } from "os";
|
|
8
|
-
import { dirname as
|
|
8
|
+
import { dirname as dirname12, 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"));
|
|
@@ -4641,8 +4641,8 @@ function safeOp(line) {
|
|
|
4641
4641
|
}
|
|
4642
4642
|
|
|
4643
4643
|
// src/deployment.ts
|
|
4644
|
-
import { chmodSync as chmodSync3, existsSync as
|
|
4645
|
-
import { createHash as
|
|
4644
|
+
import { chmodSync as chmodSync3, existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, statfsSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4645
|
+
import { createHash as createHash4, randomUUID } from "crypto";
|
|
4646
4646
|
import { dirname, isAbsolute as isAbsolute2, join as join2 } from "path";
|
|
4647
4647
|
|
|
4648
4648
|
// ../runtime/dist/queue.js
|
|
@@ -4953,6 +4953,7 @@ import {
|
|
|
4953
4953
|
accessSync,
|
|
4954
4954
|
chmodSync as chmodSync2,
|
|
4955
4955
|
copyFileSync,
|
|
4956
|
+
existsSync as existsSync2,
|
|
4956
4957
|
mkdtempSync,
|
|
4957
4958
|
mkdirSync,
|
|
4958
4959
|
readFileSync,
|
|
@@ -4973,6 +4974,7 @@ var OS_CATALOG = [
|
|
|
4973
4974
|
];
|
|
4974
4975
|
var SOFTWARE_CATALOG = [
|
|
4975
4976
|
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4977
|
+
{ id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4976
4978
|
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4977
4979
|
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4978
4980
|
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
@@ -4982,6 +4984,7 @@ var SOFTWARE_CATALOG = [
|
|
|
4982
4984
|
];
|
|
4983
4985
|
var UBUNTU_2604_X64 = [
|
|
4984
4986
|
{ requirement: { id: "bun", version: "1.3.14" } },
|
|
4987
|
+
{ requirement: { id: "docker", version: "ubuntu-26.04" } },
|
|
4985
4988
|
{ requirement: { id: "nginx", version: "ubuntu-26.04" } },
|
|
4986
4989
|
{ requirement: { id: "arangodb", version: "3.11.14" } },
|
|
4987
4990
|
{ requirement: { id: "cloudflared", version: "2026.7.3" } },
|
|
@@ -4990,6 +4993,15 @@ var UBUNTU_2604_X64 = [
|
|
|
4990
4993
|
{ requirement: { id: "openssh-client", version: "ubuntu-26.04" } }
|
|
4991
4994
|
];
|
|
4992
4995
|
var path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
4996
|
+
var DOCKER_DAEMON_PATH = "/etc/docker/daemon.json";
|
|
4997
|
+
var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
4998
|
+
"data-root": "/var/lib/docker",
|
|
4999
|
+
"storage-driver": "overlay2",
|
|
5000
|
+
"live-restore": true,
|
|
5001
|
+
"log-driver": "local",
|
|
5002
|
+
"log-opts": { "max-size": "10m", "max-file": "3" }
|
|
5003
|
+
}, null, 2)}
|
|
5004
|
+
`;
|
|
4993
5005
|
var softwareSpawnFailure = (cause, argv) => cause.code === "ENOENT" ? { exitCode: 127, output: `executable is absent: ${argv[0]}` } : undefined;
|
|
4994
5006
|
var runSoftwareCommand = async (argv, env = {}) => {
|
|
4995
5007
|
let child;
|
|
@@ -5032,6 +5044,15 @@ async function executeSoftwareOperation(operation) {
|
|
|
5032
5044
|
if (operation.kind === "check") {
|
|
5033
5045
|
if (software === "bun")
|
|
5034
5046
|
return run(["/usr/local/bin/bun", "--version"]).then((r) => ({ ...r, exitCode: successful(r, /^1\.3\.14\s*$/m) ? 0 : 1 }));
|
|
5047
|
+
if (software === "docker") {
|
|
5048
|
+
const binary = await run(["/usr/bin/docker", "--version"]);
|
|
5049
|
+
if (!successful(binary, /Docker version/))
|
|
5050
|
+
return { ...binary, exitCode: 1 };
|
|
5051
|
+
if (!existsSync2(DOCKER_DAEMON_PATH) || readFileSync(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG)
|
|
5052
|
+
return { exitCode: 1, output: "Docker daemon policy is absent or differs from the reviewed ForgeZero policy" };
|
|
5053
|
+
const active = await run(["/usr/bin/systemctl", "is-active", "--quiet", "docker.service"]);
|
|
5054
|
+
return active.exitCode === 0 ? { exitCode: 0, output: binary.output } : active;
|
|
5055
|
+
}
|
|
5035
5056
|
if (software === "nginx") {
|
|
5036
5057
|
const binary = await run(["/usr/sbin/nginx", "-v"]);
|
|
5037
5058
|
return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
|
|
@@ -5064,11 +5085,22 @@ async function executeSoftwareOperation(operation) {
|
|
|
5064
5085
|
return { exitCode: 1, output: cause instanceof Error ? cause.message : String(cause) };
|
|
5065
5086
|
}
|
|
5066
5087
|
}
|
|
5067
|
-
if (software === "nginx" || software === "ufw" || software === "openssh-client") {
|
|
5068
|
-
const
|
|
5069
|
-
|
|
5088
|
+
if (software === "docker" || software === "nginx" || software === "ufw" || software === "openssh-client") {
|
|
5089
|
+
const packageName = software === "openssh-client" ? "openssh-client" : software === "docker" ? "docker.io" : software;
|
|
5090
|
+
const installed = await aptInstall(packageName);
|
|
5091
|
+
if (installed.exitCode !== 0 || software !== "nginx" && software !== "docker")
|
|
5070
5092
|
return installed;
|
|
5071
|
-
|
|
5093
|
+
if (software === "docker") {
|
|
5094
|
+
mkdirSync("/etc/docker", { recursive: true, mode: 493 });
|
|
5095
|
+
if (existsSync2(DOCKER_DAEMON_PATH) && readFileSync(DOCKER_DAEMON_PATH, "utf8") !== DOCKER_DAEMON_CONFIG) {
|
|
5096
|
+
return { exitCode: 2, output: "refusing to overwrite an existing Docker daemon configuration that differs from the reviewed ForgeZero policy" };
|
|
5097
|
+
}
|
|
5098
|
+
if (!existsSync2(DOCKER_DAEMON_PATH))
|
|
5099
|
+
writeFileSync(DOCKER_DAEMON_PATH, DOCKER_DAEMON_CONFIG, { mode: 420, flag: "wx" });
|
|
5100
|
+
const enabled = await run(["/usr/bin/systemctl", "enable", "docker.service"]);
|
|
5101
|
+
return enabled.exitCode === 0 ? run(["/usr/bin/systemctl", "restart", "docker.service"]) : enabled;
|
|
5102
|
+
}
|
|
5103
|
+
return run(["/usr/bin/systemctl", "enable", "--now", `${software}.service`]);
|
|
5072
5104
|
}
|
|
5073
5105
|
const directory = mkdtempSync(join(tmpdir(), "forgezero-software-"));
|
|
5074
5106
|
try {
|
|
@@ -5153,7 +5185,7 @@ function validateSoftwareRequirements(value, _options = {}) {
|
|
|
5153
5185
|
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
5154
5186
|
throw new Error("software requirement contains an unknown field");
|
|
5155
5187
|
}
|
|
5156
|
-
if (!["bun", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
5188
|
+
if (!["bun", "docker", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
5157
5189
|
throw new Error("software requirement coordinate is invalid");
|
|
5158
5190
|
}
|
|
5159
5191
|
const requirement = { id: row.id, version: row.version };
|
|
@@ -5888,6 +5920,1206 @@ async function runPipeline(options) {
|
|
|
5888
5920
|
return { pipeline: pipeline.name, ok: !failed, assurance, steps };
|
|
5889
5921
|
}
|
|
5890
5922
|
|
|
5923
|
+
// src/deploy-plan.ts
|
|
5924
|
+
import { createHash as createHash3 } from "crypto";
|
|
5925
|
+
|
|
5926
|
+
// src/deploy-actions.ts
|
|
5927
|
+
var row = (value, where) => {
|
|
5928
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5929
|
+
throw new Error(`${where} must be an object`);
|
|
5930
|
+
return value;
|
|
5931
|
+
};
|
|
5932
|
+
var exact = (value, keys, where) => {
|
|
5933
|
+
const unknown = Object.keys(value).filter((key) => !keys.includes(key));
|
|
5934
|
+
if (unknown.length)
|
|
5935
|
+
throw new Error(`${where} contains unknown field(s): ${unknown.join(", ")}`);
|
|
5936
|
+
for (const key of keys)
|
|
5937
|
+
if (value[key] === undefined)
|
|
5938
|
+
throw new Error(`${where} requires ${key}`);
|
|
5939
|
+
};
|
|
5940
|
+
var component = (value, components, where) => {
|
|
5941
|
+
if (typeof value !== "string" || !components[value])
|
|
5942
|
+
throw new Error(`${where}.component must name an existing component`);
|
|
5943
|
+
return components[value];
|
|
5944
|
+
};
|
|
5945
|
+
var exactArgv = (value, where) => {
|
|
5946
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 256 || value.some((entry) => typeof entry !== "string" || entry.length < 1 || entry.length > 16384 || entry.includes("\x00")))
|
|
5947
|
+
throw new Error(`${where} requires a bounded exact argv array`);
|
|
5948
|
+
const executable = value[0].split("/").at(-1).toLowerCase();
|
|
5949
|
+
if (["sh", "bash", "dash", "zsh", "ksh", "fish", "busybox", "env"].includes(executable))
|
|
5950
|
+
throw new Error(`${where} cannot invoke a command dispatcher`);
|
|
5951
|
+
};
|
|
5952
|
+
function validateBuiltInAction(definition, context, where) {
|
|
5953
|
+
if (!definition.uses.startsWith("forgezero."))
|
|
5954
|
+
return;
|
|
5955
|
+
const withValue = row(definition.with, `${where}.with`);
|
|
5956
|
+
if (definition.uses === "forgezero.software/ensure@1") {
|
|
5957
|
+
exact(withValue, ["requirements"], `${where}.with`);
|
|
5958
|
+
if (!Array.isArray(withValue.requirements) || withValue.requirements.length < 1 || withValue.requirements.length > 64 || withValue.requirements.some((name) => typeof name !== "string" || !context.requirements[name]))
|
|
5959
|
+
throw new Error(`${where}.with.requirements must name existing requirements`);
|
|
5960
|
+
return;
|
|
5961
|
+
}
|
|
5962
|
+
if (definition.uses === "forgezero.exec/argv@1") {
|
|
5963
|
+
const allowed = ["component", "argv", "credentials"];
|
|
5964
|
+
const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
|
|
5965
|
+
if (unknown.length)
|
|
5966
|
+
throw new Error(`${where}.with contains unknown field(s): ${unknown.join(", ")}`);
|
|
5967
|
+
component(withValue.component, context.components, `${where}.with`);
|
|
5968
|
+
exactArgv(withValue.argv, `${where}.with.argv`);
|
|
5969
|
+
if (withValue.credentials !== undefined) {
|
|
5970
|
+
const bindings = row(withValue.credentials, `${where}.with.credentials`);
|
|
5971
|
+
for (const [environment, credentialName] of Object.entries(bindings))
|
|
5972
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(environment) || typeof credentialName !== "string" || !context.credentials[credentialName])
|
|
5973
|
+
throw new Error(`${where}.with.credentials contains an invalid binding`);
|
|
5974
|
+
}
|
|
5975
|
+
return;
|
|
5976
|
+
}
|
|
5977
|
+
if (["forgezero.oci/build@1", "forgezero.oci/pull@1"].includes(definition.uses)) {
|
|
5978
|
+
const allowed = definition.uses.endsWith("/build@1") ? ["component", "noCache"] : ["component"];
|
|
5979
|
+
const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
|
|
5980
|
+
if (unknown.length)
|
|
5981
|
+
throw new Error(`${where}.with contains unknown field(s): ${unknown.join(", ")}`);
|
|
5982
|
+
const selected = component(withValue.component, context.components, `${where}.with`);
|
|
5983
|
+
if (selected.kind !== "application" || selected.runtime.kind !== "container")
|
|
5984
|
+
throw new Error(`${where}.with.component must use a container runtime`);
|
|
5985
|
+
if (withValue.noCache !== undefined && typeof withValue.noCache !== "boolean")
|
|
5986
|
+
throw new Error(`${where}.with.noCache must be a boolean`);
|
|
5987
|
+
return;
|
|
5988
|
+
}
|
|
5989
|
+
if (["forgezero.service/deploy@1", "forgezero.service/promote@1", "forgezero.health/http@1", "forgezero.storage/verify@1"].includes(definition.uses)) {
|
|
5990
|
+
const allowed = definition.uses === "forgezero.service/deploy@1" || definition.uses === "forgezero.service/promote@1" ? ["component", "imageDigest"] : ["component"];
|
|
5991
|
+
const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
|
|
5992
|
+
if (unknown.length)
|
|
5993
|
+
throw new Error(`${where}.with contains unknown field(s): ${unknown.join(", ")}`);
|
|
5994
|
+
component(withValue.component, context.components, `${where}.with`);
|
|
5995
|
+
return;
|
|
5996
|
+
}
|
|
5997
|
+
throw new Error(`${where}.uses is an unknown ForgeZero action`);
|
|
5998
|
+
}
|
|
5999
|
+
// src/deploy-providers.ts
|
|
6000
|
+
var plain = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
6001
|
+
var exact2 = (row2, keys, provider) => {
|
|
6002
|
+
const unknown = Object.keys(row2).filter((key) => !keys.includes(key));
|
|
6003
|
+
if (unknown.length)
|
|
6004
|
+
throw new Error(`${provider} config contains unknown field(s): ${unknown.join(", ")}`);
|
|
6005
|
+
for (const key of keys)
|
|
6006
|
+
if (row2[key] === undefined)
|
|
6007
|
+
throw new Error(`${provider} config requires ${key}`);
|
|
6008
|
+
};
|
|
6009
|
+
var absolute = (value) => typeof value === "string" && /^\/(?:[A-Za-z0-9._-]+\/?)*$/.test(value) && !value.includes("..");
|
|
6010
|
+
function validateBuiltInProvider(requirement2, where) {
|
|
6011
|
+
const row2 = plain(requirement2.config);
|
|
6012
|
+
if (!requirement2.provider.startsWith("forgezero."))
|
|
6013
|
+
return;
|
|
6014
|
+
const expected = {
|
|
6015
|
+
"forgezero.bun": { capability: "runtime.javascript", versions: ["1.3.14"], keys: ["installScope"] },
|
|
6016
|
+
"forgezero.docker": { capability: "runtime.container", versions: ["ubuntu-26.04"], keys: ["installScope", "storageDriver", "dataRoot", "liveRestore", "logDriver", "defaultNetwork", "rootless"] },
|
|
6017
|
+
"forgezero.nginx": { capability: "proxy.http", versions: ["ubuntu-26.04"], keys: ["installScope", "websocket", "maximumBodyMiB", "workerProcesses", "workerConnections"] },
|
|
6018
|
+
"forgezero.arangodb": { capability: "database.arangodb", versions: ["3.11.14"], keys: ["installScope", "runtime", "dataPath", "bind"] },
|
|
6019
|
+
"forgezero.cloudflared": { capability: "network.public-tunnel", versions: ["2026.7.3"], keys: ["installScope", "mode"] },
|
|
6020
|
+
"forgezero.warp": { capability: "network.private", versions: ["2026.6.822.0-min"], keys: ["installScope", "mode"] },
|
|
6021
|
+
"forgezero.ufw": { capability: "security.firewall", versions: ["ubuntu-26.04"], keys: ["installScope", "defaultIncoming", "defaultOutgoing"] },
|
|
6022
|
+
"forgezero.openssh": { capability: "transport.ssh", versions: ["ubuntu-26.04"], keys: ["installScope", "mode"] },
|
|
6023
|
+
"forgezero.systemd": { capability: "service.manager", versions: ["ubuntu-26.04"], keys: ["installScope", "credentials", "serviceSandbox"] }
|
|
6024
|
+
};
|
|
6025
|
+
const contract = expected[requirement2.provider];
|
|
6026
|
+
if (!contract)
|
|
6027
|
+
throw new Error(`${where} uses unknown built-in provider ${requirement2.provider}`);
|
|
6028
|
+
if (requirement2.contract !== 1 || requirement2.capability !== contract.capability || !contract.versions.includes(requirement2.version))
|
|
6029
|
+
throw new Error(`${where} provider coordinate is unsupported`);
|
|
6030
|
+
exact2(row2, contract.keys, requirement2.provider);
|
|
6031
|
+
if (row2.installScope !== "host")
|
|
6032
|
+
throw new Error(`${where}.config.installScope must be host`);
|
|
6033
|
+
if (requirement2.provider === "forgezero.docker") {
|
|
6034
|
+
if (row2.storageDriver !== "overlay2" || row2.dataRoot !== "/var/lib/docker" || row2.liveRestore !== true || row2.logDriver !== "local" || row2.defaultNetwork !== "bridge" || row2.rootless !== false)
|
|
6035
|
+
throw new Error(`${where} Docker config is unsupported by the fixed host strategy`);
|
|
6036
|
+
}
|
|
6037
|
+
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)))
|
|
6038
|
+
throw new Error(`${where} Nginx config is invalid`);
|
|
6039
|
+
if (requirement2.provider === "forgezero.arangodb" && (row2.runtime !== "native" || !absolute(row2.dataPath) || row2.bind !== "private"))
|
|
6040
|
+
throw new Error(`${where} ArangoDB config is invalid`);
|
|
6041
|
+
}
|
|
6042
|
+
|
|
6043
|
+
// src/deploy-plan.ts
|
|
6044
|
+
var DEPLOY_PLAN_FORMAT = "forgezero-deployment-plan";
|
|
6045
|
+
var DEPLOY_PLAN_VERSION = 1;
|
|
6046
|
+
|
|
6047
|
+
class DeploymentPlanError extends Error {
|
|
6048
|
+
constructor(message) {
|
|
6049
|
+
super(message);
|
|
6050
|
+
this.name = "DeploymentPlanError";
|
|
6051
|
+
}
|
|
6052
|
+
}
|
|
6053
|
+
var NAME2 = /^[a-z][A-Za-z0-9-]{0,62}$/;
|
|
6054
|
+
var COORDINATE = /^[a-z][a-z0-9.-]{0,127}$/;
|
|
6055
|
+
var ACTION = /^[a-z][a-z0-9.-]{0,127}\/[a-z][a-z0-9.-]{0,127}@[1-9][0-9]{0,5}$/;
|
|
6056
|
+
var VERSION = /^[A-Za-z0-9][A-Za-z0-9.+_-]{0,63}$/;
|
|
6057
|
+
var ABSOLUTE_PATH = /^\/(?:[A-Za-z0-9._-]+\/?)*$/;
|
|
6058
|
+
var HEALTH_PATH = /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]{0,255}$/;
|
|
6059
|
+
var fail = (where, message) => {
|
|
6060
|
+
throw new DeploymentPlanError(`${where} ${message}`);
|
|
6061
|
+
};
|
|
6062
|
+
var object = (value, where) => {
|
|
6063
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
6064
|
+
fail(where, "must be an object.");
|
|
6065
|
+
return value;
|
|
6066
|
+
};
|
|
6067
|
+
var exact3 = (value, allowed, where) => {
|
|
6068
|
+
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
6069
|
+
if (unknown.length)
|
|
6070
|
+
fail(where, `contains unknown field(s): ${unknown.join(", ")}.`);
|
|
6071
|
+
};
|
|
6072
|
+
var named = (value, where) => {
|
|
6073
|
+
if (typeof value !== "string" || !NAME2.test(value))
|
|
6074
|
+
fail(where, "must be a lowercase typed name.");
|
|
6075
|
+
return value;
|
|
6076
|
+
};
|
|
6077
|
+
var boundedString = (value, where, maximum = 256) => {
|
|
6078
|
+
if (typeof value !== "string" || value.length < 1 || value.length > maximum || value.includes("\x00"))
|
|
6079
|
+
fail(where, `must be a non-empty string of at most ${maximum} characters.`);
|
|
6080
|
+
return value;
|
|
6081
|
+
};
|
|
6082
|
+
var integer = (value, where, minimum, maximum) => {
|
|
6083
|
+
if (!Number.isInteger(value) || value < minimum || value > maximum)
|
|
6084
|
+
fail(where, `must be an integer from ${minimum} to ${maximum}.`);
|
|
6085
|
+
return value;
|
|
6086
|
+
};
|
|
6087
|
+
var number = (value, where, minimum, maximum) => {
|
|
6088
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum)
|
|
6089
|
+
fail(where, `must be a finite number from ${minimum} to ${maximum}.`);
|
|
6090
|
+
return value;
|
|
6091
|
+
};
|
|
6092
|
+
function canonicalJson(value) {
|
|
6093
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
6094
|
+
return JSON.stringify(value);
|
|
6095
|
+
if (typeof value === "number") {
|
|
6096
|
+
if (!Number.isFinite(value))
|
|
6097
|
+
throw new DeploymentPlanError("deployment data contains a non-finite number.");
|
|
6098
|
+
return JSON.stringify(value);
|
|
6099
|
+
}
|
|
6100
|
+
if (Array.isArray(value))
|
|
6101
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
6102
|
+
if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
|
6103
|
+
return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
6104
|
+
}
|
|
6105
|
+
throw new DeploymentPlanError("deployment data must contain only plain JSON values.");
|
|
6106
|
+
}
|
|
6107
|
+
function deploymentSourceDigest(source) {
|
|
6108
|
+
return `sha256:${createHash3("sha256").update(canonicalJson(source)).digest("hex")}`;
|
|
6109
|
+
}
|
|
6110
|
+
function reference(value, where) {
|
|
6111
|
+
const row2 = object(value, where);
|
|
6112
|
+
exact3(row2, ["$ref"], where);
|
|
6113
|
+
const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
|
|
6114
|
+
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)) {
|
|
6115
|
+
fail(where, "contains an unsupported reference.");
|
|
6116
|
+
}
|
|
6117
|
+
return { $ref: coordinate };
|
|
6118
|
+
}
|
|
6119
|
+
function operand(value, where) {
|
|
6120
|
+
if (value === null || ["string", "number", "boolean"].includes(typeof value)) {
|
|
6121
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
6122
|
+
fail(where, "must be finite.");
|
|
6123
|
+
return value;
|
|
6124
|
+
}
|
|
6125
|
+
return reference(value, where);
|
|
6126
|
+
}
|
|
6127
|
+
function condition(value, where, depth = 0) {
|
|
6128
|
+
if (depth > 16)
|
|
6129
|
+
fail(where, "is nested too deeply.");
|
|
6130
|
+
const row2 = object(value, where);
|
|
6131
|
+
if (Object.keys(row2).length !== 1)
|
|
6132
|
+
fail(where, "must contain exactly one operator.");
|
|
6133
|
+
const [operator, body] = Object.entries(row2)[0];
|
|
6134
|
+
if (operator === "all" || operator === "any") {
|
|
6135
|
+
if (!Array.isArray(body) || body.length < 1 || body.length > 32)
|
|
6136
|
+
fail(where, `${operator} must contain 1 to 32 conditions.`);
|
|
6137
|
+
const entries = body;
|
|
6138
|
+
return { [operator]: entries.map((entry, index) => condition(entry, `${where}.${operator}[${index}]`, depth + 1)) };
|
|
6139
|
+
}
|
|
6140
|
+
if (operator === "not")
|
|
6141
|
+
return { not: condition(body, `${where}.not`, depth + 1) };
|
|
6142
|
+
if (["equals", "notEquals", "greaterThan", "greaterThanOrEqual", "lessThan", "lessThanOrEqual", "contains", "startsWith"].includes(operator)) {
|
|
6143
|
+
if (!Array.isArray(body) || body.length !== 2)
|
|
6144
|
+
fail(where, `${operator} must contain two operands.`);
|
|
6145
|
+
const entries = body;
|
|
6146
|
+
return { [operator]: [operand(entries[0], `${where}.${operator}[0]`), operand(entries[1], `${where}.${operator}[1]`)] };
|
|
6147
|
+
}
|
|
6148
|
+
if (operator === "exists")
|
|
6149
|
+
return { exists: reference(body, `${where}.exists`) };
|
|
6150
|
+
if (["succeeded", "failed", "changed"].includes(operator))
|
|
6151
|
+
return { [operator]: named(body, `${where}.${operator}`) };
|
|
6152
|
+
return fail(where, `uses unsupported operator ${operator}.`);
|
|
6153
|
+
}
|
|
6154
|
+
function jsonValue(value, where, depth = 0) {
|
|
6155
|
+
if (depth > 16)
|
|
6156
|
+
fail(where, "is nested too deeply.");
|
|
6157
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
6158
|
+
return value;
|
|
6159
|
+
if (typeof value === "number") {
|
|
6160
|
+
if (!Number.isFinite(value))
|
|
6161
|
+
fail(where, "must be finite.");
|
|
6162
|
+
return value;
|
|
6163
|
+
}
|
|
6164
|
+
if (Array.isArray(value)) {
|
|
6165
|
+
if (value.length > 1000)
|
|
6166
|
+
fail(where, "contains more than 1000 entries.");
|
|
6167
|
+
return value.map((entry, index) => jsonValue(entry, `${where}[${index}]`, depth + 1));
|
|
6168
|
+
}
|
|
6169
|
+
const row2 = object(value, where);
|
|
6170
|
+
if (Object.keys(row2).length > 1000)
|
|
6171
|
+
fail(where, "contains more than 1000 fields.");
|
|
6172
|
+
return Object.fromEntries(Object.entries(row2).map(([key, entry]) => [key, jsonValue(entry, `${where}.${key}`, depth + 1)]));
|
|
6173
|
+
}
|
|
6174
|
+
function validateInput(value, where) {
|
|
6175
|
+
const row2 = object(value, where);
|
|
6176
|
+
const type = row2.type;
|
|
6177
|
+
const common = ["type", "required", "default"];
|
|
6178
|
+
if (row2.required !== undefined && typeof row2.required !== "boolean")
|
|
6179
|
+
fail(`${where}.required`, "must be a boolean.");
|
|
6180
|
+
if (type === "string") {
|
|
6181
|
+
exact3(row2, [...common, "minimumLength", "maximumLength", "pattern"], where);
|
|
6182
|
+
if (row2.default !== undefined)
|
|
6183
|
+
boundedString(row2.default, `${where}.default`, 16384);
|
|
6184
|
+
if (row2.minimumLength !== undefined)
|
|
6185
|
+
integer(row2.minimumLength, `${where}.minimumLength`, 0, 16384);
|
|
6186
|
+
if (row2.maximumLength !== undefined)
|
|
6187
|
+
integer(row2.maximumLength, `${where}.maximumLength`, 1, 16384);
|
|
6188
|
+
if (row2.pattern !== undefined) {
|
|
6189
|
+
const pattern = boundedString(row2.pattern, `${where}.pattern`, 512);
|
|
6190
|
+
try {
|
|
6191
|
+
new RegExp(pattern);
|
|
6192
|
+
} catch {
|
|
6193
|
+
fail(`${where}.pattern`, "must be a valid regular expression.");
|
|
6194
|
+
}
|
|
6195
|
+
}
|
|
6196
|
+
} else if (type === "integer" || type === "number") {
|
|
6197
|
+
exact3(row2, [...common, "minimum", "maximum"], where);
|
|
6198
|
+
if (row2.default !== undefined)
|
|
6199
|
+
number(row2.default, `${where}.default`, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
|
|
6200
|
+
if (row2.minimum !== undefined)
|
|
6201
|
+
number(row2.minimum, `${where}.minimum`, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
|
|
6202
|
+
if (row2.maximum !== undefined)
|
|
6203
|
+
number(row2.maximum, `${where}.maximum`, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
|
|
6204
|
+
if (type === "integer" && row2.default !== undefined && !Number.isInteger(row2.default))
|
|
6205
|
+
fail(`${where}.default`, "must be an integer.");
|
|
6206
|
+
} else if (type === "boolean") {
|
|
6207
|
+
exact3(row2, common, where);
|
|
6208
|
+
if (row2.default !== undefined && typeof row2.default !== "boolean")
|
|
6209
|
+
fail(`${where}.default`, "must be a boolean.");
|
|
6210
|
+
} else if (type === "enum") {
|
|
6211
|
+
exact3(row2, [...common, "values"], where);
|
|
6212
|
+
if (!Array.isArray(row2.values) || row2.values.length < 1 || row2.values.length > 64)
|
|
6213
|
+
fail(`${where}.values`, "must contain 1 to 64 values.");
|
|
6214
|
+
const values = row2.values.map((entry, index) => boundedString(entry, `${where}.values[${index}]`, 128));
|
|
6215
|
+
if (new Set(values).size !== values.length)
|
|
6216
|
+
fail(`${where}.values`, "must be unique.");
|
|
6217
|
+
if (row2.default !== undefined && !values.includes(row2.default))
|
|
6218
|
+
fail(`${where}.default`, "must be an enum value.");
|
|
6219
|
+
} else if (type === "hostname") {
|
|
6220
|
+
exact3(row2, common, where);
|
|
6221
|
+
if (row2.default !== undefined && !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i.test(String(row2.default)))
|
|
6222
|
+
fail(`${where}.default`, "must be a hostname.");
|
|
6223
|
+
} else
|
|
6224
|
+
fail(`${where}.type`, "is unsupported.");
|
|
6225
|
+
return row2;
|
|
6226
|
+
}
|
|
6227
|
+
function validateRequirement(value, where) {
|
|
6228
|
+
const row2 = object(value, where);
|
|
6229
|
+
exact3(row2, ["capability", "provider", "contract", "version", "config", "when"], where);
|
|
6230
|
+
if (!COORDINATE.test(String(row2.capability)))
|
|
6231
|
+
fail(`${where}.capability`, "is invalid.");
|
|
6232
|
+
if (!COORDINATE.test(String(row2.provider)))
|
|
6233
|
+
fail(`${where}.provider`, "is invalid.");
|
|
6234
|
+
integer(row2.contract, `${where}.contract`, 1, 65535);
|
|
6235
|
+
if (typeof row2.version !== "string" || !VERSION.test(row2.version))
|
|
6236
|
+
fail(`${where}.version`, "is invalid.");
|
|
6237
|
+
jsonValue(row2.config, `${where}.config`);
|
|
6238
|
+
if (row2.when !== undefined)
|
|
6239
|
+
condition(row2.when, `${where}.when`);
|
|
6240
|
+
const requirement2 = row2;
|
|
6241
|
+
validateBuiltInProvider(requirement2, where);
|
|
6242
|
+
return requirement2;
|
|
6243
|
+
}
|
|
6244
|
+
function validateArgv(value, where) {
|
|
6245
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 256)
|
|
6246
|
+
fail(where, "must contain 1 to 256 arguments.");
|
|
6247
|
+
let bytes = 0;
|
|
6248
|
+
const values = value.map((entry, index) => {
|
|
6249
|
+
const result = boundedString(entry, `${where}[${index}]`, 16384);
|
|
6250
|
+
bytes += Buffer.byteLength(result);
|
|
6251
|
+
return result;
|
|
6252
|
+
});
|
|
6253
|
+
if (bytes > 64 * 1024)
|
|
6254
|
+
fail(where, "is larger than 64 KiB.");
|
|
6255
|
+
const executable = values[0].split("/").at(-1).toLowerCase();
|
|
6256
|
+
if (["sh", "bash", "dash", "zsh", "ksh", "fish", "busybox", "env"].includes(executable))
|
|
6257
|
+
fail(`${where}[0]`, "may not invoke a command dispatcher.");
|
|
6258
|
+
return values;
|
|
6259
|
+
}
|
|
6260
|
+
function validateResources(value, where) {
|
|
6261
|
+
const row2 = object(value, where);
|
|
6262
|
+
exact3(row2, ["cpu", "memory", "pids", "io"], where);
|
|
6263
|
+
if (row2.cpu !== undefined) {
|
|
6264
|
+
const cpu = object(row2.cpu, `${where}.cpu`);
|
|
6265
|
+
exact3(cpu, ["limit", "weight", "pinning"], `${where}.cpu`);
|
|
6266
|
+
number(cpu.limit, `${where}.cpu.limit`, 0.01, 1024);
|
|
6267
|
+
if (cpu.weight !== undefined)
|
|
6268
|
+
integer(cpu.weight, `${where}.cpu.weight`, 1, 1e4);
|
|
6269
|
+
if (cpu.pinning !== undefined && !["automatic", "dedicated"].includes(String(cpu.pinning)))
|
|
6270
|
+
fail(`${where}.cpu.pinning`, "is unsupported.");
|
|
6271
|
+
}
|
|
6272
|
+
if (row2.memory !== undefined) {
|
|
6273
|
+
const memory = object(row2.memory, `${where}.memory`);
|
|
6274
|
+
exact3(memory, ["limitMiB", "reservationMiB", "swap"], `${where}.memory`);
|
|
6275
|
+
integer(memory.limitMiB, `${where}.memory.limitMiB`, 16, 16777216);
|
|
6276
|
+
if (memory.reservationMiB !== undefined && integer(memory.reservationMiB, `${where}.memory.reservationMiB`, 1, memory.limitMiB) > memory.limitMiB)
|
|
6277
|
+
fail(`${where}.memory.reservationMiB`, "must not exceed limitMiB.");
|
|
6278
|
+
if (memory.swap !== undefined && !["disabled", "bounded"].includes(String(memory.swap)))
|
|
6279
|
+
fail(`${where}.memory.swap`, "is unsupported.");
|
|
6280
|
+
}
|
|
6281
|
+
if (row2.pids !== undefined) {
|
|
6282
|
+
const pids = object(row2.pids, `${where}.pids`);
|
|
6283
|
+
exact3(pids, ["limit"], `${where}.pids`);
|
|
6284
|
+
integer(pids.limit, `${where}.pids.limit`, 1, 1048576);
|
|
6285
|
+
}
|
|
6286
|
+
if (row2.io !== undefined) {
|
|
6287
|
+
const io = object(row2.io, `${where}.io`);
|
|
6288
|
+
exact3(io, ["weight", "readBps", "writeBps", "readIops", "writeIops"], `${where}.io`);
|
|
6289
|
+
for (const key of Object.keys(io))
|
|
6290
|
+
integer(io[key], `${where}.io.${key}`, 1, Number.MAX_SAFE_INTEGER);
|
|
6291
|
+
}
|
|
6292
|
+
}
|
|
6293
|
+
function validateStorage(value, where) {
|
|
6294
|
+
const row2 = object(value, where);
|
|
6295
|
+
if (row2.class === "ephemeral") {
|
|
6296
|
+
exact3(row2, ["class", "path", "type", "sizeMiB"], where);
|
|
6297
|
+
if (row2.type !== "tmpfs")
|
|
6298
|
+
fail(`${where}.type`, "must be tmpfs.");
|
|
6299
|
+
integer(row2.sizeMiB, `${where}.sizeMiB`, 1, 1048576);
|
|
6300
|
+
} else if (row2.class === "persistent") {
|
|
6301
|
+
exact3(row2, ["class", "name", "path", "minimumFreePercent"], where);
|
|
6302
|
+
named(row2.name, `${where}.name`);
|
|
6303
|
+
if (row2.minimumFreePercent !== undefined)
|
|
6304
|
+
integer(row2.minimumFreePercent, `${where}.minimumFreePercent`, 1, 95);
|
|
6305
|
+
} else if (row2.class === "database") {
|
|
6306
|
+
exact3(row2, ["class", "name", "path", "minimumFreePercent", "minimumIops", "latencyTargetMs"], where);
|
|
6307
|
+
named(row2.name, `${where}.name`);
|
|
6308
|
+
integer(row2.minimumFreePercent, `${where}.minimumFreePercent`, 1, 95);
|
|
6309
|
+
if (row2.minimumIops !== undefined)
|
|
6310
|
+
integer(row2.minimumIops, `${where}.minimumIops`, 1, 1e8);
|
|
6311
|
+
if (row2.latencyTargetMs !== undefined)
|
|
6312
|
+
number(row2.latencyTargetMs, `${where}.latencyTargetMs`, 0.01, 60000);
|
|
6313
|
+
} else
|
|
6314
|
+
fail(`${where}.class`, "is unsupported.");
|
|
6315
|
+
if (typeof row2.path !== "string" || !ABSOLUTE_PATH.test(row2.path) || row2.path.includes(".."))
|
|
6316
|
+
fail(`${where}.path`, "must be a normalized absolute path.");
|
|
6317
|
+
}
|
|
6318
|
+
function validateNetwork(value, where) {
|
|
6319
|
+
const row2 = object(value, where);
|
|
6320
|
+
exact3(row2, ["ingress", "private", "public", "container"], where);
|
|
6321
|
+
if (row2.ingress !== undefined) {
|
|
6322
|
+
const ingress = object(row2.ingress, `${where}.ingress`);
|
|
6323
|
+
exact3(ingress, ["exposure", "stablePort"], `${where}.ingress`);
|
|
6324
|
+
if (!["loopback", "private", "public"].includes(String(ingress.exposure)))
|
|
6325
|
+
fail(`${where}.ingress.exposure`, "is unsupported.");
|
|
6326
|
+
if (ingress.stablePort !== undefined)
|
|
6327
|
+
integer(ingress.stablePort, `${where}.ingress.stablePort`, 1, 65535);
|
|
6328
|
+
}
|
|
6329
|
+
if (row2.private !== undefined) {
|
|
6330
|
+
const privateNetwork = object(row2.private, `${where}.private`);
|
|
6331
|
+
exact3(privateNetwork, ["mode", "preferences"], `${where}.private`);
|
|
6332
|
+
if (!["private-lan", "cloudflare-warp", "auto"].includes(String(privateNetwork.mode)))
|
|
6333
|
+
fail(`${where}.private.mode`, "is unsupported.");
|
|
6334
|
+
if (privateNetwork.preferences !== undefined && (!Array.isArray(privateNetwork.preferences) || privateNetwork.preferences.length < 1 || privateNetwork.preferences.length > 2 || privateNetwork.preferences.some((entry) => !["private-lan", "cloudflare-warp"].includes(String(entry))) || new Set(privateNetwork.preferences).size !== privateNetwork.preferences.length))
|
|
6335
|
+
fail(`${where}.private.preferences`, "is invalid.");
|
|
6336
|
+
}
|
|
6337
|
+
if (row2.public !== undefined) {
|
|
6338
|
+
const publicNetwork = object(row2.public, `${where}.public`);
|
|
6339
|
+
exact3(publicNetwork, ["mode", "optional"], `${where}.public`);
|
|
6340
|
+
if (!["disabled", "cloudflare-tunnel"].includes(String(publicNetwork.mode)))
|
|
6341
|
+
fail(`${where}.public.mode`, "is unsupported.");
|
|
6342
|
+
if (publicNetwork.optional !== undefined && typeof publicNetwork.optional !== "boolean")
|
|
6343
|
+
fail(`${where}.public.optional`, "must be a boolean.");
|
|
6344
|
+
}
|
|
6345
|
+
if (row2.container !== undefined) {
|
|
6346
|
+
const container = object(row2.container, `${where}.container`);
|
|
6347
|
+
exact3(container, ["mode", "network"], `${where}.container`);
|
|
6348
|
+
if (!["bridge", "host"].includes(String(container.mode)))
|
|
6349
|
+
fail(`${where}.container.mode`, "is unsupported.");
|
|
6350
|
+
if (container.network !== undefined)
|
|
6351
|
+
named(container.network, `${where}.container.network`);
|
|
6352
|
+
}
|
|
6353
|
+
}
|
|
6354
|
+
function validateComponent(value, where, targetNames, requirements) {
|
|
6355
|
+
const row2 = object(value, where);
|
|
6356
|
+
if (!targetNames.has(String(row2.target)))
|
|
6357
|
+
fail(`${where}.target`, "must name an existing target.");
|
|
6358
|
+
if (row2.kind === "application") {
|
|
6359
|
+
exact3(row2, ["kind", "target", "runtime", "service", "resources", "storage", "network", "rollout"], where);
|
|
6360
|
+
const runtime = object(row2.runtime, `${where}.runtime`);
|
|
6361
|
+
const runtimeRequirement = requirements.get(String(runtime.requirement));
|
|
6362
|
+
if (!runtimeRequirement)
|
|
6363
|
+
fail(`${where}.runtime.requirement`, "must name an existing requirement.");
|
|
6364
|
+
if (runtime.kind === "native") {
|
|
6365
|
+
exact3(runtime, ["kind", "provider", "requirement", "argv"], `${where}.runtime`);
|
|
6366
|
+
validateArgv(runtime.argv, `${where}.runtime.argv`);
|
|
6367
|
+
} else if (runtime.kind === "container") {
|
|
6368
|
+
exact3(runtime, ["kind", "provider", "requirement", "image", "entrypoint", "security"], `${where}.runtime`);
|
|
6369
|
+
const image = object(runtime.image, `${where}.runtime.image`);
|
|
6370
|
+
exact3(image, ["source"], `${where}.runtime.image`);
|
|
6371
|
+
const source = object(image.source, `${where}.runtime.image.source`);
|
|
6372
|
+
if (source.kind === "build") {
|
|
6373
|
+
exact3(source, ["kind", "context", "dockerfile"], `${where}.runtime.image.source`);
|
|
6374
|
+
boundedString(source.context, `${where}.runtime.image.source.context`, 512);
|
|
6375
|
+
boundedString(source.dockerfile, `${where}.runtime.image.source.dockerfile`, 512);
|
|
6376
|
+
} else if (source.kind === "registry") {
|
|
6377
|
+
exact3(source, ["kind", "reference"], `${where}.runtime.image.source`);
|
|
6378
|
+
if (!/^[-a-zA-Z0-9./:_@]+$/.test(boundedString(source.reference, `${where}.runtime.image.source.reference`, 512)))
|
|
6379
|
+
fail(`${where}.runtime.image.source.reference`, "is invalid.");
|
|
6380
|
+
} else
|
|
6381
|
+
fail(`${where}.runtime.image.source.kind`, "is unsupported.");
|
|
6382
|
+
if (runtime.entrypoint !== undefined)
|
|
6383
|
+
validateArgv(runtime.entrypoint, `${where}.runtime.entrypoint`);
|
|
6384
|
+
const security = object(runtime.security, `${where}.runtime.security`);
|
|
6385
|
+
exact3(security, ["privileged", "noNewPrivileges", "root", "dropCapabilities"], `${where}.runtime.security`);
|
|
6386
|
+
if (security.privileged !== false || security.noNewPrivileges !== true)
|
|
6387
|
+
fail(`${where}.runtime.security`, "must disable privileged mode and enable noNewPrivileges.");
|
|
6388
|
+
if (!["read-only", "writable"].includes(String(security.root)))
|
|
6389
|
+
fail(`${where}.runtime.security.root`, "is unsupported.");
|
|
6390
|
+
if (!Array.isArray(security.dropCapabilities) || !security.dropCapabilities.includes("ALL"))
|
|
6391
|
+
fail(`${where}.runtime.security.dropCapabilities`, "must include ALL.");
|
|
6392
|
+
} else
|
|
6393
|
+
fail(`${where}.runtime.kind`, "is unsupported.");
|
|
6394
|
+
if (!COORDINATE.test(String(runtime.provider)))
|
|
6395
|
+
fail(`${where}.runtime.provider`, "is invalid.");
|
|
6396
|
+
if (runtimeRequirement && runtime.provider !== runtimeRequirement.provider)
|
|
6397
|
+
fail(`${where}.runtime.provider`, "must match its requirement provider.");
|
|
6398
|
+
if (runtimeRequirement && runtime.kind === "container" && runtimeRequirement.capability !== "runtime.container")
|
|
6399
|
+
fail(`${where}.runtime.requirement`, "must provide runtime.container.");
|
|
6400
|
+
if (runtimeRequirement && runtime.kind === "native" && !runtimeRequirement.capability.startsWith("runtime."))
|
|
6401
|
+
fail(`${where}.runtime.requirement`, "must provide a runtime capability.");
|
|
6402
|
+
const service = object(row2.service, `${where}.service`);
|
|
6403
|
+
exact3(service, ["protocol", "port", "health", "websocket", "maximumConnections"], `${where}.service`);
|
|
6404
|
+
if (service.protocol !== "http")
|
|
6405
|
+
fail(`${where}.service.protocol`, "must be http.");
|
|
6406
|
+
integer(service.port, `${where}.service.port`, 1, 65535);
|
|
6407
|
+
const health = object(service.health, `${where}.service.health`);
|
|
6408
|
+
exact3(health, ["protocol", "method", "path", "expectedStatus", "timeoutMs", "intervalMs", "attempts"], `${where}.service.health`);
|
|
6409
|
+
if (health.protocol !== "http" || !["GET", "HEAD"].includes(String(health.method)) || typeof health.path !== "string" || !HEALTH_PATH.test(health.path) || health.path.includes("..") || health.path.includes("//"))
|
|
6410
|
+
fail(`${where}.service.health`, "is invalid.");
|
|
6411
|
+
if (!Array.isArray(health.expectedStatus) || health.expectedStatus.length < 1 || health.expectedStatus.length > 16 || health.expectedStatus.some((status) => !Number.isInteger(status) || status < 100 || status > 599))
|
|
6412
|
+
fail(`${where}.service.health.expectedStatus`, "is invalid.");
|
|
6413
|
+
integer(health.timeoutMs, `${where}.service.health.timeoutMs`, 100, 300000);
|
|
6414
|
+
if (health.intervalMs !== undefined)
|
|
6415
|
+
integer(health.intervalMs, `${where}.service.health.intervalMs`, 100, 300000);
|
|
6416
|
+
if (health.attempts !== undefined)
|
|
6417
|
+
integer(health.attempts, `${where}.service.health.attempts`, 1, 1000);
|
|
6418
|
+
if (service.websocket !== undefined && typeof service.websocket !== "boolean")
|
|
6419
|
+
fail(`${where}.service.websocket`, "must be a boolean.");
|
|
6420
|
+
if (service.maximumConnections !== undefined)
|
|
6421
|
+
integer(service.maximumConnections, `${where}.service.maximumConnections`, 1, 1e7);
|
|
6422
|
+
validateResources(row2.resources, `${where}.resources`);
|
|
6423
|
+
if (runtime.kind === "container") {
|
|
6424
|
+
const resources = object(row2.resources, `${where}.resources`);
|
|
6425
|
+
for (const required of ["cpu", "memory", "pids"])
|
|
6426
|
+
if (resources[required] === undefined)
|
|
6427
|
+
fail(`${where}.resources.${required}`, "is required for a bounded container.");
|
|
6428
|
+
if (row2.network === undefined)
|
|
6429
|
+
fail(`${where}.network`, "is required for a container.");
|
|
6430
|
+
const network = object(row2.network, `${where}.network`);
|
|
6431
|
+
if (network.container === undefined)
|
|
6432
|
+
fail(`${where}.network.container`, "must select bridge or reviewed host networking.");
|
|
6433
|
+
}
|
|
6434
|
+
if (row2.storage !== undefined) {
|
|
6435
|
+
if (!Array.isArray(row2.storage) || row2.storage.length > 32)
|
|
6436
|
+
fail(`${where}.storage`, "must contain at most 32 mounts.");
|
|
6437
|
+
row2.storage.forEach((entry, index) => validateStorage(entry, `${where}.storage[${index}]`));
|
|
6438
|
+
}
|
|
6439
|
+
if (row2.network !== undefined)
|
|
6440
|
+
validateNetwork(row2.network, `${where}.network`);
|
|
6441
|
+
const rollout = object(row2.rollout, `${where}.rollout`);
|
|
6442
|
+
exact3(rollout, ["strategy", "proxy", "drainMs", "automaticRollback"], `${where}.rollout`);
|
|
6443
|
+
if (!["direct", "blue-green", "rolling", "canary"].includes(String(rollout.strategy)))
|
|
6444
|
+
fail(`${where}.rollout.strategy`, "is unsupported.");
|
|
6445
|
+
if (rollout.proxy !== undefined)
|
|
6446
|
+
named(rollout.proxy, `${where}.rollout.proxy`);
|
|
6447
|
+
if (rollout.strategy !== "direct") {
|
|
6448
|
+
const proxyRequirement = requirements.get(String(rollout.proxy));
|
|
6449
|
+
if (!proxyRequirement || proxyRequirement.capability !== "proxy.http")
|
|
6450
|
+
fail(`${where}.rollout.proxy`, "must name a proxy.http requirement for managed rollout.");
|
|
6451
|
+
}
|
|
6452
|
+
if (rollout.drainMs !== undefined)
|
|
6453
|
+
integer(rollout.drainMs, `${where}.rollout.drainMs`, 0, 600000);
|
|
6454
|
+
if (rollout.automaticRollback !== undefined && typeof rollout.automaticRollback !== "boolean")
|
|
6455
|
+
fail(`${where}.rollout.automaticRollback`, "must be a boolean.");
|
|
6456
|
+
} else if (row2.kind === "database") {
|
|
6457
|
+
exact3(row2, ["kind", "target", "runtime", "topology", "resources", "storage", "network"], where);
|
|
6458
|
+
const runtime = object(row2.runtime, `${where}.runtime`);
|
|
6459
|
+
exact3(runtime, ["provider", "requirement", "mode"], `${where}.runtime`);
|
|
6460
|
+
const databaseRequirement = requirements.get(String(runtime.requirement));
|
|
6461
|
+
if (!databaseRequirement)
|
|
6462
|
+
fail(`${where}.runtime.requirement`, "must name an existing requirement.");
|
|
6463
|
+
if (databaseRequirement && (runtime.provider !== databaseRequirement.provider || databaseRequirement.capability !== "database.arangodb"))
|
|
6464
|
+
fail(`${where}.runtime.requirement`, "must match a database.arangodb provider requirement.");
|
|
6465
|
+
if (databaseRequirement && databaseRequirement.config.runtime !== runtime.mode)
|
|
6466
|
+
fail(`${where}.runtime.mode`, "must match the provider runtime mode.");
|
|
6467
|
+
if (runtime.mode !== "native")
|
|
6468
|
+
fail(`${where}.runtime.mode`, "must be native in deployment plan v1.");
|
|
6469
|
+
const topology = object(row2.topology, `${where}.topology`);
|
|
6470
|
+
if (topology.mode === "standalone")
|
|
6471
|
+
exact3(topology, ["mode"], `${where}.topology`);
|
|
6472
|
+
else if (topology.mode === "cluster") {
|
|
6473
|
+
exact3(topology, ["mode", "bootstrapMembers", "replicationFactor", "writeConcern", "additionalJoiners"], `${where}.topology`);
|
|
6474
|
+
integer(topology.bootstrapMembers, `${where}.topology.bootstrapMembers`, 3, 9);
|
|
6475
|
+
integer(topology.replicationFactor, `${where}.topology.replicationFactor`, 1, 16);
|
|
6476
|
+
integer(topology.writeConcern, `${where}.topology.writeConcern`, 1, topology.replicationFactor);
|
|
6477
|
+
if (!["allowed", "disabled"].includes(String(topology.additionalJoiners)))
|
|
6478
|
+
fail(`${where}.topology.additionalJoiners`, "is unsupported.");
|
|
6479
|
+
} else
|
|
6480
|
+
fail(`${where}.topology.mode`, "is unsupported.");
|
|
6481
|
+
if (row2.resources !== undefined)
|
|
6482
|
+
validateResources(row2.resources, `${where}.resources`);
|
|
6483
|
+
validateStorage(row2.storage, `${where}.storage`);
|
|
6484
|
+
if (row2.storage.class !== "database")
|
|
6485
|
+
fail(`${where}.storage.class`, "must be database.");
|
|
6486
|
+
validateNetwork(row2.network, `${where}.network`);
|
|
6487
|
+
} else
|
|
6488
|
+
fail(`${where}.kind`, "is unsupported.");
|
|
6489
|
+
return row2;
|
|
6490
|
+
}
|
|
6491
|
+
function validateStep(value, where, targets, context) {
|
|
6492
|
+
const row2 = object(value, where);
|
|
6493
|
+
exact3(row2, ["uses", "with", "if", "scope", "timeoutMs", "retry", "failure", "compensate"], where);
|
|
6494
|
+
if (typeof row2.uses !== "string" || !ACTION.test(row2.uses))
|
|
6495
|
+
fail(`${where}.uses`, "must be a versioned action coordinate.");
|
|
6496
|
+
jsonValue(row2.with, `${where}.with`);
|
|
6497
|
+
if (row2.if !== undefined)
|
|
6498
|
+
condition(row2.if, `${where}.if`);
|
|
6499
|
+
const scope = object(row2.scope, `${where}.scope`);
|
|
6500
|
+
if (scope.kind === "release-executor")
|
|
6501
|
+
exact3(scope, ["kind"], `${where}.scope`);
|
|
6502
|
+
else if (scope.kind === "elected-one") {
|
|
6503
|
+
exact3(scope, ["kind", "group"], `${where}.scope`);
|
|
6504
|
+
named(scope.group, `${where}.scope.group`);
|
|
6505
|
+
} else if (scope.kind === "each-target") {
|
|
6506
|
+
exact3(scope, ["kind", "target"], `${where}.scope`);
|
|
6507
|
+
if (!targets.has(String(scope.target)))
|
|
6508
|
+
fail(`${where}.scope.target`, "must name an existing target.");
|
|
6509
|
+
} else if (scope.kind === "target-batches") {
|
|
6510
|
+
exact3(scope, ["kind", "target", "size"], `${where}.scope`);
|
|
6511
|
+
if (!targets.has(String(scope.target)))
|
|
6512
|
+
fail(`${where}.scope.target`, "must name an existing target.");
|
|
6513
|
+
integer(scope.size, `${where}.scope.size`, 1, 32);
|
|
6514
|
+
} else
|
|
6515
|
+
fail(`${where}.scope.kind`, "is unsupported.");
|
|
6516
|
+
if (row2.timeoutMs !== undefined)
|
|
6517
|
+
integer(row2.timeoutMs, `${where}.timeoutMs`, 1, 86400000);
|
|
6518
|
+
if (row2.retry !== undefined) {
|
|
6519
|
+
const retry = object(row2.retry, `${where}.retry`);
|
|
6520
|
+
exact3(retry, ["attempts", "backoff", "retryOn"], `${where}.retry`);
|
|
6521
|
+
integer(retry.attempts, `${where}.retry.attempts`, 1, 20);
|
|
6522
|
+
const backoff = object(retry.backoff, `${where}.retry.backoff`);
|
|
6523
|
+
exact3(backoff, ["kind", "initialMs", "maximumMs"], `${where}.retry.backoff`);
|
|
6524
|
+
if (!["fixed", "linear", "exponential"].includes(String(backoff.kind)))
|
|
6525
|
+
fail(`${where}.retry.backoff.kind`, "is unsupported.");
|
|
6526
|
+
integer(backoff.initialMs, `${where}.retry.backoff.initialMs`, 0, 300000);
|
|
6527
|
+
integer(backoff.maximumMs, `${where}.retry.backoff.maximumMs`, backoff.initialMs, 3600000);
|
|
6528
|
+
if (retry.retryOn !== undefined && (!Array.isArray(retry.retryOn) || retry.retryOn.length > 8 || retry.retryOn.some((entry) => !["network", "timeout", "provider-unavailable", "rate-limited", "conflict"].includes(String(entry))) || new Set(retry.retryOn).size !== retry.retryOn.length))
|
|
6529
|
+
fail(`${where}.retry.retryOn`, "is invalid.");
|
|
6530
|
+
}
|
|
6531
|
+
if (row2.failure !== undefined) {
|
|
6532
|
+
const failure = object(row2.failure, `${where}.failure`);
|
|
6533
|
+
exact3(failure, ["policy"], `${where}.failure`);
|
|
6534
|
+
if (!["continue", "stop", "rollback-stage", "rollback-deployment", "compensate", "manual-intervention"].includes(String(failure.policy)))
|
|
6535
|
+
fail(`${where}.failure.policy`, "is unsupported.");
|
|
6536
|
+
}
|
|
6537
|
+
if (row2.compensate !== undefined) {
|
|
6538
|
+
const compensate = object(row2.compensate, `${where}.compensate`);
|
|
6539
|
+
exact3(compensate, ["uses", "with"], `${where}.compensate`);
|
|
6540
|
+
if (typeof compensate.uses !== "string" || !ACTION.test(compensate.uses))
|
|
6541
|
+
fail(`${where}.compensate.uses`, "must be a versioned action coordinate.");
|
|
6542
|
+
jsonValue(compensate.with, `${where}.compensate.with`);
|
|
6543
|
+
if (row2.failure?.policy !== "compensate")
|
|
6544
|
+
fail(`${where}.compensate`, "requires failure.policy compensate.");
|
|
6545
|
+
}
|
|
6546
|
+
const definition = row2;
|
|
6547
|
+
validateBuiltInAction(definition, context, where);
|
|
6548
|
+
if (definition.compensate)
|
|
6549
|
+
validateBuiltInAction({ uses: definition.compensate.uses, with: definition.compensate.with, scope: definition.scope }, context, `${where}.compensate`);
|
|
6550
|
+
return definition;
|
|
6551
|
+
}
|
|
6552
|
+
function topologicalStages(stages, workflow) {
|
|
6553
|
+
const names = Object.keys(stages);
|
|
6554
|
+
const nameSet = new Set(names);
|
|
6555
|
+
for (const name of names)
|
|
6556
|
+
for (const dependency of stages[name].dependsOn ?? []) {
|
|
6557
|
+
if (!nameSet.has(dependency))
|
|
6558
|
+
fail(`workflows.${workflow}.stages.${name}.dependsOn`, `names missing stage ${dependency}.`);
|
|
6559
|
+
if (dependency === name)
|
|
6560
|
+
fail(`workflows.${workflow}.stages.${name}.dependsOn`, "cannot depend on itself.");
|
|
6561
|
+
}
|
|
6562
|
+
const visiting = new Set;
|
|
6563
|
+
const visited = new Set;
|
|
6564
|
+
const ordered = [];
|
|
6565
|
+
const visit = (name) => {
|
|
6566
|
+
if (visiting.has(name))
|
|
6567
|
+
fail(`workflows.${workflow}.stages`, "contains a dependency cycle.");
|
|
6568
|
+
if (visited.has(name))
|
|
6569
|
+
return;
|
|
6570
|
+
visiting.add(name);
|
|
6571
|
+
for (const dependency of stages[name].dependsOn ?? [])
|
|
6572
|
+
visit(dependency);
|
|
6573
|
+
visiting.delete(name);
|
|
6574
|
+
visited.add(name);
|
|
6575
|
+
ordered.push(name);
|
|
6576
|
+
};
|
|
6577
|
+
for (const name of names.sort())
|
|
6578
|
+
visit(name);
|
|
6579
|
+
return ordered;
|
|
6580
|
+
}
|
|
6581
|
+
function validateStage(value, where, targets, context) {
|
|
6582
|
+
const row2 = object(value, where);
|
|
6583
|
+
exact3(row2, ["dependsOn", "if", "strategy", "steps"], where);
|
|
6584
|
+
if (row2.dependsOn !== undefined && (!Array.isArray(row2.dependsOn) || row2.dependsOn.length > 64 || row2.dependsOn.some((entry) => typeof entry !== "string" || !NAME2.test(entry)) || new Set(row2.dependsOn).size !== row2.dependsOn.length))
|
|
6585
|
+
fail(`${where}.dependsOn`, "is invalid.");
|
|
6586
|
+
if (row2.if !== undefined)
|
|
6587
|
+
condition(row2.if, `${where}.if`);
|
|
6588
|
+
const strategy = object(row2.strategy, `${where}.strategy`);
|
|
6589
|
+
exact3(strategy, ["mode", "maximumConcurrency", "batchSize", "minimumHealthy", "increments", "observationMs"], `${where}.strategy`);
|
|
6590
|
+
if (!["sequential", "parallel", "rolling", "blue-green", "canary"].includes(String(strategy.mode)))
|
|
6591
|
+
fail(`${where}.strategy.mode`, "is unsupported.");
|
|
6592
|
+
if (strategy.maximumConcurrency !== undefined)
|
|
6593
|
+
integer(strategy.maximumConcurrency, `${where}.strategy.maximumConcurrency`, 1, 1024);
|
|
6594
|
+
if (strategy.batchSize !== undefined)
|
|
6595
|
+
integer(strategy.batchSize, `${where}.strategy.batchSize`, 1, 1024);
|
|
6596
|
+
if (strategy.minimumHealthy !== undefined)
|
|
6597
|
+
integer(strategy.minimumHealthy, `${where}.strategy.minimumHealthy`, 0, 1024);
|
|
6598
|
+
if (strategy.increments !== undefined && (!Array.isArray(strategy.increments) || strategy.increments.length < 1 || strategy.increments.length > 20 || strategy.increments.some((entry) => !Number.isInteger(entry) || entry < 1 || entry > 100) || strategy.increments.at(-1) !== 100))
|
|
6599
|
+
fail(`${where}.strategy.increments`, "must end at 100 and contain bounded percentages.");
|
|
6600
|
+
if (strategy.observationMs !== undefined)
|
|
6601
|
+
integer(strategy.observationMs, `${where}.strategy.observationMs`, 1000, 86400000);
|
|
6602
|
+
if (strategy.mode === "canary" && strategy.increments === undefined)
|
|
6603
|
+
fail(`${where}.strategy.increments`, "is required for canary mode.");
|
|
6604
|
+
if (strategy.mode === "rolling" && strategy.batchSize === undefined)
|
|
6605
|
+
fail(`${where}.strategy.batchSize`, "is required for rolling mode.");
|
|
6606
|
+
const steps = object(row2.steps, `${where}.steps`);
|
|
6607
|
+
const entries = Object.entries(steps);
|
|
6608
|
+
if (entries.length < 1 || entries.length > 256)
|
|
6609
|
+
fail(`${where}.steps`, "must contain 1 to 256 steps.");
|
|
6610
|
+
for (const [name, stepValue] of entries) {
|
|
6611
|
+
named(name, `${where}.steps key`);
|
|
6612
|
+
validateStep(stepValue, `${where}.steps.${name}`, targets, context);
|
|
6613
|
+
}
|
|
6614
|
+
return row2;
|
|
6615
|
+
}
|
|
6616
|
+
function compileDeployment(sourceValue) {
|
|
6617
|
+
const source = object(sourceValue, "deployment");
|
|
6618
|
+
exact3(source, ["apiVersion", "kind", "metadata", "spec"], "deployment");
|
|
6619
|
+
if (source.apiVersion !== "deploy.forgezero.net/v1")
|
|
6620
|
+
fail("deployment.apiVersion", "must be deploy.forgezero.net/v1.");
|
|
6621
|
+
if (source.kind !== "Deployment")
|
|
6622
|
+
fail("deployment.kind", "must be Deployment.");
|
|
6623
|
+
const metadata = object(source.metadata, "deployment.metadata");
|
|
6624
|
+
exact3(metadata, ["name", "description"], "deployment.metadata");
|
|
6625
|
+
const name = named(metadata.name, "deployment.metadata.name");
|
|
6626
|
+
if (metadata.description !== undefined)
|
|
6627
|
+
boundedString(metadata.description, "deployment.metadata.description", 1024);
|
|
6628
|
+
const spec = object(source.spec, "deployment.spec");
|
|
6629
|
+
exact3(spec, ["security", "inputs", "credentials", "targets", "requirements", "components", "workflows"], "deployment.spec");
|
|
6630
|
+
if (spec.security !== undefined) {
|
|
6631
|
+
const security = object(spec.security, "deployment.spec.security");
|
|
6632
|
+
exact3(security, ["attestation"], "deployment.spec.security");
|
|
6633
|
+
if (security.attestation !== undefined && !["required", "preferred", "disabled"].includes(String(security.attestation)))
|
|
6634
|
+
fail("deployment.spec.security.attestation", "is unsupported.");
|
|
6635
|
+
}
|
|
6636
|
+
const inputs = object(spec.inputs ?? {}, "deployment.spec.inputs");
|
|
6637
|
+
if (Object.keys(inputs).length > 128)
|
|
6638
|
+
fail("deployment.spec.inputs", "contains more than 128 inputs.");
|
|
6639
|
+
for (const [inputName, value] of Object.entries(inputs)) {
|
|
6640
|
+
named(inputName, "deployment.spec.inputs key");
|
|
6641
|
+
validateInput(value, `deployment.spec.inputs.${inputName}`);
|
|
6642
|
+
}
|
|
6643
|
+
const credentials = object(spec.credentials ?? {}, "deployment.spec.credentials");
|
|
6644
|
+
if (Object.keys(credentials).length > 128)
|
|
6645
|
+
fail("deployment.spec.credentials", "contains more than 128 credentials.");
|
|
6646
|
+
for (const [credentialName, value] of Object.entries(credentials)) {
|
|
6647
|
+
named(credentialName, "deployment.spec.credentials key");
|
|
6648
|
+
const credential = object(value, `deployment.spec.credentials.${credentialName}`);
|
|
6649
|
+
exact3(credential, ["schema", "source"], `deployment.spec.credentials.${credentialName}`);
|
|
6650
|
+
if (!ACTION.test(String(credential.schema)))
|
|
6651
|
+
fail(`deployment.spec.credentials.${credentialName}.schema`, "must be a versioned schema coordinate.");
|
|
6652
|
+
const credentialSource = object(credential.source, `deployment.spec.credentials.${credentialName}.source`);
|
|
6653
|
+
exact3(credentialSource, ["kind", "name", "fallback"], `deployment.spec.credentials.${credentialName}.source`);
|
|
6654
|
+
if (!["vault", "systemd"].includes(String(credentialSource.kind)))
|
|
6655
|
+
fail(`deployment.spec.credentials.${credentialName}.source.kind`, "is unsupported.");
|
|
6656
|
+
named(credentialSource.name, `deployment.spec.credentials.${credentialName}.source.name`);
|
|
6657
|
+
if (credentialSource.fallback !== undefined && (credentialSource.kind !== "vault" || credentialSource.fallback !== "systemd"))
|
|
6658
|
+
fail(`deployment.spec.credentials.${credentialName}.source.fallback`, "may only be systemd behind Vault.");
|
|
6659
|
+
}
|
|
6660
|
+
const targets = object(spec.targets, "deployment.spec.targets");
|
|
6661
|
+
const targetEntries = Object.entries(targets);
|
|
6662
|
+
if (targetEntries.length < 1 || targetEntries.length > 64)
|
|
6663
|
+
fail("deployment.spec.targets", "must contain 1 to 64 targets.");
|
|
6664
|
+
const targetNames = new Set(targetEntries.map(([targetName]) => named(targetName, "deployment.spec.targets key")));
|
|
6665
|
+
for (const [targetName, value] of targetEntries) {
|
|
6666
|
+
const target = object(value, `deployment.spec.targets.${targetName}`);
|
|
6667
|
+
exact3(target, ["kind", "selector", "cardinality", "placement"], `deployment.spec.targets.${targetName}`);
|
|
6668
|
+
if (target.kind !== "compute")
|
|
6669
|
+
fail(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
|
|
6670
|
+
const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
|
|
6671
|
+
exact3(selector, ["profiles", "confidentialCompute", "labels"], `deployment.spec.targets.${targetName}.selector`);
|
|
6672
|
+
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)
|
|
6673
|
+
fail(`deployment.spec.targets.${targetName}.selector.profiles`, "is invalid.");
|
|
6674
|
+
if (selector.confidentialCompute !== undefined && !["required", "preferred", "disabled"].includes(String(selector.confidentialCompute)))
|
|
6675
|
+
fail(`deployment.spec.targets.${targetName}.selector.confidentialCompute`, "is unsupported.");
|
|
6676
|
+
if (selector.labels !== undefined)
|
|
6677
|
+
jsonValue(selector.labels, `deployment.spec.targets.${targetName}.selector.labels`);
|
|
6678
|
+
const cardinality = object(target.cardinality, `deployment.spec.targets.${targetName}.cardinality`);
|
|
6679
|
+
exact3(cardinality, ["minimum", "desired", "maximum"], `deployment.spec.targets.${targetName}.cardinality`);
|
|
6680
|
+
const minimum = integer(cardinality.minimum, `deployment.spec.targets.${targetName}.cardinality.minimum`, 1, 1024);
|
|
6681
|
+
const maximum = integer(cardinality.maximum, `deployment.spec.targets.${targetName}.cardinality.maximum`, minimum, 1024);
|
|
6682
|
+
if (typeof cardinality.desired === "number")
|
|
6683
|
+
integer(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`, minimum, maximum);
|
|
6684
|
+
else
|
|
6685
|
+
reference(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`);
|
|
6686
|
+
if (target.placement !== undefined)
|
|
6687
|
+
jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
|
|
6688
|
+
}
|
|
6689
|
+
const requirements = object(spec.requirements ?? {}, "deployment.spec.requirements");
|
|
6690
|
+
if (Object.keys(requirements).length > 64)
|
|
6691
|
+
fail("deployment.spec.requirements", "contains more than 64 requirements.");
|
|
6692
|
+
const requirementDefinitions = new Map;
|
|
6693
|
+
const requiredProviders = new Map;
|
|
6694
|
+
for (const [requirementName, value] of Object.entries(requirements)) {
|
|
6695
|
+
named(requirementName, "deployment.spec.requirements key");
|
|
6696
|
+
const requirementValue = validateRequirement(value, `deployment.spec.requirements.${requirementName}`);
|
|
6697
|
+
requirementDefinitions.set(requirementName, requirementValue);
|
|
6698
|
+
const key = `${requirementValue.provider}@${requirementValue.contract}:${requirementValue.capability}`;
|
|
6699
|
+
requiredProviders.set(key, { provider: requirementValue.provider, contract: requirementValue.contract, capability: requirementValue.capability });
|
|
6700
|
+
}
|
|
6701
|
+
const components = object(spec.components, "deployment.spec.components");
|
|
6702
|
+
const componentEntries = Object.entries(components);
|
|
6703
|
+
if (componentEntries.length < 1 || componentEntries.length > 128)
|
|
6704
|
+
fail("deployment.spec.components", "must contain 1 to 128 components.");
|
|
6705
|
+
for (const [componentName, value] of componentEntries) {
|
|
6706
|
+
named(componentName, "deployment.spec.components key");
|
|
6707
|
+
validateComponent(value, `deployment.spec.components.${componentName}`, targetNames, requirementDefinitions);
|
|
6708
|
+
}
|
|
6709
|
+
const actionContext = {
|
|
6710
|
+
components,
|
|
6711
|
+
requirements,
|
|
6712
|
+
credentials
|
|
6713
|
+
};
|
|
6714
|
+
const workflows = object(spec.workflows, "deployment.spec.workflows");
|
|
6715
|
+
const workflowEntries = Object.entries(workflows);
|
|
6716
|
+
if (workflowEntries.length < 1 || workflowEntries.length > 32)
|
|
6717
|
+
fail("deployment.spec.workflows", "must contain 1 to 32 workflows.");
|
|
6718
|
+
const plannedWorkflows = [];
|
|
6719
|
+
for (const [workflowName, value] of workflowEntries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
6720
|
+
named(workflowName, "deployment.spec.workflows key");
|
|
6721
|
+
const workflowValue = object(value, `deployment.spec.workflows.${workflowName}`);
|
|
6722
|
+
exact3(workflowValue, ["concurrency", "stages"], `deployment.spec.workflows.${workflowName}`);
|
|
6723
|
+
if (workflowValue.concurrency !== undefined) {
|
|
6724
|
+
const concurrency = object(workflowValue.concurrency, `deployment.spec.workflows.${workflowName}.concurrency`);
|
|
6725
|
+
exact3(concurrency, ["group", "limit"], `deployment.spec.workflows.${workflowName}.concurrency`);
|
|
6726
|
+
boundedString(concurrency.group, `deployment.spec.workflows.${workflowName}.concurrency.group`, 256);
|
|
6727
|
+
integer(concurrency.limit, `deployment.spec.workflows.${workflowName}.concurrency.limit`, 1, 1024);
|
|
6728
|
+
}
|
|
6729
|
+
const stages = object(workflowValue.stages, `deployment.spec.workflows.${workflowName}.stages`);
|
|
6730
|
+
if (Object.keys(stages).length < 1 || Object.keys(stages).length > 64)
|
|
6731
|
+
fail(`deployment.spec.workflows.${workflowName}.stages`, "must contain 1 to 64 stages.");
|
|
6732
|
+
for (const [stageName, stageValue] of Object.entries(stages)) {
|
|
6733
|
+
named(stageName, `deployment.spec.workflows.${workflowName}.stages key`);
|
|
6734
|
+
validateStage(stageValue, `deployment.spec.workflows.${workflowName}.stages.${stageName}`, targetNames, actionContext);
|
|
6735
|
+
}
|
|
6736
|
+
const stepIds = new Set;
|
|
6737
|
+
const plannedStages = topologicalStages(stages, workflowName).map((stageName) => {
|
|
6738
|
+
const stageValue = stages[stageName];
|
|
6739
|
+
const plannedSteps = Object.entries(stageValue.steps).map(([stepName, stepValue]) => {
|
|
6740
|
+
if (stepIds.has(stepName))
|
|
6741
|
+
fail(`deployment.spec.workflows.${workflowName}`, `contains duplicate step id ${stepName}.`);
|
|
6742
|
+
stepIds.add(stepName);
|
|
6743
|
+
return { id: stepName, ...stepValue };
|
|
6744
|
+
});
|
|
6745
|
+
return { id: stageName, ...stageValue.dependsOn ? { dependsOn: [...stageValue.dependsOn].sort() } : {}, ...stageValue.if ? { if: stageValue.if } : {}, strategy: stageValue.strategy, steps: plannedSteps };
|
|
6746
|
+
});
|
|
6747
|
+
plannedWorkflows.push({ id: workflowName, ...workflowValue.concurrency ? { concurrency: workflowValue.concurrency } : {}, stages: plannedStages });
|
|
6748
|
+
}
|
|
6749
|
+
const typedSource = source;
|
|
6750
|
+
return {
|
|
6751
|
+
format: DEPLOY_PLAN_FORMAT,
|
|
6752
|
+
version: DEPLOY_PLAN_VERSION,
|
|
6753
|
+
sourceDigest: deploymentSourceDigest(typedSource),
|
|
6754
|
+
name,
|
|
6755
|
+
...metadata.description ? { description: metadata.description } : {},
|
|
6756
|
+
requiredProviders: [...requiredProviders.values()].sort((left, right) => `${left.provider}:${left.contract}:${left.capability}`.localeCompare(`${right.provider}:${right.contract}:${right.capability}`)),
|
|
6757
|
+
spec: {
|
|
6758
|
+
...spec.security ? { security: spec.security } : {},
|
|
6759
|
+
inputs,
|
|
6760
|
+
credentials,
|
|
6761
|
+
targets,
|
|
6762
|
+
requirements,
|
|
6763
|
+
components,
|
|
6764
|
+
workflows: plannedWorkflows
|
|
6765
|
+
}
|
|
6766
|
+
};
|
|
6767
|
+
}
|
|
6768
|
+
function parseDeploymentPlan(value) {
|
|
6769
|
+
const row2 = object(value, "plan");
|
|
6770
|
+
exact3(row2, ["format", "version", "sourceDigest", "name", "description", "requiredProviders", "spec"], "plan");
|
|
6771
|
+
if (row2.format !== DEPLOY_PLAN_FORMAT || row2.version !== DEPLOY_PLAN_VERSION)
|
|
6772
|
+
fail("plan", "uses an unsupported format or version.");
|
|
6773
|
+
if (typeof row2.sourceDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(row2.sourceDigest))
|
|
6774
|
+
fail("plan.sourceDigest", "is invalid.");
|
|
6775
|
+
named(row2.name, "plan.name");
|
|
6776
|
+
if (row2.description !== undefined)
|
|
6777
|
+
boundedString(row2.description, "plan.description", 1024);
|
|
6778
|
+
if (!Array.isArray(row2.requiredProviders) || row2.requiredProviders.length > 64)
|
|
6779
|
+
fail("plan.requiredProviders", "must be a bounded array.");
|
|
6780
|
+
for (const [index, provider] of row2.requiredProviders.entries()) {
|
|
6781
|
+
const item = object(provider, `plan.requiredProviders[${index}]`);
|
|
6782
|
+
exact3(item, ["provider", "contract", "capability"], `plan.requiredProviders[${index}]`);
|
|
6783
|
+
if (!COORDINATE.test(String(item.provider)) || !COORDINATE.test(String(item.capability)))
|
|
6784
|
+
fail(`plan.requiredProviders[${index}]`, "contains an invalid coordinate.");
|
|
6785
|
+
integer(item.contract, `plan.requiredProviders[${index}].contract`, 1, 65535);
|
|
6786
|
+
}
|
|
6787
|
+
const spec = object(row2.spec, "plan.spec");
|
|
6788
|
+
const workflows = spec.workflows;
|
|
6789
|
+
if (!Array.isArray(workflows))
|
|
6790
|
+
fail("plan.spec.workflows", "must be an array.");
|
|
6791
|
+
const sourceWorkflows = {};
|
|
6792
|
+
for (const [workflowIndex, workflow] of workflows.entries()) {
|
|
6793
|
+
const item = object(workflow, `plan.spec.workflows[${workflowIndex}]`);
|
|
6794
|
+
exact3(item, ["id", "concurrency", "stages"], `plan.spec.workflows[${workflowIndex}]`);
|
|
6795
|
+
const workflowId = named(item.id, `plan.spec.workflows[${workflowIndex}].id`);
|
|
6796
|
+
if (sourceWorkflows[workflowId])
|
|
6797
|
+
fail("plan.spec.workflows", `contains duplicate workflow ${workflowId}.`);
|
|
6798
|
+
if (!Array.isArray(item.stages))
|
|
6799
|
+
fail(`plan.spec.workflows[${workflowIndex}].stages`, "must be an array.");
|
|
6800
|
+
const sourceStages = {};
|
|
6801
|
+
for (const [stageIndex, stage2] of item.stages.entries()) {
|
|
6802
|
+
const stageItem = object(stage2, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}]`);
|
|
6803
|
+
exact3(stageItem, ["id", "dependsOn", "if", "strategy", "steps"], `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}]`);
|
|
6804
|
+
const stageId = named(stageItem.id, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].id`);
|
|
6805
|
+
if (sourceStages[stageId])
|
|
6806
|
+
fail(`plan.spec.workflows[${workflowIndex}].stages`, `contains duplicate stage ${stageId}.`);
|
|
6807
|
+
if (!Array.isArray(stageItem.steps))
|
|
6808
|
+
fail(`plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps`, "must be an array.");
|
|
6809
|
+
const sourceSteps = {};
|
|
6810
|
+
for (const [stepIndex, step2] of stageItem.steps.entries()) {
|
|
6811
|
+
const stepItem = object(step2, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps[${stepIndex}]`);
|
|
6812
|
+
const stepId = named(stepItem.id, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps[${stepIndex}].id`);
|
|
6813
|
+
if (sourceSteps[stepId])
|
|
6814
|
+
fail(`plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps`, `contains duplicate step ${stepId}.`);
|
|
6815
|
+
const { id: _id3, ...stepWithoutId } = stepItem;
|
|
6816
|
+
sourceSteps[stepId] = stepWithoutId;
|
|
6817
|
+
}
|
|
6818
|
+
const { id: _id2, steps: _steps, ...stageWithoutId } = stageItem;
|
|
6819
|
+
sourceStages[stageId] = { ...stageWithoutId, steps: sourceSteps };
|
|
6820
|
+
}
|
|
6821
|
+
const { id: _id, stages: _stages, ...workflowWithoutId } = item;
|
|
6822
|
+
sourceWorkflows[workflowId] = { ...workflowWithoutId, stages: sourceStages };
|
|
6823
|
+
}
|
|
6824
|
+
const { workflows: _workflows, ...specWithoutWorkflows } = spec;
|
|
6825
|
+
const reconstructed = {
|
|
6826
|
+
apiVersion: "deploy.forgezero.net/v1",
|
|
6827
|
+
kind: "Deployment",
|
|
6828
|
+
metadata: { name: row2.name, ...row2.description ? { description: row2.description } : {} },
|
|
6829
|
+
spec: { ...specWithoutWorkflows, workflows: sourceWorkflows }
|
|
6830
|
+
};
|
|
6831
|
+
const compiled = compileDeployment(reconstructed);
|
|
6832
|
+
return { ...compiled, sourceDigest: row2.sourceDigest };
|
|
6833
|
+
}
|
|
6834
|
+
|
|
6835
|
+
// src/deploy-plan-runner.ts
|
|
6836
|
+
class DeploymentActionError extends Error {
|
|
6837
|
+
kind;
|
|
6838
|
+
constructor(kind, message) {
|
|
6839
|
+
super(message);
|
|
6840
|
+
this.kind = kind;
|
|
6841
|
+
this.name = "DeploymentActionError";
|
|
6842
|
+
}
|
|
6843
|
+
}
|
|
6844
|
+
function nested(value, path2) {
|
|
6845
|
+
let current = value;
|
|
6846
|
+
for (const part of path2) {
|
|
6847
|
+
if (!current || typeof current !== "object" || Array.isArray(current))
|
|
6848
|
+
return;
|
|
6849
|
+
current = current[part];
|
|
6850
|
+
}
|
|
6851
|
+
return current;
|
|
6852
|
+
}
|
|
6853
|
+
function resolveReference(reference2, state) {
|
|
6854
|
+
const parts = reference2.$ref.split(".");
|
|
6855
|
+
if (parts[0] === "inputs")
|
|
6856
|
+
return state.inputs[parts[1]];
|
|
6857
|
+
if (parts[0] === "steps") {
|
|
6858
|
+
const step2 = state.steps.get(parts[1]);
|
|
6859
|
+
if (parts[2] === "status")
|
|
6860
|
+
return step2?.status;
|
|
6861
|
+
if (parts[2] === "outputs")
|
|
6862
|
+
return nested(step2?.outputs, parts.slice(3));
|
|
6863
|
+
}
|
|
6864
|
+
if (parts[0] === "components")
|
|
6865
|
+
return nested(state.plan.spec.components[parts[1]], parts.slice(2));
|
|
6866
|
+
if (parts[0] === "targets")
|
|
6867
|
+
return nested(state.plan.spec.targets[parts[1]], parts.slice(2));
|
|
6868
|
+
if (parts[0] === "deployment")
|
|
6869
|
+
return nested(state.plan, parts.slice(1));
|
|
6870
|
+
return;
|
|
6871
|
+
}
|
|
6872
|
+
function resolveOperand(value, state) {
|
|
6873
|
+
return value && typeof value === "object" && "$ref" in value ? resolveReference(value, state) : value;
|
|
6874
|
+
}
|
|
6875
|
+
function stepMatches(id2, status, state) {
|
|
6876
|
+
return state.steps.get(id2)?.status === status;
|
|
6877
|
+
}
|
|
6878
|
+
function evaluateCondition(value, state) {
|
|
6879
|
+
if ("all" in value)
|
|
6880
|
+
return value.all.every((entry) => evaluateCondition(entry, state));
|
|
6881
|
+
if ("any" in value)
|
|
6882
|
+
return value.any.some((entry) => evaluateCondition(entry, state));
|
|
6883
|
+
if ("not" in value)
|
|
6884
|
+
return !evaluateCondition(value.not, state);
|
|
6885
|
+
if ("equals" in value)
|
|
6886
|
+
return resolveOperand(value.equals[0], state) === resolveOperand(value.equals[1], state);
|
|
6887
|
+
if ("notEquals" in value)
|
|
6888
|
+
return resolveOperand(value.notEquals[0], state) !== resolveOperand(value.notEquals[1], state);
|
|
6889
|
+
if ("greaterThan" in value)
|
|
6890
|
+
return Number(resolveOperand(value.greaterThan[0], state)) > Number(resolveOperand(value.greaterThan[1], state));
|
|
6891
|
+
if ("greaterThanOrEqual" in value)
|
|
6892
|
+
return Number(resolveOperand(value.greaterThanOrEqual[0], state)) >= Number(resolveOperand(value.greaterThanOrEqual[1], state));
|
|
6893
|
+
if ("lessThan" in value)
|
|
6894
|
+
return Number(resolveOperand(value.lessThan[0], state)) < Number(resolveOperand(value.lessThan[1], state));
|
|
6895
|
+
if ("lessThanOrEqual" in value)
|
|
6896
|
+
return Number(resolveOperand(value.lessThanOrEqual[0], state)) <= Number(resolveOperand(value.lessThanOrEqual[1], state));
|
|
6897
|
+
if ("contains" in value) {
|
|
6898
|
+
const [left, right] = [resolveOperand(value.contains[0], state), resolveOperand(value.contains[1], state)];
|
|
6899
|
+
return typeof left === "string" ? left.includes(String(right)) : Array.isArray(left) && left.includes(right);
|
|
6900
|
+
}
|
|
6901
|
+
if ("startsWith" in value)
|
|
6902
|
+
return String(resolveOperand(value.startsWith[0], state) ?? "").startsWith(String(resolveOperand(value.startsWith[1], state) ?? ""));
|
|
6903
|
+
if ("exists" in value)
|
|
6904
|
+
return resolveReference(value.exists, state) !== undefined;
|
|
6905
|
+
if ("succeeded" in value)
|
|
6906
|
+
return stepMatches(value.succeeded, "succeeded", state);
|
|
6907
|
+
if ("failed" in value)
|
|
6908
|
+
return stepMatches(value.failed, "failed", state);
|
|
6909
|
+
return state.steps.get(value.changed)?.changed === true;
|
|
6910
|
+
}
|
|
6911
|
+
function validateInputValue(definition, value, name) {
|
|
6912
|
+
const selected = value === undefined ? definition.default : value;
|
|
6913
|
+
if (selected === undefined) {
|
|
6914
|
+
if (definition.required)
|
|
6915
|
+
throw new DeploymentActionError("refused", `deployment input ${name} is required`);
|
|
6916
|
+
return null;
|
|
6917
|
+
}
|
|
6918
|
+
if (definition.type === "string" || definition.type === "hostname" || definition.type === "enum") {
|
|
6919
|
+
if (typeof selected !== "string")
|
|
6920
|
+
throw new DeploymentActionError("refused", `deployment input ${name} must be a string`);
|
|
6921
|
+
if (definition.type === "enum" && !definition.values.includes(selected))
|
|
6922
|
+
throw new DeploymentActionError("refused", `deployment input ${name} is not an allowed value`);
|
|
6923
|
+
if (definition.type === "hostname" && !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i.test(selected))
|
|
6924
|
+
throw new DeploymentActionError("refused", `deployment input ${name} must be a hostname`);
|
|
6925
|
+
if (definition.type === "string") {
|
|
6926
|
+
if (definition.minimumLength !== undefined && selected.length < definition.minimumLength)
|
|
6927
|
+
throw new DeploymentActionError("refused", `deployment input ${name} is too short`);
|
|
6928
|
+
if (definition.maximumLength !== undefined && selected.length > definition.maximumLength)
|
|
6929
|
+
throw new DeploymentActionError("refused", `deployment input ${name} is too long`);
|
|
6930
|
+
if (definition.pattern !== undefined && !new RegExp(definition.pattern).test(selected))
|
|
6931
|
+
throw new DeploymentActionError("refused", `deployment input ${name} does not match its pattern`);
|
|
6932
|
+
}
|
|
6933
|
+
return selected;
|
|
6934
|
+
}
|
|
6935
|
+
if (definition.type === "boolean") {
|
|
6936
|
+
if (typeof selected !== "boolean")
|
|
6937
|
+
throw new DeploymentActionError("refused", `deployment input ${name} must be a boolean`);
|
|
6938
|
+
return selected;
|
|
6939
|
+
}
|
|
6940
|
+
if (typeof selected !== "number" || !Number.isFinite(selected) || definition.type === "integer" && !Number.isInteger(selected))
|
|
6941
|
+
throw new DeploymentActionError("refused", `deployment input ${name} must be a ${definition.type}`);
|
|
6942
|
+
if (definition.minimum !== undefined && selected < definition.minimum)
|
|
6943
|
+
throw new DeploymentActionError("refused", `deployment input ${name} is below its minimum`);
|
|
6944
|
+
if (definition.maximum !== undefined && selected > definition.maximum)
|
|
6945
|
+
throw new DeploymentActionError("refused", `deployment input ${name} is above its maximum`);
|
|
6946
|
+
return selected;
|
|
6947
|
+
}
|
|
6948
|
+
function resolveJson(value, state) {
|
|
6949
|
+
if (Array.isArray(value))
|
|
6950
|
+
return value.map((entry) => resolveJson(entry, state));
|
|
6951
|
+
if (value && typeof value === "object") {
|
|
6952
|
+
if (Object.keys(value).length === 1 && "$ref" in value && typeof value.$ref === "string") {
|
|
6953
|
+
const resolved = resolveReference(value, state);
|
|
6954
|
+
if (resolved === undefined)
|
|
6955
|
+
throw new DeploymentActionError("refused", `deployment reference ${value.$ref} is unavailable`);
|
|
6956
|
+
return resolved;
|
|
6957
|
+
}
|
|
6958
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolveJson(entry, state)]));
|
|
6959
|
+
}
|
|
6960
|
+
return value;
|
|
6961
|
+
}
|
|
6962
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6963
|
+
function backoff(step2, attempt) {
|
|
6964
|
+
const retry = step2.retry;
|
|
6965
|
+
if (!retry)
|
|
6966
|
+
return 0;
|
|
6967
|
+
const factor = retry.backoff.kind === "fixed" ? 1 : retry.backoff.kind === "linear" ? attempt : 2 ** (attempt - 1);
|
|
6968
|
+
return Math.min(retry.backoff.maximumMs, retry.backoff.initialMs * factor);
|
|
6969
|
+
}
|
|
6970
|
+
async function bounded(work, timeoutMs) {
|
|
6971
|
+
const controller = new AbortController;
|
|
6972
|
+
let timer;
|
|
6973
|
+
try {
|
|
6974
|
+
return await Promise.race([
|
|
6975
|
+
work(controller.signal),
|
|
6976
|
+
new Promise((_, reject) => {
|
|
6977
|
+
timer = setTimeout(() => {
|
|
6978
|
+
controller.abort();
|
|
6979
|
+
reject(new DeploymentActionError("timeout", `action exceeded ${timeoutMs}ms`));
|
|
6980
|
+
}, timeoutMs);
|
|
6981
|
+
})
|
|
6982
|
+
]);
|
|
6983
|
+
} finally {
|
|
6984
|
+
if (timer)
|
|
6985
|
+
clearTimeout(timer);
|
|
6986
|
+
}
|
|
6987
|
+
}
|
|
6988
|
+
function actionError(cause) {
|
|
6989
|
+
return cause instanceof DeploymentActionError ? cause : new DeploymentActionError("failed", cause instanceof Error ? cause.message : "action failed");
|
|
6990
|
+
}
|
|
6991
|
+
async function compensate(record2, step2, stage2, options, state) {
|
|
6992
|
+
if (!step2.compensate)
|
|
6993
|
+
return;
|
|
6994
|
+
const handler = options.actions[step2.compensate.uses];
|
|
6995
|
+
if (!handler)
|
|
6996
|
+
throw new DeploymentActionError("refused", `compensation action ${step2.compensate.uses} is unsupported`);
|
|
6997
|
+
const compensation = { ...step2, uses: step2.compensate.uses, with: step2.compensate.with };
|
|
6998
|
+
await bounded((signal) => handler({ plan: state.plan, workflow: options.workflow, stage: stage2, step: compensation, inputs: state.inputs, resolved: resolveJson(compensation.with, state), attempt: 1, signal }), step2.timeoutMs ?? 600000);
|
|
6999
|
+
record2.status = "compensated";
|
|
7000
|
+
}
|
|
7001
|
+
async function runStep(step2, stage2, options, state) {
|
|
7002
|
+
const record2 = { id: step2.id, stage: stage2.id, action: step2.uses, status: "pending", attempts: 0, outputs: {}, changed: false };
|
|
7003
|
+
state.steps.set(step2.id, record2);
|
|
7004
|
+
if (options.shouldRun && !options.shouldRun(step2)) {
|
|
7005
|
+
record2.status = "skipped";
|
|
7006
|
+
return record2;
|
|
7007
|
+
}
|
|
7008
|
+
if (step2.if && !evaluateCondition(step2.if, state)) {
|
|
7009
|
+
record2.status = "skipped";
|
|
7010
|
+
return record2;
|
|
7011
|
+
}
|
|
7012
|
+
const handler = options.actions[step2.uses];
|
|
7013
|
+
if (!handler) {
|
|
7014
|
+
record2.status = "failed";
|
|
7015
|
+
record2.error = { kind: "refused", message: `action ${step2.uses} is unsupported` };
|
|
7016
|
+
return record2;
|
|
7017
|
+
}
|
|
7018
|
+
record2.startedAtTs = (options.now ?? Date.now)();
|
|
7019
|
+
record2.status = "running";
|
|
7020
|
+
const attempts = step2.retry?.attempts ?? 1;
|
|
7021
|
+
for (let attempt = 1;attempt <= attempts; attempt += 1) {
|
|
7022
|
+
record2.attempts = attempt;
|
|
7023
|
+
try {
|
|
7024
|
+
const result = await bounded((signal) => handler({ plan: state.plan, workflow: options.workflow, stage: stage2, step: step2, inputs: state.inputs, resolved: resolveJson(step2.with, state), attempt, signal }), step2.timeoutMs ?? 600000);
|
|
7025
|
+
record2.status = "succeeded";
|
|
7026
|
+
record2.outputs = result.outputs ?? {};
|
|
7027
|
+
record2.changed = result.changed === true;
|
|
7028
|
+
record2.evidence = result.evidence;
|
|
7029
|
+
record2.finishedAtTs = (options.now ?? Date.now)();
|
|
7030
|
+
return record2;
|
|
7031
|
+
} catch (cause) {
|
|
7032
|
+
const error = actionError(cause);
|
|
7033
|
+
const retryable = attempt < attempts && (step2.retry?.retryOn?.includes(error.kind) ?? error.kind !== "refused");
|
|
7034
|
+
if (!retryable) {
|
|
7035
|
+
record2.status = step2.failure?.policy === "manual-intervention" ? "manual-intervention" : "failed";
|
|
7036
|
+
record2.error = { kind: error.kind, message: error.message };
|
|
7037
|
+
record2.finishedAtTs = (options.now ?? Date.now)();
|
|
7038
|
+
return record2;
|
|
7039
|
+
}
|
|
7040
|
+
await (options.sleep ?? sleep)(backoff(step2, attempt));
|
|
7041
|
+
}
|
|
7042
|
+
}
|
|
7043
|
+
return record2;
|
|
7044
|
+
}
|
|
7045
|
+
async function mapLimit(values, limit, run2) {
|
|
7046
|
+
const results = new Array(values.length);
|
|
7047
|
+
let next = 0;
|
|
7048
|
+
await Promise.all(Array.from({ length: Math.min(limit, values.length) }, async () => {
|
|
7049
|
+
for (;; ) {
|
|
7050
|
+
const index = next++;
|
|
7051
|
+
if (index >= values.length)
|
|
7052
|
+
return;
|
|
7053
|
+
results[index] = await run2(values[index]);
|
|
7054
|
+
}
|
|
7055
|
+
}));
|
|
7056
|
+
return results;
|
|
7057
|
+
}
|
|
7058
|
+
async function runDeploymentPlan(options) {
|
|
7059
|
+
const plan = parseDeploymentPlan(options.plan);
|
|
7060
|
+
const workflow = plan.spec.workflows.find((candidate) => candidate.id === options.workflow);
|
|
7061
|
+
if (!workflow)
|
|
7062
|
+
throw new DeploymentActionError("refused", `workflow ${options.workflow} does not exist`);
|
|
7063
|
+
const supplied = options.inputs ?? {};
|
|
7064
|
+
const inputValues = {};
|
|
7065
|
+
const inputDefinitions = plan.spec.inputs ?? {};
|
|
7066
|
+
for (const key of Object.keys(supplied))
|
|
7067
|
+
if (!Object.hasOwn(inputDefinitions, key))
|
|
7068
|
+
throw new DeploymentActionError("refused", `unknown deployment input ${key}`);
|
|
7069
|
+
for (const [name, definition] of Object.entries(inputDefinitions))
|
|
7070
|
+
inputValues[name] = validateInputValue(definition, supplied[name], name);
|
|
7071
|
+
const execute = async () => {
|
|
7072
|
+
const state = { inputs: inputValues, steps: new Map, plan };
|
|
7073
|
+
const records = [];
|
|
7074
|
+
const successful2 = [];
|
|
7075
|
+
for (const stage2 of workflow.stages) {
|
|
7076
|
+
if (stage2.if && !evaluateCondition(stage2.if, state)) {
|
|
7077
|
+
for (const step2 of stage2.steps) {
|
|
7078
|
+
const record2 = { id: step2.id, stage: stage2.id, action: step2.uses, status: "skipped", attempts: 0, outputs: {}, changed: false };
|
|
7079
|
+
state.steps.set(step2.id, record2);
|
|
7080
|
+
records.push(record2);
|
|
7081
|
+
}
|
|
7082
|
+
continue;
|
|
7083
|
+
}
|
|
7084
|
+
const parallel = stage2.strategy.mode === "parallel";
|
|
7085
|
+
const stageRecords = parallel ? await mapLimit(stage2.steps, stage2.strategy.maximumConcurrency ?? stage2.steps.length, (step2) => runStep(step2, stage2, { ...options, plan, workflow: workflow.id }, state)) : await (async () => {
|
|
7086
|
+
const rows = [];
|
|
7087
|
+
for (const step2 of stage2.steps) {
|
|
7088
|
+
const record2 = await runStep(step2, stage2, { ...options, plan, workflow: workflow.id }, state);
|
|
7089
|
+
rows.push(record2);
|
|
7090
|
+
if (["failed", "manual-intervention"].includes(record2.status) && step2.failure?.policy !== "continue")
|
|
7091
|
+
break;
|
|
7092
|
+
}
|
|
7093
|
+
return rows;
|
|
7094
|
+
})();
|
|
7095
|
+
records.push(...stageRecords);
|
|
7096
|
+
for (const record2 of stageRecords) {
|
|
7097
|
+
const step2 = stage2.steps.find((candidate) => candidate.id === record2.id);
|
|
7098
|
+
if (record2.status === "succeeded")
|
|
7099
|
+
successful2.push({ record: record2, step: step2, stage: stage2 });
|
|
7100
|
+
if (record2.status !== "failed" && record2.status !== "manual-intervention")
|
|
7101
|
+
continue;
|
|
7102
|
+
const policy = step2.failure?.policy ?? "stop";
|
|
7103
|
+
if (policy === "continue")
|
|
7104
|
+
continue;
|
|
7105
|
+
if (policy === "compensate")
|
|
7106
|
+
await compensate(record2, step2, stage2, { ...options, plan, workflow: workflow.id }, state);
|
|
7107
|
+
if (policy === "rollback-stage" || policy === "rollback-deployment") {
|
|
7108
|
+
const candidates = successful2.filter((entry) => policy === "rollback-deployment" || entry.stage.id === stage2.id).reverse();
|
|
7109
|
+
for (const entry of candidates)
|
|
7110
|
+
await compensate(entry.record, entry.step, entry.stage, { ...options, plan, workflow: workflow.id }, state);
|
|
7111
|
+
}
|
|
7112
|
+
return { workflow: workflow.id, ok: false, status: record2.status === "manual-intervention" ? "manual-intervention" : "failed", steps: records };
|
|
7113
|
+
}
|
|
7114
|
+
}
|
|
7115
|
+
return { workflow: workflow.id, ok: true, status: "succeeded", steps: records };
|
|
7116
|
+
};
|
|
7117
|
+
return workflow.concurrency && options.lock ? options.lock(workflow.concurrency.group, workflow.concurrency.limit, execute) : execute();
|
|
7118
|
+
}
|
|
7119
|
+
|
|
7120
|
+
// src/deploy-compiler.ts
|
|
7121
|
+
var DEPLOY_PLAN_FILE = ".fz/deploy.plan.json";
|
|
7122
|
+
|
|
5891
7123
|
// src/deployment.ts
|
|
5892
7124
|
class DeploymentError extends Error {
|
|
5893
7125
|
code;
|
|
@@ -5933,7 +7165,7 @@ function persistCapacityCalibration(args) {
|
|
|
5933
7165
|
if (!isAbsolute2(args.directory)) {
|
|
5934
7166
|
throw new DeploymentError("PIPELINE_FAILED", "capacity evidence directory must be an absolute Agent-owned path");
|
|
5935
7167
|
}
|
|
5936
|
-
if (!
|
|
7168
|
+
if (!existsSync3(args.directory)) {
|
|
5937
7169
|
const parent = lstatSync(dirname(args.directory));
|
|
5938
7170
|
if (!parent.isDirectory() || parent.isSymbolicLink()) {
|
|
5939
7171
|
throw new DeploymentError("PIPELINE_FAILED", "capacity evidence parent must be a real Agent-owned directory");
|
|
@@ -5944,7 +7176,7 @@ function persistCapacityCalibration(args) {
|
|
|
5944
7176
|
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
5945
7177
|
throw new DeploymentError("PIPELINE_FAILED", "capacity evidence directory must be a real Agent-owned directory");
|
|
5946
7178
|
}
|
|
5947
|
-
const identity =
|
|
7179
|
+
const identity = createHash4("sha256").update(`${args.deploymentKey}\x00${args.profile}\x00${args.revision}`).digest("hex").slice(0, 24);
|
|
5948
7180
|
const path2 = join2(args.directory, `${identity}-${args.measuredAtTs}-${randomUUID().slice(0, 8)}.json`);
|
|
5949
7181
|
const next = `${path2}.next`;
|
|
5950
7182
|
const recommendedCoordinate = {
|
|
@@ -5989,7 +7221,7 @@ function createDeploymentManager(options) {
|
|
|
5989
7221
|
if (content.length > 16385 || /\r|\0/.test(content)) {
|
|
5990
7222
|
throw new DeploymentError("SOURCE_FAILED", "The pinned Git host keys are malformed.");
|
|
5991
7223
|
}
|
|
5992
|
-
if (
|
|
7224
|
+
if (existsSync3(knownHostsPath) && readFileSync2(knownHostsPath, "utf8") === content)
|
|
5993
7225
|
return;
|
|
5994
7226
|
mkdirSync2(dirname(knownHostsPath), { recursive: true, mode: 448 });
|
|
5995
7227
|
const next = `${knownHostsPath}.${process.pid}.${randomUUID()}.next`;
|
|
@@ -6148,6 +7380,212 @@ function createDeploymentManager(options) {
|
|
|
6148
7380
|
if (request.revision && head !== request.revision) {
|
|
6149
7381
|
throw new DeploymentError("SOURCE_FAILED", `Git checked out ${head}, not requested ${request.revision}.`);
|
|
6150
7382
|
}
|
|
7383
|
+
const planPath = join2(release, DEPLOY_PLAN_FILE);
|
|
7384
|
+
if (existsSync3(planPath)) {
|
|
7385
|
+
const plan = parseDeploymentPlan(readDefinition(planPath));
|
|
7386
|
+
for (const workflow of Object.values(plan.spec.workflows))
|
|
7387
|
+
for (const stage2 of Object.values(workflow.stages)) {
|
|
7388
|
+
if (stage2.strategy.mode === "rolling" || stage2.strategy.mode === "canary" || Object.values(stage2.steps).some((step2) => step2.scope.kind === "target-batches")) {
|
|
7389
|
+
throw new DeploymentError("PIPELINE_FAILED", "rolling/canary plans require a fenced multi-target rollout lease that this control protocol does not yet supply");
|
|
7390
|
+
}
|
|
7391
|
+
}
|
|
7392
|
+
const targetNames = new Set(Object.entries(plan.spec.targets).filter(([, target]) => target.selector.profiles.includes(options.profile)).map(([name]) => name));
|
|
7393
|
+
if (targetNames.size === 0)
|
|
7394
|
+
throw new DeploymentError("PIPELINE_FAILED", `deployment plan has no target for profile ${options.profile}`);
|
|
7395
|
+
const assurance = plan.spec.security?.attestation ?? "disabled";
|
|
7396
|
+
if (assurance !== "disabled") {
|
|
7397
|
+
if (!options.attestation && assurance === "required")
|
|
7398
|
+
throw new DeploymentError("PIPELINE_FAILED", "deployment plan requires attestation but this Agent has no source");
|
|
7399
|
+
if (options.attestation) {
|
|
7400
|
+
try {
|
|
7401
|
+
await options.attestation.report(head);
|
|
7402
|
+
} catch (cause) {
|
|
7403
|
+
if (assurance === "required")
|
|
7404
|
+
throw new DeploymentError("PIPELINE_FAILED", `deployment attestation failed: ${cause.message}`);
|
|
7405
|
+
}
|
|
7406
|
+
}
|
|
7407
|
+
}
|
|
7408
|
+
const phaseEnvironment2 = {
|
|
7409
|
+
...options.environment ?? {},
|
|
7410
|
+
FZ_RELEASE: release,
|
|
7411
|
+
FZ_DEPLOY_REVISION: head,
|
|
7412
|
+
FZ_DEPLOY_BRANCH: options.branch,
|
|
7413
|
+
FZ_DEPLOY_PROFILE: options.profile,
|
|
7414
|
+
...options.publicApiUrl ? { PUBLIC_API_URL: options.publicApiUrl } : {}
|
|
7415
|
+
};
|
|
7416
|
+
const inputValues = {};
|
|
7417
|
+
for (const [name, definition2] of Object.entries(plan.spec.inputs ?? {})) {
|
|
7418
|
+
const raw = options.environment?.[name];
|
|
7419
|
+
if (raw === undefined)
|
|
7420
|
+
continue;
|
|
7421
|
+
inputValues[name] = definition2.type === "boolean" ? raw === "true" : definition2.type === "integer" || definition2.type === "number" ? Number(raw) : raw;
|
|
7422
|
+
}
|
|
7423
|
+
const projectExec2 = options.projectExec ?? spawnExact;
|
|
7424
|
+
const materializePlanArgv = (argv2) => argv2.map((argument) => {
|
|
7425
|
+
if (argument === "${FZ_RELEASE}")
|
|
7426
|
+
return release;
|
|
7427
|
+
if (argument.includes("${")) {
|
|
7428
|
+
throw new DeploymentActionError("refused", `unsupported deployment argv coordinate ${argument}`);
|
|
7429
|
+
}
|
|
7430
|
+
return argument;
|
|
7431
|
+
});
|
|
7432
|
+
const providerSoftware = {
|
|
7433
|
+
"forgezero.bun": "bun",
|
|
7434
|
+
"forgezero.docker": "docker",
|
|
7435
|
+
"forgezero.nginx": "nginx",
|
|
7436
|
+
"forgezero.arangodb": "arangodb",
|
|
7437
|
+
"forgezero.cloudflared": "cloudflared",
|
|
7438
|
+
"forgezero.warp": "cloudflare-warp",
|
|
7439
|
+
"forgezero.ufw": "ufw",
|
|
7440
|
+
"forgezero.openssh": "openssh-client"
|
|
7441
|
+
};
|
|
7442
|
+
const actions2 = {
|
|
7443
|
+
"forgezero.software/ensure@1": async ({ resolved }) => {
|
|
7444
|
+
if (!options.ensureSoftware)
|
|
7445
|
+
throw new DeploymentActionError("provider-unavailable", "the supervised software helper is unavailable");
|
|
7446
|
+
if (!Array.isArray(resolved.requirements))
|
|
7447
|
+
throw new DeploymentActionError("refused", "software.ensure requires a requirements array");
|
|
7448
|
+
const requirements = resolved.requirements.map((name) => {
|
|
7449
|
+
if (typeof name !== "string")
|
|
7450
|
+
throw new DeploymentActionError("refused", "software requirement name is invalid");
|
|
7451
|
+
const requirement2 = plan.spec.requirements?.[name];
|
|
7452
|
+
const id2 = requirement2 && providerSoftware[requirement2.provider];
|
|
7453
|
+
if (!requirement2 || !id2)
|
|
7454
|
+
throw new DeploymentActionError("refused", `software requirement ${name} is unsupported on this Agent`);
|
|
7455
|
+
return { id: id2, version: requirement2.version };
|
|
7456
|
+
});
|
|
7457
|
+
await options.ensureSoftware(requirements);
|
|
7458
|
+
return { changed: true };
|
|
7459
|
+
},
|
|
7460
|
+
"forgezero.exec/argv@1": async ({ resolved, step: step2 }) => {
|
|
7461
|
+
if (!Array.isArray(resolved.argv) || resolved.argv.some((value) => typeof value !== "string"))
|
|
7462
|
+
throw new DeploymentActionError("refused", "exec.argv requires an exact argv array");
|
|
7463
|
+
const environment = { ...phaseEnvironment2 };
|
|
7464
|
+
const bindings = resolved.credentials;
|
|
7465
|
+
if (bindings !== undefined) {
|
|
7466
|
+
if (!bindings || typeof bindings !== "object" || Array.isArray(bindings))
|
|
7467
|
+
throw new DeploymentActionError("refused", "exec.argv credentials must map environment names to credential declarations");
|
|
7468
|
+
for (const [environmentName, credentialName] of Object.entries(bindings)) {
|
|
7469
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(environmentName) || typeof credentialName !== "string")
|
|
7470
|
+
throw new DeploymentActionError("refused", "exec.argv credential binding is invalid");
|
|
7471
|
+
const credential = plan.spec.credentials?.[credentialName];
|
|
7472
|
+
if (!credential || !options.cache)
|
|
7473
|
+
throw new DeploymentActionError("refused", `credential ${credentialName} is unavailable`);
|
|
7474
|
+
environment[environmentName] = await options.cache.get(credential.source.name);
|
|
7475
|
+
}
|
|
7476
|
+
}
|
|
7477
|
+
const result = await projectExec2({ argv: materializePlanArgv(resolved.argv), cwd: release, env: environment, timeoutMs: step2.timeoutMs });
|
|
7478
|
+
if (result.exitCode !== 0)
|
|
7479
|
+
throw new DeploymentActionError("failed", result.output.slice(0, 2000) || `command exited ${result.exitCode}`);
|
|
7480
|
+
return { outputs: { exitCode: result.exitCode } };
|
|
7481
|
+
},
|
|
7482
|
+
"forgezero.health/http@1": async ({ resolved }) => {
|
|
7483
|
+
const componentName = String(resolved.component ?? "");
|
|
7484
|
+
const component2 = plan.spec.components[componentName];
|
|
7485
|
+
if (!component2 || component2.kind !== "application")
|
|
7486
|
+
throw new DeploymentActionError("refused", `health component ${componentName} is invalid`);
|
|
7487
|
+
const health = component2.service.health;
|
|
7488
|
+
try {
|
|
7489
|
+
const response = await fetch(`http://127.0.0.1:${component2.service.port}${health.path}`, { method: health.method, redirect: "manual", signal: AbortSignal.timeout(health.timeoutMs) });
|
|
7490
|
+
if (!health.expectedStatus.includes(response.status))
|
|
7491
|
+
throw new DeploymentActionError("failed", `health returned HTTP ${response.status}`);
|
|
7492
|
+
return { evidence: { status: response.status } };
|
|
7493
|
+
} catch (cause) {
|
|
7494
|
+
throw cause instanceof DeploymentActionError ? cause : new DeploymentActionError("network", cause.message);
|
|
7495
|
+
}
|
|
7496
|
+
},
|
|
7497
|
+
"forgezero.storage/verify@1": async ({ resolved }) => {
|
|
7498
|
+
const component2 = plan.spec.components[String(resolved.component ?? "")];
|
|
7499
|
+
const mounts = component2?.kind === "database" ? [component2.storage] : component2?.storage ?? [];
|
|
7500
|
+
for (const mount of mounts) {
|
|
7501
|
+
const candidate = existsSync3(mount.path) ? mount.path : dirname(mount.path);
|
|
7502
|
+
const stats = statfsSync(candidate);
|
|
7503
|
+
const freePercent = Number(stats.bavail) / Number(stats.blocks) * 100;
|
|
7504
|
+
const minimum = mount.class === "ephemeral" ? 0 : mount.minimumFreePercent ?? 0;
|
|
7505
|
+
if (!Number.isFinite(freePercent) || freePercent < minimum)
|
|
7506
|
+
throw new DeploymentActionError("refused", `storage ${mount.path} has ${freePercent.toFixed(1)}% free; ${minimum}% required`);
|
|
7507
|
+
}
|
|
7508
|
+
return { evidence: { mounts: mounts.length } };
|
|
7509
|
+
},
|
|
7510
|
+
"forgezero.oci/build@1": async ({ resolved }) => {
|
|
7511
|
+
if (!options.buildContainer)
|
|
7512
|
+
throw new DeploymentActionError("provider-unavailable", "the supervised container builder is unavailable");
|
|
7513
|
+
const componentName = String(resolved.component ?? "");
|
|
7514
|
+
const application = plan.spec.components[componentName];
|
|
7515
|
+
if (!application || application.kind !== "application" || application.runtime.kind !== "container")
|
|
7516
|
+
throw new DeploymentActionError("refused", `container component ${componentName} is invalid`);
|
|
7517
|
+
const result = await options.buildContainer({ key: options.key, revision: head, root: options.root, release, component: componentName, application, noCache: resolved.noCache === true });
|
|
7518
|
+
return { changed: true, outputs: { image: result.image, digest: result.digest } };
|
|
7519
|
+
},
|
|
7520
|
+
"forgezero.oci/pull@1": async ({ resolved }) => {
|
|
7521
|
+
if (!options.buildContainer)
|
|
7522
|
+
throw new DeploymentActionError("provider-unavailable", "the supervised container puller is unavailable");
|
|
7523
|
+
const componentName = String(resolved.component ?? "");
|
|
7524
|
+
const application = plan.spec.components[componentName];
|
|
7525
|
+
if (!application || application.kind !== "application" || application.runtime.kind !== "container")
|
|
7526
|
+
throw new DeploymentActionError("refused", `container component ${componentName} is invalid`);
|
|
7527
|
+
const result = await options.buildContainer({ key: options.key, revision: head, root: options.root, release, component: componentName, application });
|
|
7528
|
+
return { changed: true, outputs: { image: result.image, digest: result.digest } };
|
|
7529
|
+
},
|
|
7530
|
+
"forgezero.service/deploy@1": async ({ resolved }) => {
|
|
7531
|
+
if (!options.activateContainer)
|
|
7532
|
+
throw new DeploymentActionError("provider-unavailable", "the supervised container activator is unavailable");
|
|
7533
|
+
const componentName = String(resolved.component ?? "");
|
|
7534
|
+
const application = plan.spec.components[componentName];
|
|
7535
|
+
const image = String(resolved.imageDigest ?? "");
|
|
7536
|
+
if (!application || application.kind !== "application" || application.runtime.kind !== "container" || !image)
|
|
7537
|
+
throw new DeploymentActionError("refused", `container deployment ${componentName} is invalid`);
|
|
7538
|
+
const state = await options.activateContainer({ key: options.key, revision: head, root: options.root, release, component: componentName, application, image });
|
|
7539
|
+
return { changed: true, outputs: { activeSlot: state.activeSlot, publicPort: state.publicPort } };
|
|
7540
|
+
},
|
|
7541
|
+
"forgezero.service/promote@1": async ({ resolved }) => {
|
|
7542
|
+
if (!options.activateContainer)
|
|
7543
|
+
throw new DeploymentActionError("provider-unavailable", "the supervised container activator is unavailable");
|
|
7544
|
+
const componentName = String(resolved.component ?? "");
|
|
7545
|
+
const application = plan.spec.components[componentName];
|
|
7546
|
+
const image = String(resolved.imageDigest ?? "");
|
|
7547
|
+
if (!application || application.kind !== "application" || application.runtime.kind !== "container" || !image)
|
|
7548
|
+
throw new DeploymentActionError("refused", `container promotion ${componentName} is invalid`);
|
|
7549
|
+
const state = await options.activateContainer({ key: options.key, revision: head, root: options.root, release, component: componentName, application, image });
|
|
7550
|
+
return { changed: true, outputs: { activeSlot: state.activeSlot, publicPort: state.publicPort } };
|
|
7551
|
+
}
|
|
7552
|
+
};
|
|
7553
|
+
const run2 = await runDeploymentPlan({
|
|
7554
|
+
plan,
|
|
7555
|
+
workflow: "deploy",
|
|
7556
|
+
inputs: inputValues,
|
|
7557
|
+
actions: actions2,
|
|
7558
|
+
shouldRun: (step2) => step2.scope.kind === "release-executor" || step2.scope.kind === "elected-one" ? request.releaseExecutor === true : targetNames.has(step2.scope.target)
|
|
7559
|
+
});
|
|
7560
|
+
const phase = {
|
|
7561
|
+
pipeline: `${plan.name}:deploy`,
|
|
7562
|
+
ok: run2.ok,
|
|
7563
|
+
assurance: assurance === "disabled" ? "enrolled" : options.attestation ? "attested" : "enrolled",
|
|
7564
|
+
steps: run2.steps.map((step2) => ({
|
|
7565
|
+
name: step2.id,
|
|
7566
|
+
outcome: step2.status === "succeeded" || step2.status === "compensated" ? "ok" : step2.status === "skipped" ? "skipped" : "failed",
|
|
7567
|
+
exitCode: step2.status === "succeeded" || step2.status === "compensated" ? 0 : step2.status === "skipped" ? null : 1,
|
|
7568
|
+
log: step2.error?.message ?? "",
|
|
7569
|
+
durationMs: (step2.finishedAtTs ?? step2.startedAtTs ?? 0) - (step2.startedAtTs ?? 0)
|
|
7570
|
+
}))
|
|
7571
|
+
};
|
|
7572
|
+
if (!run2.ok) {
|
|
7573
|
+
const failed = run2.steps.find((step2) => step2.status === "failed" || step2.status === "manual-intervention");
|
|
7574
|
+
throw new DeploymentError("PIPELINE_FAILED", `deployment plan failed at ${failed?.id ?? "unknown step"}: ${failed?.error?.message ?? run2.status}`);
|
|
7575
|
+
}
|
|
7576
|
+
return {
|
|
7577
|
+
key: options.key,
|
|
7578
|
+
repository: options.repository,
|
|
7579
|
+
branch: options.branch,
|
|
7580
|
+
revision: head,
|
|
7581
|
+
definitionDigest: plan.sourceDigest,
|
|
7582
|
+
definitionVersion: plan.version,
|
|
7583
|
+
profile: options.profile,
|
|
7584
|
+
release,
|
|
7585
|
+
ok: true,
|
|
7586
|
+
phases: [phase]
|
|
7587
|
+
};
|
|
7588
|
+
}
|
|
6151
7589
|
const definition = parseDeployDefinition(readDefinition(join2(release, ".fz", "deploy.json")));
|
|
6152
7590
|
const definitionDigest = deployDefinitionDigest(definition);
|
|
6153
7591
|
const selectedProfile = definition.profiles[options.profile];
|
|
@@ -6193,7 +7631,7 @@ function createDeploymentManager(options) {
|
|
|
6193
7631
|
phase,
|
|
6194
7632
|
name: `${definition.name}:${phase}`,
|
|
6195
7633
|
requireAttestation: definition.requireAttestation,
|
|
6196
|
-
steps: definition.steps.filter((
|
|
7634
|
+
steps: definition.steps.filter((step2) => step2.phase === phase && (!step2.profiles || step2.profiles.includes(options.profile)) && (step2.scope === "target" || request.releaseExecutor === true) && (!step2.when || Object.entries(step2.when).every(([name, expected]) => phaseEnvironment[name] === expected)))
|
|
6197
7635
|
};
|
|
6198
7636
|
})
|
|
6199
7637
|
].filter((pipeline) => pipeline.steps.length > 0);
|
|
@@ -6220,7 +7658,7 @@ function createDeploymentManager(options) {
|
|
|
6220
7658
|
const result = await runPipeline({ pipeline, secret, exec: phaseExec, attest });
|
|
6221
7659
|
phases.push(result);
|
|
6222
7660
|
if (!result.ok) {
|
|
6223
|
-
const failedStep = result.steps.find((
|
|
7661
|
+
const failedStep = result.steps.find((step2) => step2.outcome === "failed");
|
|
6224
7662
|
const detail = failedStep?.log.trim().slice(0, 2000);
|
|
6225
7663
|
throw new DeploymentError("PIPELINE_FAILED", `${pipeline.name} failed at ${failedStep?.name ?? "unknown step"}` + (detail ? `: ${detail}` : ` (exit ${failedStep?.exitCode ?? "unknown"}).`));
|
|
6226
7664
|
}
|
|
@@ -6312,7 +7750,7 @@ function createDeploymentManager(options) {
|
|
|
6312
7750
|
}
|
|
6313
7751
|
|
|
6314
7752
|
// src/control.ts
|
|
6315
|
-
import { chmodSync as chmodSync4, existsSync as
|
|
7753
|
+
import { chmodSync as chmodSync4, existsSync as existsSync4, unlinkSync as unlinkSync3 } from "fs";
|
|
6316
7754
|
import { connect, createServer as createServer2 } from "net";
|
|
6317
7755
|
var DEFAULT_CONTROL_SOCKET = "/run/forgezero/control.sock";
|
|
6318
7756
|
var MAX_REQUEST_BYTES = 16 * 1024;
|
|
@@ -6369,7 +7807,7 @@ async function handleControl(manager, request) {
|
|
|
6369
7807
|
}
|
|
6370
7808
|
}
|
|
6371
7809
|
function startControlServer(manager, socketPath = DEFAULT_CONTROL_SOCKET) {
|
|
6372
|
-
if (
|
|
7810
|
+
if (existsSync4(socketPath))
|
|
6373
7811
|
unlinkSync3(socketPath);
|
|
6374
7812
|
const server = createServer2((socket) => {
|
|
6375
7813
|
let buffer = "";
|
|
@@ -6487,7 +7925,7 @@ async function deployClaim(options, claim) {
|
|
|
6487
7925
|
}
|
|
6488
7926
|
async function completeSigned(options, body) {
|
|
6489
7927
|
const attempts = Math.max(1, Math.min(options.completionAttempts ?? 5, 10));
|
|
6490
|
-
const
|
|
7928
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
6491
7929
|
let last;
|
|
6492
7930
|
for (let attempt = 1;attempt <= attempts; attempt += 1) {
|
|
6493
7931
|
try {
|
|
@@ -6498,7 +7936,7 @@ async function completeSigned(options, body) {
|
|
|
6498
7936
|
if (cause instanceof SignedNodeHttpError && cause.status < 500)
|
|
6499
7937
|
throw cause;
|
|
6500
7938
|
if (attempt < attempts)
|
|
6501
|
-
await
|
|
7939
|
+
await sleep2(Math.min(2000, 250 * 2 ** (attempt - 1)));
|
|
6502
7940
|
}
|
|
6503
7941
|
}
|
|
6504
7942
|
throw last;
|
|
@@ -6589,7 +8027,7 @@ function startDeploymentPull(options) {
|
|
|
6589
8027
|
// src/guest-enrolment.ts
|
|
6590
8028
|
import {
|
|
6591
8029
|
chmodSync as chmodSync5,
|
|
6592
|
-
existsSync as
|
|
8030
|
+
existsSync as existsSync5,
|
|
6593
8031
|
mkdirSync as mkdirSync3,
|
|
6594
8032
|
readFileSync as readFileSync3,
|
|
6595
8033
|
renameSync as renameSync3,
|
|
@@ -6621,11 +8059,11 @@ function privateNetworkAttachmentFromEnvironment(env = process.env) {
|
|
|
6621
8059
|
var validBinding = (value, expectedNodeKey) => {
|
|
6622
8060
|
if (!value || typeof value !== "object")
|
|
6623
8061
|
return false;
|
|
6624
|
-
const
|
|
6625
|
-
return ["nodeKey", "computeReference", "projectKey", "environmentKey"].every((key) => typeof
|
|
8062
|
+
const row2 = value;
|
|
8063
|
+
return ["nodeKey", "computeReference", "projectKey", "environmentKey"].every((key) => typeof row2[key] === "string" && row2[key].length > 0) && (row2.realm === "platform" || row2.realm === "tenant" && typeof row2.tenantSlug === "string" && row2.tenantSlug.length > 0) && (!expectedNodeKey || row2.nodeKey === expectedNodeKey);
|
|
6626
8064
|
};
|
|
6627
8065
|
function loadGuestBinding(path2, expectedNodeKey) {
|
|
6628
|
-
if (!
|
|
8066
|
+
if (!existsSync5(path2))
|
|
6629
8067
|
return null;
|
|
6630
8068
|
const mode = statSync(path2).mode & 511;
|
|
6631
8069
|
if ((mode & 63) !== 0) {
|
|
@@ -6751,7 +8189,7 @@ async function runClaim(options, claim) {
|
|
|
6751
8189
|
}
|
|
6752
8190
|
async function complete(options, body) {
|
|
6753
8191
|
const attempts = Math.max(1, Math.min(options.completionAttempts ?? 5, 10));
|
|
6754
|
-
const
|
|
8192
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
6755
8193
|
let last;
|
|
6756
8194
|
for (let attempt = 1;attempt <= attempts; attempt += 1) {
|
|
6757
8195
|
try {
|
|
@@ -6762,7 +8200,7 @@ async function complete(options, body) {
|
|
|
6762
8200
|
if (cause instanceof SignedNodeHttpError && cause.status < 500)
|
|
6763
8201
|
throw cause;
|
|
6764
8202
|
if (attempt < attempts)
|
|
6765
|
-
await
|
|
8203
|
+
await sleep2(Math.min(2000, 250 * 2 ** (attempt - 1)));
|
|
6766
8204
|
}
|
|
6767
8205
|
}
|
|
6768
8206
|
throw last;
|
|
@@ -6860,7 +8298,7 @@ var remoteOutcome3 = (cause) => cause instanceof SignedNodeHttpError && cause.st
|
|
|
6860
8298
|
var post2 = (options, operation, body) => postSignedNode(options, `v1/node/migrations/${operation}`, body);
|
|
6861
8299
|
async function complete2(options, body) {
|
|
6862
8300
|
const attempts = Math.max(1, Math.min(options.completionAttempts ?? 5, 10));
|
|
6863
|
-
const
|
|
8301
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
6864
8302
|
let last;
|
|
6865
8303
|
for (let attempt = 1;attempt <= attempts; attempt += 1) {
|
|
6866
8304
|
try {
|
|
@@ -6871,7 +8309,7 @@ async function complete2(options, body) {
|
|
|
6871
8309
|
if (cause instanceof SignedNodeHttpError && cause.status < 500)
|
|
6872
8310
|
throw cause;
|
|
6873
8311
|
if (attempt < attempts)
|
|
6874
|
-
await
|
|
8312
|
+
await sleep2(Math.min(2000, 250 * 2 ** (attempt - 1)));
|
|
6875
8313
|
}
|
|
6876
8314
|
}
|
|
6877
8315
|
throw last;
|
|
@@ -6988,7 +8426,7 @@ function startMigrationPull(options) {
|
|
|
6988
8426
|
}
|
|
6989
8427
|
|
|
6990
8428
|
// src/ssh-bootstrap.ts
|
|
6991
|
-
import { createHash as
|
|
8429
|
+
import { createHash as createHash5 } from "crypto";
|
|
6992
8430
|
import { chmodSync as chmodSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
6993
8431
|
import { tmpdir as tmpdir2 } from "os";
|
|
6994
8432
|
import { join as join3 } from "path";
|
|
@@ -7010,7 +8448,7 @@ function assertSupportedGuestImage(imageKey) {
|
|
|
7010
8448
|
}
|
|
7011
8449
|
|
|
7012
8450
|
// src/version.ts
|
|
7013
|
-
var
|
|
8451
|
+
var VERSION2 = "0.1.56";
|
|
7014
8452
|
|
|
7015
8453
|
// src/ssh-bootstrap.ts
|
|
7016
8454
|
class SshBootstrapError extends Error {
|
|
@@ -7138,7 +8576,7 @@ var verifiedBunArchive = async (directory) => {
|
|
|
7138
8576
|
if (!response.ok)
|
|
7139
8577
|
throw new SshBootstrapError("BUN_DOWNLOAD_FAILED");
|
|
7140
8578
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
7141
|
-
if (
|
|
8579
|
+
if (createHash5("sha256").update(bytes).digest("hex") !== BUN_RELEASE_SHA256) {
|
|
7142
8580
|
throw new SshBootstrapError("BUN_CHECKSUM_MISMATCH");
|
|
7143
8581
|
}
|
|
7144
8582
|
const archive = join3(directory, "bun.zip");
|
|
@@ -7522,7 +8960,7 @@ async function executeMetalAdmission(claim, options) {
|
|
|
7522
8960
|
} },
|
|
7523
8961
|
bunVersion: PINNED_BUN_VERSION,
|
|
7524
8962
|
bunReleaseSha256: BUN_RELEASE_SHA256,
|
|
7525
|
-
agentVersion:
|
|
8963
|
+
agentVersion: VERSION2
|
|
7526
8964
|
}
|
|
7527
8965
|
};
|
|
7528
8966
|
const configPath = join3(directory, "metal.json");
|
|
@@ -7713,13 +9151,13 @@ function startMetalAdmissionPull(options) {
|
|
|
7713
9151
|
}
|
|
7714
9152
|
|
|
7715
9153
|
// src/metal-helper-socket.ts
|
|
7716
|
-
import { chmodSync as chmodSync7, existsSync as
|
|
9154
|
+
import { chmodSync as chmodSync7, existsSync as existsSync7, unlinkSync as unlinkSync6 } from "fs";
|
|
7717
9155
|
import { connect as connect2, createServer as createServer3 } from "net";
|
|
7718
9156
|
|
|
7719
9157
|
// src/metal-provision.ts
|
|
7720
|
-
import { createHash as
|
|
9158
|
+
import { createHash as createHash6 } from "crypto";
|
|
7721
9159
|
import {
|
|
7722
|
-
existsSync as
|
|
9160
|
+
existsSync as existsSync6,
|
|
7723
9161
|
mkdirSync as mkdirSync4,
|
|
7724
9162
|
readFileSync as readFileSync5,
|
|
7725
9163
|
readdirSync,
|
|
@@ -7856,8 +9294,8 @@ function membersOfLinuxList(value, label) {
|
|
|
7856
9294
|
throw new MetalProvisionError(`${label} list overlaps itself`);
|
|
7857
9295
|
return members;
|
|
7858
9296
|
}
|
|
7859
|
-
var guestNameFor = (computeKey) => `fzg-${
|
|
7860
|
-
var tapNameFor = (computeKey) => `fzt${
|
|
9297
|
+
var guestNameFor = (computeKey) => `fzg-${createHash6("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
|
|
9298
|
+
var tapNameFor = (computeKey) => `fzt${createHash6("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
|
|
7861
9299
|
var macForAddress = (address) => {
|
|
7862
9300
|
const octets = address.split(".").map(Number);
|
|
7863
9301
|
if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) {
|
|
@@ -7937,17 +9375,17 @@ function validateMetalProfile(profile) {
|
|
|
7937
9375
|
}
|
|
7938
9376
|
}
|
|
7939
9377
|
var readManifests = (stateDir) => {
|
|
7940
|
-
if (!
|
|
9378
|
+
if (!existsSync6(stateDir))
|
|
7941
9379
|
return [];
|
|
7942
9380
|
return readdirSync(stateDir).filter((name) => name.endsWith(".json")).map((name) => JSON.parse(readFileSync5(join4(stateDir, name), "utf8")));
|
|
7943
9381
|
};
|
|
7944
9382
|
function allocateAddress(profile, computeKey, rows) {
|
|
7945
|
-
const existing = rows.find((
|
|
9383
|
+
const existing = rows.find((row2) => row2.computeKey === computeKey);
|
|
7946
9384
|
if (existing)
|
|
7947
9385
|
return existing.address;
|
|
7948
|
-
const used = new Set(rows.map((
|
|
9386
|
+
const used = new Set(rows.map((row2) => row2.address));
|
|
7949
9387
|
const width = profile.addressEnd - profile.addressStart + 1;
|
|
7950
|
-
const start =
|
|
9388
|
+
const start = createHash6("sha256").update(computeKey).digest().readUInt16BE(0) % width;
|
|
7951
9389
|
for (let offset = 0;offset < width; offset += 1) {
|
|
7952
9390
|
const last = profile.addressStart + (start + offset) % width;
|
|
7953
9391
|
const address = `${profile.subnetPrefix}.${last}`;
|
|
@@ -7966,13 +9404,13 @@ function requestedAddress(profile, claim, rows) {
|
|
|
7966
9404
|
if (!Number.isInteger(last) || last < profile.addressStart || last > profile.addressEnd) {
|
|
7967
9405
|
throw new MetalProvisionError("requested guest address is outside the allocatable range");
|
|
7968
9406
|
}
|
|
7969
|
-
const occupied = rows.find((
|
|
9407
|
+
const occupied = rows.find((row2) => row2.address === claim.spec.guestAddress && row2.computeKey !== claim.computeKey);
|
|
7970
9408
|
if (occupied)
|
|
7971
9409
|
throw new MetalProvisionError("requested guest address is already allocated");
|
|
7972
9410
|
return claim.spec.guestAddress;
|
|
7973
9411
|
}
|
|
7974
9412
|
function allocateCpuPool(profile, claim, rows) {
|
|
7975
|
-
const prior = rows.find((
|
|
9413
|
+
const prior = rows.find((row2) => row2.computeKey === claim.computeKey);
|
|
7976
9414
|
if (prior) {
|
|
7977
9415
|
const retained = profile.cpuPools.find((pool) => pool.key === prior.cpuPoolKey);
|
|
7978
9416
|
if (!retained || retained.cpus !== prior.allowedCpus || retained.memoryNodes !== prior.allowedMemoryNodes) {
|
|
@@ -7980,7 +9418,7 @@ function allocateCpuPool(profile, claim, rows) {
|
|
|
7980
9418
|
}
|
|
7981
9419
|
return retained;
|
|
7982
9420
|
}
|
|
7983
|
-
const used = new Set(rows.map((
|
|
9421
|
+
const used = new Set(rows.map((row2) => row2.cpuPoolKey));
|
|
7984
9422
|
if (claim.spec.cpuPoolKey) {
|
|
7985
9423
|
const requested = profile.cpuPools.find((pool) => pool.key === claim.spec.cpuPoolKey);
|
|
7986
9424
|
if (!requested || used.has(requested.key)) {
|
|
@@ -8134,11 +9572,11 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
8134
9572
|
throw new MetalProvisionError("invalid SSH public key list");
|
|
8135
9573
|
}
|
|
8136
9574
|
const name = claim.spec.guestName ?? guestNameFor(claim.computeKey);
|
|
8137
|
-
const conflictingName = manifests.find((
|
|
9575
|
+
const conflictingName = manifests.find((row2) => row2.name === name && row2.computeKey !== claim.computeKey);
|
|
8138
9576
|
if (conflictingName)
|
|
8139
9577
|
throw new MetalProvisionError("requested guest name is already allocated");
|
|
8140
9578
|
const manifestPath = join4(profile.stateDir, `${name}.json`);
|
|
8141
|
-
const prior = manifests.find((
|
|
9579
|
+
const prior = manifests.find((row2) => row2.computeKey === claim.computeKey);
|
|
8142
9580
|
if (prior && prior.reference !== claim.spec.reference)
|
|
8143
9581
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
8144
9582
|
const address = requestedAddress(profile, claim, manifests);
|
|
@@ -8221,7 +9659,7 @@ function legacyPlatformManifest(profile, claim, name) {
|
|
|
8221
9659
|
return;
|
|
8222
9660
|
const legacyDir = profile.legacyStateDir ?? "/etc/forgezero/guests";
|
|
8223
9661
|
const legacyPath = join4(legacyDir, `${name}.conf`);
|
|
8224
|
-
if (!
|
|
9662
|
+
if (!existsSync6(legacyPath))
|
|
8225
9663
|
return;
|
|
8226
9664
|
const values = new Map;
|
|
8227
9665
|
for (const line of readFileSync5(legacyPath, "utf8").split(/\r?\n/)) {
|
|
@@ -8258,7 +9696,7 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
8258
9696
|
const service = `forgezero-guest@${name}.service`;
|
|
8259
9697
|
const unitPath = join4(profile.unitDir, service);
|
|
8260
9698
|
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
8261
|
-
const manifest =
|
|
9699
|
+
const manifest = existsSync6(manifestPath) ? JSON.parse(readFileSync5(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
|
|
8262
9700
|
if (!manifest) {
|
|
8263
9701
|
const seedBase = join4(profile.seedDir, name);
|
|
8264
9702
|
const localArtifacts = [
|
|
@@ -8268,7 +9706,7 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
8268
9706
|
`${seedBase}-meta-data`,
|
|
8269
9707
|
`${seedBase}-network-config`
|
|
8270
9708
|
];
|
|
8271
|
-
if (localArtifacts.some(
|
|
9709
|
+
if (localArtifacts.some(existsSync6)) {
|
|
8272
9710
|
throw new MetalProvisionError("guest artifacts exist without owned inventory");
|
|
8273
9711
|
}
|
|
8274
9712
|
const [volume, active] = await Promise.all([
|
|
@@ -8285,7 +9723,7 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
8285
9723
|
if (!currentIdentity && !legacyPlatformIdentity) {
|
|
8286
9724
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
8287
9725
|
}
|
|
8288
|
-
if (
|
|
9726
|
+
if (existsSync6(unitPath))
|
|
8289
9727
|
await checked2(exec, ["systemctl", "disable", "--now", service]);
|
|
8290
9728
|
else if (legacyPlatformIdentity)
|
|
8291
9729
|
await checked2(exec, ["systemctl", "disable", "--now", service]);
|
|
@@ -8302,14 +9740,14 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
8302
9740
|
join4(profile.seedDir, `${name}-network-config`),
|
|
8303
9741
|
manifestPath
|
|
8304
9742
|
])
|
|
8305
|
-
if (
|
|
9743
|
+
if (existsSync6(path2))
|
|
8306
9744
|
unlinkSync5(path2);
|
|
8307
9745
|
if (legacyPlatformIdentity) {
|
|
8308
9746
|
const legacyConfig = join4(profile.legacyStateDir ?? "/etc/forgezero/guests", `${name}.conf`);
|
|
8309
9747
|
const legacyDropIn = join4(profile.unitDir, `${service}.d`);
|
|
8310
|
-
if (
|
|
9748
|
+
if (existsSync6(legacyConfig))
|
|
8311
9749
|
unlinkSync5(legacyConfig);
|
|
8312
|
-
if (
|
|
9750
|
+
if (existsSync6(legacyDropIn))
|
|
8313
9751
|
rmSync3(legacyDropIn, { recursive: true });
|
|
8314
9752
|
}
|
|
8315
9753
|
await checked2(exec, ["systemctl", "daemon-reload"]);
|
|
@@ -8348,7 +9786,7 @@ command exceeded ${COMMAND_TIMEOUT_MS}ms`.trim() : stderr
|
|
|
8348
9786
|
function startMetalHelper(options) {
|
|
8349
9787
|
validateMetalProfile(options.profile);
|
|
8350
9788
|
const socketPath = options.socketPath ?? DEFAULT_METAL_HELPER_SOCKET;
|
|
8351
|
-
if (
|
|
9789
|
+
if (existsSync7(socketPath))
|
|
8352
9790
|
unlinkSync6(socketPath);
|
|
8353
9791
|
let tail = Promise.resolve();
|
|
8354
9792
|
const server = createServer3((socket) => {
|
|
@@ -8435,7 +9873,7 @@ function requestMetalProvision(claim, socketPath = DEFAULT_METAL_HELPER_SOCKET)
|
|
|
8435
9873
|
}
|
|
8436
9874
|
|
|
8437
9875
|
// src/deployment-runner.ts
|
|
8438
|
-
import { chmodSync as chmodSync8, existsSync as
|
|
9876
|
+
import { chmodSync as chmodSync8, existsSync as existsSync8, realpathSync, unlinkSync as unlinkSync7 } from "fs";
|
|
8439
9877
|
import { isAbsolute as isAbsolute4, resolve, sep } from "path";
|
|
8440
9878
|
import { connect as connect3, createServer as createServer4 } from "net";
|
|
8441
9879
|
var DEFAULT_DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
@@ -8552,7 +9990,7 @@ function startDeploymentRunner(options) {
|
|
|
8552
9990
|
const root = resolve(options.root);
|
|
8553
9991
|
const home = resolve(options.home);
|
|
8554
9992
|
const socketPath = options.socketPath ?? DEFAULT_DEPLOYMENT_RUNNER_SOCKET;
|
|
8555
|
-
if (
|
|
9993
|
+
if (existsSync8(socketPath))
|
|
8556
9994
|
unlinkSync7(socketPath);
|
|
8557
9995
|
const active = new Set;
|
|
8558
9996
|
const server = createServer4((socket) => {
|
|
@@ -8648,7 +10086,7 @@ function requestDeploymentCommand(input, socketPath = DEFAULT_DEPLOYMENT_RUNNER_
|
|
|
8648
10086
|
}
|
|
8649
10087
|
|
|
8650
10088
|
// src/snp-attestation.ts
|
|
8651
|
-
import { existsSync as
|
|
10089
|
+
import { existsSync as existsSync9 } from "fs";
|
|
8652
10090
|
|
|
8653
10091
|
// ../runtime/dist/snp.js
|
|
8654
10092
|
var REPORT_BYTES = 1184;
|
|
@@ -8741,7 +10179,7 @@ function createSnpAttestationSource(options = {}) {
|
|
|
8741
10179
|
const python = options.python ?? "python3";
|
|
8742
10180
|
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
8743
10181
|
const spawn = options.spawn ?? Bun.spawn;
|
|
8744
|
-
const exists = options.exists ??
|
|
10182
|
+
const exists = options.exists ?? existsSync9;
|
|
8745
10183
|
return {
|
|
8746
10184
|
name: "linux-sev-guest",
|
|
8747
10185
|
async report(nonce) {
|
|
@@ -8949,7 +10387,7 @@ async function closeServerWithin(server, timeoutMs) {
|
|
|
8949
10387
|
}
|
|
8950
10388
|
|
|
8951
10389
|
// src/lifecycle-helper.ts
|
|
8952
|
-
import { chmodSync as chmodSync9, existsSync as
|
|
10390
|
+
import { chmodSync as chmodSync9, existsSync as existsSync10, readFileSync as readFileSync6, unlinkSync as unlinkSync8 } from "fs";
|
|
8953
10391
|
import { connect as connect4, createConnection, createServer as createServer5, isIP as isIP3 } from "net";
|
|
8954
10392
|
var DEFAULT_LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
|
|
8955
10393
|
var MAX_REQUEST_BYTES4 = 16 * 1024;
|
|
@@ -9090,7 +10528,7 @@ async function executeLifecycleAction(profile, claim, exec = spawnLifecycleComma
|
|
|
9090
10528
|
function startLifecycleHelper(options) {
|
|
9091
10529
|
validateLifecycleProfile(options.profile);
|
|
9092
10530
|
const socketPath = options.socketPath ?? DEFAULT_LIFECYCLE_HELPER_SOCKET;
|
|
9093
|
-
if (
|
|
10531
|
+
if (existsSync10(socketPath))
|
|
9094
10532
|
unlinkSync8(socketPath);
|
|
9095
10533
|
let tail = Promise.resolve();
|
|
9096
10534
|
const server = createServer5((socket) => {
|
|
@@ -9252,11 +10690,11 @@ async function configureMeshConnector(input) {
|
|
|
9252
10690
|
}
|
|
9253
10691
|
|
|
9254
10692
|
// src/agent-update.ts
|
|
9255
|
-
import { createHash as
|
|
10693
|
+
import { createHash as createHash7, timingSafeEqual, randomUUID as randomUUID2 } from "crypto";
|
|
9256
10694
|
import {
|
|
9257
10695
|
chmodSync as chmodSync11,
|
|
9258
10696
|
closeSync as closeSync2,
|
|
9259
|
-
existsSync as
|
|
10697
|
+
existsSync as existsSync11,
|
|
9260
10698
|
fsyncSync,
|
|
9261
10699
|
mkdirSync as mkdirSync7,
|
|
9262
10700
|
openSync as openSync2,
|
|
@@ -9271,7 +10709,7 @@ import { dirname as dirname4, join as join7, resolve as resolve2 } from "path";
|
|
|
9271
10709
|
var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
|
|
9272
10710
|
var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
|
|
9273
10711
|
var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
|
|
9274
|
-
var
|
|
10712
|
+
var VERSION3 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
9275
10713
|
var REGISTRY = "registry.npmjs.org";
|
|
9276
10714
|
var syncPath = (path2) => {
|
|
9277
10715
|
const descriptor = openSync2(path2, "r");
|
|
@@ -9296,7 +10734,7 @@ var syncReleaseDirectory = (directory) => {
|
|
|
9296
10734
|
function validateAgentRelease(release) {
|
|
9297
10735
|
if (release?.package !== "@forgezero/agent")
|
|
9298
10736
|
throw new Error("agent update package is fixed");
|
|
9299
|
-
if (!
|
|
10737
|
+
if (!VERSION3.test(release.version))
|
|
9300
10738
|
throw new Error("agent update version must be exact semver");
|
|
9301
10739
|
const expectedTarball = `/@forgezero/agent/-/agent-${release.version}.tgz`;
|
|
9302
10740
|
let url;
|
|
@@ -9314,7 +10752,7 @@ function validateAgentRelease(release) {
|
|
|
9314
10752
|
return release;
|
|
9315
10753
|
}
|
|
9316
10754
|
function compareVersions(left, right) {
|
|
9317
|
-
if (!
|
|
10755
|
+
if (!VERSION3.test(left) || !VERSION3.test(right))
|
|
9318
10756
|
throw new Error("agent version must be exact semver");
|
|
9319
10757
|
const a = left.split(".").map(Number);
|
|
9320
10758
|
const b = right.split(".").map(Number);
|
|
@@ -9396,7 +10834,7 @@ async function stageAgentRelease(releaseInput, options) {
|
|
|
9396
10834
|
throw new Error("agent update tarball is empty or exceeds the size limit");
|
|
9397
10835
|
}
|
|
9398
10836
|
const expected = Buffer.from(release.integrity.slice("sha512-".length), "base64");
|
|
9399
|
-
const actual =
|
|
10837
|
+
const actual = createHash7("sha512").update(bytes).digest();
|
|
9400
10838
|
if (!timingSafeEqual(actual, expected))
|
|
9401
10839
|
throw new Error("agent update integrity mismatch");
|
|
9402
10840
|
writeFileSync8(archive, bytes, { mode: 384, flag: "wx" });
|
|
@@ -9415,19 +10853,19 @@ async function stageAgentRelease(releaseInput, options) {
|
|
|
9415
10853
|
writeFileSync8(destination, extracted.output, { mode: 384, flag: "wx" });
|
|
9416
10854
|
}
|
|
9417
10855
|
await validateReleaseDirectory(unpacked, release, run2);
|
|
9418
|
-
if (!
|
|
10856
|
+
if (!existsSync11(finalDirectory)) {
|
|
9419
10857
|
renameSync5(unpacked, finalDirectory);
|
|
9420
10858
|
syncReleaseDirectory(finalDirectory);
|
|
9421
10859
|
} else
|
|
9422
10860
|
await validateReleaseDirectory(finalDirectory, release, run2);
|
|
9423
|
-
if (!
|
|
10861
|
+
if (!existsSync11(currentLink)) {
|
|
9424
10862
|
throw new Error("agent update requires an active immutable release to roll back to");
|
|
9425
10863
|
}
|
|
9426
10864
|
const previousTarget = readlinkSync(currentLink);
|
|
9427
10865
|
if (previousTarget !== join7("versions", options.currentVersion)) {
|
|
9428
10866
|
throw new Error("agent update current release does not match the running version");
|
|
9429
10867
|
}
|
|
9430
|
-
if (!
|
|
10868
|
+
if (!existsSync11(join7(root, previousTarget))) {
|
|
9431
10869
|
throw new Error("agent update rollback release is missing");
|
|
9432
10870
|
}
|
|
9433
10871
|
return {
|
|
@@ -9468,7 +10906,7 @@ import { randomUUID as randomUUID3 } from "crypto";
|
|
|
9468
10906
|
import {
|
|
9469
10907
|
chmodSync as chmodSync12,
|
|
9470
10908
|
closeSync as closeSync3,
|
|
9471
|
-
existsSync as
|
|
10909
|
+
existsSync as existsSync12,
|
|
9472
10910
|
fsyncSync as fsyncSync2,
|
|
9473
10911
|
mkdirSync as mkdirSync8,
|
|
9474
10912
|
openSync as openSync3,
|
|
@@ -9491,7 +10929,7 @@ var COMPUTE_HELPER_UNITS = [
|
|
|
9491
10929
|
"forgezero-lifecycle-helper.service",
|
|
9492
10930
|
"forgezero-software-helper.service"
|
|
9493
10931
|
];
|
|
9494
|
-
var
|
|
10932
|
+
var VERSION4 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
9495
10933
|
var ATTEMPT_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
9496
10934
|
var REASON_CODE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
9497
10935
|
var MAX_REASON_BYTES = 512;
|
|
@@ -9514,9 +10952,9 @@ function validateReceipt(value) {
|
|
|
9514
10952
|
const receipt = value;
|
|
9515
10953
|
if (!receipt.attemptId || !ATTEMPT_ID.test(receipt.attemptId))
|
|
9516
10954
|
throw new Error("Agent update attempt ID is invalid");
|
|
9517
|
-
if (!receipt.fromVersion || !
|
|
10955
|
+
if (!receipt.fromVersion || !VERSION4.test(receipt.fromVersion))
|
|
9518
10956
|
throw new Error("Agent update source version is invalid");
|
|
9519
|
-
if (!receipt.targetVersion || !
|
|
10957
|
+
if (!receipt.targetVersion || !VERSION4.test(receipt.targetVersion))
|
|
9520
10958
|
throw new Error("Agent update target version is invalid");
|
|
9521
10959
|
if (!["activating", "active", "rolled-back", "failed"].includes(receipt.outcome ?? "")) {
|
|
9522
10960
|
throw new Error("Agent update outcome is invalid");
|
|
@@ -9549,7 +10987,7 @@ function validateJournal(value, root) {
|
|
|
9549
10987
|
throw new Error("Agent update journal is malformed");
|
|
9550
10988
|
const legacy = value;
|
|
9551
10989
|
if (legacy.schemaVersion === undefined) {
|
|
9552
|
-
if (typeof legacy.version === "string" &&
|
|
10990
|
+
if (typeof legacy.version === "string" && VERSION4.test(legacy.version) && legacy.outcome === "active" && validTime(legacy.updatedAtTs) && Object.keys(value).every((key) => ["version", "outcome", "updatedAtTs"].includes(key)))
|
|
9553
10991
|
return;
|
|
9554
10992
|
throw new Error("Agent update legacy receipt is malformed");
|
|
9555
10993
|
}
|
|
@@ -9574,7 +11012,7 @@ function validateJournal(value, root) {
|
|
|
9574
11012
|
return journal;
|
|
9575
11013
|
}
|
|
9576
11014
|
function readJournal(path2, root) {
|
|
9577
|
-
if (!
|
|
11015
|
+
if (!existsSync12(path2))
|
|
9578
11016
|
return;
|
|
9579
11017
|
return validateJournal(JSON.parse(readFileSync9(path2, "utf8")), root);
|
|
9580
11018
|
}
|
|
@@ -9632,7 +11070,7 @@ function writeUpdateState(journalPath, receiptPath, journal) {
|
|
|
9632
11070
|
}
|
|
9633
11071
|
function readAgentUpdateReceipt(path2 = AGENT_UPDATE_RECEIPT) {
|
|
9634
11072
|
try {
|
|
9635
|
-
if (!
|
|
11073
|
+
if (!existsSync12(path2))
|
|
9636
11074
|
return;
|
|
9637
11075
|
return validateReceipt(JSON.parse(readFileSync9(path2, "utf8")));
|
|
9638
11076
|
} catch {
|
|
@@ -9788,7 +11226,7 @@ async function recoverInterruptedAgentUpdate(options = {}) {
|
|
|
9788
11226
|
return publicReceipt(journal);
|
|
9789
11227
|
}
|
|
9790
11228
|
const staged = stagedFromJournal(journal, root);
|
|
9791
|
-
if (!
|
|
11229
|
+
if (!existsSync12(join8(root, journal.previousTarget))) {
|
|
9792
11230
|
throw new Error("Agent update rollback release is missing");
|
|
9793
11231
|
}
|
|
9794
11232
|
const run2 = options.run ?? runCommand;
|
|
@@ -9818,7 +11256,7 @@ async function recoverInterruptedAgentUpdate(options = {}) {
|
|
|
9818
11256
|
}
|
|
9819
11257
|
function startAgentUpdateHelper(options = {}) {
|
|
9820
11258
|
const socketPath = options.socketPath ?? DEFAULT_AGENT_UPDATE_SOCKET;
|
|
9821
|
-
if (
|
|
11259
|
+
if (existsSync12(socketPath))
|
|
9822
11260
|
unlinkSync10(socketPath);
|
|
9823
11261
|
mkdirSync8(dirname5(socketPath), { recursive: true, mode: 488 });
|
|
9824
11262
|
const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
|
|
@@ -9935,7 +11373,7 @@ var remoteOutcome4 = (cause) => cause instanceof SignedNodeHttpError && cause.st
|
|
|
9935
11373
|
|
|
9936
11374
|
class AgentUpdateRefusedError extends Error {
|
|
9937
11375
|
}
|
|
9938
|
-
function observeAgentHost(version =
|
|
11376
|
+
function observeAgentHost(version = VERSION2, mode = "enrolled", osRelease = readFileSync10("/etc/os-release", "utf8"), architecture = process.arch) {
|
|
9939
11377
|
const values = Object.fromEntries(osRelease.split(`
|
|
9940
11378
|
`).flatMap((line) => {
|
|
9941
11379
|
const separator = line.indexOf("=");
|
|
@@ -9949,7 +11387,7 @@ function observeAgentHost(version = VERSION, mode = "enrolled", osRelease = read
|
|
|
9949
11387
|
};
|
|
9950
11388
|
}
|
|
9951
11389
|
async function heartbeatAgentOnce(options) {
|
|
9952
|
-
let observation = (options.observation ?? (() => observeAgentHost(options.version ??
|
|
11390
|
+
let observation = (options.observation ?? (() => observeAgentHost(options.version ?? VERSION2, options.mode)))();
|
|
9953
11391
|
const update = observation.update ?? readAgentUpdateReceipt(options.receiptPath ?? AGENT_UPDATE_RECEIPT);
|
|
9954
11392
|
if (update)
|
|
9955
11393
|
observation = { ...observation, update };
|
|
@@ -10053,19 +11491,19 @@ function startAgentHeartbeat(options) {
|
|
|
10053
11491
|
}
|
|
10054
11492
|
|
|
10055
11493
|
// src/software-helper.ts
|
|
10056
|
-
import { chmodSync as chmodSync13, existsSync as
|
|
11494
|
+
import { chmodSync as chmodSync13, existsSync as existsSync15, mkdirSync as mkdirSync11, unlinkSync as unlinkSync11 } from "fs";
|
|
10057
11495
|
import { connect as connect6, createServer as createServer7 } from "net";
|
|
10058
|
-
import { dirname as
|
|
11496
|
+
import { dirname as dirname8 } from "path";
|
|
10059
11497
|
|
|
10060
11498
|
// src/service-supervisor.ts
|
|
10061
|
-
import { createHash as
|
|
10062
|
-
import { existsSync as
|
|
11499
|
+
import { createHash as createHash8 } from "crypto";
|
|
11500
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync11, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync7, rmSync as rmSync6, writeFileSync as writeFileSync10 } from "fs";
|
|
10063
11501
|
import { dirname as dirname6, join as join9, resolve as resolve4, sep as sep2 } from "path";
|
|
10064
11502
|
var SERVICE_STATE_DIRECTORY = "/var/lib/forgezero/services";
|
|
10065
11503
|
var SERVICE_CONFIG_DIRECTORY = "/etc/forgezero/services";
|
|
10066
11504
|
var SERVICE_UNIT_DIRECTORY = "/etc/systemd/system";
|
|
10067
11505
|
var SERVICE_NGINX_DIRECTORY = "/etc/nginx/conf.d";
|
|
10068
|
-
var idFor = (key) =>
|
|
11506
|
+
var idFor = (key) => createHash8("sha256").update(key).digest("hex").slice(0, 24);
|
|
10069
11507
|
var statePath = (id2) => `${SERVICE_STATE_DIRECTORY}/${id2}.json`;
|
|
10070
11508
|
var within2 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep2}`);
|
|
10071
11509
|
var defaultHost = {
|
|
@@ -10076,8 +11514,8 @@ var defaultHost = {
|
|
|
10076
11514
|
renameSync7(next, path2);
|
|
10077
11515
|
},
|
|
10078
11516
|
read: (path2) => readFileSync11(path2, "utf8"),
|
|
10079
|
-
exists:
|
|
10080
|
-
list: (path2) =>
|
|
11517
|
+
exists: existsSync13,
|
|
11518
|
+
list: (path2) => existsSync13(path2) ? readdirSync2(path2) : [],
|
|
10081
11519
|
realpath: realpathSync2,
|
|
10082
11520
|
mkdir: (path2, mode) => mkdirSync9(path2, { recursive: true, mode }),
|
|
10083
11521
|
remove: (path2) => rmSync6(path2, { force: true }),
|
|
@@ -10138,7 +11576,7 @@ function allocatedPorts(request, id2, previous, states) {
|
|
|
10138
11576
|
if (previous && previous.applicationPorts.length === required && previous.applicationPorts.every((port) => port >= allocation.from && port <= allocation.to && !occupied.has(port)))
|
|
10139
11577
|
return [...previous.applicationPorts];
|
|
10140
11578
|
const width = allocation.to - allocation.from + 1;
|
|
10141
|
-
const start = Number.parseInt(
|
|
11579
|
+
const start = Number.parseInt(createHash8("sha256").update(request.key).digest("hex").slice(0, 8), 16) % width;
|
|
10142
11580
|
for (let offset = 0;offset < width; offset += 1) {
|
|
10143
11581
|
const first = allocation.from + (start + offset) % width;
|
|
10144
11582
|
const candidate = Array.from({ length: required }, (_, index) => first + index);
|
|
@@ -10274,6 +11712,297 @@ async function activateSupervisedService(request, host = defaultHost) {
|
|
|
10274
11712
|
return state;
|
|
10275
11713
|
}
|
|
10276
11714
|
|
|
11715
|
+
// src/container-supervisor.ts
|
|
11716
|
+
import { createHash as createHash9 } from "crypto";
|
|
11717
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync12, readdirSync as readdirSync3, realpathSync as realpathSync3, renameSync as renameSync8, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "fs";
|
|
11718
|
+
import { dirname as dirname7, resolve as resolve5, sep as sep3 } from "path";
|
|
11719
|
+
var defaultHost2 = {
|
|
11720
|
+
realpath: realpathSync3,
|
|
11721
|
+
exists: existsSync14,
|
|
11722
|
+
read: (path2) => readFileSync12(path2, "utf8"),
|
|
11723
|
+
write(path2, content, mode) {
|
|
11724
|
+
mkdirSync10(dirname7(path2), { recursive: true, mode: 493 });
|
|
11725
|
+
const next = `${path2}.next`;
|
|
11726
|
+
writeFileSync11(next, content, { mode });
|
|
11727
|
+
renameSync8(next, path2);
|
|
11728
|
+
},
|
|
11729
|
+
remove: (path2) => rmSync7(path2, { force: true }),
|
|
11730
|
+
list: (path2) => existsSync14(path2) ? readdirSync3(path2) : [],
|
|
11731
|
+
mkdir: (path2, mode) => mkdirSync10(path2, { recursive: true, mode }),
|
|
11732
|
+
async exec(argv2) {
|
|
11733
|
+
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" } });
|
|
11734
|
+
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
11735
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
11736
|
+
},
|
|
11737
|
+
async health(port, path2, timeoutMs, method, expectedStatus) {
|
|
11738
|
+
try {
|
|
11739
|
+
const response = await fetch(`http://127.0.0.1:${port}${path2}`, { method, redirect: "manual", signal: AbortSignal.timeout(timeoutMs) });
|
|
11740
|
+
return expectedStatus.includes(response.status);
|
|
11741
|
+
} catch {
|
|
11742
|
+
return false;
|
|
11743
|
+
}
|
|
11744
|
+
},
|
|
11745
|
+
sleep: (ms) => Bun.sleep(ms),
|
|
11746
|
+
now: Date.now
|
|
11747
|
+
};
|
|
11748
|
+
var within3 = (root, path2) => path2 === root || path2.startsWith(`${root}${sep3}`);
|
|
11749
|
+
var idFor2 = (key) => createHash9("sha256").update(key).digest("hex").slice(0, 24);
|
|
11750
|
+
var statePath2 = (id2) => `${SERVICE_STATE_DIRECTORY}/${id2}.json`;
|
|
11751
|
+
var nameFor = (id2, slot) => `forgezero-${id2}-slot${slot}`;
|
|
11752
|
+
function validateRequest(request, host) {
|
|
11753
|
+
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))
|
|
11754
|
+
throw new Error("CONTAINER_IDENTITY_INVALID");
|
|
11755
|
+
const root = host.realpath(resolve5(request.root));
|
|
11756
|
+
const release = host.realpath(resolve5(request.release));
|
|
11757
|
+
if (!within3(root, release))
|
|
11758
|
+
throw new Error("CONTAINER_RELEASE_OUTSIDE_ROOT");
|
|
11759
|
+
const application = request.application;
|
|
11760
|
+
if (application.kind !== "application" || application.runtime.kind !== "container")
|
|
11761
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
11762
|
+
if (!application.resources.cpu || !application.resources.memory || !application.resources.pids)
|
|
11763
|
+
throw new Error("CONTAINER_LIMITS_REQUIRED");
|
|
11764
|
+
if (application.runtime.security.privileged !== false || application.runtime.security.noNewPrivileges !== true || !application.runtime.security.dropCapabilities.includes("ALL"))
|
|
11765
|
+
throw new Error("CONTAINER_SECURITY_INVALID");
|
|
11766
|
+
if (!application.network?.container || !["bridge", "host"].includes(application.network.container.mode))
|
|
11767
|
+
throw new Error("CONTAINER_NETWORK_INVALID");
|
|
11768
|
+
if (!application.network.ingress?.stablePort)
|
|
11769
|
+
throw new Error("CONTAINER_STABLE_PORT_REQUIRED");
|
|
11770
|
+
return { ...request, root, release };
|
|
11771
|
+
}
|
|
11772
|
+
async function checked6(host, argv2, code) {
|
|
11773
|
+
const result = await host.exec(argv2);
|
|
11774
|
+
if (result.exitCode !== 0)
|
|
11775
|
+
throw new Error(`${code}: ${result.output.slice(0, 512)}`);
|
|
11776
|
+
return result.output.trim();
|
|
11777
|
+
}
|
|
11778
|
+
async function buildContainerImage(requestValue, host = defaultHost2) {
|
|
11779
|
+
const request = validateRequest(requestValue, host);
|
|
11780
|
+
const runtime = request.application.runtime;
|
|
11781
|
+
if (runtime.kind !== "container")
|
|
11782
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
11783
|
+
const source = runtime.image.source;
|
|
11784
|
+
if (source.kind === "registry") {
|
|
11785
|
+
if (!source.reference.includes("@sha256:"))
|
|
11786
|
+
throw new Error("CONTAINER_IMAGE_DIGEST_REQUIRED");
|
|
11787
|
+
await checked6(host, ["/usr/bin/docker", "pull", "--quiet", source.reference], "CONTAINER_PULL_FAILED");
|
|
11788
|
+
const digest2 = await checked6(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", source.reference], "CONTAINER_INSPECT_FAILED");
|
|
11789
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(digest2))
|
|
11790
|
+
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
11791
|
+
return { image: source.reference, digest: digest2 };
|
|
11792
|
+
}
|
|
11793
|
+
const context = host.realpath(resolve5(request.release, source.context));
|
|
11794
|
+
const dockerfile = host.realpath(resolve5(request.release, source.dockerfile));
|
|
11795
|
+
if (!within3(request.release, context) || !within3(context, dockerfile))
|
|
11796
|
+
throw new Error("CONTAINER_BUILD_PATH_INVALID");
|
|
11797
|
+
const tag = `forgezero/${idFor2(request.key)}:${request.revision}`;
|
|
11798
|
+
await checked6(host, [
|
|
11799
|
+
"/usr/bin/docker",
|
|
11800
|
+
"build",
|
|
11801
|
+
"--pull",
|
|
11802
|
+
...request.noCache ? ["--no-cache"] : [],
|
|
11803
|
+
"--file",
|
|
11804
|
+
dockerfile,
|
|
11805
|
+
"--tag",
|
|
11806
|
+
tag,
|
|
11807
|
+
"--label",
|
|
11808
|
+
`net.forgezero.revision=${request.revision}`,
|
|
11809
|
+
"--label",
|
|
11810
|
+
`net.forgezero.component=${request.component}`,
|
|
11811
|
+
context
|
|
11812
|
+
], "CONTAINER_BUILD_FAILED");
|
|
11813
|
+
const digest = await checked6(host, ["/usr/bin/docker", "image", "inspect", "--format={{.Id}}", tag], "CONTAINER_INSPECT_FAILED");
|
|
11814
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(digest))
|
|
11815
|
+
throw new Error("CONTAINER_DIGEST_INVALID");
|
|
11816
|
+
return { image: tag, digest };
|
|
11817
|
+
}
|
|
11818
|
+
function readState2(host, id2) {
|
|
11819
|
+
if (!host.exists(statePath2(id2)))
|
|
11820
|
+
return null;
|
|
11821
|
+
try {
|
|
11822
|
+
const value = JSON.parse(host.read(statePath2(id2)));
|
|
11823
|
+
return value.format === 1 ? value : null;
|
|
11824
|
+
} catch {
|
|
11825
|
+
return null;
|
|
11826
|
+
}
|
|
11827
|
+
}
|
|
11828
|
+
function usedPorts(host, exceptId) {
|
|
11829
|
+
const ports = new Set;
|
|
11830
|
+
for (const name of host.list(SERVICE_STATE_DIRECTORY).filter((entry) => /^[a-f0-9]{24}\.json$/.test(entry))) {
|
|
11831
|
+
try {
|
|
11832
|
+
const state = JSON.parse(host.read(`${SERVICE_STATE_DIRECTORY}/${name}`));
|
|
11833
|
+
if (state.id !== exceptId) {
|
|
11834
|
+
ports.add(state.publicPort);
|
|
11835
|
+
state.applicationPorts.forEach((port) => ports.add(port));
|
|
11836
|
+
}
|
|
11837
|
+
} catch {}
|
|
11838
|
+
}
|
|
11839
|
+
return ports;
|
|
11840
|
+
}
|
|
11841
|
+
function allocatePorts(host, id2, count, stablePort, previous) {
|
|
11842
|
+
const used = usedPorts(host, id2);
|
|
11843
|
+
if (used.has(stablePort))
|
|
11844
|
+
throw new Error("CONTAINER_PUBLIC_PORT_CONFLICT");
|
|
11845
|
+
if (previous?.applicationPorts.length === count && previous.applicationPorts.every((port) => !used.has(port)))
|
|
11846
|
+
return [...previous.applicationPorts];
|
|
11847
|
+
const from = 20000;
|
|
11848
|
+
const to = 49999;
|
|
11849
|
+
const width = to - from + 1;
|
|
11850
|
+
const start = Number.parseInt(id2.slice(0, 8), 16) % width;
|
|
11851
|
+
for (let offset = 0;offset < width; offset += 1) {
|
|
11852
|
+
const first = from + (start + offset) % width;
|
|
11853
|
+
const values = Array.from({ length: count }, (_, index) => first + index);
|
|
11854
|
+
if (values.at(-1) <= to && values.every((port) => port !== stablePort && !used.has(port)))
|
|
11855
|
+
return values;
|
|
11856
|
+
}
|
|
11857
|
+
throw new Error("CONTAINER_PORTS_EXHAUSTED");
|
|
11858
|
+
}
|
|
11859
|
+
function nginx2(request, id2, port) {
|
|
11860
|
+
const service = request.application.service;
|
|
11861
|
+
const stablePort = request.application.network.ingress.stablePort;
|
|
11862
|
+
return `# ForgeZero container ${id2}
|
|
11863
|
+
${service.maximumConnections ? `limit_conn_zone $server_name zone=fz_${id2}:64k;
|
|
11864
|
+
` : ""}server {
|
|
11865
|
+
listen 127.0.0.1:${stablePort};
|
|
11866
|
+
server_name _;
|
|
11867
|
+
${service.maximumConnections ? ` limit_conn fz_${id2} ${service.maximumConnections};
|
|
11868
|
+
limit_conn_status 503;
|
|
11869
|
+
` : ""} location / {
|
|
11870
|
+
proxy_pass http://127.0.0.1:${port};
|
|
11871
|
+
proxy_http_version 1.1;
|
|
11872
|
+
${service.websocket ? ` proxy_set_header Upgrade $http_upgrade;
|
|
11873
|
+
proxy_set_header Connection "upgrade";
|
|
11874
|
+
` : ""} proxy_set_header Host $host;
|
|
11875
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
11876
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
11877
|
+
}
|
|
11878
|
+
}
|
|
11879
|
+
`;
|
|
11880
|
+
}
|
|
11881
|
+
function runArgv(request, id2, slot, port, networkName) {
|
|
11882
|
+
const { application } = request;
|
|
11883
|
+
const resources = application.resources;
|
|
11884
|
+
const runtime = application.runtime;
|
|
11885
|
+
if (runtime.kind !== "container")
|
|
11886
|
+
throw new Error("CONTAINER_RUNTIME_REQUIRED");
|
|
11887
|
+
const memory = resources.memory;
|
|
11888
|
+
const network = application.network.container;
|
|
11889
|
+
const argv2 = [
|
|
11890
|
+
"/usr/bin/docker",
|
|
11891
|
+
"run",
|
|
11892
|
+
"--detach",
|
|
11893
|
+
"--name",
|
|
11894
|
+
nameFor(id2, slot),
|
|
11895
|
+
"--restart",
|
|
11896
|
+
"unless-stopped",
|
|
11897
|
+
"--label",
|
|
11898
|
+
`net.forgezero.key=${request.key}`,
|
|
11899
|
+
"--label",
|
|
11900
|
+
`net.forgezero.revision=${request.revision}`,
|
|
11901
|
+
"--cpus",
|
|
11902
|
+
String(resources.cpu.limit),
|
|
11903
|
+
"--cpu-shares",
|
|
11904
|
+
String(resources.cpu.weight ?? 100),
|
|
11905
|
+
"--memory",
|
|
11906
|
+
`${memory.limitMiB}m`,
|
|
11907
|
+
...memory.reservationMiB ? ["--memory-reservation", `${memory.reservationMiB}m`] : [],
|
|
11908
|
+
...memory.swap === "disabled" ? ["--memory-swap", `${memory.limitMiB}m`] : [],
|
|
11909
|
+
"--pids-limit",
|
|
11910
|
+
String(resources.pids.limit),
|
|
11911
|
+
"--security-opt",
|
|
11912
|
+
"no-new-privileges=true",
|
|
11913
|
+
"--cap-drop",
|
|
11914
|
+
"ALL",
|
|
11915
|
+
...runtime.security.root === "read-only" ? ["--read-only"] : [],
|
|
11916
|
+
...network.mode === "host" ? ["--network", "host", "--env", `FZ_APP_PORT=${port}`] : ["--network", networkName, "--publish", `127.0.0.1:${port}:${application.service.port}`]
|
|
11917
|
+
];
|
|
11918
|
+
for (const mount of application.storage ?? []) {
|
|
11919
|
+
if (mount.class === "ephemeral")
|
|
11920
|
+
argv2.push("--tmpfs", `${mount.path}:rw,noexec,nosuid,size=${mount.sizeMiB}m`);
|
|
11921
|
+
else
|
|
11922
|
+
argv2.push("--volume", `forgezero-${id2}-${mount.name}:${mount.path}`);
|
|
11923
|
+
}
|
|
11924
|
+
argv2.push(request.image, ...runtime.entrypoint ?? []);
|
|
11925
|
+
return argv2;
|
|
11926
|
+
}
|
|
11927
|
+
async function activateContainer(requestValue, host = defaultHost2) {
|
|
11928
|
+
const request = validateRequest(requestValue, host);
|
|
11929
|
+
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:"))
|
|
11930
|
+
throw new Error("CONTAINER_IMAGE_INVALID");
|
|
11931
|
+
const id2 = idFor2(request.key);
|
|
11932
|
+
host.mkdir(SERVICE_STATE_DIRECTORY, 448);
|
|
11933
|
+
host.mkdir(SERVICE_NGINX_DIRECTORY, 493);
|
|
11934
|
+
const previous = readState2(host, id2);
|
|
11935
|
+
const slots = request.application.rollout.strategy === "blue-green" ? 2 : 1;
|
|
11936
|
+
if (!["direct", "blue-green"].includes(request.application.rollout.strategy))
|
|
11937
|
+
throw new Error("CONTAINER_ROLLOUT_REQUIRES_CONTROL_PLANE");
|
|
11938
|
+
const containerNetwork = request.application.network.container;
|
|
11939
|
+
let networkName = "host";
|
|
11940
|
+
if (containerNetwork.mode === "bridge") {
|
|
11941
|
+
networkName = containerNetwork.network ? `forgezero-${id2}-${containerNetwork.network}` : "bridge";
|
|
11942
|
+
if (networkName !== "bridge") {
|
|
11943
|
+
const inspected = await host.exec(["/usr/bin/docker", "network", "inspect", '--format={{index .Labels "net.forgezero.owner"}}', networkName]);
|
|
11944
|
+
if (inspected.exitCode !== 0) {
|
|
11945
|
+
await checked6(host, ["/usr/bin/docker", "network", "create", "--driver", "bridge", "--label", `net.forgezero.owner=${id2}`, networkName], "CONTAINER_NETWORK_CREATE_FAILED");
|
|
11946
|
+
} else if (inspected.output.trim() !== id2)
|
|
11947
|
+
throw new Error("CONTAINER_NETWORK_OWNERSHIP_INVALID");
|
|
11948
|
+
}
|
|
11949
|
+
}
|
|
11950
|
+
const ports = allocatePorts(host, id2, slots, request.application.network.ingress.stablePort, previous);
|
|
11951
|
+
const nextSlot = slots === 2 && previous?.activeSlot === 0 ? 1 : 0;
|
|
11952
|
+
const port = ports[nextSlot];
|
|
11953
|
+
const name = nameFor(id2, nextSlot);
|
|
11954
|
+
await host.exec(["/usr/bin/docker", "rm", "--force", name]);
|
|
11955
|
+
await checked6(host, runArgv(request, id2, nextSlot, port, networkName), "CONTAINER_START_FAILED");
|
|
11956
|
+
let healthy = false;
|
|
11957
|
+
const health = request.application.service.health;
|
|
11958
|
+
for (let attempt = 0;attempt < (health.attempts ?? 30); attempt += 1) {
|
|
11959
|
+
if (await host.health(port, health.path, health.timeoutMs, health.method, health.expectedStatus)) {
|
|
11960
|
+
healthy = true;
|
|
11961
|
+
break;
|
|
11962
|
+
}
|
|
11963
|
+
await host.sleep(health.intervalMs ?? 1000);
|
|
11964
|
+
}
|
|
11965
|
+
if (!healthy) {
|
|
11966
|
+
await host.exec(["/usr/bin/docker", "rm", "--force", name]);
|
|
11967
|
+
throw new Error("CONTAINER_HEALTH_FAILED");
|
|
11968
|
+
}
|
|
11969
|
+
const nginxPath = `${SERVICE_NGINX_DIRECTORY}/forgezero-${id2}.conf`;
|
|
11970
|
+
const previousNginx = host.exists(nginxPath) ? host.read(nginxPath) : null;
|
|
11971
|
+
host.write(nginxPath, nginx2(request, id2, port), 420);
|
|
11972
|
+
try {
|
|
11973
|
+
await checked6(host, ["/usr/sbin/nginx", "-t"], "CONTAINER_NGINX_INVALID");
|
|
11974
|
+
await checked6(host, ["/usr/bin/systemctl", "reload", "nginx.service"], "CONTAINER_NGINX_RELOAD_FAILED");
|
|
11975
|
+
} catch (cause) {
|
|
11976
|
+
if (previousNginx === null)
|
|
11977
|
+
host.remove(nginxPath);
|
|
11978
|
+
else
|
|
11979
|
+
host.write(nginxPath, previousNginx, 420);
|
|
11980
|
+
await host.exec(["/usr/sbin/nginx", "-t"]);
|
|
11981
|
+
await host.exec(["/usr/bin/systemctl", "reload", "nginx.service"]);
|
|
11982
|
+
await host.exec(["/usr/bin/docker", "rm", "--force", name]);
|
|
11983
|
+
throw cause;
|
|
11984
|
+
}
|
|
11985
|
+
const state = {
|
|
11986
|
+
format: 1,
|
|
11987
|
+
id: id2,
|
|
11988
|
+
key: request.key,
|
|
11989
|
+
revision: request.revision,
|
|
11990
|
+
strategy: request.application.rollout.strategy,
|
|
11991
|
+
publicPort: request.application.network.ingress.stablePort,
|
|
11992
|
+
applicationPorts: ports,
|
|
11993
|
+
activeSlot: nextSlot,
|
|
11994
|
+
healthPath: health.path,
|
|
11995
|
+
updatedAtTs: host.now()
|
|
11996
|
+
};
|
|
11997
|
+
host.write(statePath2(id2), `${JSON.stringify(state, null, 2)}
|
|
11998
|
+
`, 384);
|
|
11999
|
+
if (previous && previous.activeSlot !== nextSlot) {
|
|
12000
|
+
await host.sleep(request.application.rollout.drainMs ?? 30000);
|
|
12001
|
+
await host.exec(["/usr/bin/docker", "rm", "--force", nameFor(id2, previous.activeSlot)]);
|
|
12002
|
+
}
|
|
12003
|
+
return state;
|
|
12004
|
+
}
|
|
12005
|
+
|
|
10277
12006
|
// src/software-helper.ts
|
|
10278
12007
|
var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
|
|
10279
12008
|
var SOFTWARE_HELPER_GROUP = "forgezero-software";
|
|
@@ -10282,11 +12011,13 @@ var MAX_REQUEST_BYTES6 = 128 * 1024;
|
|
|
10282
12011
|
var MAX_PENDING_REQUESTS = 128;
|
|
10283
12012
|
function startSoftwareHelper(options = {}) {
|
|
10284
12013
|
const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
|
|
10285
|
-
if (
|
|
12014
|
+
if (existsSync15(socketPath))
|
|
10286
12015
|
unlinkSync11(socketPath);
|
|
10287
|
-
|
|
12016
|
+
mkdirSync11(dirname8(socketPath), { recursive: true, mode: 488 });
|
|
10288
12017
|
const ensure = options.ensure ?? ensureSoftwareRequirements;
|
|
10289
12018
|
const activate = options.activate ?? activateSupervisedService;
|
|
12019
|
+
const buildContainer = options.buildContainer ?? buildContainerImage;
|
|
12020
|
+
const activateTypedContainer = options.activateContainer ?? activateContainer;
|
|
10290
12021
|
let tail = Promise.resolve();
|
|
10291
12022
|
let pending = 0;
|
|
10292
12023
|
const server = createServer7((socket) => {
|
|
@@ -10319,10 +12050,29 @@ function startSoftwareHelper(options = {}) {
|
|
|
10319
12050
|
return;
|
|
10320
12051
|
}
|
|
10321
12052
|
if (request.op === "activate-service" && request.request) {
|
|
10322
|
-
|
|
12053
|
+
const serviceRequest = request.request;
|
|
12054
|
+
if (!options.allowedRoot || serviceRequest.root !== options.allowedRoot) {
|
|
10323
12055
|
throw new Error("service deployment root is not owned by this helper");
|
|
10324
12056
|
}
|
|
10325
|
-
const state = await activate(
|
|
12057
|
+
const state = await activate(serviceRequest);
|
|
12058
|
+
socket.end(`${JSON.stringify({ ok: true, state })}
|
|
12059
|
+
`);
|
|
12060
|
+
return;
|
|
12061
|
+
}
|
|
12062
|
+
if (request.op === "build-container" && request.request) {
|
|
12063
|
+
const containerRequest = request.request;
|
|
12064
|
+
if (!options.allowedRoot || containerRequest.root !== options.allowedRoot)
|
|
12065
|
+
throw new Error("container deployment root is not owned by this helper");
|
|
12066
|
+
const result = await buildContainer(containerRequest);
|
|
12067
|
+
socket.end(`${JSON.stringify({ ok: true, result })}
|
|
12068
|
+
`);
|
|
12069
|
+
return;
|
|
12070
|
+
}
|
|
12071
|
+
if (request.op === "activate-container" && request.request) {
|
|
12072
|
+
const containerRequest = request.request;
|
|
12073
|
+
if (!options.allowedRoot || containerRequest.root !== options.allowedRoot)
|
|
12074
|
+
throw new Error("container deployment root is not owned by this helper");
|
|
12075
|
+
const state = await activateTypedContainer(containerRequest);
|
|
10326
12076
|
socket.end(`${JSON.stringify({ ok: true, state })}
|
|
10327
12077
|
`);
|
|
10328
12078
|
return;
|
|
@@ -10346,8 +12096,43 @@ function startSoftwareHelper(options = {}) {
|
|
|
10346
12096
|
server.listen(socketPath, () => chmodSync13(socketPath, 432));
|
|
10347
12097
|
return server;
|
|
10348
12098
|
}
|
|
12099
|
+
function requestContainer(op, request, socketPath, timeoutMs) {
|
|
12100
|
+
return new Promise((resolve6, reject) => {
|
|
12101
|
+
const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op, request })}
|
|
12102
|
+
`));
|
|
12103
|
+
let buffer = "";
|
|
12104
|
+
socket.setTimeout(timeoutMs, () => {
|
|
12105
|
+
socket.destroy();
|
|
12106
|
+
reject(new Error("container helper timed out"));
|
|
12107
|
+
});
|
|
12108
|
+
socket.on("data", (chunk) => {
|
|
12109
|
+
buffer += chunk.toString("utf8");
|
|
12110
|
+
const newline = buffer.indexOf(`
|
|
12111
|
+
`);
|
|
12112
|
+
if (newline < 0)
|
|
12113
|
+
return;
|
|
12114
|
+
socket.end();
|
|
12115
|
+
try {
|
|
12116
|
+
const response = JSON.parse(buffer.slice(0, newline));
|
|
12117
|
+
const result = response.result ?? response.state;
|
|
12118
|
+
if (!response.ok || !result)
|
|
12119
|
+
throw new Error(response.error?.message ?? "container helper refused request");
|
|
12120
|
+
resolve6(result);
|
|
12121
|
+
} catch (cause) {
|
|
12122
|
+
reject(cause);
|
|
12123
|
+
}
|
|
12124
|
+
});
|
|
12125
|
+
socket.on("error", reject);
|
|
12126
|
+
});
|
|
12127
|
+
}
|
|
12128
|
+
function requestContainerBuild(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 30 * 60000) {
|
|
12129
|
+
return requestContainer("build-container", request, socketPath, timeoutMs);
|
|
12130
|
+
}
|
|
12131
|
+
function requestContainerActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
12132
|
+
return requestContainer("activate-container", request, socketPath, timeoutMs);
|
|
12133
|
+
}
|
|
10349
12134
|
function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 10 * 60000) {
|
|
10350
|
-
return new Promise((
|
|
12135
|
+
return new Promise((resolve6, reject) => {
|
|
10351
12136
|
const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op: "activate-service", request })}
|
|
10352
12137
|
`));
|
|
10353
12138
|
let buffer = "";
|
|
@@ -10366,7 +12151,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
|
|
|
10366
12151
|
const response = JSON.parse(buffer.slice(0, newline));
|
|
10367
12152
|
if (!response.ok || !response.state)
|
|
10368
12153
|
throw new Error(response.error?.message ?? "service helper refused activation");
|
|
10369
|
-
|
|
12154
|
+
resolve6(response.state);
|
|
10370
12155
|
} catch (cause) {
|
|
10371
12156
|
reject(cause);
|
|
10372
12157
|
}
|
|
@@ -10376,7 +12161,7 @@ function requestServiceActivation(request, socketPath = DEFAULT_SOFTWARE_HELPER_
|
|
|
10376
12161
|
}
|
|
10377
12162
|
function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
10378
12163
|
validateSoftwareRequirements(requirements);
|
|
10379
|
-
return new Promise((
|
|
12164
|
+
return new Promise((resolve6, reject) => {
|
|
10380
12165
|
const socket = connect6(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
|
|
10381
12166
|
`));
|
|
10382
12167
|
let buffer = "";
|
|
@@ -10395,7 +12180,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
10395
12180
|
const response = JSON.parse(buffer.slice(0, newline));
|
|
10396
12181
|
if (!response.ok || !response.results)
|
|
10397
12182
|
throw new Error(response.error?.message ?? "software helper refused the request");
|
|
10398
|
-
|
|
12183
|
+
resolve6(response.results);
|
|
10399
12184
|
} catch (cause) {
|
|
10400
12185
|
reject(cause);
|
|
10401
12186
|
}
|
|
@@ -10405,7 +12190,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
10405
12190
|
}
|
|
10406
12191
|
|
|
10407
12192
|
// src/egress-policy.ts
|
|
10408
|
-
import { realpathSync as
|
|
12193
|
+
import { realpathSync as realpathSync4 } from "fs";
|
|
10409
12194
|
var AGENT_EGRESS_TABLE = "forgezero_agent_egress";
|
|
10410
12195
|
var SYSTEMD_RESOLVED_STUB = "/run/systemd/resolve/stub-resolv.conf";
|
|
10411
12196
|
var SYSTEMD_RESOLVED_ADDRESS = "127.0.0.53";
|
|
@@ -10569,7 +12354,7 @@ function resolveServiceUid(user, command2 = systemCommand) {
|
|
|
10569
12354
|
assertUid(uid);
|
|
10570
12355
|
return uid;
|
|
10571
12356
|
}
|
|
10572
|
-
function assertResolvedStub(realpath =
|
|
12357
|
+
function assertResolvedStub(realpath = realpathSync4) {
|
|
10573
12358
|
let source;
|
|
10574
12359
|
try {
|
|
10575
12360
|
source = realpath("/etc/resolv.conf");
|
|
@@ -10680,7 +12465,7 @@ async function superviseAgentEgressPolicy(options) {
|
|
|
10680
12465
|
if (result.exitCode !== 0)
|
|
10681
12466
|
throw new Error("Could not notify systemd that Agent egress is ready.");
|
|
10682
12467
|
}))();
|
|
10683
|
-
const wait = options.wait ?? ((milliseconds) => new Promise((
|
|
12468
|
+
const wait = options.wait ?? ((milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)));
|
|
10684
12469
|
const intervalMs = options.intervalMs ?? 1000;
|
|
10685
12470
|
if (!Number.isSafeInteger(intervalMs) || intervalMs < 100 || intervalMs > 60000) {
|
|
10686
12471
|
throw new Error("Agent egress monitor interval is invalid.");
|
|
@@ -10901,7 +12686,7 @@ function createAgentTelemetry(input, options = {}) {
|
|
|
10901
12686
|
const warn = options.warn ?? ((message) => console.warn(`[telemetry] ${message}`));
|
|
10902
12687
|
const now = options.now ?? Date.now;
|
|
10903
12688
|
const random = options.random ?? Math.random;
|
|
10904
|
-
const wait = options.wait ?? ((milliseconds) => new Promise((
|
|
12689
|
+
const wait = options.wait ?? ((milliseconds) => new Promise((resolve6) => setTimeout(resolve6, milliseconds)));
|
|
10905
12690
|
const processStartedAtMs = Math.max(0, Math.floor(now()));
|
|
10906
12691
|
const processInstanceId = randomHex(16);
|
|
10907
12692
|
const resource = {
|
|
@@ -10971,10 +12756,10 @@ function createAgentTelemetry(input, options = {}) {
|
|
|
10971
12756
|
if (!transient || attempt + 1 >= AGENT_OTLP_MAX_ATTEMPTS)
|
|
10972
12757
|
throw new PermanentExportError;
|
|
10973
12758
|
const retryAfter2 = cause instanceof RetryableExportError ? cause.retryAfterMs : null;
|
|
10974
|
-
const
|
|
12759
|
+
const backoff2 = AGENT_OTLP_RETRY_BASE_MS * 2 ** attempt;
|
|
10975
12760
|
const randomValue = random();
|
|
10976
|
-
const jitter = Math.floor((Number.isFinite(randomValue) ? Math.max(0, Math.min(1, randomValue)) : 0) *
|
|
10977
|
-
await wait(Math.min(AGENT_OTLP_MAX_RETRY_DELAY_MS, Math.max(retryAfter2 ?? 0,
|
|
12761
|
+
const jitter = Math.floor((Number.isFinite(randomValue) ? Math.max(0, Math.min(1, randomValue)) : 0) * backoff2);
|
|
12762
|
+
await wait(Math.min(AGENT_OTLP_MAX_RETRY_DELAY_MS, Math.max(retryAfter2 ?? 0, backoff2 + jitter)));
|
|
10978
12763
|
}
|
|
10979
12764
|
}
|
|
10980
12765
|
throw new PermanentExportError;
|
|
@@ -11109,8 +12894,8 @@ function createAgentTelemetry(input, options = {}) {
|
|
|
11109
12894
|
} }, { name: "forgezero.agent.operations", sum: {
|
|
11110
12895
|
aggregationTemporality: 2,
|
|
11111
12896
|
isMonotonic: true,
|
|
11112
|
-
dataPoints: metricRows.map((
|
|
11113
|
-
attributes:
|
|
12897
|
+
dataPoints: metricRows.map((row2, index) => ({
|
|
12898
|
+
attributes: row2.attributes,
|
|
11114
12899
|
asInt: String(metricValues[index].count),
|
|
11115
12900
|
startTimeUnixNano,
|
|
11116
12901
|
timeUnixNano
|
|
@@ -11340,17 +13125,17 @@ import {
|
|
|
11340
13125
|
chmodSync as chmodSync14,
|
|
11341
13126
|
chownSync,
|
|
11342
13127
|
copyFileSync as copyFileSync2,
|
|
11343
|
-
existsSync as
|
|
13128
|
+
existsSync as existsSync16,
|
|
11344
13129
|
lstatSync as lstatSync2,
|
|
11345
|
-
mkdirSync as
|
|
11346
|
-
readFileSync as
|
|
11347
|
-
readdirSync as
|
|
11348
|
-
realpathSync as
|
|
11349
|
-
renameSync as
|
|
11350
|
-
rmSync as
|
|
13130
|
+
mkdirSync as mkdirSync12,
|
|
13131
|
+
readFileSync as readFileSync13,
|
|
13132
|
+
readdirSync as readdirSync4,
|
|
13133
|
+
realpathSync as realpathSync5,
|
|
13134
|
+
renameSync as renameSync9,
|
|
13135
|
+
rmSync as rmSync8,
|
|
11351
13136
|
statSync as statSync3,
|
|
11352
13137
|
symlinkSync as symlinkSync4,
|
|
11353
|
-
writeFileSync as
|
|
13138
|
+
writeFileSync as writeFileSync12
|
|
11354
13139
|
} from "fs";
|
|
11355
13140
|
import { join as join10 } from "path";
|
|
11356
13141
|
var boundedInteger2 = (name, value, minimum, maximum) => {
|
|
@@ -11404,13 +13189,13 @@ var platformExec = async (argv2) => {
|
|
|
11404
13189
|
};
|
|
11405
13190
|
var replaceLink = (path2, target2) => {
|
|
11406
13191
|
const pending = `${path2}.next`;
|
|
11407
|
-
|
|
13192
|
+
rmSync8(pending, { force: true });
|
|
11408
13193
|
if (!target2) {
|
|
11409
|
-
|
|
13194
|
+
rmSync8(path2, { force: true });
|
|
11410
13195
|
return;
|
|
11411
13196
|
}
|
|
11412
13197
|
symlinkSync4(target2, pending);
|
|
11413
|
-
|
|
13198
|
+
renameSync9(pending, path2);
|
|
11414
13199
|
};
|
|
11415
13200
|
var secureRelease = (path2, uid, gid) => {
|
|
11416
13201
|
const visit = (current) => {
|
|
@@ -11420,7 +13205,7 @@ var secureRelease = (path2, uid, gid) => {
|
|
|
11420
13205
|
chownSync(current, uid, gid);
|
|
11421
13206
|
chmodSync14(current, metadata.isDirectory() ? 365 : 292);
|
|
11422
13207
|
if (metadata.isDirectory())
|
|
11423
|
-
for (const name of
|
|
13208
|
+
for (const name of readdirSync4(current))
|
|
11424
13209
|
visit(join10(current, name));
|
|
11425
13210
|
};
|
|
11426
13211
|
visit(path2);
|
|
@@ -11428,28 +13213,28 @@ var secureRelease = (path2, uid, gid) => {
|
|
|
11428
13213
|
async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
11429
13214
|
const rendered = renderPlatformActivationFiles(config);
|
|
11430
13215
|
const normalized = JSON.parse(rendered.helper);
|
|
11431
|
-
const releases =
|
|
11432
|
-
const release =
|
|
13216
|
+
const releases = realpathSync5(join10(normalized.root, "releases"));
|
|
13217
|
+
const release = realpathSync5(requestedRelease);
|
|
11433
13218
|
if (!release.startsWith(`${releases}/`) || release === releases)
|
|
11434
13219
|
throw new Error("release is outside configured releases directory");
|
|
11435
|
-
for (const required of [".fz/deploy.json", "src/index.ts", "bun.lock"]) {
|
|
13220
|
+
for (const required of ["forgezero.deploy.ts", ".fz/deploy.plan.json", "src/index.ts", "bun.lock"]) {
|
|
11436
13221
|
const metadata = statSync3(join10(release, required));
|
|
11437
13222
|
if (!metadata.isFile() || metadata.size < 1)
|
|
11438
13223
|
throw new Error(`release is incomplete: ${required}`);
|
|
11439
13224
|
}
|
|
11440
13225
|
const exec = options.exec ?? platformExec;
|
|
11441
13226
|
const request = options.fetch ?? fetch;
|
|
11442
|
-
const
|
|
13227
|
+
const sleep2 = options.sleep ?? Bun.sleep;
|
|
11443
13228
|
const slots = join10(normalized.root, "slots");
|
|
11444
|
-
|
|
13229
|
+
mkdirSync12(slots, { recursive: true, mode: 493 });
|
|
11445
13230
|
const slotFile = join10(normalized.root, ".forge-slot");
|
|
11446
|
-
const previousSlot =
|
|
13231
|
+
const previousSlot = existsSync16(slotFile) ? readFileSync13(slotFile, "utf8").trim() : undefined;
|
|
11447
13232
|
const target2 = previousSlot === "blue" ? "green" : "blue";
|
|
11448
13233
|
const port = target2 === "blue" ? normalized.bluePort : normalized.greenPort;
|
|
11449
13234
|
const targetLink = join10(slots, target2);
|
|
11450
13235
|
let previousTarget;
|
|
11451
13236
|
try {
|
|
11452
|
-
previousTarget =
|
|
13237
|
+
previousTarget = realpathSync5(targetLink);
|
|
11453
13238
|
} catch {}
|
|
11454
13239
|
const user = await exec(["/usr/bin/id", "-u", normalized.serviceUser]);
|
|
11455
13240
|
const group = await exec(["/usr/bin/id", "-g", normalized.serviceUser]);
|
|
@@ -11471,7 +13256,7 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
11471
13256
|
} catch {}
|
|
11472
13257
|
if (healthy)
|
|
11473
13258
|
break;
|
|
11474
|
-
await
|
|
13259
|
+
await sleep2(1000);
|
|
11475
13260
|
}
|
|
11476
13261
|
const stopTarget = async () => {
|
|
11477
13262
|
await exec(["/usr/bin/systemctl", "stop", service]);
|
|
@@ -11483,40 +13268,40 @@ async function activatePlatformRelease(config, requestedRelease, options = {}) {
|
|
|
11483
13268
|
}
|
|
11484
13269
|
const upstream = "/etc/nginx/conf.d/forgezero-upstream.conf";
|
|
11485
13270
|
const backup = `${upstream}.forgezero-backup`;
|
|
11486
|
-
if (
|
|
13271
|
+
if (existsSync16(upstream))
|
|
11487
13272
|
copyFileSync2(upstream, backup);
|
|
11488
13273
|
else
|
|
11489
|
-
|
|
11490
|
-
|
|
13274
|
+
rmSync8(backup, { force: true });
|
|
13275
|
+
writeFileSync12(upstream, `upstream forgezero { server 127.0.0.1:${port}; }
|
|
11491
13276
|
`, { mode: 420 });
|
|
11492
13277
|
const test = await exec(["/usr/sbin/nginx", "-t"]);
|
|
11493
13278
|
const reload = test.exitCode === 0 ? await exec(["/usr/sbin/nginx", "-s", "reload"]) : test;
|
|
11494
13279
|
if (reload.exitCode !== 0) {
|
|
11495
|
-
if (
|
|
11496
|
-
|
|
13280
|
+
if (existsSync16(backup))
|
|
13281
|
+
renameSync9(backup, upstream);
|
|
11497
13282
|
else
|
|
11498
|
-
|
|
13283
|
+
rmSync8(upstream, { force: true });
|
|
11499
13284
|
await exec(["/usr/sbin/nginx", "-t"]);
|
|
11500
13285
|
await exec(["/usr/sbin/nginx", "-s", "reload"]);
|
|
11501
13286
|
await stopTarget();
|
|
11502
13287
|
throw new Error("nginx refused the promoted upstream");
|
|
11503
13288
|
}
|
|
11504
|
-
|
|
11505
|
-
|
|
13289
|
+
rmSync8(backup, { force: true });
|
|
13290
|
+
writeFileSync12(slotFile, `${target2}
|
|
11506
13291
|
`, { mode: 420 });
|
|
11507
13292
|
if (previousSlot && previousSlot !== target2) {
|
|
11508
|
-
await
|
|
13293
|
+
await sleep2(normalized.drainDeadlineMs);
|
|
11509
13294
|
await exec(["/usr/bin/systemctl", "stop", `forgezero@${previousSlot}.service`]);
|
|
11510
13295
|
}
|
|
11511
|
-
const old =
|
|
13296
|
+
const old = readdirSync4(releases, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join10(releases, entry.name)).sort((left, right) => statSync3(right).mtimeMs - statSync3(left).mtimeMs).slice(normalized.keepReleases);
|
|
11512
13297
|
for (const path2 of old)
|
|
11513
13298
|
if (path2 !== release)
|
|
11514
|
-
|
|
13299
|
+
rmSync8(path2, { recursive: true, force: true });
|
|
11515
13300
|
return { release, slot: target2 };
|
|
11516
13301
|
}
|
|
11517
13302
|
|
|
11518
13303
|
// src/recovery-host.ts
|
|
11519
|
-
import { existsSync as
|
|
13304
|
+
import { existsSync as existsSync17, readFileSync as readFileSync14, statSync as statSync4 } from "fs";
|
|
11520
13305
|
import { hostname } from "os";
|
|
11521
13306
|
import { join as join11 } from "path";
|
|
11522
13307
|
var execute2 = async (argv2) => {
|
|
@@ -11524,25 +13309,25 @@ var execute2 = async (argv2) => {
|
|
|
11524
13309
|
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
|
11525
13310
|
return { stdout, stderr, exitCode };
|
|
11526
13311
|
};
|
|
11527
|
-
var
|
|
13312
|
+
var checked7 = async (run2, argv2, expected = 0) => {
|
|
11528
13313
|
const result = await run2(argv2);
|
|
11529
13314
|
if (result.exitCode !== expected)
|
|
11530
13315
|
throw new Error(`${argv2[0]} failed (${result.exitCode})`);
|
|
11531
13316
|
return result;
|
|
11532
13317
|
};
|
|
11533
13318
|
var activeSlot = () => {
|
|
11534
|
-
const slot =
|
|
13319
|
+
const slot = readFileSync14("/opt/forgezero/.forge-slot", "utf8").trim();
|
|
11535
13320
|
if (slot !== "blue" && slot !== "green")
|
|
11536
13321
|
throw new Error("invalid active slot");
|
|
11537
13322
|
return slot;
|
|
11538
13323
|
};
|
|
11539
13324
|
var exactState = (path2, expected) => {
|
|
11540
|
-
if (!
|
|
13325
|
+
if (!readFileSync14(path2, "utf8").split(`
|
|
11541
13326
|
`).includes(expected))
|
|
11542
13327
|
throw new Error(`state mismatch: ${expected}`);
|
|
11543
13328
|
};
|
|
11544
13329
|
var nonEmpty = (path2) => {
|
|
11545
|
-
if (!
|
|
13330
|
+
if (!existsSync17(path2) || !statSync4(path2).isFile() || statSync4(path2).size < 1)
|
|
11546
13331
|
throw new Error(`required file missing: ${path2}`);
|
|
11547
13332
|
};
|
|
11548
13333
|
var option = (args, name) => {
|
|
@@ -11586,29 +13371,29 @@ async function runRecoveryHost(args, run2 = execute2) {
|
|
|
11586
13371
|
const slot = activeSlot();
|
|
11587
13372
|
nonEmpty(join11("/opt/forgezero/slots", slot, "src/server/maintenance/restore-backup.ts"));
|
|
11588
13373
|
nonEmpty(join11("/opt/forgezero/slots", slot, "src/server/maintenance/verify-recovery-cluster.ts"));
|
|
11589
|
-
await
|
|
11590
|
-
await
|
|
13374
|
+
await checked7(run2, ["/usr/bin/test", "-x", "/usr/local/bin/fz"]);
|
|
13375
|
+
await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-db.service"], hasDatabase ? 0 : 3);
|
|
11591
13376
|
if (args.includes("--require-stopped")) {
|
|
11592
|
-
await
|
|
11593
|
-
await
|
|
13377
|
+
await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@blue.service"], 3);
|
|
13378
|
+
await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@green.service"], 3);
|
|
11594
13379
|
}
|
|
11595
13380
|
return;
|
|
11596
13381
|
}
|
|
11597
13382
|
if (action === "fence") {
|
|
11598
|
-
await
|
|
11599
|
-
await
|
|
11600
|
-
await
|
|
13383
|
+
await checked7(run2, ["/usr/bin/systemctl", "stop", "forgezero@blue.service", "forgezero@green.service"]);
|
|
13384
|
+
await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@blue.service"], 3);
|
|
13385
|
+
await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero@green.service"], 3);
|
|
11601
13386
|
const role = option(args, "db-role");
|
|
11602
|
-
await
|
|
13387
|
+
await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", "forgezero-db.service"], role === "none" ? 3 : 0);
|
|
11603
13388
|
return;
|
|
11604
13389
|
}
|
|
11605
13390
|
if (action === "start-active") {
|
|
11606
13391
|
const slot = activeSlot();
|
|
11607
13392
|
const inactive = slot === "blue" ? "green" : "blue";
|
|
11608
|
-
await
|
|
11609
|
-
await
|
|
11610
|
-
await
|
|
11611
|
-
await
|
|
13393
|
+
await checked7(run2, ["/usr/bin/systemctl", "stop", `forgezero@${inactive}.service`]);
|
|
13394
|
+
await checked7(run2, ["/usr/bin/systemctl", "start", `forgezero@${slot}.service`]);
|
|
13395
|
+
await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", `forgezero@${slot}.service`]);
|
|
13396
|
+
await checked7(run2, ["/usr/bin/systemctl", "is-active", "--quiet", `forgezero@${inactive}.service`], 3);
|
|
11612
13397
|
return;
|
|
11613
13398
|
}
|
|
11614
13399
|
if (action === "maintenance") {
|
|
@@ -11631,7 +13416,7 @@ async function runRecoveryHost(args, run2 = execute2) {
|
|
|
11631
13416
|
"--property=LoadCredentialEncrypted=backup-recovery-root:/etc/forgezero/creds/backup-recovery-root.cred"
|
|
11632
13417
|
] : []
|
|
11633
13418
|
];
|
|
11634
|
-
await
|
|
13419
|
+
await checked7(run2, ["/usr/bin/systemd-run", "--quiet", "--wait", "--pipe", "--collect", `--unit=${unit2(option(args, "unit"))}`, ...properties, "/usr/local/bin/fz", ...fzArgs]);
|
|
11635
13420
|
return;
|
|
11636
13421
|
}
|
|
11637
13422
|
throw new Error("unknown recovery-host action");
|
|
@@ -11643,15 +13428,15 @@ import { lstatSync as lstatSync5 } from "fs";
|
|
|
11643
13428
|
// src/bootstrap.ts
|
|
11644
13429
|
import {
|
|
11645
13430
|
chmodSync as chmodSync16,
|
|
11646
|
-
existsSync as
|
|
13431
|
+
existsSync as existsSync19,
|
|
11647
13432
|
lstatSync as lstatSync4,
|
|
11648
|
-
mkdirSync as
|
|
11649
|
-
readFileSync as
|
|
11650
|
-
renameSync as
|
|
11651
|
-
rmSync as
|
|
11652
|
-
writeFileSync as
|
|
13433
|
+
mkdirSync as mkdirSync14,
|
|
13434
|
+
readFileSync as readFileSync16,
|
|
13435
|
+
renameSync as renameSync11,
|
|
13436
|
+
rmSync as rmSync10,
|
|
13437
|
+
writeFileSync as writeFileSync14
|
|
11653
13438
|
} from "fs";
|
|
11654
|
-
import { dirname as
|
|
13439
|
+
import { dirname as dirname10 } from "path";
|
|
11655
13440
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
11656
13441
|
|
|
11657
13442
|
// src/provision.ts
|
|
@@ -12354,7 +14139,7 @@ fz-agent egress-policy-check`;
|
|
|
12354
14139
|
return "/usr/bin/apt-get install -y cloudflare-warp";
|
|
12355
14140
|
return "/usr/bin/warp-cli --accept-tos status";
|
|
12356
14141
|
};
|
|
12357
|
-
var
|
|
14142
|
+
var step2 = (label, operation, optional = false) => ({
|
|
12358
14143
|
label,
|
|
12359
14144
|
operation,
|
|
12360
14145
|
optional: optional || undefined,
|
|
@@ -12477,44 +14262,44 @@ function planProvision(options) {
|
|
|
12477
14262
|
socketPath: options.socketPath,
|
|
12478
14263
|
user,
|
|
12479
14264
|
steps: [
|
|
12480
|
-
...options.enforceEgress ? [
|
|
14265
|
+
...options.enforceEgress ? [step2("Ubuntu Agent egress prerequisites", { kind: "commands", commands: [
|
|
12481
14266
|
{ argv: ["/usr/bin/apt-get", "update", "-qq"] },
|
|
12482
14267
|
{ argv: ["/usr/bin/apt-get", "install", "-y", "nftables"] },
|
|
12483
14268
|
{ argv: ["/usr/bin/systemctl", "enable", "--now", "systemd-resolved.service"] }
|
|
12484
|
-
] }),
|
|
12485
|
-
|
|
12486
|
-
|
|
12487
|
-
...sourceBinPath && binPath ? [
|
|
12488
|
-
...warpEnabled ? [
|
|
12489
|
-
...deploymentEnabled ? [
|
|
14269
|
+
] }), step2("prove systemd-resolved stub ownership", { kind: "verify-resolved-stub" })] : [],
|
|
14270
|
+
step2("vault socket access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", VAULT_GROUP], acceptedExitCodes: [0, 9] }] }),
|
|
14271
|
+
step2("Agent update helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", AGENT_UPDATE_GROUP], acceptedExitCodes: [0, 9] }] }),
|
|
14272
|
+
...sourceBinPath && binPath ? [step2("root-owned agent runtime", { kind: "install-runtime", source: sourceBinPath, binary: binPath, version: VERSION2 })] : [],
|
|
14273
|
+
...warpEnabled ? [step2("Cloudflare One client for Ubuntu 26.04", { kind: "install-warp" })] : [],
|
|
14274
|
+
...deploymentEnabled ? [step2("deployment isolation group", { kind: "commands", commands: [
|
|
12490
14275
|
{ argv: ["/usr/sbin/groupadd", "--system", DEPLOYMENT_GROUP], acceptedExitCodes: [0, 9] },
|
|
12491
14276
|
{ argv: ["/usr/sbin/groupadd", "--system", SOFTWARE_HELPER_GROUP], acceptedExitCodes: [0, 9] }
|
|
12492
14277
|
] })] : [],
|
|
12493
|
-
...lifecycleEnabled ? [
|
|
12494
|
-
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
...lifecycleEnabled ? [
|
|
12498
|
-
...deploymentEnabled ? [
|
|
14278
|
+
...lifecycleEnabled ? [step2("lifecycle helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", LIFECYCLE_GROUP], acceptedExitCodes: [0, 9] }] })] : [],
|
|
14279
|
+
step2("service account", { kind: "commands", commands: [{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", user], acceptedExitCodes: [0, 9] }] }),
|
|
14280
|
+
step2("bind service account to vault group", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-g", VAULT_GROUP, user] }] }),
|
|
14281
|
+
step2("grant verified Agent update access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", AGENT_UPDATE_GROUP, user] }] }),
|
|
14282
|
+
...lifecycleEnabled ? [step2("grant lifecycle helper socket access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", LIFECYCLE_GROUP, user] }] })] : [],
|
|
14283
|
+
...deploymentEnabled ? [step2("credential-free deployment account", { kind: "commands", commands: [
|
|
12499
14284
|
{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", DEPLOYMENT_GROUP, DEPLOYMENT_RUNNER_USER], acceptedExitCodes: [0, 9] },
|
|
12500
14285
|
{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", VAULT_GROUP, APPLICATION_RUNTIME_USER], acceptedExitCodes: [0, 9] },
|
|
12501
14286
|
{ argv: ["/usr/sbin/usermod", "-a", "-G", DEPLOYMENT_GROUP, APPLICATION_RUNTIME_USER] },
|
|
12502
14287
|
{ argv: ["/usr/sbin/usermod", "-a", "-G", `${DEPLOYMENT_GROUP},${SOFTWARE_HELPER_GROUP}`, user] }
|
|
12503
14288
|
] })] : [],
|
|
12504
|
-
|
|
14289
|
+
step2("credential and state directories", { kind: "directories", directories: [
|
|
12505
14290
|
{ path: credentialDir, mode: 448, owner: "root", group: "root" },
|
|
12506
14291
|
{ path: "/var/lib/forgezero", mode: 488, owner: "root", group: "root" }
|
|
12507
14292
|
] }),
|
|
12508
|
-
|
|
14293
|
+
step2("encrypted node identity", { kind: "ensure-seed", credential: seedCredentialPath }),
|
|
12509
14294
|
...options.generateGitIdentity && gitCredentialPath && gitPublicKeyPath ? [
|
|
12510
|
-
|
|
12511
|
-
|
|
14295
|
+
step2("Git deploy identity directory", { kind: "directories", directories: [{ path: gitPublicKeyDir, mode: 493, owner: "root", group: "root" }] }),
|
|
14296
|
+
step2("unique encrypted Git deploy identity", { kind: "ensure-git-identity", credential: gitCredentialPath, publicKey: gitPublicKeyPath })
|
|
12512
14297
|
] : [],
|
|
12513
14298
|
...bootstrapEnabled ? [
|
|
12514
|
-
|
|
14299
|
+
step2("bootstrap SSH public identity directory", { kind: "directories", directories: [
|
|
12515
14300
|
{ path: bootstrapSshPublicKeyDir, mode: 493, owner: "root", group: "root" }
|
|
12516
14301
|
] }),
|
|
12517
|
-
|
|
14302
|
+
step2("unique encrypted bootstrap SSH identity", {
|
|
12518
14303
|
kind: "ensure-bootstrap-ssh-identity",
|
|
12519
14304
|
credential: bootstrapSshCredentialPath,
|
|
12520
14305
|
publicKey: bootstrapSshPublicKeyPath,
|
|
@@ -12522,10 +14307,10 @@ function planProvision(options) {
|
|
|
12522
14307
|
})
|
|
12523
14308
|
] : [],
|
|
12524
14309
|
...enrolmentEnabled ? [
|
|
12525
|
-
|
|
12526
|
-
|
|
14310
|
+
step2("enrolment state directory", { kind: "directories", directories: [{ path: enrolStateDir, mode: 448, owner: user, group: user }] }),
|
|
14311
|
+
step2("encrypted one-time enrolment capability", { kind: "ensure-enrolment", state: enrolStatePath, source: enrolTokenSourcePath, credential: enrolTokenCredentialPath })
|
|
12527
14312
|
] : [],
|
|
12528
|
-
...deploymentEnabled ? [
|
|
14313
|
+
...deploymentEnabled ? [step2("deployment directories", { kind: "directories", directories: [
|
|
12529
14314
|
{ path: deployRoot, mode: 493, owner: "root", group: "root" },
|
|
12530
14315
|
{ path: `${deployRoot}/releases`, mode: 2040, owner: "root", group: DEPLOYMENT_GROUP },
|
|
12531
14316
|
{ path: `${deployRoot}/cache`, mode: 488, owner: user, group: user },
|
|
@@ -12535,13 +14320,13 @@ function planProvision(options) {
|
|
|
12535
14320
|
{ path: `${deployRoot}/runner-home/cache`, mode: 448, owner: DEPLOYMENT_RUNNER_USER, group: DEPLOYMENT_GROUP },
|
|
12536
14321
|
{ path: `${deployRoot}/app-home`, mode: 448, owner: APPLICATION_RUNTIME_USER, group: VAULT_GROUP }
|
|
12537
14322
|
] })] : [],
|
|
12538
|
-
|
|
12539
|
-
...deploymentEnabled ? [
|
|
14323
|
+
step2("reload units", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "daemon-reload"] }] }),
|
|
14324
|
+
...deploymentEnabled ? [step2("remove unsupported deployment socket activation", { kind: "commands", commands: [
|
|
12540
14325
|
{ argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-deploy-runner.socket"], acceptedExitCodes: [0, 1, 5] },
|
|
12541
14326
|
{ argv: ["/usr/bin/rm", "-f", "/etc/systemd/system/forgezero-deploy-runner.socket"] },
|
|
12542
14327
|
{ argv: ["/usr/bin/systemctl", "daemon-reload"] }
|
|
12543
14328
|
] })] : [],
|
|
12544
|
-
|
|
14329
|
+
step2("enable and converge services", { kind: "commands", commands: [
|
|
12545
14330
|
{ argv: ["/usr/bin/systemctl", "enable", ...enabledUnits] },
|
|
12546
14331
|
{ argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
|
|
12547
14332
|
...restartedUnits.length ? [{ argv: ["/usr/bin/systemctl", "restart", ...restartedUnits] }] : [],
|
|
@@ -12549,19 +14334,19 @@ function planProvision(options) {
|
|
|
12549
14334
|
{ argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
|
|
12550
14335
|
{ argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.service"] }
|
|
12551
14336
|
] }),
|
|
12552
|
-
...enrolmentEnabled ? [
|
|
12553
|
-
...options.enforceEgress ? [
|
|
12554
|
-
|
|
12555
|
-
|
|
12556
|
-
|
|
12557
|
-
|
|
14337
|
+
...enrolmentEnabled ? [step2("prove the compute binding is durable", { kind: "verify-file", path: enrolStatePath })] : [],
|
|
14338
|
+
...options.enforceEgress ? [step2("prove the Agent egress policy is active", { kind: "verify-egress", runnerPublicTcpPorts, runnerLoopbackPorts })] : [],
|
|
14339
|
+
step2("prove it is running", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "is-active", "forgezero-agent.service"] }] }),
|
|
14340
|
+
step2("prove the public Vault socket exists", { kind: "wait-socket", path: options.socketPath, attempts: 100, intervalMs: 100 }),
|
|
14341
|
+
step2("prove the Agent Vault backend exists", { kind: "wait-socket", path: agentBackendSocketPath(options.socketPath), attempts: 100, intervalMs: 100 }),
|
|
14342
|
+
step2("prove the Agent update helper exists", { kind: "wait-socket", path: DEFAULT_AGENT_UPDATE_SOCKET, attempts: 100, intervalMs: 100 }),
|
|
12558
14343
|
...deploymentEnabled ? [
|
|
12559
|
-
|
|
12560
|
-
|
|
14344
|
+
step2("prove the deployment runner socket exists", { kind: "wait-socket", path: DEPLOYMENT_RUNNER_SOCKET, attempts: 100, intervalMs: 100 }),
|
|
14345
|
+
step2("prove the software strategy helper socket exists", { kind: "wait-socket", path: DEFAULT_SOFTWARE_HELPER_SOCKET, attempts: 100, intervalMs: 100 })
|
|
12561
14346
|
] : [],
|
|
12562
|
-
...lifecycleEnabled ? [
|
|
12563
|
-
...warpEnabled ? [
|
|
12564
|
-
...options.repository ? [
|
|
14347
|
+
...lifecycleEnabled ? [step2("prove the lifecycle helper socket exists", { kind: "wait-socket", path: lifecycleHelperSocketPath, attempts: 100, intervalMs: 100 })] : [],
|
|
14348
|
+
...warpEnabled ? [step2("prove Cloudflare WARP is connected", { kind: "verify-warp" })] : [],
|
|
14349
|
+
...options.repository ? [step2("prove the deployment control socket exists", { kind: "wait-socket", path: options.controlSocketPath ?? "/run/forgezero/control.sock", attempts: 100, intervalMs: 100 })] : []
|
|
12565
14350
|
]
|
|
12566
14351
|
};
|
|
12567
14352
|
}
|
|
@@ -12571,17 +14356,17 @@ import { randomBytes as randomBytes5 } from "crypto";
|
|
|
12571
14356
|
import {
|
|
12572
14357
|
chmodSync as chmodSync15,
|
|
12573
14358
|
copyFileSync as copyFileSync3,
|
|
12574
|
-
existsSync as
|
|
14359
|
+
existsSync as existsSync18,
|
|
12575
14360
|
lstatSync as lstatSync3,
|
|
12576
|
-
mkdirSync as
|
|
12577
|
-
readFileSync as
|
|
12578
|
-
realpathSync as
|
|
12579
|
-
renameSync as
|
|
12580
|
-
rmSync as
|
|
14361
|
+
mkdirSync as mkdirSync13,
|
|
14362
|
+
readFileSync as readFileSync15,
|
|
14363
|
+
realpathSync as realpathSync6,
|
|
14364
|
+
renameSync as renameSync10,
|
|
14365
|
+
rmSync as rmSync9,
|
|
12581
14366
|
symlinkSync as symlinkSync5,
|
|
12582
|
-
writeFileSync as
|
|
14367
|
+
writeFileSync as writeFileSync13
|
|
12583
14368
|
} from "fs";
|
|
12584
|
-
import { dirname as
|
|
14369
|
+
import { dirname as dirname9 } from "path";
|
|
12585
14370
|
async function readCapabilities(run2) {
|
|
12586
14371
|
const answers = {};
|
|
12587
14372
|
const checks = Object.entries(CAPABILITY_CHECKS);
|
|
@@ -12640,28 +14425,28 @@ var runProvisionOperation = async (operation) => {
|
|
|
12640
14425
|
}
|
|
12641
14426
|
if (operation.kind === "install-runtime") {
|
|
12642
14427
|
const release = `/opt/forgezero/agent/versions/${operation.version}`;
|
|
12643
|
-
|
|
12644
|
-
|
|
14428
|
+
mkdirSync13(`${release}/dist`, { recursive: true, mode: 493 });
|
|
14429
|
+
mkdirSync13(dirname9(operation.binary), { recursive: true, mode: 493 });
|
|
12645
14430
|
copyFileSync3(operation.source, `${release}/dist/fz-agent.js`);
|
|
12646
14431
|
chmodSync15(`${release}/dist/fz-agent.js`, 493);
|
|
12647
|
-
const gitSshSource = `${
|
|
12648
|
-
if (!
|
|
14432
|
+
const gitSshSource = `${dirname9(operation.source)}/fz-git-ssh.js`;
|
|
14433
|
+
if (!existsSync18(gitSshSource))
|
|
12649
14434
|
return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
|
|
12650
14435
|
copyFileSync3(gitSshSource, `${release}/dist/fz-git-ssh.js`);
|
|
12651
14436
|
chmodSync15(`${release}/dist/fz-git-ssh.js`, 493);
|
|
12652
14437
|
const pending = "/opt/forgezero/agent/current.next";
|
|
12653
|
-
|
|
14438
|
+
rmSync9(pending, { force: true });
|
|
12654
14439
|
symlinkSync5(`versions/${operation.version}`, pending);
|
|
12655
|
-
|
|
12656
|
-
|
|
14440
|
+
renameSync10(pending, "/opt/forgezero/agent/current");
|
|
14441
|
+
rmSync9(operation.binary, { force: true });
|
|
12657
14442
|
symlinkSync5("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
|
|
12658
14443
|
const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
|
|
12659
|
-
|
|
14444
|
+
rmSync9(gitSshBinary, { force: true });
|
|
12660
14445
|
symlinkSync5("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
|
|
12661
14446
|
return { stdout: "", exitCode: 0 };
|
|
12662
14447
|
}
|
|
12663
14448
|
if (operation.kind === "ensure-seed") {
|
|
12664
|
-
if (
|
|
14449
|
+
if (existsSync18(operation.credential) && lstatSync3(operation.credential).size > 0)
|
|
12665
14450
|
return { stdout: "", exitCode: 0 };
|
|
12666
14451
|
const seed = randomBytes5(32).toString("base64url");
|
|
12667
14452
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
|
|
@@ -12673,9 +14458,9 @@ var runProvisionOperation = async (operation) => {
|
|
|
12673
14458
|
const key = "/run/forgezero-git-deploy-key";
|
|
12674
14459
|
const publicKey = `${key}.pub`;
|
|
12675
14460
|
try {
|
|
12676
|
-
if (!
|
|
12677
|
-
|
|
12678
|
-
|
|
14461
|
+
if (!existsSync18(operation.credential) || lstatSync3(operation.credential).size < 1) {
|
|
14462
|
+
rmSync9(key, { force: true });
|
|
14463
|
+
rmSync9(publicKey, { force: true });
|
|
12679
14464
|
let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
|
|
12680
14465
|
if (result.exitCode !== 0)
|
|
12681
14466
|
return result;
|
|
@@ -12684,8 +14469,8 @@ var runProvisionOperation = async (operation) => {
|
|
|
12684
14469
|
return result;
|
|
12685
14470
|
chmodSync15(operation.credential, 256);
|
|
12686
14471
|
}
|
|
12687
|
-
if (!
|
|
12688
|
-
if (!
|
|
14472
|
+
if (!existsSync18(operation.publicKey) || lstatSync3(operation.publicKey).size < 1) {
|
|
14473
|
+
if (!existsSync18(key)) {
|
|
12689
14474
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
|
|
12690
14475
|
if (decrypted.exitCode !== 0)
|
|
12691
14476
|
return decrypted;
|
|
@@ -12693,25 +14478,25 @@ var runProvisionOperation = async (operation) => {
|
|
|
12693
14478
|
const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
|
|
12694
14479
|
if (derived.exitCode !== 0)
|
|
12695
14480
|
return derived;
|
|
12696
|
-
|
|
14481
|
+
writeFileSync13(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
|
|
12697
14482
|
`, { mode: 292 });
|
|
12698
14483
|
}
|
|
12699
14484
|
return { stdout: "", exitCode: 0 };
|
|
12700
14485
|
} finally {
|
|
12701
|
-
|
|
12702
|
-
|
|
14486
|
+
rmSync9(key, { force: true });
|
|
14487
|
+
rmSync9(publicKey, { force: true });
|
|
12703
14488
|
}
|
|
12704
14489
|
}
|
|
12705
14490
|
if (operation.kind === "ensure-bootstrap-ssh-identity") {
|
|
12706
14491
|
const key = "/run/forgezero-bootstrap-ssh-key";
|
|
12707
14492
|
const generatedPublicKey = `${key}.pub`;
|
|
12708
14493
|
try {
|
|
12709
|
-
if (!
|
|
12710
|
-
|
|
12711
|
-
|
|
14494
|
+
if (!existsSync18(operation.credential) || lstatSync3(operation.credential).size < 1) {
|
|
14495
|
+
rmSync9(key, { force: true });
|
|
14496
|
+
rmSync9(generatedPublicKey, { force: true });
|
|
12712
14497
|
let result;
|
|
12713
14498
|
if (operation.source) {
|
|
12714
|
-
const source =
|
|
14499
|
+
const source = existsSync18(operation.source) ? lstatSync3(operation.source) : undefined;
|
|
12715
14500
|
if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
|
|
12716
14501
|
return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
|
|
12717
14502
|
}
|
|
@@ -12731,8 +14516,8 @@ var runProvisionOperation = async (operation) => {
|
|
|
12731
14516
|
return result;
|
|
12732
14517
|
chmodSync15(operation.credential, 256);
|
|
12733
14518
|
}
|
|
12734
|
-
if (!
|
|
12735
|
-
if (!
|
|
14519
|
+
if (!existsSync18(operation.publicKey) || lstatSync3(operation.publicKey).size < 1) {
|
|
14520
|
+
if (!existsSync18(key)) {
|
|
12736
14521
|
const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
|
|
12737
14522
|
if (decrypted.exitCode !== 0)
|
|
12738
14523
|
return decrypted;
|
|
@@ -12742,28 +14527,28 @@ var runProvisionOperation = async (operation) => {
|
|
|
12742
14527
|
if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
|
|
12743
14528
|
return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
|
|
12744
14529
|
}
|
|
12745
|
-
|
|
12746
|
-
|
|
14530
|
+
mkdirSync13(dirname9(operation.publicKey), { recursive: true, mode: 493 });
|
|
14531
|
+
writeFileSync13(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
|
|
12747
14532
|
`, { mode: 292 });
|
|
12748
14533
|
chmodSync15(operation.publicKey, 292);
|
|
12749
14534
|
}
|
|
12750
14535
|
if (operation.source)
|
|
12751
|
-
|
|
14536
|
+
rmSync9(operation.source, { force: true });
|
|
12752
14537
|
return { stdout: "", exitCode: 0 };
|
|
12753
14538
|
} finally {
|
|
12754
|
-
|
|
12755
|
-
|
|
14539
|
+
rmSync9(key, { force: true });
|
|
14540
|
+
rmSync9(generatedPublicKey, { force: true });
|
|
12756
14541
|
}
|
|
12757
14542
|
}
|
|
12758
14543
|
if (operation.kind === "ensure-enrolment") {
|
|
12759
|
-
if (
|
|
14544
|
+
if (existsSync18(operation.state) && lstatSync3(operation.state).size > 0 || existsSync18(operation.credential) && lstatSync3(operation.credential).size > 0)
|
|
12760
14545
|
return { stdout: "", exitCode: 0 };
|
|
12761
|
-
if (!
|
|
14546
|
+
if (!existsSync18(operation.source))
|
|
12762
14547
|
return { stdout: "enrolment source is missing", exitCode: 1 };
|
|
12763
14548
|
const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
|
|
12764
14549
|
if (result.exitCode === 0) {
|
|
12765
14550
|
chmodSync15(operation.credential, 256);
|
|
12766
|
-
|
|
14551
|
+
rmSync9(operation.source, { force: true });
|
|
12767
14552
|
}
|
|
12768
14553
|
return result;
|
|
12769
14554
|
}
|
|
@@ -12778,7 +14563,7 @@ var runProvisionOperation = async (operation) => {
|
|
|
12778
14563
|
return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
|
|
12779
14564
|
}
|
|
12780
14565
|
if (operation.kind === "verify-file")
|
|
12781
|
-
return
|
|
14566
|
+
return existsSync18(operation.path) && lstatSync3(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
|
|
12782
14567
|
if (operation.kind === "verify-egress") {
|
|
12783
14568
|
const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
|
|
12784
14569
|
if (active.exitCode !== 0)
|
|
@@ -12794,31 +14579,31 @@ var runProvisionOperation = async (operation) => {
|
|
|
12794
14579
|
if (operation.kind === "verify-resolved-stub") {
|
|
12795
14580
|
try {
|
|
12796
14581
|
const expected = "/run/systemd/resolve/stub-resolv.conf";
|
|
12797
|
-
return
|
|
14582
|
+
return realpathSync6("/etc/resolv.conf") === expected && realpathSync6(expected) === expected ? { stdout: expected, exitCode: 0 } : { stdout: "resolver stub mismatch", exitCode: 1 };
|
|
12798
14583
|
} catch {
|
|
12799
14584
|
return { stdout: "resolver stub missing", exitCode: 1 };
|
|
12800
14585
|
}
|
|
12801
14586
|
}
|
|
12802
14587
|
if (operation.kind === "install-warp") {
|
|
12803
|
-
const os =
|
|
14588
|
+
const os = readFileSync15("/etc/os-release", "utf8");
|
|
12804
14589
|
if (!/^ID=ubuntu$/m.test(os) || !/^VERSION_ID="?26\.04"?$/m.test(os))
|
|
12805
14590
|
return { stdout: "unsupported WARP host OS", exitCode: 1 };
|
|
12806
14591
|
const response = await fetch("https://pkg.cloudflareclient.com/pubkey.gpg", { signal: AbortSignal.timeout(30000) });
|
|
12807
14592
|
if (!response.ok)
|
|
12808
14593
|
return { stdout: `WARP key HTTP ${response.status}`, exitCode: 1 };
|
|
12809
|
-
|
|
12810
|
-
|
|
12811
|
-
|
|
14594
|
+
mkdirSync13("/usr/share/keyrings", { recursive: true, mode: 493 });
|
|
14595
|
+
mkdirSync13("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
|
|
14596
|
+
mkdirSync13("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
|
|
12812
14597
|
const key = "/run/cloudflare-warp-key.gpg";
|
|
12813
|
-
|
|
14598
|
+
writeFileSync13(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
|
|
12814
14599
|
let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
|
|
12815
|
-
|
|
14600
|
+
rmSync9(key, { force: true });
|
|
12816
14601
|
if (result.exitCode !== 0)
|
|
12817
14602
|
return result;
|
|
12818
14603
|
const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
|
|
12819
14604
|
if (!codename)
|
|
12820
14605
|
return { stdout: "Ubuntu codename missing", exitCode: 1 };
|
|
12821
|
-
|
|
14606
|
+
writeFileSync13("/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
|
|
12822
14607
|
`, { mode: 420 });
|
|
12823
14608
|
result = await fixed(["/usr/bin/apt-get", "update", "-qq"]);
|
|
12824
14609
|
return result.exitCode === 0 ? fixed(["/usr/bin/apt-get", "install", "-y", "cloudflare-warp"]) : result;
|
|
@@ -12849,11 +14634,11 @@ function planInstall(options) {
|
|
|
12849
14634
|
}
|
|
12850
14635
|
async function applyPlan(plan, run2) {
|
|
12851
14636
|
const transcript = [];
|
|
12852
|
-
for (const
|
|
12853
|
-
const result = await run2(
|
|
12854
|
-
transcript.push({ label:
|
|
12855
|
-
if (result.exitCode !== 0 && !
|
|
12856
|
-
throw new Error(`${
|
|
14637
|
+
for (const step3 of plan.steps) {
|
|
14638
|
+
const result = await run2(step3.operation);
|
|
14639
|
+
transcript.push({ label: step3.label, command: step3.command, exitCode: result.exitCode });
|
|
14640
|
+
if (result.exitCode !== 0 && !step3.optional) {
|
|
14641
|
+
throw new Error(`${step3.label} failed (exit ${result.exitCode}): ${step3.command}`);
|
|
12857
14642
|
}
|
|
12858
14643
|
}
|
|
12859
14644
|
return transcript;
|
|
@@ -13024,9 +14809,9 @@ async function bootstrapStatus(host = localBootstrapHost()) {
|
|
|
13024
14809
|
if (health.exitCode !== 0)
|
|
13025
14810
|
problems.push("platform API health check failed");
|
|
13026
14811
|
}
|
|
13027
|
-
const
|
|
13028
|
-
services["nginx-config"] =
|
|
13029
|
-
if (
|
|
14812
|
+
const nginx3 = await host.exec(["nginx", "-t"]);
|
|
14813
|
+
services["nginx-config"] = nginx3.exitCode === 0;
|
|
14814
|
+
if (nginx3.exitCode !== 0)
|
|
13030
14815
|
problems.push("nginx configuration is invalid");
|
|
13031
14816
|
if (state.databaseRole !== "none") {
|
|
13032
14817
|
const unitPath = "/etc/systemd/system/forgezero-db.service";
|
|
@@ -13071,17 +14856,17 @@ function localBootstrapHost() {
|
|
|
13071
14856
|
};
|
|
13072
14857
|
return {
|
|
13073
14858
|
uid: () => process.getuid?.() ?? -1,
|
|
13074
|
-
exists:
|
|
13075
|
-
read: (path2) =>
|
|
14859
|
+
exists: existsSync19,
|
|
14860
|
+
read: (path2) => readFileSync16(path2, "utf8"),
|
|
13076
14861
|
write(path2, content, mode) {
|
|
13077
|
-
|
|
14862
|
+
mkdirSync14(dirname10(path2), { recursive: true, mode: 493 });
|
|
13078
14863
|
const temporary = `${path2}.next.${process.pid}`;
|
|
13079
|
-
|
|
14864
|
+
writeFileSync14(temporary, content, { mode });
|
|
13080
14865
|
chmodSync16(temporary, mode);
|
|
13081
|
-
|
|
14866
|
+
renameSync11(temporary, path2);
|
|
13082
14867
|
},
|
|
13083
|
-
mkdir: (path2, mode) =>
|
|
13084
|
-
remove: (path2) =>
|
|
14868
|
+
mkdir: (path2, mode) => mkdirSync14(path2, { recursive: true, mode }),
|
|
14869
|
+
remove: (path2) => rmSync10(path2, { force: true }),
|
|
13085
14870
|
inspect(path2) {
|
|
13086
14871
|
const value = lstatSync4(path2);
|
|
13087
14872
|
return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
|
|
@@ -13105,7 +14890,7 @@ function localBootstrapHost() {
|
|
|
13105
14890
|
async installAgent(config, enrolTokenSourcePath) {
|
|
13106
14891
|
const capabilities = await readCapabilities(localRunner);
|
|
13107
14892
|
const deployRoot = config.deployRoot ?? "/opt/forgezero";
|
|
13108
|
-
const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) ||
|
|
14893
|
+
const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) || existsSync19("/var/lib/forgezero/enrolment.json");
|
|
13109
14894
|
if (config.kind === "platform") {
|
|
13110
14895
|
const lifecycle = config.database.role === "none" ? {
|
|
13111
14896
|
apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
|
|
@@ -13117,8 +14902,8 @@ function localBootstrapHost() {
|
|
|
13117
14902
|
databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
|
|
13118
14903
|
databasePorts: [8529]
|
|
13119
14904
|
};
|
|
13120
|
-
|
|
13121
|
-
|
|
14905
|
+
mkdirSync14(dirname10(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
|
|
14906
|
+
writeFileSync14(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
|
|
13122
14907
|
`, { mode: 256 });
|
|
13123
14908
|
}
|
|
13124
14909
|
const plan = planInstall({
|
|
@@ -13159,8 +14944,8 @@ function localBootstrapHost() {
|
|
|
13159
14944
|
} : {}
|
|
13160
14945
|
});
|
|
13161
14946
|
for (const unit3 of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
|
|
13162
|
-
|
|
13163
|
-
|
|
14947
|
+
mkdirSync14(dirname10(unit3.path), { recursive: true, mode: 493 });
|
|
14948
|
+
writeFileSync14(unit3.path, unit3.unit, { mode: 420 });
|
|
13164
14949
|
}
|
|
13165
14950
|
await applyPlan(plan, localRunner);
|
|
13166
14951
|
return plan;
|
|
@@ -13194,8 +14979,8 @@ async function verifyPlatformFleetHost(expectedVersion, runtime = localRuntime()
|
|
|
13194
14979
|
throw new Error("expected Agent version is invalid");
|
|
13195
14980
|
if (runtime.uid() !== 0)
|
|
13196
14981
|
throw new Error("platform-fleet-verify must run as root");
|
|
13197
|
-
if (expectedVersion !==
|
|
13198
|
-
throw new Error(`Agent version ${
|
|
14982
|
+
if (expectedVersion !== VERSION2) {
|
|
14983
|
+
throw new Error(`Agent version ${VERSION2} does not match expected ${expectedVersion}`);
|
|
13199
14984
|
}
|
|
13200
14985
|
const status = await runtime.bootstrapStatus();
|
|
13201
14986
|
if (!status.initialized || status.problems.length > 0) {
|
|
@@ -13208,16 +14993,17 @@ async function verifyPlatformFleetHost(expectedVersion, runtime = localRuntime()
|
|
|
13208
14993
|
throw new Error("could not inspect failed systemd units");
|
|
13209
14994
|
if (failed.output.trim())
|
|
13210
14995
|
throw new Error(`failed systemd units are present: ${failed.output.trim()}`);
|
|
13211
|
-
return { ok: true, agentVersion:
|
|
14996
|
+
return { ok: true, agentVersion: VERSION2, bootstrap: status, sevGuest: true, failedUnits: [] };
|
|
13212
14997
|
}
|
|
13213
14998
|
|
|
13214
14999
|
// src/community-rehearsal-host.ts
|
|
13215
|
-
import { createHash as
|
|
13216
|
-
import { lstatSync as lstatSync6, mkdirSync as
|
|
13217
|
-
import { dirname as
|
|
15000
|
+
import { createHash as createHash10 } from "crypto";
|
|
15001
|
+
import { lstatSync as lstatSync6, mkdirSync as mkdirSync15, readFileSync as readFileSync17, rmSync as rmSync11, symlinkSync as symlinkSync6, unlinkSync as unlinkSync12, writeFileSync as writeFileSync15 } from "fs";
|
|
15002
|
+
import { dirname as dirname11 } from "path";
|
|
13218
15003
|
var COMMUNITY_REHEARSAL_VERSION = "3.11.14";
|
|
13219
15004
|
var COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
|
|
13220
15005
|
var COMMUNITY_DATABASE_ROOT = "/var/lib/forgezero-rehearsal-cluster";
|
|
15006
|
+
var COMMUNITY_CREDENTIAL_PARENT = "/etc/forgezero-rehearsal";
|
|
13221
15007
|
var COMMUNITY_CREDENTIAL_ROOT = "/etc/forgezero-rehearsal/creds";
|
|
13222
15008
|
var NODES = new Map([
|
|
13223
15009
|
["dev-fz-n1", { address: "10.42.0.21", agency: true }],
|
|
@@ -13227,11 +15013,11 @@ var NODES = new Map([
|
|
|
13227
15013
|
]);
|
|
13228
15014
|
var RELEASE = /^[a-f0-9]{64}$/;
|
|
13229
15015
|
async function ensureCommunityRehearsalArango(execute3 = executeSoftwareOperation) {
|
|
13230
|
-
const
|
|
13231
|
-
if ((await execute3({ kind: "check", ...
|
|
15016
|
+
const requirement2 = { software: "arangodb", version: COMMUNITY_REHEARSAL_VERSION };
|
|
15017
|
+
if ((await execute3({ kind: "check", ...requirement2 })).exitCode === 0)
|
|
13232
15018
|
return;
|
|
13233
|
-
const installed = await execute3({ kind: "install", ...
|
|
13234
|
-
if (installed.exitCode !== 0 || (await execute3({ kind: "check", ...
|
|
15019
|
+
const installed = await execute3({ kind: "install", ...requirement2 });
|
|
15020
|
+
if (installed.exitCode !== 0 || (await execute3({ kind: "check", ...requirement2 })).exitCode !== 0) {
|
|
13235
15021
|
throw new Error(`unable to install and verify ArangoDB ${COMMUNITY_REHEARSAL_VERSION}`);
|
|
13236
15022
|
}
|
|
13237
15023
|
}
|
|
@@ -13249,30 +15035,36 @@ var exactNode = (node) => {
|
|
|
13249
15035
|
return { name: node, ...expected };
|
|
13250
15036
|
};
|
|
13251
15037
|
function parseCommunityRehearsalHostRequest(value) {
|
|
13252
|
-
const
|
|
13253
|
-
const node = exactNode(
|
|
13254
|
-
const action =
|
|
15038
|
+
const row2 = exactObject(value);
|
|
15039
|
+
const node = exactNode(row2.node);
|
|
15040
|
+
const action = row2.action;
|
|
13255
15041
|
if (action === "prepare") {
|
|
13256
15042
|
const allowed = ["action", "node", "address", "agency", "release", "archivePath", "archiveSha256", "jwt"];
|
|
13257
|
-
if (Object.keys(
|
|
15043
|
+
if (Object.keys(row2).some((key) => !allowed.includes(key)) || row2.address !== node.address || row2.agency !== node.agency || typeof row2.release !== "string" || !RELEASE.test(row2.release) || row2.archiveSha256 !== row2.release || row2.archivePath !== `/tmp/forgezero-community-${row2.release}.tar.gz` || typeof row2.jwt !== "string" || !/^[a-f0-9]{64}$/.test(row2.jwt)) {
|
|
13258
15044
|
throw new Error("community rehearsal prepare request does not match the dedicated fleet contract");
|
|
13259
15045
|
}
|
|
13260
|
-
return
|
|
15046
|
+
return row2;
|
|
15047
|
+
}
|
|
15048
|
+
if (action === "cleanup") {
|
|
15049
|
+
if (Object.keys(row2).some((key) => !["action", "node"].includes(key))) {
|
|
15050
|
+
throw new Error("community rehearsal cleanup has unknown fields");
|
|
15051
|
+
}
|
|
15052
|
+
return { action, node: node.name };
|
|
13261
15053
|
}
|
|
13262
15054
|
if (["database-enable", "api-enable", "database-ready", "starter-ready", "api-ready", "database-status", "api-status"].includes(String(action))) {
|
|
13263
|
-
if (Object.keys(
|
|
15055
|
+
if (Object.keys(row2).some((key) => !["action", "node"].includes(key)))
|
|
13264
15056
|
throw new Error("community rehearsal host action has unknown fields");
|
|
13265
15057
|
return { action, node: node.name };
|
|
13266
15058
|
}
|
|
13267
15059
|
if (action === "api") {
|
|
13268
|
-
if (Object.keys(
|
|
15060
|
+
if (Object.keys(row2).some((key) => !["action", "node", "operation", "key", "value"].includes(key)) || !["health", "init", "write", "read", "query", "cluster"].includes(String(row2.operation)) || row2.key !== undefined && (typeof row2.key !== "string" || !/^[A-Za-z0-9_.:-]{1,128}$/.test(row2.key)) || row2.value !== undefined && (typeof row2.value !== "string" || row2.value.length > 4096)) {
|
|
13269
15061
|
throw new Error("community rehearsal API request is invalid");
|
|
13270
15062
|
}
|
|
13271
|
-
if ((
|
|
15063
|
+
if ((row2.operation === "write" || row2.operation === "read") && typeof row2.key !== "string")
|
|
13272
15064
|
throw new Error("community rehearsal API key is required");
|
|
13273
|
-
if ((
|
|
15065
|
+
if ((row2.operation === "write" || row2.operation === "query") && typeof row2.value !== "string")
|
|
13274
15066
|
throw new Error("community rehearsal API value is required");
|
|
13275
|
-
return
|
|
15067
|
+
return row2;
|
|
13276
15068
|
}
|
|
13277
15069
|
throw new Error("unsupported community rehearsal host action");
|
|
13278
15070
|
}
|
|
@@ -13374,6 +15166,19 @@ function planCommunityRehearsalPrepare(request) {
|
|
|
13374
15166
|
{ kind: "unlink", path: request.archivePath }
|
|
13375
15167
|
];
|
|
13376
15168
|
}
|
|
15169
|
+
function planCommunityRehearsalCleanup() {
|
|
15170
|
+
return [
|
|
15171
|
+
{ kind: "exec", argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-community-api.service"], accepted: [0, 1, 5] },
|
|
15172
|
+
{ kind: "exec", argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-rehearsal-db.service"], accepted: [0, 1, 5] },
|
|
15173
|
+
{ kind: "unlink", path: "/etc/systemd/system/forgezero-community-api.service" },
|
|
15174
|
+
{ kind: "unlink", path: "/etc/systemd/system/forgezero-rehearsal-db.service" },
|
|
15175
|
+
{ kind: "remove-tree", path: COMMUNITY_DATABASE_ROOT },
|
|
15176
|
+
{ kind: "remove-tree", path: COMMUNITY_REHEARSAL_ROOT },
|
|
15177
|
+
{ kind: "remove-tree", path: COMMUNITY_CREDENTIAL_PARENT },
|
|
15178
|
+
{ kind: "exec", argv: ["/usr/bin/systemctl", "daemon-reload"] },
|
|
15179
|
+
{ kind: "exec", argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-community-api.service", "forgezero-rehearsal-db.service"], accepted: [0, 1, 5] }
|
|
15180
|
+
];
|
|
15181
|
+
}
|
|
13377
15182
|
async function exec(argv2, stdin) {
|
|
13378
15183
|
const child = Bun.spawn([...argv2], { stdin: stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
|
|
13379
15184
|
if (stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
|
|
@@ -13390,10 +15195,10 @@ async function exec(argv2, stdin) {
|
|
|
13390
15195
|
async function applyOperations(operations) {
|
|
13391
15196
|
for (const operation of operations) {
|
|
13392
15197
|
if (operation.kind === "remove-tree")
|
|
13393
|
-
|
|
15198
|
+
rmSync11(operation.path, { recursive: true, force: true });
|
|
13394
15199
|
else if (operation.kind === "write") {
|
|
13395
|
-
|
|
13396
|
-
|
|
15200
|
+
mkdirSync15(dirname11(operation.path), { recursive: true });
|
|
15201
|
+
writeFileSync15(operation.path, operation.content, { mode: operation.mode });
|
|
13397
15202
|
} else if (operation.kind === "unlink") {
|
|
13398
15203
|
try {
|
|
13399
15204
|
unlinkSync12(operation.path);
|
|
@@ -13436,9 +15241,13 @@ async function runCommunityRehearsalHost(request) {
|
|
|
13436
15241
|
if ((process.getuid?.() ?? -1) !== 0)
|
|
13437
15242
|
throw new Error("community-rehearsal must run as root");
|
|
13438
15243
|
const node = exactNode(request.node);
|
|
15244
|
+
if (request.action === "cleanup") {
|
|
15245
|
+
await applyOperations(planCommunityRehearsalCleanup());
|
|
15246
|
+
return { ok: true, action: request.action, node: node.name };
|
|
15247
|
+
}
|
|
13439
15248
|
if (request.action === "prepare") {
|
|
13440
15249
|
const metadata = lstatSync6(request.archivePath);
|
|
13441
|
-
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > 64 * 1024 * 1024 ||
|
|
15250
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1 || metadata.size > 64 * 1024 * 1024 || createHash10("sha256").update(readFileSync17(request.archivePath)).digest("hex") !== request.archiveSha256) {
|
|
13442
15251
|
throw new Error("community rehearsal archive is not the declared bounded release");
|
|
13443
15252
|
}
|
|
13444
15253
|
await ensureCommunityRehearsalArango();
|
|
@@ -13486,13 +15295,13 @@ async function runCommunityRehearsalHost(request) {
|
|
|
13486
15295
|
}
|
|
13487
15296
|
|
|
13488
15297
|
// src/supervised-app.ts
|
|
13489
|
-
import { lstatSync as lstatSync7, readFileSync as
|
|
15298
|
+
import { lstatSync as lstatSync7, readFileSync as readFileSync18 } from "fs";
|
|
13490
15299
|
function readConfig(path2) {
|
|
13491
15300
|
const stat = lstatSync7(path2);
|
|
13492
15301
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== 0 || (stat.mode & 18) !== 0 || stat.size > 128 * 1024) {
|
|
13493
15302
|
throw new Error("supervised app config is unsafe");
|
|
13494
15303
|
}
|
|
13495
|
-
const value = JSON.parse(
|
|
15304
|
+
const value = JSON.parse(readFileSync18(path2, "utf8"));
|
|
13496
15305
|
if (value.format !== 1 || typeof value.root !== "string" || !value.root.startsWith("/") || typeof value.release !== "string" || !value.release.startsWith(`${value.root}/releases/`) || typeof value.home !== "string" || value.home !== `${value.root}/app-home` || !Array.isArray(value.argv) || value.argv.length < 1 || value.argv.length > 256 || value.argv.some((argument) => typeof argument !== "string" || !argument || argument.includes("\x00")) || !value.environment || typeof value.environment !== "object" || Array.isArray(value.environment) || Object.entries(value.environment).some(([name, item]) => !/^FZ_(?:APP_PORT|RELEASE)$/.test(name) || typeof item !== "string" || /[\r\n\0]/.test(item))) {
|
|
13497
15306
|
throw new Error("supervised app config is malformed");
|
|
13498
15307
|
}
|
|
@@ -13538,16 +15347,16 @@ function agentCommandArgs(args, command2) {
|
|
|
13538
15347
|
return args.slice(index + 1);
|
|
13539
15348
|
}
|
|
13540
15349
|
function loadOrCreateSeed(path2) {
|
|
13541
|
-
if (
|
|
13542
|
-
const seed2 = new Uint8Array(Buffer.from(
|
|
15350
|
+
if (existsSync20(path2)) {
|
|
15351
|
+
const seed2 = new Uint8Array(Buffer.from(readFileSync19(path2, "utf8").trim(), "base64url"));
|
|
13543
15352
|
if (seed2.length < 32) {
|
|
13544
15353
|
throw new Error(`agent: the seed at ${path2} is too short to derive a key from.`);
|
|
13545
15354
|
}
|
|
13546
15355
|
return seed2;
|
|
13547
15356
|
}
|
|
13548
|
-
|
|
15357
|
+
mkdirSync16(dirname12(path2), { recursive: true });
|
|
13549
15358
|
const seed = new Uint8Array(randomBytes6(32));
|
|
13550
|
-
|
|
15359
|
+
writeFileSync16(path2, Buffer.from(seed).toString("base64url"), { mode: 384 });
|
|
13551
15360
|
chmodSync17(path2, 384);
|
|
13552
15361
|
return seed;
|
|
13553
15362
|
}
|
|
@@ -13577,9 +15386,9 @@ function loadSeedCredential(name = DEFAULT_SEED_CREDENTIAL, directory = process.
|
|
|
13577
15386
|
if (!directory)
|
|
13578
15387
|
throw new Error("agent: CREDENTIALS_DIRECTORY is missing; systemd did not load the node identity.");
|
|
13579
15388
|
const path2 = `${directory}/${name}`;
|
|
13580
|
-
if (!
|
|
15389
|
+
if (!existsSync20(path2))
|
|
13581
15390
|
throw new Error(`agent: the systemd credential ${name} is missing at ${path2}.`);
|
|
13582
|
-
const seed = new Uint8Array(Buffer.from(
|
|
15391
|
+
const seed = new Uint8Array(Buffer.from(readFileSync19(path2, "utf8").trim(), "base64url"));
|
|
13583
15392
|
if (seed.length < 32)
|
|
13584
15393
|
throw new Error(`agent: the systemd credential ${name} is too short to derive a key from.`);
|
|
13585
15394
|
return seed;
|
|
@@ -13589,7 +15398,7 @@ function loadTextCredential(name, directory = process.env.CREDENTIALS_DIRECTORY)
|
|
|
13589
15398
|
throw new Error("agent: CREDENTIALS_DIRECTORY is missing; systemd did not load the credential.");
|
|
13590
15399
|
if (!/^[A-Za-z0-9_.-]+$/.test(name))
|
|
13591
15400
|
throw new Error("agent: invalid systemd credential name.");
|
|
13592
|
-
const value =
|
|
15401
|
+
const value = readFileSync19(`${directory}/${name}`, "utf8").trim();
|
|
13593
15402
|
if (!value)
|
|
13594
15403
|
throw new Error(`agent: systemd credential ${name} is empty.`);
|
|
13595
15404
|
return value;
|
|
@@ -13669,7 +15478,7 @@ if (import.meta.main) {
|
|
|
13669
15478
|
const args = process.argv.slice(2);
|
|
13670
15479
|
if (args.includes("--help") || args.includes("-h")) {
|
|
13671
15480
|
console.log([
|
|
13672
|
-
`fz-agent ${
|
|
15481
|
+
`fz-agent ${VERSION2}`,
|
|
13673
15482
|
"",
|
|
13674
15483
|
"Runs inside managed compute and answers secret requests over a local socket.",
|
|
13675
15484
|
"It holds no configuration of its own \u2014 everything comes from the",
|
|
@@ -13702,7 +15511,7 @@ if (import.meta.main) {
|
|
|
13702
15511
|
process.exit(0);
|
|
13703
15512
|
}
|
|
13704
15513
|
if (args.includes("--version") || args.includes("-v")) {
|
|
13705
|
-
console.log(
|
|
15514
|
+
console.log(VERSION2);
|
|
13706
15515
|
process.exit(0);
|
|
13707
15516
|
}
|
|
13708
15517
|
const command2 = args.find((arg) => !arg.startsWith("-"));
|
|
@@ -13714,7 +15523,7 @@ if (import.meta.main) {
|
|
|
13714
15523
|
if (configPath !== "/etc/forgezero/deploy-activation.json" || !release) {
|
|
13715
15524
|
throw new Error("platform-activate requires the fixed config and one absolute release path");
|
|
13716
15525
|
}
|
|
13717
|
-
const config = JSON.parse(
|
|
15526
|
+
const config = JSON.parse(readFileSync19(configPath, "utf8"));
|
|
13718
15527
|
const result = await activatePlatformRelease(config, release);
|
|
13719
15528
|
console.log(`promoted ${result.release} on ${result.slot}`);
|
|
13720
15529
|
process.exit(0);
|
|
@@ -13745,7 +15554,7 @@ if (import.meta.main) {
|
|
|
13745
15554
|
}
|
|
13746
15555
|
}
|
|
13747
15556
|
for (const relative of ["package.json", "bun.lock"]) {
|
|
13748
|
-
const contents =
|
|
15557
|
+
const contents = readFileSync19(join12(process.cwd(), relative), "utf8");
|
|
13749
15558
|
if (/(?:workspace|file):/.test(contents)) {
|
|
13750
15559
|
throw new Error(`project release ${relative} contains a local dependency`);
|
|
13751
15560
|
}
|
|
@@ -13790,8 +15599,8 @@ if (import.meta.main) {
|
|
|
13790
15599
|
keys: keys2,
|
|
13791
15600
|
label: process.env.FZ_NODE_LABEL,
|
|
13792
15601
|
edgeHostname: process.env.FZ_NODE_HOSTNAME,
|
|
13793
|
-
gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ?
|
|
13794
|
-
bootstrapSshPublicKey: process.env.FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE ?
|
|
15602
|
+
gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ? readFileSync19(process.env.FZ_GIT_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
|
|
15603
|
+
bootstrapSshPublicKey: process.env.FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE ? readFileSync19(process.env.FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
|
|
13795
15604
|
privateNetworkAttachment: privateNetworkAttachmentFromEnvironment()
|
|
13796
15605
|
});
|
|
13797
15606
|
console.log(`[agent] enrolled ${binding2.computeReference} in project ${binding2.projectKey}/${binding2.environmentKey}`);
|
|
@@ -13833,7 +15642,7 @@ if (import.meta.main) {
|
|
|
13833
15642
|
const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
13834
15643
|
if (!profilePath)
|
|
13835
15644
|
throw new Error("metal-helper requires --profile=/absolute/path.json");
|
|
13836
|
-
const profile2 = JSON.parse(
|
|
15645
|
+
const profile2 = JSON.parse(readFileSync19(profilePath, "utf8"));
|
|
13837
15646
|
const helper = startMetalHelper({
|
|
13838
15647
|
profile: profile2,
|
|
13839
15648
|
socketPath: process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET
|
|
@@ -13928,7 +15737,7 @@ if (import.meta.main) {
|
|
|
13928
15737
|
if (stopping)
|
|
13929
15738
|
return;
|
|
13930
15739
|
stopping = true;
|
|
13931
|
-
await new Promise((
|
|
15740
|
+
await new Promise((resolve6) => helper.close(() => resolve6()));
|
|
13932
15741
|
process.exit(0);
|
|
13933
15742
|
};
|
|
13934
15743
|
process.on("SIGTERM", () => void stop());
|
|
@@ -13950,7 +15759,7 @@ if (import.meta.main) {
|
|
|
13950
15759
|
if (stopping)
|
|
13951
15760
|
return;
|
|
13952
15761
|
stopping = true;
|
|
13953
|
-
await new Promise((
|
|
15762
|
+
await new Promise((resolve6) => helper.close(() => resolve6()));
|
|
13954
15763
|
process.exit(0);
|
|
13955
15764
|
};
|
|
13956
15765
|
process.on("SIGTERM", () => void stop());
|
|
@@ -13983,7 +15792,7 @@ if (import.meta.main) {
|
|
|
13983
15792
|
const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
13984
15793
|
if (!profilePath)
|
|
13985
15794
|
throw new Error("metal-isolation requires --profile=/absolute/path.json");
|
|
13986
|
-
const profile2 = JSON.parse(
|
|
15795
|
+
const profile2 = JSON.parse(readFileSync19(profilePath, "utf8"));
|
|
13987
15796
|
await applyMetalIsolation(profile2);
|
|
13988
15797
|
console.log("[metal-isolation] host and guest cgroup boundaries active");
|
|
13989
15798
|
process.exit(0);
|
|
@@ -14028,7 +15837,7 @@ if (import.meta.main) {
|
|
|
14028
15837
|
if (claimArg !== "-" && !claimArg.startsWith("/")) {
|
|
14029
15838
|
throw new Error("metal-apply claim path must be absolute");
|
|
14030
15839
|
}
|
|
14031
|
-
const raw =
|
|
15840
|
+
const raw = readFileSync19(claimArg === "-" ? "/dev/stdin" : claimArg, "utf8");
|
|
14032
15841
|
if (Buffer.byteLength(raw) > 32 * 1024)
|
|
14033
15842
|
throw new Error("metal-apply claim exceeds 32 KiB");
|
|
14034
15843
|
const claim = JSON.parse(raw);
|
|
@@ -14053,7 +15862,7 @@ if (import.meta.main) {
|
|
|
14053
15862
|
if (commandArgs.length !== 1 || commandArgs[0] !== "--request=-") {
|
|
14054
15863
|
throw new Error("community-rehearsal requires exactly --request=-");
|
|
14055
15864
|
}
|
|
14056
|
-
const raw =
|
|
15865
|
+
const raw = readFileSync19("/dev/stdin", "utf8");
|
|
14057
15866
|
if (Buffer.byteLength(raw) > 128 * 1024)
|
|
14058
15867
|
throw new Error("community rehearsal request exceeds 128 KiB");
|
|
14059
15868
|
const request = parseCommunityRehearsalHostRequest(JSON.parse(raw));
|
|
@@ -14064,7 +15873,7 @@ if (import.meta.main) {
|
|
|
14064
15873
|
if (args.length !== 1 || args[0] !== "--request=-") {
|
|
14065
15874
|
throw new Error("metal-admission-proof requires exactly --request=-");
|
|
14066
15875
|
}
|
|
14067
|
-
const raw =
|
|
15876
|
+
const raw = readFileSync19("/dev/stdin", "utf8");
|
|
14068
15877
|
if (Buffer.byteLength(raw) > 32 * 1024)
|
|
14069
15878
|
throw new Error("metal admission proof request exceeds 32 KiB");
|
|
14070
15879
|
const request = JSON.parse(raw);
|
|
@@ -14074,7 +15883,7 @@ if (import.meta.main) {
|
|
|
14074
15883
|
const seed = configuredAgentSeed({ credential: process.env.FZ_SEED_CREDENTIAL, allowFileSeed: false });
|
|
14075
15884
|
const keys2 = deriveKeysFromSeed(seed);
|
|
14076
15885
|
seed.fill(0);
|
|
14077
|
-
const filesystem =
|
|
15886
|
+
const filesystem = statfsSync2("/var/lib/forgezero");
|
|
14078
15887
|
const body = {
|
|
14079
15888
|
...request,
|
|
14080
15889
|
publicKeys: { ed25519: keys2.ed25519.publicKey, mlDsa: keys2.mlDsa.publicKey },
|
|
@@ -14082,9 +15891,9 @@ if (import.meta.main) {
|
|
|
14082
15891
|
vcpu: cpus().length,
|
|
14083
15892
|
memoryGib: Math.max(1, Math.floor(totalmem() / 1024 ** 3)),
|
|
14084
15893
|
diskGib: Math.max(1, Math.floor(Number(filesystem.blocks) * Number(filesystem.bsize) / 1024 ** 3)),
|
|
14085
|
-
kvm:
|
|
14086
|
-
snpHost:
|
|
14087
|
-
helper:
|
|
15894
|
+
kvm: existsSync20("/dev/kvm"),
|
|
15895
|
+
snpHost: existsSync20("/dev/sev"),
|
|
15896
|
+
helper: existsSync20(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
|
|
14088
15897
|
}
|
|
14089
15898
|
};
|
|
14090
15899
|
const envelope = signRequest(keys2, keys2.ed25519.publicKey, {
|
|
@@ -14119,9 +15928,9 @@ if (import.meta.main) {
|
|
|
14119
15928
|
metalHostname: process.env.FZ_METAL_HOSTNAME,
|
|
14120
15929
|
run: (claim) => requestMetalProvision(claim, process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET),
|
|
14121
15930
|
metalPreflight: () => ({
|
|
14122
|
-
snpHost:
|
|
14123
|
-
kvm:
|
|
14124
|
-
helper:
|
|
15931
|
+
snpHost: existsSync20("/dev/sev"),
|
|
15932
|
+
kvm: existsSync20("/dev/kvm"),
|
|
15933
|
+
helper: existsSync20(process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET)
|
|
14125
15934
|
}),
|
|
14126
15935
|
onEvent: (event, detail) => console.log(`[metal-agent] ${event}${detail ? ` ${JSON.stringify(detail)}` : ""}`)
|
|
14127
15936
|
});
|
|
@@ -14204,7 +16013,7 @@ if (import.meta.main) {
|
|
|
14204
16013
|
const telemetry = new AgentTelemetryRuntime(createAgentTelemetry(resolveAgentTelemetryConfig()));
|
|
14205
16014
|
telemetry.event("agent.started");
|
|
14206
16015
|
telemetry.setDraining(false);
|
|
14207
|
-
const attestationSource =
|
|
16016
|
+
const attestationSource = existsSync20("/dev/sev-guest") ? createSnpAttestationSource() : undefined;
|
|
14208
16017
|
const running = runAgent({
|
|
14209
16018
|
socketPath: process.env.FZ_SOCKET_PATH ?? DEFAULT_SOCKET_PATH,
|
|
14210
16019
|
seedCredential: process.env.FZ_SEED_CREDENTIAL,
|
|
@@ -14215,12 +16024,12 @@ if (import.meta.main) {
|
|
|
14215
16024
|
record: (entry) => console.log(`[agent] ${entry.op} ${entry.outcome}${entry.detail ? ` ${entry.detail}` : ""}`)
|
|
14216
16025
|
});
|
|
14217
16026
|
const { nodeKey, keys, server } = running;
|
|
14218
|
-
console.log(`[agent] ${
|
|
16027
|
+
console.log(`[agent] ${VERSION2} signing as ${nodeKey}`);
|
|
14219
16028
|
const enrolmentStatePath = process.env.FZ_ENROL_STATE_FILE ?? DEFAULT_ENROLMENT_STATE_PATH;
|
|
14220
16029
|
let binding = loadGuestBinding(enrolmentStatePath, nodeKey);
|
|
14221
16030
|
const enrolmentCredential = process.env.FZ_ENROL_TOKEN_CREDENTIAL;
|
|
14222
|
-
const enrolmentCredentialAvailable = Boolean(enrolmentCredential && process.env.CREDENTIALS_DIRECTORY &&
|
|
14223
|
-
const legacyEnrolmentFileAvailable = Boolean(process.env.FZ_ENROL_TOKEN_FILE &&
|
|
16031
|
+
const enrolmentCredentialAvailable = Boolean(enrolmentCredential && process.env.CREDENTIALS_DIRECTORY && existsSync20(`${process.env.CREDENTIALS_DIRECTORY}/${enrolmentCredential}`));
|
|
16032
|
+
const legacyEnrolmentFileAvailable = Boolean(process.env.FZ_ENROL_TOKEN_FILE && existsSync20(process.env.FZ_ENROL_TOKEN_FILE));
|
|
14224
16033
|
if (!binding && (enrolmentCredentialAvailable || legacyEnrolmentFileAvailable) && process.env.FZ_API) {
|
|
14225
16034
|
binding = await enrolGuestIdentity({
|
|
14226
16035
|
apiUrl: process.env.FZ_API,
|
|
@@ -14231,8 +16040,8 @@ if (import.meta.main) {
|
|
|
14231
16040
|
keys,
|
|
14232
16041
|
label: process.env.FZ_NODE_LABEL,
|
|
14233
16042
|
edgeHostname: process.env.FZ_NODE_HOSTNAME,
|
|
14234
|
-
gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ?
|
|
14235
|
-
bootstrapSshPublicKey: process.env.FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE ?
|
|
16043
|
+
gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ? readFileSync19(process.env.FZ_GIT_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
|
|
16044
|
+
bootstrapSshPublicKey: process.env.FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE ? readFileSync19(process.env.FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
|
|
14236
16045
|
privateNetworkAttachment: privateNetworkAttachmentFromEnvironment()
|
|
14237
16046
|
});
|
|
14238
16047
|
console.log(`[agent] enrolled ${binding.computeReference} in project ${binding.projectKey}/${binding.environmentKey}`);
|
|
@@ -14298,7 +16107,7 @@ if (import.meta.main) {
|
|
|
14298
16107
|
const bootstrapEnabled = process.env.FZ_BOOTSTRAP_PULL === "true";
|
|
14299
16108
|
const bootstrapCredential = process.env.FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL;
|
|
14300
16109
|
const bootstrapKeyPath = bootstrapCredential && process.env.CREDENTIALS_DIRECTORY ? `${process.env.CREDENTIALS_DIRECTORY}/${bootstrapCredential}` : undefined;
|
|
14301
|
-
if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !
|
|
16110
|
+
if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !existsSync20(bootstrapKeyPath) || !process.env.FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT)) {
|
|
14302
16111
|
throw new Error("agent: bootstrap runner requires enrolment, API, local SSH credential and target OTLP coordinate");
|
|
14303
16112
|
}
|
|
14304
16113
|
const bootstrapPull = bootstrapEnabled ? startSshBootstrapPull({
|
|
@@ -14363,6 +16172,8 @@ if (import.meta.main) {
|
|
|
14363
16172
|
capacityEvidenceDirectory: process.env.FZ_CAPACITY_EVIDENCE_DIR ?? join12(root, "cache", "capacity"),
|
|
14364
16173
|
ensureSoftware: (requirements) => requestSoftware(requirements, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
14365
16174
|
activateService: (request) => requestServiceActivation(request, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
16175
|
+
buildContainer: (request) => requestContainerBuild(request, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
16176
|
+
activateContainer: (request) => requestContainerActivation(request, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
14366
16177
|
projectExec: process.env.FZ_DEPLOY_RUNNER_SOCKET ? (input) => requestDeploymentCommand(input, process.env.FZ_DEPLOY_RUNNER_SOCKET) : undefined
|
|
14367
16178
|
});
|
|
14368
16179
|
const staticManager = repository && branch && profile ? createDeploymentManager(managerOptions({ repository, branch, profile }, process.env.FZ_DEPLOY_KEY ?? `${repository}:${branch}:${profile}`)) : undefined;
|
|
@@ -14454,7 +16265,7 @@ if (import.meta.main) {
|
|
|
14454
16265
|
const deadlineMs = Math.max(1, Number(process.env.FZ_DRAIN_DEADLINE_MS ?? 30000));
|
|
14455
16266
|
const deadline = Date.now() + deadlineMs;
|
|
14456
16267
|
const remaining = () => Math.max(1, deadline - Date.now());
|
|
14457
|
-
const controlClosed = control ? await settleWithin(new Promise((
|
|
16268
|
+
const controlClosed = control ? await settleWithin(new Promise((resolve6) => control.close(() => resolve6())), remaining()) : true;
|
|
14458
16269
|
const pullDrain = pull?.stop() ?? Promise.resolve();
|
|
14459
16270
|
const vaultDrain = vaultSync?.stop() ?? Promise.resolve();
|
|
14460
16271
|
const attestationDrain = attestationLoop?.stop() ?? Promise.resolve();
|
|
@@ -14639,7 +16450,7 @@ export {
|
|
|
14639
16450
|
allocateAddress,
|
|
14640
16451
|
agentCommandArgs,
|
|
14641
16452
|
activateAgentRelease,
|
|
14642
|
-
VERSION,
|
|
16453
|
+
VERSION2 as VERSION,
|
|
14643
16454
|
SYSTEMD_RESOLVED_STUB,
|
|
14644
16455
|
SYSTEMD_RESOLVED_ADDRESS,
|
|
14645
16456
|
SUPPORTED_GUEST_IMAGE,
|