@forgezero/agent 0.1.35 → 0.1.36
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 +29 -0
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.d.ts +5 -0
- package/dist/bootstrap.js +60 -5
- package/dist/cli/agent-install.d.ts +3 -0
- package/dist/definition.js +9 -2
- package/dist/deploy-file.js +9 -2
- package/dist/fz-agent.js +492 -150
- package/dist/fz.js +69 -8
- package/dist/index.d.ts +2 -0
- package/dist/metal-bootstrap.js +1 -1
- package/dist/provision.d.ts +4 -0
- package/dist/provision.js +33 -5
- package/dist/software-helper.js +9 -2
- package/dist/software.d.ts +3 -1
- package/dist/software.js +12 -3
- package/dist/ssh-bootstrap.d.ts +72 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/schema/deploy-v2.json +1 -1
package/dist/fz-agent.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
6
|
-
import { readFileSync as
|
|
7
|
-
import { dirname as dirname7, join as
|
|
6
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, existsSync as existsSync13, mkdirSync as mkdirSync9, chmodSync as chmodSync13 } from "fs";
|
|
7
|
+
import { dirname as dirname7, join as join7 } from "path";
|
|
8
8
|
|
|
9
9
|
// ../access/dist/security.js
|
|
10
10
|
var HEX = Array.from({ length: 256 }, (_, index) => index.toString(16).padStart(2, "0"));
|
|
@@ -4884,6 +4884,7 @@ function createQueue(options = {}) {
|
|
|
4884
4884
|
|
|
4885
4885
|
// src/software.ts
|
|
4886
4886
|
import { readFileSync } from "fs";
|
|
4887
|
+
var PINNED_BUN_VERSION = "1.3.14";
|
|
4887
4888
|
var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
|
|
4888
4889
|
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
4889
4890
|
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
@@ -4895,7 +4896,8 @@ var SOFTWARE_CATALOG = [
|
|
|
4895
4896
|
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4896
4897
|
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4897
4898
|
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4898
|
-
{ id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
|
|
4899
|
+
{ id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4900
|
+
{ id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
|
|
4899
4901
|
];
|
|
4900
4902
|
var UBUNTU_2604_X64 = [
|
|
4901
4903
|
{
|
|
@@ -4922,6 +4924,11 @@ var UBUNTU_2604_X64 = [
|
|
|
4922
4924
|
requirement: { id: "ufw", version: "ubuntu-26.04" },
|
|
4923
4925
|
check: "command -v ufw >/dev/null",
|
|
4924
4926
|
install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
|
|
4927
|
+
},
|
|
4928
|
+
{
|
|
4929
|
+
requirement: { id: "openssh-client", version: "ubuntu-26.04" },
|
|
4930
|
+
check: "command -v ssh >/dev/null && command -v scp >/dev/null && command -v ssh-keyscan >/dev/null && command -v ssh-keygen >/dev/null",
|
|
4931
|
+
install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y openssh-client"
|
|
4925
4932
|
}
|
|
4926
4933
|
];
|
|
4927
4934
|
function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8"), architecture = process.arch) {
|
|
@@ -4946,7 +4953,7 @@ function validateSoftwareRequirements(value, _options = {}) {
|
|
|
4946
4953
|
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
4947
4954
|
throw new Error("software requirement contains an unknown field");
|
|
4948
4955
|
}
|
|
4949
|
-
if (!["bun", "nginx", "arangodb", "cloudflared", "ufw"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
4956
|
+
if (!["bun", "nginx", "arangodb", "cloudflared", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
4950
4957
|
throw new Error("software requirement coordinate is invalid");
|
|
4951
4958
|
}
|
|
4952
4959
|
const requirement = { id: row.id, version: row.version };
|
|
@@ -6633,8 +6640,317 @@ function startMigrationPull(options) {
|
|
|
6633
6640
|
};
|
|
6634
6641
|
}
|
|
6635
6642
|
|
|
6643
|
+
// src/ssh-bootstrap.ts
|
|
6644
|
+
import { chmodSync as chmodSync5, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
6645
|
+
import { tmpdir } from "os";
|
|
6646
|
+
import { join as join2 } from "path";
|
|
6647
|
+
import { fileURLToPath } from "url";
|
|
6648
|
+
class SshBootstrapError extends Error {
|
|
6649
|
+
code;
|
|
6650
|
+
constructor(code, message = code) {
|
|
6651
|
+
super(message);
|
|
6652
|
+
this.code = code;
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6655
|
+
var defaultExec = async (argv, options = {}) => {
|
|
6656
|
+
const child = Bun.spawn([...argv], {
|
|
6657
|
+
stdin: options.stdin === undefined ? "ignore" : "pipe",
|
|
6658
|
+
stdout: "pipe",
|
|
6659
|
+
stderr: "pipe",
|
|
6660
|
+
env: { PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", LANG: "C", LC_ALL: "C" }
|
|
6661
|
+
});
|
|
6662
|
+
if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
|
|
6663
|
+
child.stdin.write(options.stdin);
|
|
6664
|
+
child.stdin.end();
|
|
6665
|
+
}
|
|
6666
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
6667
|
+
new Response(child.stdout).text(),
|
|
6668
|
+
new Response(child.stderr).text(),
|
|
6669
|
+
child.exited
|
|
6670
|
+
]);
|
|
6671
|
+
return { exitCode, output: options.secret ? "" : `${stdout}${stderr}`.slice(0, 8192) };
|
|
6672
|
+
};
|
|
6673
|
+
var checked = async (exec, argv, code, options) => {
|
|
6674
|
+
const result = await exec(argv, options);
|
|
6675
|
+
if (result.exitCode !== 0)
|
|
6676
|
+
throw new SshBootstrapError(code);
|
|
6677
|
+
return result.output.trim();
|
|
6678
|
+
};
|
|
6679
|
+
var validateClaim = (claim) => {
|
|
6680
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(claim.computeKey) || !/^[A-Za-z0-9_-]{16,128}$/.test(claim.claimToken) || !/^fze_[A-Za-z0-9_-]{32,128}$/.test(claim.enrolmentToken) || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(claim.realm) || !/^[A-Za-z0-9_-]{1,64}$/.test(claim.projectKey) || !/^[A-Za-z0-9_-]{1,64}$/.test(claim.environmentKey))
|
|
6681
|
+
throw new SshBootstrapError("CLAIM_INVALID");
|
|
6682
|
+
const { host, port, user, hostKeySha256, nodeHostname } = claim.target;
|
|
6683
|
+
if (!host || host.length > 253 || /[\s/@]/.test(host) || !Number.isSafeInteger(port) || port < 1 || port > 65535 || !/^[a-z_][a-z0-9_-]{0,31}$/.test(user) || !/^SHA256:[A-Za-z0-9+/]{43}$/.test(hostKeySha256) || !/^[a-z0-9](?:[a-z0-9.-]{1,251}[a-z0-9])$/.test(nodeHostname) || !nodeHostname.includes(".")) {
|
|
6684
|
+
throw new SshBootstrapError("CLAIM_INVALID");
|
|
6685
|
+
}
|
|
6686
|
+
};
|
|
6687
|
+
var target = (claim, pinned) => {
|
|
6688
|
+
const address = pinned.addresses[0];
|
|
6689
|
+
return `${claim.target.user}@${address.includes(":") ? `[${address}]` : address}`;
|
|
6690
|
+
};
|
|
6691
|
+
var sshOptions = (claim, pinned, knownHosts, keyPath) => [
|
|
6692
|
+
"-F",
|
|
6693
|
+
"/dev/null",
|
|
6694
|
+
"-o",
|
|
6695
|
+
"BatchMode=yes",
|
|
6696
|
+
"-o",
|
|
6697
|
+
"IdentitiesOnly=yes",
|
|
6698
|
+
"-o",
|
|
6699
|
+
`IdentityFile=${keyPath}`,
|
|
6700
|
+
"-o",
|
|
6701
|
+
"IdentityAgent=none",
|
|
6702
|
+
"-o",
|
|
6703
|
+
`UserKnownHostsFile=${knownHosts}`,
|
|
6704
|
+
"-o",
|
|
6705
|
+
"StrictHostKeyChecking=yes",
|
|
6706
|
+
"-o",
|
|
6707
|
+
`HostKeyAlias=${pinned.hostKeyAlias}`,
|
|
6708
|
+
"-o",
|
|
6709
|
+
"CheckHostIP=no",
|
|
6710
|
+
"-o",
|
|
6711
|
+
"ProxyCommand=none",
|
|
6712
|
+
"-o",
|
|
6713
|
+
"ProxyJump=none",
|
|
6714
|
+
"-o",
|
|
6715
|
+
"ForwardAgent=no",
|
|
6716
|
+
"-o",
|
|
6717
|
+
"ClearAllForwardings=yes",
|
|
6718
|
+
"-o",
|
|
6719
|
+
"ConnectTimeout=10",
|
|
6720
|
+
"-p",
|
|
6721
|
+
String(claim.target.port)
|
|
6722
|
+
];
|
|
6723
|
+
var scpOptions = (claim, pinned, knownHosts, keyPath) => {
|
|
6724
|
+
const options = sshOptions(claim, pinned, knownHosts, keyPath);
|
|
6725
|
+
const portFlag = options.lastIndexOf("-p");
|
|
6726
|
+
if (portFlag < 0)
|
|
6727
|
+
throw new SshBootstrapError("SSH_OPTIONS_INVALID");
|
|
6728
|
+
options[portFlag] = "-P";
|
|
6729
|
+
return options;
|
|
6730
|
+
};
|
|
6731
|
+
async function pinnedKnownHost(claim, pinned, directory, exec) {
|
|
6732
|
+
const address = pinned.addresses[0];
|
|
6733
|
+
const scanned = await checked(exec, [
|
|
6734
|
+
"ssh-keyscan",
|
|
6735
|
+
"-T",
|
|
6736
|
+
"5",
|
|
6737
|
+
"-p",
|
|
6738
|
+
String(claim.target.port),
|
|
6739
|
+
"-t",
|
|
6740
|
+
"ed25519",
|
|
6741
|
+
address
|
|
6742
|
+
], "HOST_KEY_SCAN_FAILED");
|
|
6743
|
+
const candidates = scanned.split(`
|
|
6744
|
+
`).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
6745
|
+
let matched;
|
|
6746
|
+
for (const line of candidates) {
|
|
6747
|
+
const fingerprint = await checked(exec, ["ssh-keygen", "-E", "sha256", "-lf", "-"], "HOST_KEY_INVALID", { stdin: `${line}
|
|
6748
|
+
` });
|
|
6749
|
+
if (fingerprint.split(/\s+/).includes(claim.target.hostKeySha256)) {
|
|
6750
|
+
matched = line;
|
|
6751
|
+
break;
|
|
6752
|
+
}
|
|
6753
|
+
}
|
|
6754
|
+
if (!matched)
|
|
6755
|
+
throw new SshBootstrapError("HOST_KEY_MISMATCH");
|
|
6756
|
+
const key = matched.split(/\s+/).slice(1).join(" ");
|
|
6757
|
+
if (!/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}$/.test(key))
|
|
6758
|
+
throw new SshBootstrapError("HOST_KEY_INVALID");
|
|
6759
|
+
const path = join2(directory, "known_hosts");
|
|
6760
|
+
writeFileSync3(path, `${pinned.hostKeyAlias} ${key}
|
|
6761
|
+
`, { mode: 384, flag: "wx" });
|
|
6762
|
+
return path;
|
|
6763
|
+
}
|
|
6764
|
+
var remoteInstallScript = `set -Eeuo pipefail
|
|
6765
|
+
umask 077
|
|
6766
|
+
work="$1"
|
|
6767
|
+
if [[ "$(/usr/local/bin/bun --version 2>/dev/null || true)" != "${PINNED_BUN_VERSION}" ]]; then
|
|
6768
|
+
tmp="$(mktemp -d)"; trap 'rm -rf -- "$tmp"' EXIT
|
|
6769
|
+
curl -fsSL https://bun.sh/install -o "$tmp/install"
|
|
6770
|
+
echo "${BUN_INSTALLER_SHA256} $tmp/install" | sha256sum -c -
|
|
6771
|
+
BUN_INSTALL="$tmp/bun" BUN_VERSION=${PINNED_BUN_VERSION} bash "$tmp/install" >/dev/null
|
|
6772
|
+
sudo -n install -d -m 0755 /usr/local/lib/forgezero/runtime
|
|
6773
|
+
sudo -n install -m 0755 "$tmp/bun/bin/bun" /usr/local/lib/forgezero/runtime/bun.next
|
|
6774
|
+
sudo -n mv -Tf /usr/local/lib/forgezero/runtime/bun.next /usr/local/lib/forgezero/runtime/bun
|
|
6775
|
+
sudo -n ln -sfn /usr/local/lib/forgezero/runtime/bun /usr/local/bin/bun
|
|
6776
|
+
fi
|
|
6777
|
+
sudo -n install -d -m 0755 /usr/local/lib/forgezero/agent /usr/local/bin /run/forgezero-bootstrap
|
|
6778
|
+
sudo -n install -m 0755 "$work/fz.js" /usr/local/lib/forgezero/agent/fz.js
|
|
6779
|
+
sudo -n install -m 0755 "$work/fz-agent.js" /usr/local/lib/forgezero/agent/fz-agent.js
|
|
6780
|
+
sudo -n ln -sfn /usr/local/lib/forgezero/agent/fz.js /usr/local/bin/fz
|
|
6781
|
+
sudo -n install -m 0600 "$work/config.json" /run/forgezero-bootstrap/config.json
|
|
6782
|
+
sudo -n install -m 0600 "$work/enrol.token" /run/forgezero-bootstrap/enrol.token
|
|
6783
|
+
sudo -n /usr/local/bin/fz bootstrap tenant --bootstrap-config /run/forgezero-bootstrap/config.json --apply
|
|
6784
|
+
sudo -n rm -rf -- /run/forgezero-bootstrap
|
|
6785
|
+
`;
|
|
6786
|
+
async function executeSshBootstrap(claim, options) {
|
|
6787
|
+
validateClaim(claim);
|
|
6788
|
+
if (!options.sshKeyPath.startsWith("/") || /[\r\n]/.test(options.sshKeyPath))
|
|
6789
|
+
throw new SshBootstrapError("SSH_KEY_INVALID");
|
|
6790
|
+
const telemetry = new URL(options.targetTelemetryEndpoint);
|
|
6791
|
+
if (telemetry.protocol !== "https:" || telemetry.username || telemetry.password || telemetry.search || telemetry.hash || telemetry.port && telemetry.port !== "443")
|
|
6792
|
+
throw new SshBootstrapError("TARGET_TELEMETRY_INVALID");
|
|
6793
|
+
const api = new URL(options.platformApiUrl);
|
|
6794
|
+
if (api.protocol !== "https:" || api.username || api.password || api.search || api.hash)
|
|
6795
|
+
throw new SshBootstrapError("TARGET_API_INVALID");
|
|
6796
|
+
const exec = options.exec ?? defaultExec;
|
|
6797
|
+
const directory = mkdtempSync(join2(tmpdir(), "forgezero-ssh-bootstrap-"));
|
|
6798
|
+
chmodSync5(directory, 448);
|
|
6799
|
+
let remoteDirectory = "";
|
|
6800
|
+
let pinned;
|
|
6801
|
+
try {
|
|
6802
|
+
const sourceHost = claim.target.host.includes(":") ? `[${claim.target.host}]` : claim.target.host;
|
|
6803
|
+
const resolved = await resolvePinnedGitTarget(`ssh://${claim.target.user}@${sourceHost}:${claim.target.port}/`, options.resolveHost);
|
|
6804
|
+
if (!resolved || resolved.protocol !== "ssh")
|
|
6805
|
+
throw new SshBootstrapError("TARGET_ADDRESS_REFUSED");
|
|
6806
|
+
pinned = resolved;
|
|
6807
|
+
const knownHosts = await pinnedKnownHost(claim, pinned, directory, exec);
|
|
6808
|
+
const ssh = sshOptions(claim, pinned, knownHosts, options.sshKeyPath);
|
|
6809
|
+
remoteDirectory = await checked(exec, ["ssh", ...ssh, target(claim, pinned), "--", "mktemp", "-d", "/tmp/forgezero-bootstrap.XXXXXXXX"], "SSH_UNREACHABLE");
|
|
6810
|
+
if (!/^\/tmp\/forgezero-bootstrap\.[A-Za-z0-9]{8}$/.test(remoteDirectory))
|
|
6811
|
+
throw new SshBootstrapError("REMOTE_PATH_INVALID");
|
|
6812
|
+
const config = {
|
|
6813
|
+
kind: "tenant",
|
|
6814
|
+
apiUrl: api.origin,
|
|
6815
|
+
realm: claim.realm,
|
|
6816
|
+
nodeHostname: claim.target.nodeHostname,
|
|
6817
|
+
telemetryEndpoint: telemetry.origin,
|
|
6818
|
+
enrolTokenFile: "/run/forgezero-bootstrap/enrol.token",
|
|
6819
|
+
profile: "app",
|
|
6820
|
+
software: [{ id: "bun", version: PINNED_BUN_VERSION }]
|
|
6821
|
+
};
|
|
6822
|
+
const files = {
|
|
6823
|
+
"config.json": `${JSON.stringify(config, null, 2)}
|
|
6824
|
+
`,
|
|
6825
|
+
"enrol.token": `${claim.enrolmentToken}
|
|
6826
|
+
`
|
|
6827
|
+
};
|
|
6828
|
+
for (const [name, content] of Object.entries(files))
|
|
6829
|
+
writeFileSync3(join2(directory, name), content, { mode: 384, flag: "wx" });
|
|
6830
|
+
const fzCliPath = options.fzCliPath ?? fileURLToPath(new URL("./fz.js", import.meta.url));
|
|
6831
|
+
const fzAgentPath = options.fzAgentPath ?? fileURLToPath(new URL("./fz-agent.js", import.meta.url));
|
|
6832
|
+
for (const [source, name] of [[fzCliPath, "fz.js"], [fzAgentPath, "fz-agent.js"]]) {
|
|
6833
|
+
if (!readFileSync4(source).length)
|
|
6834
|
+
throw new SshBootstrapError("PACKAGE_ARTIFACT_MISSING");
|
|
6835
|
+
await checked(exec, [
|
|
6836
|
+
"scp",
|
|
6837
|
+
...scpOptions(claim, pinned, knownHosts, options.sshKeyPath),
|
|
6838
|
+
source,
|
|
6839
|
+
`${target(claim, pinned)}:${remoteDirectory}/${name}`
|
|
6840
|
+
], "SCP_FAILED");
|
|
6841
|
+
}
|
|
6842
|
+
for (const name of Object.keys(files))
|
|
6843
|
+
await checked(exec, [
|
|
6844
|
+
"scp",
|
|
6845
|
+
...scpOptions(claim, pinned, knownHosts, options.sshKeyPath),
|
|
6846
|
+
join2(directory, name),
|
|
6847
|
+
`${target(claim, pinned)}:${remoteDirectory}/${name}`
|
|
6848
|
+
], "SCP_FAILED", { secret: name === "enrol.token" });
|
|
6849
|
+
await checked(exec, ["ssh", ...ssh, target(claim, pinned), "--", "bash", "-s", "--", remoteDirectory], "REMOTE_BOOTSTRAP_FAILED", { stdin: remoteInstallScript, secret: true });
|
|
6850
|
+
} finally {
|
|
6851
|
+
if (remoteDirectory && pinned) {
|
|
6852
|
+
try {
|
|
6853
|
+
const knownHosts = join2(directory, "known_hosts");
|
|
6854
|
+
await exec([
|
|
6855
|
+
"ssh",
|
|
6856
|
+
...sshOptions(claim, pinned, knownHosts, options.sshKeyPath),
|
|
6857
|
+
target(claim, pinned),
|
|
6858
|
+
"--",
|
|
6859
|
+
"sudo",
|
|
6860
|
+
"-n",
|
|
6861
|
+
"rm",
|
|
6862
|
+
"-rf",
|
|
6863
|
+
"--",
|
|
6864
|
+
remoteDirectory
|
|
6865
|
+
]);
|
|
6866
|
+
} catch {}
|
|
6867
|
+
}
|
|
6868
|
+
rmSync(directory, { recursive: true, force: true });
|
|
6869
|
+
}
|
|
6870
|
+
}
|
|
6871
|
+
var post3 = (options, operation, body) => postSignedNode(options, `v1/node/bootstrap/${operation}`, body);
|
|
6872
|
+
async function pullSshBootstrapOnce(options) {
|
|
6873
|
+
const response = await post3(options, "claim", {});
|
|
6874
|
+
if (!response.claim)
|
|
6875
|
+
return { status: "idle" };
|
|
6876
|
+
const claim = response.claim;
|
|
6877
|
+
const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
|
|
6878
|
+
const clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
6879
|
+
const now = options.now ?? Date.now;
|
|
6880
|
+
let expires = claim.claimExpiresAtTs;
|
|
6881
|
+
let timer;
|
|
6882
|
+
let stopped = false;
|
|
6883
|
+
let renewal = null;
|
|
6884
|
+
const renew = () => {
|
|
6885
|
+
if (stopped)
|
|
6886
|
+
return;
|
|
6887
|
+
timer = setTimer(() => {
|
|
6888
|
+
renewal = post3(options, "renew", {
|
|
6889
|
+
computeKey: claim.computeKey,
|
|
6890
|
+
claimToken: claim.claimToken
|
|
6891
|
+
}).then((value) => {
|
|
6892
|
+
expires = value.claimExpiresAtTs;
|
|
6893
|
+
}).finally(() => {
|
|
6894
|
+
renewal = null;
|
|
6895
|
+
if (!stopped)
|
|
6896
|
+
renew();
|
|
6897
|
+
});
|
|
6898
|
+
}, Math.max(1000, Math.floor(Math.max(0, expires - now()) / 3)));
|
|
6899
|
+
};
|
|
6900
|
+
renew();
|
|
6901
|
+
try {
|
|
6902
|
+
await executeSshBootstrap(claim, options);
|
|
6903
|
+
stopped = true;
|
|
6904
|
+
clearTimer(timer);
|
|
6905
|
+
await renewal;
|
|
6906
|
+
await post3(options, "complete", { computeKey: claim.computeKey, claimToken: claim.claimToken, ok: true });
|
|
6907
|
+
return { status: "completed", computeKey: claim.computeKey, attempt: claim.attempt };
|
|
6908
|
+
} catch (cause) {
|
|
6909
|
+
stopped = true;
|
|
6910
|
+
clearTimer(timer);
|
|
6911
|
+
await renewal;
|
|
6912
|
+
if (cause instanceof SignedNodeHttpError && cause.status < 500)
|
|
6913
|
+
throw cause;
|
|
6914
|
+
const failureCode = cause instanceof SshBootstrapError ? cause.code : "BOOTSTRAP_FAILED";
|
|
6915
|
+
await post3(options, "complete", { computeKey: claim.computeKey, claimToken: claim.claimToken, ok: false, failureCode });
|
|
6916
|
+
return { status: "failed", computeKey: claim.computeKey, attempt: claim.attempt, failureCode };
|
|
6917
|
+
}
|
|
6918
|
+
}
|
|
6919
|
+
function startSshBootstrapPull(options) {
|
|
6920
|
+
const interval = Math.max(1000, options.intervalMs ?? 5000);
|
|
6921
|
+
const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
|
|
6922
|
+
const clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
6923
|
+
let stopped = false;
|
|
6924
|
+
let timer;
|
|
6925
|
+
let active = null;
|
|
6926
|
+
const tick = () => {
|
|
6927
|
+
if (stopped || active)
|
|
6928
|
+
return;
|
|
6929
|
+
active = pullSshBootstrapOnce(options).then((result) => options.onEvent?.(result.status, result.status === "idle" ? undefined : {
|
|
6930
|
+
computeKey: result.computeKey,
|
|
6931
|
+
attempt: result.attempt,
|
|
6932
|
+
...result.status === "failed" ? { failureCode: result.failureCode } : {}
|
|
6933
|
+
})).catch((cause) => options.onEvent?.("poll-failed", cause)).finally(() => {
|
|
6934
|
+
active = null;
|
|
6935
|
+
if (!stopped)
|
|
6936
|
+
timer = setTimer(tick, interval);
|
|
6937
|
+
});
|
|
6938
|
+
};
|
|
6939
|
+
tick();
|
|
6940
|
+
return {
|
|
6941
|
+
async stop() {
|
|
6942
|
+
stopped = true;
|
|
6943
|
+
clearTimer(timer);
|
|
6944
|
+
await active;
|
|
6945
|
+
},
|
|
6946
|
+
get active() {
|
|
6947
|
+
return !stopped;
|
|
6948
|
+
}
|
|
6949
|
+
};
|
|
6950
|
+
}
|
|
6951
|
+
|
|
6636
6952
|
// src/metal-helper-socket.ts
|
|
6637
|
-
import { chmodSync as
|
|
6953
|
+
import { chmodSync as chmodSync6, existsSync as existsSync6, unlinkSync as unlinkSync5 } from "fs";
|
|
6638
6954
|
import { connect as connect2, createServer as createServer3 } from "net";
|
|
6639
6955
|
|
|
6640
6956
|
// src/metal-provision.ts
|
|
@@ -6642,14 +6958,14 @@ import { createHash as createHash3 } from "crypto";
|
|
|
6642
6958
|
import {
|
|
6643
6959
|
existsSync as existsSync5,
|
|
6644
6960
|
mkdirSync as mkdirSync3,
|
|
6645
|
-
readFileSync as
|
|
6961
|
+
readFileSync as readFileSync5,
|
|
6646
6962
|
readdirSync,
|
|
6647
|
-
rmSync,
|
|
6963
|
+
rmSync as rmSync2,
|
|
6648
6964
|
statSync as statSync2,
|
|
6649
6965
|
unlinkSync as unlinkSync4,
|
|
6650
|
-
writeFileSync as
|
|
6966
|
+
writeFileSync as writeFileSync4
|
|
6651
6967
|
} from "fs";
|
|
6652
|
-
import { dirname as dirname3, isAbsolute as isAbsolute3, join as
|
|
6968
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join3 } from "path";
|
|
6653
6969
|
import { isIP as isIP2 } from "net";
|
|
6654
6970
|
|
|
6655
6971
|
// src/compute.ts
|
|
@@ -6875,7 +7191,7 @@ function validateMetalProfile(profile) {
|
|
|
6875
7191
|
var readManifests = (stateDir) => {
|
|
6876
7192
|
if (!existsSync5(stateDir))
|
|
6877
7193
|
return [];
|
|
6878
|
-
return readdirSync(stateDir).filter((name) => name.endsWith(".json")).map((name) => JSON.parse(
|
|
7194
|
+
return readdirSync(stateDir).filter((name) => name.endsWith(".json")).map((name) => JSON.parse(readFileSync5(join3(stateDir, name), "utf8")));
|
|
6879
7195
|
};
|
|
6880
7196
|
function allocateAddress(profile, computeKey, rows) {
|
|
6881
7197
|
const existing = rows.find((row) => row.computeKey === computeKey);
|
|
@@ -7008,7 +7324,7 @@ ethernets:
|
|
|
7008
7324
|
`
|
|
7009
7325
|
};
|
|
7010
7326
|
}
|
|
7011
|
-
var
|
|
7327
|
+
var checked2 = async (exec, argv) => {
|
|
7012
7328
|
const result = await exec(argv);
|
|
7013
7329
|
if (result.exitCode !== 0) {
|
|
7014
7330
|
throw new MetalProvisionError(`${argv[0]} failed: ${(result.stderr || result.stdout).trim()}`);
|
|
@@ -7026,7 +7342,7 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
7026
7342
|
}
|
|
7027
7343
|
if (!statSync2(image.path).isFile())
|
|
7028
7344
|
throw new MetalProvisionError("configured image is not a file");
|
|
7029
|
-
const digest = (await
|
|
7345
|
+
const digest = (await checked2(exec, ["sha256sum", image.path])).stdout.trim().split(/\s+/)[0];
|
|
7030
7346
|
if (digest !== image.sha256)
|
|
7031
7347
|
throw new MetalProvisionError("configured image checksum mismatch");
|
|
7032
7348
|
for (const path of [profile.stateDir, profile.seedDir, profile.unitDir])
|
|
@@ -7043,7 +7359,7 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
7043
7359
|
const conflictingName = manifests.find((row) => row.name === name && row.computeKey !== claim.computeKey);
|
|
7044
7360
|
if (conflictingName)
|
|
7045
7361
|
throw new MetalProvisionError("requested guest name is already allocated");
|
|
7046
|
-
const manifestPath =
|
|
7362
|
+
const manifestPath = join3(profile.stateDir, `${name}.json`);
|
|
7047
7363
|
const prior = manifests.find((row) => row.computeKey === claim.computeKey);
|
|
7048
7364
|
if (prior && prior.reference !== claim.spec.reference)
|
|
7049
7365
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
@@ -7060,25 +7376,25 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
7060
7376
|
allowedMemoryNodes: cpuPool.memoryNodes,
|
|
7061
7377
|
phase: "allocating"
|
|
7062
7378
|
};
|
|
7063
|
-
const save = () =>
|
|
7379
|
+
const save = () => writeFileSync4(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
7064
7380
|
`, { mode: 384 });
|
|
7065
7381
|
save();
|
|
7066
7382
|
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
7067
7383
|
const exists = (await exec(["lvs", "--noheadings", lv])).exitCode === 0;
|
|
7068
7384
|
if (!exists)
|
|
7069
|
-
await
|
|
7385
|
+
await checked2(exec, ["lvcreate", "-y", "-n", name, "-L", `${claim.spec.diskGib}G`, profile.volumeGroup]);
|
|
7070
7386
|
if (manifest.phase === "allocating") {
|
|
7071
|
-
await
|
|
7387
|
+
await checked2(exec, ["qemu-img", "convert", "-O", "raw", image.path, lv]);
|
|
7072
7388
|
manifest.phase = "image-ready";
|
|
7073
7389
|
save();
|
|
7074
7390
|
}
|
|
7075
|
-
const seedBase =
|
|
7391
|
+
const seedBase = join3(profile.seedDir, name);
|
|
7076
7392
|
const init = cloudInit(profile, claim, manifest);
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7393
|
+
writeFileSync4(`${seedBase}-user-data`, init.userData, { mode: 384 });
|
|
7394
|
+
writeFileSync4(`${seedBase}-meta-data`, init.metaData, { mode: 384 });
|
|
7395
|
+
writeFileSync4(`${seedBase}-network-config`, init.networkConfig, { mode: 384 });
|
|
7080
7396
|
const seed = `${seedBase}-seed.iso`;
|
|
7081
|
-
await
|
|
7397
|
+
await checked2(exec, [
|
|
7082
7398
|
"cloud-localds",
|
|
7083
7399
|
"-N",
|
|
7084
7400
|
`${seedBase}-network-config`,
|
|
@@ -7108,16 +7424,16 @@ async function provisionMetalGuest(profile, claim, exec) {
|
|
|
7108
7424
|
throw new MetalProvisionError("confidential compute requested but host SNP profile is absent");
|
|
7109
7425
|
}
|
|
7110
7426
|
const service = `forgezero-guest@${name}.service`;
|
|
7111
|
-
const unitPath =
|
|
7427
|
+
const unitPath = join3(profile.unitDir, service);
|
|
7112
7428
|
mkdirSync3(dirname3(unitPath), { recursive: true });
|
|
7113
|
-
|
|
7114
|
-
await
|
|
7429
|
+
writeFileSync4(unitPath, guestUnit(spec), { mode: 420 });
|
|
7430
|
+
await checked2(exec, ["systemctl", "daemon-reload"]);
|
|
7115
7431
|
if (prior?.phase === "running") {
|
|
7116
|
-
await
|
|
7432
|
+
await checked2(exec, ["systemctl", "restart", service]);
|
|
7117
7433
|
} else {
|
|
7118
|
-
await
|
|
7434
|
+
await checked2(exec, ["systemctl", "enable", "--now", service]);
|
|
7119
7435
|
}
|
|
7120
|
-
await
|
|
7436
|
+
await checked2(exec, ["systemctl", "is-active", service]);
|
|
7121
7437
|
manifest.phase = "running";
|
|
7122
7438
|
save();
|
|
7123
7439
|
return { guestAddress: address };
|
|
@@ -7126,11 +7442,11 @@ function legacyPlatformManifest(profile, claim, name) {
|
|
|
7126
7442
|
if (claim.bootstrap?.kind !== "platform-genesis" || claim.computeKey !== `platform:${name}` || claim.spec.reference !== `platform:${name}` || claim.spec.guestAddress === undefined)
|
|
7127
7443
|
return;
|
|
7128
7444
|
const legacyDir = profile.legacyStateDir ?? "/etc/forgezero/guests";
|
|
7129
|
-
const legacyPath =
|
|
7445
|
+
const legacyPath = join3(legacyDir, `${name}.conf`);
|
|
7130
7446
|
if (!existsSync5(legacyPath))
|
|
7131
7447
|
return;
|
|
7132
7448
|
const values = new Map;
|
|
7133
|
-
for (const line of
|
|
7449
|
+
for (const line of readFileSync5(legacyPath, "utf8").split(/\r?\n/)) {
|
|
7134
7450
|
if (!line || line.startsWith("#"))
|
|
7135
7451
|
continue;
|
|
7136
7452
|
const match = /^([A-Z][A-Z0-9_]*)=([^\s'"`$;|&<>]+)$/.exec(line);
|
|
@@ -7160,13 +7476,13 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
7160
7476
|
if (claim.action !== "delete")
|
|
7161
7477
|
throw new MetalProvisionError("create claim cannot remove a guest");
|
|
7162
7478
|
const name = claim.spec.guestName ?? guestNameFor(claim.computeKey);
|
|
7163
|
-
const manifestPath =
|
|
7479
|
+
const manifestPath = join3(profile.stateDir, `${name}.json`);
|
|
7164
7480
|
const service = `forgezero-guest@${name}.service`;
|
|
7165
|
-
const unitPath =
|
|
7481
|
+
const unitPath = join3(profile.unitDir, service);
|
|
7166
7482
|
const lv = `/dev/${profile.volumeGroup}/${name}`;
|
|
7167
|
-
const manifest = existsSync5(manifestPath) ? JSON.parse(
|
|
7483
|
+
const manifest = existsSync5(manifestPath) ? JSON.parse(readFileSync5(manifestPath, "utf8")) : legacyPlatformManifest(profile, claim, name);
|
|
7168
7484
|
if (!manifest) {
|
|
7169
|
-
const seedBase =
|
|
7485
|
+
const seedBase = join3(profile.seedDir, name);
|
|
7170
7486
|
const localArtifacts = [
|
|
7171
7487
|
unitPath,
|
|
7172
7488
|
`${seedBase}-seed.iso`,
|
|
@@ -7192,33 +7508,33 @@ async function removeMetalGuest(profile, claim, exec) {
|
|
|
7192
7508
|
throw new MetalProvisionError("compute identity conflicts with host inventory");
|
|
7193
7509
|
}
|
|
7194
7510
|
if (existsSync5(unitPath))
|
|
7195
|
-
await
|
|
7511
|
+
await checked2(exec, ["systemctl", "disable", "--now", service]);
|
|
7196
7512
|
else if (legacyPlatformIdentity)
|
|
7197
|
-
await
|
|
7513
|
+
await checked2(exec, ["systemctl", "disable", "--now", service]);
|
|
7198
7514
|
else if ((await exec(["systemctl", "is-active", service])).exitCode === 0) {
|
|
7199
7515
|
throw new MetalProvisionError("guest unit is active but its owned unit file is missing");
|
|
7200
7516
|
}
|
|
7201
7517
|
if ((await exec(["lvs", "--noheadings", lv])).exitCode === 0)
|
|
7202
|
-
await
|
|
7518
|
+
await checked2(exec, ["lvremove", "-fy", lv]);
|
|
7203
7519
|
for (const path of [
|
|
7204
7520
|
unitPath,
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7521
|
+
join3(profile.seedDir, `${name}-seed.iso`),
|
|
7522
|
+
join3(profile.seedDir, `${name}-user-data`),
|
|
7523
|
+
join3(profile.seedDir, `${name}-meta-data`),
|
|
7524
|
+
join3(profile.seedDir, `${name}-network-config`),
|
|
7209
7525
|
manifestPath
|
|
7210
7526
|
])
|
|
7211
7527
|
if (existsSync5(path))
|
|
7212
7528
|
unlinkSync4(path);
|
|
7213
7529
|
if (legacyPlatformIdentity) {
|
|
7214
|
-
const legacyConfig =
|
|
7215
|
-
const legacyDropIn =
|
|
7530
|
+
const legacyConfig = join3(profile.legacyStateDir ?? "/etc/forgezero/guests", `${name}.conf`);
|
|
7531
|
+
const legacyDropIn = join3(profile.unitDir, `${service}.d`);
|
|
7216
7532
|
if (existsSync5(legacyConfig))
|
|
7217
7533
|
unlinkSync4(legacyConfig);
|
|
7218
7534
|
if (existsSync5(legacyDropIn))
|
|
7219
|
-
|
|
7535
|
+
rmSync2(legacyDropIn, { recursive: true });
|
|
7220
7536
|
}
|
|
7221
|
-
await
|
|
7537
|
+
await checked2(exec, ["systemctl", "daemon-reload"]);
|
|
7222
7538
|
return {};
|
|
7223
7539
|
}
|
|
7224
7540
|
|
|
@@ -7301,7 +7617,7 @@ function startMetalHelper(options) {
|
|
|
7301
7617
|
});
|
|
7302
7618
|
socket.on("error", () => socket.destroy());
|
|
7303
7619
|
});
|
|
7304
|
-
server.listen(socketPath, () =>
|
|
7620
|
+
server.listen(socketPath, () => chmodSync6(socketPath, 432));
|
|
7305
7621
|
return {
|
|
7306
7622
|
server,
|
|
7307
7623
|
async stop() {
|
|
@@ -7341,7 +7657,7 @@ function requestMetalProvision(claim, socketPath = DEFAULT_METAL_HELPER_SOCKET)
|
|
|
7341
7657
|
}
|
|
7342
7658
|
|
|
7343
7659
|
// src/deployment-runner.ts
|
|
7344
|
-
import { chmodSync as
|
|
7660
|
+
import { chmodSync as chmodSync7, existsSync as existsSync7, realpathSync, unlinkSync as unlinkSync6 } from "fs";
|
|
7345
7661
|
import { isAbsolute as isAbsolute4, resolve, sep } from "path";
|
|
7346
7662
|
import { connect as connect3, createServer as createServer4 } from "net";
|
|
7347
7663
|
var DEFAULT_DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
@@ -7490,7 +7806,7 @@ function startDeploymentRunner(options) {
|
|
|
7490
7806
|
const ready = new Promise((resolveReady, rejectReady) => {
|
|
7491
7807
|
server.once("error", rejectReady);
|
|
7492
7808
|
server.listen(socketPath, () => {
|
|
7493
|
-
|
|
7809
|
+
chmodSync7(socketPath, options.socketMode ?? 432);
|
|
7494
7810
|
server.off("error", rejectReady);
|
|
7495
7811
|
resolveReady();
|
|
7496
7812
|
});
|
|
@@ -7720,8 +8036,8 @@ function startNodeAttestation(options) {
|
|
|
7720
8036
|
}
|
|
7721
8037
|
|
|
7722
8038
|
// src/metal-isolation.ts
|
|
7723
|
-
import { mkdirSync as mkdirSync4, writeFileSync as
|
|
7724
|
-
import { join as
|
|
8039
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
8040
|
+
import { join as join4 } from "path";
|
|
7725
8041
|
var members = (list) => list.split(",").flatMap((part) => {
|
|
7726
8042
|
const [first, last = first] = part.split("-").map(Number);
|
|
7727
8043
|
return Array.from({ length: last - first + 1 }, (_, index) => first + index);
|
|
@@ -7758,7 +8074,7 @@ function metalHousekeepingDropIn(profile, kind) {
|
|
|
7758
8074
|
AllowedCPUs=${profile.housekeepingCpus}
|
|
7759
8075
|
${memoryDirective(profile.housekeepingMemoryNodes)}`;
|
|
7760
8076
|
}
|
|
7761
|
-
var
|
|
8077
|
+
var defaultExec2 = async (argv) => {
|
|
7762
8078
|
const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe" });
|
|
7763
8079
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
7764
8080
|
child.exited,
|
|
@@ -7767,14 +8083,14 @@ var defaultExec = async (argv) => {
|
|
|
7767
8083
|
]);
|
|
7768
8084
|
return { exitCode, stdout, stderr };
|
|
7769
8085
|
};
|
|
7770
|
-
var
|
|
8086
|
+
var checked3 = async (exec, argv) => {
|
|
7771
8087
|
const result = await exec(argv);
|
|
7772
8088
|
if (result.exitCode !== 0)
|
|
7773
8089
|
throw new Error(`${argv[0]} failed: ${(result.stderr || result.stdout).trim()}`);
|
|
7774
8090
|
return result;
|
|
7775
8091
|
};
|
|
7776
8092
|
var requireGuestsInSlice = async (exec) => {
|
|
7777
|
-
const active = await
|
|
8093
|
+
const active = await checked3(exec, [
|
|
7778
8094
|
"systemctl",
|
|
7779
8095
|
"list-units",
|
|
7780
8096
|
"--type=service",
|
|
@@ -7788,33 +8104,33 @@ var requireGuestsInSlice = async (exec) => {
|
|
|
7788
8104
|
const service = line.trim().split(/\s+/)[0];
|
|
7789
8105
|
if (!service)
|
|
7790
8106
|
continue;
|
|
7791
|
-
const cgroup = await
|
|
8107
|
+
const cgroup = await checked3(exec, ["systemctl", "show", "-p", "ControlGroup", "--value", service]);
|
|
7792
8108
|
if (!cgroup.stdout.trim().includes("/forgezero-guests.slice/")) {
|
|
7793
8109
|
throw new Error(`${service} must be drained and restarted into forgezero-guests.slice`);
|
|
7794
8110
|
}
|
|
7795
8111
|
}
|
|
7796
8112
|
};
|
|
7797
|
-
async function applyMetalIsolation(profile, exec =
|
|
8113
|
+
async function applyMetalIsolation(profile, exec = defaultExec2) {
|
|
7798
8114
|
validateMetalProfile(profile);
|
|
7799
8115
|
await requireGuestsInSlice(exec);
|
|
7800
8116
|
const unitDir = profile.unitDir;
|
|
7801
8117
|
mkdirSync4(unitDir, { recursive: true });
|
|
7802
|
-
|
|
8118
|
+
writeFileSync5(join4(unitDir, "forgezero-guests.slice"), metalGuestSliceUnit(profile), { mode: 420 });
|
|
7803
8119
|
for (const unit of ["system.slice", "user.slice"]) {
|
|
7804
|
-
const directory =
|
|
8120
|
+
const directory = join4(unitDir, `${unit}.d`);
|
|
7805
8121
|
mkdirSync4(directory, { recursive: true });
|
|
7806
|
-
|
|
8122
|
+
writeFileSync5(join4(directory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "slice"), { mode: 420 });
|
|
7807
8123
|
}
|
|
7808
|
-
const initDirectory =
|
|
8124
|
+
const initDirectory = join4(unitDir, "init.scope.d");
|
|
7809
8125
|
mkdirSync4(initDirectory, { recursive: true });
|
|
7810
|
-
|
|
7811
|
-
await
|
|
8126
|
+
writeFileSync5(join4(initDirectory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "scope"), { mode: 420 });
|
|
8127
|
+
await checked3(exec, ["systemctl", "daemon-reload"]);
|
|
7812
8128
|
await requireGuestsInSlice(exec);
|
|
7813
8129
|
const properties = [`AllowedCPUs=${profile.housekeepingCpus}`];
|
|
7814
8130
|
if (profile.housekeepingMemoryNodes)
|
|
7815
8131
|
properties.push(`AllowedMemoryNodes=${profile.housekeepingMemoryNodes}`);
|
|
7816
8132
|
for (const unit of ["system.slice", "user.slice", "init.scope"]) {
|
|
7817
|
-
await
|
|
8133
|
+
await checked3(exec, ["systemctl", "set-property", "--runtime", unit, ...properties]);
|
|
7818
8134
|
}
|
|
7819
8135
|
}
|
|
7820
8136
|
|
|
@@ -7842,7 +8158,7 @@ async function closeServerWithin(server, timeoutMs) {
|
|
|
7842
8158
|
}
|
|
7843
8159
|
|
|
7844
8160
|
// src/lifecycle-helper.ts
|
|
7845
|
-
import { chmodSync as
|
|
8161
|
+
import { chmodSync as chmodSync8, existsSync as existsSync9, readFileSync as readFileSync6, unlinkSync as unlinkSync7 } from "fs";
|
|
7846
8162
|
import { connect as connect4, createConnection, createServer as createServer5, isIP as isIP3 } from "net";
|
|
7847
8163
|
var DEFAULT_LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
|
|
7848
8164
|
var MAX_REQUEST_BYTES4 = 16 * 1024;
|
|
@@ -7886,7 +8202,7 @@ function validateLifecycleProfile(profile) {
|
|
|
7886
8202
|
}
|
|
7887
8203
|
}
|
|
7888
8204
|
function loadLifecycleProfile(path) {
|
|
7889
|
-
const profile = JSON.parse(
|
|
8205
|
+
const profile = JSON.parse(readFileSync6(path, "utf8"));
|
|
7890
8206
|
validateLifecycleProfile(profile);
|
|
7891
8207
|
return profile;
|
|
7892
8208
|
}
|
|
@@ -8024,7 +8340,7 @@ function startLifecycleHelper(options) {
|
|
|
8024
8340
|
});
|
|
8025
8341
|
socket.on("error", () => socket.destroy());
|
|
8026
8342
|
});
|
|
8027
|
-
server.listen(socketPath, () =>
|
|
8343
|
+
server.listen(socketPath, () => chmodSync8(socketPath, 432));
|
|
8028
8344
|
return {
|
|
8029
8345
|
server,
|
|
8030
8346
|
async stop() {
|
|
@@ -8064,7 +8380,7 @@ function requestLifecycleAction(claim, socketPath = DEFAULT_LIFECYCLE_HELPER_SOC
|
|
|
8064
8380
|
}
|
|
8065
8381
|
|
|
8066
8382
|
// src/warp-config.ts
|
|
8067
|
-
import { chmodSync as
|
|
8383
|
+
import { chmodSync as chmodSync9, mkdirSync as mkdirSync5, renameSync as renameSync3, symlinkSync, unlinkSync as unlinkSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
8068
8384
|
var xml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
8069
8385
|
function renderWarpMdm(options) {
|
|
8070
8386
|
if (!/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.organization))
|
|
@@ -8088,8 +8404,8 @@ function materializeWarpMdm(options) {
|
|
|
8088
8404
|
mkdirSync5(runtimePath.replace(/\/[^/]+$/, ""), { recursive: true, mode: 448 });
|
|
8089
8405
|
mkdirSync5(servicePath.replace(/\/[^/]+$/, ""), { recursive: true, mode: 448 });
|
|
8090
8406
|
const next = `${runtimePath}.next`;
|
|
8091
|
-
|
|
8092
|
-
|
|
8407
|
+
writeFileSync6(next, renderWarpMdm(options), { mode: 384 });
|
|
8408
|
+
chmodSync9(next, 384);
|
|
8093
8409
|
renameSync3(next, runtimePath);
|
|
8094
8410
|
try {
|
|
8095
8411
|
unlinkSync8(servicePath);
|
|
@@ -8104,20 +8420,20 @@ function materializeWarpMdm(options) {
|
|
|
8104
8420
|
// src/agent-update.ts
|
|
8105
8421
|
import { createHash as createHash4, timingSafeEqual, randomUUID as randomUUID2 } from "crypto";
|
|
8106
8422
|
import {
|
|
8107
|
-
chmodSync as
|
|
8423
|
+
chmodSync as chmodSync10,
|
|
8108
8424
|
closeSync,
|
|
8109
8425
|
existsSync as existsSync10,
|
|
8110
8426
|
fsyncSync,
|
|
8111
8427
|
mkdirSync as mkdirSync6,
|
|
8112
8428
|
openSync,
|
|
8113
|
-
readFileSync as
|
|
8429
|
+
readFileSync as readFileSync7,
|
|
8114
8430
|
readlinkSync,
|
|
8115
8431
|
renameSync as renameSync4,
|
|
8116
|
-
rmSync as
|
|
8432
|
+
rmSync as rmSync3,
|
|
8117
8433
|
symlinkSync as symlinkSync2,
|
|
8118
|
-
writeFileSync as
|
|
8434
|
+
writeFileSync as writeFileSync7
|
|
8119
8435
|
} from "fs";
|
|
8120
|
-
import { dirname as dirname4, join as
|
|
8436
|
+
import { dirname as dirname4, join as join5, resolve as resolve2 } from "path";
|
|
8121
8437
|
var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
|
|
8122
8438
|
var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
|
|
8123
8439
|
var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
|
|
@@ -8133,10 +8449,10 @@ var syncPath = (path) => {
|
|
|
8133
8449
|
};
|
|
8134
8450
|
var syncReleaseDirectory = (directory) => {
|
|
8135
8451
|
for (const path of [
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8452
|
+
join5(directory, "package.json"),
|
|
8453
|
+
join5(directory, "dist", "fz-agent.js"),
|
|
8454
|
+
join5(directory, "dist", "fz.js"),
|
|
8455
|
+
join5(directory, "dist"),
|
|
8140
8456
|
directory,
|
|
8141
8457
|
dirname4(directory)
|
|
8142
8458
|
])
|
|
@@ -8189,27 +8505,27 @@ var command = async (input) => {
|
|
|
8189
8505
|
]);
|
|
8190
8506
|
return { exitCode, output: `${stdout}${stderr}` };
|
|
8191
8507
|
};
|
|
8192
|
-
var
|
|
8508
|
+
var checked4 = async (run, input, label) => {
|
|
8193
8509
|
const result = await run(input);
|
|
8194
8510
|
if (result.exitCode !== 0)
|
|
8195
8511
|
throw new Error(`${label} failed: ${result.output.trim()}`);
|
|
8196
8512
|
return result;
|
|
8197
8513
|
};
|
|
8198
8514
|
async function validateReleaseDirectory(directory, release, run) {
|
|
8199
|
-
const manifest = JSON.parse(
|
|
8515
|
+
const manifest = JSON.parse(readFileSync7(join5(directory, "package.json"), "utf8"));
|
|
8200
8516
|
if (manifest.name !== release.package || manifest.version !== release.version) {
|
|
8201
8517
|
throw new Error("agent update manifest does not match the selected release");
|
|
8202
8518
|
}
|
|
8203
|
-
const agent =
|
|
8204
|
-
const cli =
|
|
8519
|
+
const agent = join5(directory, "dist", "fz-agent.js");
|
|
8520
|
+
const cli = join5(directory, "dist", "fz.js");
|
|
8205
8521
|
for (const binary of [agent, cli]) {
|
|
8206
|
-
if (!
|
|
8522
|
+
if (!readFileSync7(binary, "utf8").startsWith(`#!/usr/bin/env bun
|
|
8207
8523
|
`)) {
|
|
8208
8524
|
throw new Error("agent update artifact is not a self-contained Bun executable");
|
|
8209
8525
|
}
|
|
8210
|
-
|
|
8526
|
+
chmodSync10(binary, 493);
|
|
8211
8527
|
}
|
|
8212
|
-
const version = (await
|
|
8528
|
+
const version = (await checked4(run, { command: agent, args: ["--version"] }, "agent update smoke test")).output.trim();
|
|
8213
8529
|
if (version !== release.version)
|
|
8214
8530
|
throw new Error(`agent update binary reports ${version}`);
|
|
8215
8531
|
}
|
|
@@ -8219,12 +8535,12 @@ async function stageAgentRelease(releaseInput, options) {
|
|
|
8219
8535
|
throw new Error(`agent update ${release.version} is not newer than ${options.currentVersion}`);
|
|
8220
8536
|
}
|
|
8221
8537
|
const root = resolve2(options.root ?? DEFAULT_AGENT_RELEASE_ROOT);
|
|
8222
|
-
const versions =
|
|
8223
|
-
const finalDirectory =
|
|
8224
|
-
const currentLink =
|
|
8225
|
-
const stage2 =
|
|
8226
|
-
const archive =
|
|
8227
|
-
const unpacked =
|
|
8538
|
+
const versions = join5(root, "versions");
|
|
8539
|
+
const finalDirectory = join5(versions, release.version);
|
|
8540
|
+
const currentLink = join5(root, "current");
|
|
8541
|
+
const stage2 = join5(versions, `.${release.version}.${randomUUID2()}.staging`);
|
|
8542
|
+
const archive = join5(stage2, "agent.tgz");
|
|
8543
|
+
const unpacked = join5(stage2, "unpacked");
|
|
8228
8544
|
const run = options.run ?? command;
|
|
8229
8545
|
mkdirSync6(unpacked, { recursive: true, mode: 448 });
|
|
8230
8546
|
try {
|
|
@@ -8245,8 +8561,8 @@ async function stageAgentRelease(releaseInput, options) {
|
|
|
8245
8561
|
const actual = createHash4("sha512").update(bytes).digest();
|
|
8246
8562
|
if (!timingSafeEqual(actual, expected))
|
|
8247
8563
|
throw new Error("agent update integrity mismatch");
|
|
8248
|
-
|
|
8249
|
-
await
|
|
8564
|
+
writeFileSync7(archive, bytes, { mode: 384, flag: "wx" });
|
|
8565
|
+
await checked4(run, {
|
|
8250
8566
|
command: "/usr/bin/tar",
|
|
8251
8567
|
args: [
|
|
8252
8568
|
"-xzf",
|
|
@@ -8269,10 +8585,10 @@ async function stageAgentRelease(releaseInput, options) {
|
|
|
8269
8585
|
throw new Error("agent update requires an active immutable release to roll back to");
|
|
8270
8586
|
}
|
|
8271
8587
|
const previousTarget = readlinkSync(currentLink);
|
|
8272
|
-
if (previousTarget !==
|
|
8588
|
+
if (previousTarget !== join5("versions", options.currentVersion)) {
|
|
8273
8589
|
throw new Error("agent update current release does not match the running version");
|
|
8274
8590
|
}
|
|
8275
|
-
if (!existsSync10(
|
|
8591
|
+
if (!existsSync10(join5(root, previousTarget))) {
|
|
8276
8592
|
throw new Error("agent update rollback release is missing");
|
|
8277
8593
|
}
|
|
8278
8594
|
return {
|
|
@@ -8280,51 +8596,51 @@ async function stageAgentRelease(releaseInput, options) {
|
|
|
8280
8596
|
fromVersion: options.currentVersion,
|
|
8281
8597
|
directory: finalDirectory,
|
|
8282
8598
|
previousTarget,
|
|
8283
|
-
nextTarget:
|
|
8599
|
+
nextTarget: join5("versions", release.version),
|
|
8284
8600
|
currentLink
|
|
8285
8601
|
};
|
|
8286
8602
|
} finally {
|
|
8287
|
-
|
|
8603
|
+
rmSync3(stage2, { recursive: true, force: true });
|
|
8288
8604
|
}
|
|
8289
8605
|
}
|
|
8290
8606
|
function selectAgentRelease(staged) {
|
|
8291
|
-
const next =
|
|
8607
|
+
const next = join5(dirname4(staged.currentLink), `.current.${randomUUID2()}.next`);
|
|
8292
8608
|
try {
|
|
8293
8609
|
symlinkSync2(staged.nextTarget, next);
|
|
8294
8610
|
renameSync4(next, staged.currentLink);
|
|
8295
8611
|
syncPath(dirname4(staged.currentLink));
|
|
8296
8612
|
} finally {
|
|
8297
|
-
|
|
8613
|
+
rmSync3(next, { force: true });
|
|
8298
8614
|
}
|
|
8299
8615
|
}
|
|
8300
8616
|
function restoreAgentRelease(staged) {
|
|
8301
|
-
const next =
|
|
8617
|
+
const next = join5(dirname4(staged.currentLink), `.current.${randomUUID2()}.rollback`);
|
|
8302
8618
|
try {
|
|
8303
8619
|
symlinkSync2(staged.previousTarget, next);
|
|
8304
8620
|
renameSync4(next, staged.currentLink);
|
|
8305
8621
|
syncPath(dirname4(staged.currentLink));
|
|
8306
8622
|
} finally {
|
|
8307
|
-
|
|
8623
|
+
rmSync3(next, { force: true });
|
|
8308
8624
|
}
|
|
8309
8625
|
}
|
|
8310
8626
|
|
|
8311
8627
|
// src/agent-update-helper.ts
|
|
8312
8628
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
8313
8629
|
import {
|
|
8314
|
-
chmodSync as
|
|
8630
|
+
chmodSync as chmodSync11,
|
|
8315
8631
|
closeSync as closeSync2,
|
|
8316
8632
|
existsSync as existsSync11,
|
|
8317
8633
|
fsyncSync as fsyncSync2,
|
|
8318
8634
|
mkdirSync as mkdirSync7,
|
|
8319
8635
|
openSync as openSync2,
|
|
8320
|
-
readFileSync as
|
|
8636
|
+
readFileSync as readFileSync8,
|
|
8321
8637
|
renameSync as renameSync5,
|
|
8322
|
-
rmSync as
|
|
8638
|
+
rmSync as rmSync4,
|
|
8323
8639
|
unlinkSync as unlinkSync9,
|
|
8324
|
-
writeFileSync as
|
|
8640
|
+
writeFileSync as writeFileSync8
|
|
8325
8641
|
} from "fs";
|
|
8326
8642
|
import { connect as connect5, createServer as createServer6 } from "net";
|
|
8327
|
-
import { dirname as dirname5, join as
|
|
8643
|
+
import { dirname as dirname5, join as join6, resolve as resolve3 } from "path";
|
|
8328
8644
|
var AGENT_UPDATE_GROUP = "forgezero-update";
|
|
8329
8645
|
var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-helper.service";
|
|
8330
8646
|
var AGENT_UPDATE_JOURNAL = "/var/lib/forgezero/agent-update.json";
|
|
@@ -8408,12 +8724,12 @@ function validateJournal(value, root) {
|
|
|
8408
8724
|
throw new Error("Agent update failure count is invalid");
|
|
8409
8725
|
}
|
|
8410
8726
|
const releaseRoot = resolve3(root);
|
|
8411
|
-
if (journal.currentLink !==
|
|
8727
|
+
if (journal.currentLink !== join6(releaseRoot, "current"))
|
|
8412
8728
|
throw new Error("Agent update current link is invalid");
|
|
8413
|
-
if (journal.previousTarget !==
|
|
8729
|
+
if (journal.previousTarget !== join6("versions", receipt.fromVersion)) {
|
|
8414
8730
|
throw new Error("Agent update rollback target is invalid");
|
|
8415
8731
|
}
|
|
8416
|
-
if (journal.nextTarget !==
|
|
8732
|
+
if (journal.nextTarget !== join6("versions", receipt.targetVersion)) {
|
|
8417
8733
|
throw new Error("Agent update next target is invalid");
|
|
8418
8734
|
}
|
|
8419
8735
|
return journal;
|
|
@@ -8421,7 +8737,7 @@ function validateJournal(value, root) {
|
|
|
8421
8737
|
function readJournal(path, root) {
|
|
8422
8738
|
if (!existsSync11(path))
|
|
8423
8739
|
return;
|
|
8424
|
-
return validateJournal(JSON.parse(
|
|
8740
|
+
return validateJournal(JSON.parse(readFileSync8(path, "utf8")), root);
|
|
8425
8741
|
}
|
|
8426
8742
|
function writeAtomic(path, value, mode) {
|
|
8427
8743
|
mkdirSync7(dirname5(path), { recursive: true, mode: 493 });
|
|
@@ -8429,7 +8745,7 @@ function writeAtomic(path, value, mode) {
|
|
|
8429
8745
|
let file;
|
|
8430
8746
|
try {
|
|
8431
8747
|
file = openSync2(next, "wx", mode);
|
|
8432
|
-
|
|
8748
|
+
writeFileSync8(file, `${JSON.stringify(value)}
|
|
8433
8749
|
`);
|
|
8434
8750
|
fsyncSync2(file);
|
|
8435
8751
|
closeSync2(file);
|
|
@@ -8444,7 +8760,7 @@ function writeAtomic(path, value, mode) {
|
|
|
8444
8760
|
} finally {
|
|
8445
8761
|
if (file !== undefined)
|
|
8446
8762
|
closeSync2(file);
|
|
8447
|
-
|
|
8763
|
+
rmSync4(next, { force: true });
|
|
8448
8764
|
}
|
|
8449
8765
|
}
|
|
8450
8766
|
var publicReceipt = (journal) => {
|
|
@@ -8479,7 +8795,7 @@ function readAgentUpdateReceipt(path = AGENT_UPDATE_RECEIPT) {
|
|
|
8479
8795
|
try {
|
|
8480
8796
|
if (!existsSync11(path))
|
|
8481
8797
|
return;
|
|
8482
|
-
return validateReceipt(JSON.parse(
|
|
8798
|
+
return validateReceipt(JSON.parse(readFileSync8(path, "utf8")));
|
|
8483
8799
|
} catch {
|
|
8484
8800
|
return;
|
|
8485
8801
|
}
|
|
@@ -8503,21 +8819,21 @@ var retryAfter = (now, failures) => now + Math.min(UPDATE_RETRY_MAX_MS, UPDATE_R
|
|
|
8503
8819
|
var stagedFromJournal = (journal, root) => ({
|
|
8504
8820
|
version: journal.targetVersion,
|
|
8505
8821
|
fromVersion: journal.fromVersion,
|
|
8506
|
-
directory:
|
|
8822
|
+
directory: join6(resolve3(root), journal.nextTarget),
|
|
8507
8823
|
previousTarget: journal.previousTarget,
|
|
8508
8824
|
nextTarget: journal.nextTarget,
|
|
8509
8825
|
currentLink: journal.currentLink
|
|
8510
8826
|
});
|
|
8511
|
-
var restartAgent = async (
|
|
8512
|
-
const helpers =
|
|
8827
|
+
var restartAgent = async (target2, run) => {
|
|
8828
|
+
const helpers = target2 === "compute" ? COMPUTE_HELPER_UNITS : ["forgezero-metal-helper.service"];
|
|
8513
8829
|
for (const unit of helpers)
|
|
8514
8830
|
await run({ command: "/usr/bin/systemctl", args: ["try-restart", unit] });
|
|
8515
|
-
const service =
|
|
8831
|
+
const service = target2 === "compute" ? "forgezero-agent.service" : "forgezero-metal-agent.service";
|
|
8516
8832
|
if (!await runOk(run, "/usr/bin/systemctl", ["restart", service])) {
|
|
8517
8833
|
throw new Error(`systemd could not restart ${service}`);
|
|
8518
8834
|
}
|
|
8519
8835
|
};
|
|
8520
|
-
var targetProbe = (
|
|
8836
|
+
var targetProbe = (target2, run) => target2 === "compute" ? () => probeAgentSocket() : async () => await runOk(run, "/usr/bin/systemctl", ["is-active", "--quiet", "forgezero-metal-agent.service"]) && await runOk(run, "/usr/bin/systemctl", ["is-active", "--quiet", "forgezero-metal-helper.service"]);
|
|
8521
8837
|
function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
|
|
8522
8838
|
return new Promise((resolve4) => {
|
|
8523
8839
|
const socket = connect5(socketPath);
|
|
@@ -8552,8 +8868,8 @@ function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
|
|
|
8552
8868
|
}
|
|
8553
8869
|
async function activateAgentRelease(staged, options = {}) {
|
|
8554
8870
|
const run = options.run ?? runCommand;
|
|
8555
|
-
const
|
|
8556
|
-
const probe = options.probe ?? targetProbe(
|
|
8871
|
+
const target2 = options.target ?? "compute";
|
|
8872
|
+
const probe = options.probe ?? targetProbe(target2, run);
|
|
8557
8873
|
const now = options.now ?? Date.now;
|
|
8558
8874
|
const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
|
|
8559
8875
|
const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
|
|
@@ -8566,7 +8882,7 @@ async function activateAgentRelease(staged, options = {}) {
|
|
|
8566
8882
|
let journal = {
|
|
8567
8883
|
schemaVersion: 1,
|
|
8568
8884
|
attemptId,
|
|
8569
|
-
target,
|
|
8885
|
+
target: target2,
|
|
8570
8886
|
fromVersion: staged.fromVersion,
|
|
8571
8887
|
targetVersion: staged.version,
|
|
8572
8888
|
outcome: "activating",
|
|
@@ -8582,7 +8898,7 @@ async function activateAgentRelease(staged, options = {}) {
|
|
|
8582
8898
|
try {
|
|
8583
8899
|
selectionAttempted = true;
|
|
8584
8900
|
selectAgentRelease(staged);
|
|
8585
|
-
await restartAgent(
|
|
8901
|
+
await restartAgent(target2, run);
|
|
8586
8902
|
if (!await probe())
|
|
8587
8903
|
throw new Error("the replacement Agent did not answer its retained Vault socket");
|
|
8588
8904
|
journal = { ...journal, outcome: "active", updatedAtTs: now(), rollbackHealthy: undefined };
|
|
@@ -8600,7 +8916,7 @@ async function activateAgentRelease(staged, options = {}) {
|
|
|
8600
8916
|
try {
|
|
8601
8917
|
restoreAgentRelease(staged);
|
|
8602
8918
|
restored = true;
|
|
8603
|
-
await restartAgent(
|
|
8919
|
+
await restartAgent(target2, run);
|
|
8604
8920
|
rollbackHealthy = await probe();
|
|
8605
8921
|
} catch {
|
|
8606
8922
|
rollbackHealthy = false;
|
|
@@ -8633,7 +8949,7 @@ async function recoverInterruptedAgentUpdate(options = {}) {
|
|
|
8633
8949
|
return publicReceipt(journal);
|
|
8634
8950
|
}
|
|
8635
8951
|
const staged = stagedFromJournal(journal, root);
|
|
8636
|
-
if (!existsSync11(
|
|
8952
|
+
if (!existsSync11(join6(root, journal.previousTarget))) {
|
|
8637
8953
|
throw new Error("Agent update rollback release is missing");
|
|
8638
8954
|
}
|
|
8639
8955
|
const run = options.run ?? runCommand;
|
|
@@ -8670,7 +8986,7 @@ function startAgentUpdateHelper(options = {}) {
|
|
|
8670
8986
|
const receiptPath = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
|
|
8671
8987
|
const journalPath = options.journalPath ?? AGENT_UPDATE_JOURNAL;
|
|
8672
8988
|
const releaseRoot = options.root ?? DEFAULT_AGENT_RELEASE_ROOT;
|
|
8673
|
-
const activate = options.activate ?? ((staged,
|
|
8989
|
+
const activate = options.activate ?? ((staged, target2, attemptId) => activateAgentRelease(staged, { target: target2, attemptId, journalPath, receiptPath, now: options.now }));
|
|
8674
8990
|
let busy = true;
|
|
8675
8991
|
let blocked;
|
|
8676
8992
|
(options.recover ?? (() => recoverInterruptedAgentUpdate({
|
|
@@ -8744,7 +9060,7 @@ function startAgentUpdateHelper(options = {}) {
|
|
|
8744
9060
|
});
|
|
8745
9061
|
socket.on("error", () => socket.destroy());
|
|
8746
9062
|
});
|
|
8747
|
-
server.listen(socketPath, () =>
|
|
9063
|
+
server.listen(socketPath, () => chmodSync11(socketPath, 432));
|
|
8748
9064
|
return server;
|
|
8749
9065
|
}
|
|
8750
9066
|
function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, timeoutMs = 90000) {
|
|
@@ -8774,10 +9090,10 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
|
|
|
8774
9090
|
}
|
|
8775
9091
|
|
|
8776
9092
|
// src/agent-heartbeat.ts
|
|
8777
|
-
import { readFileSync as
|
|
9093
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
8778
9094
|
|
|
8779
9095
|
// src/version.ts
|
|
8780
|
-
var VERSION3 = "0.1.
|
|
9096
|
+
var VERSION3 = "0.1.36";
|
|
8781
9097
|
|
|
8782
9098
|
// src/agent-heartbeat.ts
|
|
8783
9099
|
var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
|
|
@@ -8785,7 +9101,7 @@ var remoteOutcome4 = (cause) => cause instanceof SignedNodeHttpError && cause.st
|
|
|
8785
9101
|
|
|
8786
9102
|
class AgentUpdateRefusedError extends Error {
|
|
8787
9103
|
}
|
|
8788
|
-
function observeAgentHost(version = VERSION3, mode = "enrolled", osRelease =
|
|
9104
|
+
function observeAgentHost(version = VERSION3, mode = "enrolled", osRelease = readFileSync9("/etc/os-release", "utf8"), architecture = process.arch) {
|
|
8789
9105
|
const values = Object.fromEntries(osRelease.split(`
|
|
8790
9106
|
`).flatMap((line) => {
|
|
8791
9107
|
const separator = line.indexOf("=");
|
|
@@ -8895,7 +9211,7 @@ function startAgentHeartbeat(options) {
|
|
|
8895
9211
|
}
|
|
8896
9212
|
|
|
8897
9213
|
// src/software-helper.ts
|
|
8898
|
-
import { chmodSync as
|
|
9214
|
+
import { chmodSync as chmodSync12, existsSync as existsSync12, mkdirSync as mkdirSync8, unlinkSync as unlinkSync10 } from "fs";
|
|
8899
9215
|
import { connect as connect6, createServer as createServer7 } from "net";
|
|
8900
9216
|
import { dirname as dirname6 } from "path";
|
|
8901
9217
|
var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
|
|
@@ -8967,7 +9283,7 @@ function startSoftwareHelper(options = {}) {
|
|
|
8967
9283
|
});
|
|
8968
9284
|
socket.on("error", () => socket.destroy());
|
|
8969
9285
|
});
|
|
8970
|
-
server.listen(socketPath, () =>
|
|
9286
|
+
server.listen(socketPath, () => chmodSync12(socketPath, 432));
|
|
8971
9287
|
return server;
|
|
8972
9288
|
}
|
|
8973
9289
|
function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
@@ -9530,7 +9846,7 @@ function createAgentTelemetry(input, options = {}) {
|
|
|
9530
9846
|
logs.push(logRecord("telemetry.queue_overflow"));
|
|
9531
9847
|
}
|
|
9532
9848
|
};
|
|
9533
|
-
const
|
|
9849
|
+
const post4 = async (path, body, sentItems) => {
|
|
9534
9850
|
for (let attempt = 0;attempt < AGENT_OTLP_MAX_ATTEMPTS; attempt += 1) {
|
|
9535
9851
|
try {
|
|
9536
9852
|
const response = await send(`${config.endpoint}${path}`, {
|
|
@@ -9581,9 +9897,9 @@ function createAgentTelemetry(input, options = {}) {
|
|
|
9581
9897
|
addExportDiscards(records.length);
|
|
9582
9898
|
return;
|
|
9583
9899
|
}
|
|
9584
|
-
const
|
|
9900
|
+
const target2 = kind === "traces" ? spans : logs;
|
|
9585
9901
|
const maximum = kind === "traces" ? AGENT_TELEMETRY_MAX_SPANS : AGENT_TELEMETRY_MAX_LOGS;
|
|
9586
|
-
const combined = [...records, ...
|
|
9902
|
+
const combined = [...records, ...target2];
|
|
9587
9903
|
const overflow = Math.max(0, combined.length - maximum);
|
|
9588
9904
|
const retained = combined.slice(-maximum);
|
|
9589
9905
|
if (kind === "traces")
|
|
@@ -9736,7 +10052,7 @@ function createAgentTelemetry(input, options = {}) {
|
|
|
9736
10052
|
exports.push({
|
|
9737
10053
|
kind: "traces",
|
|
9738
10054
|
itemCount: currentSpans.length,
|
|
9739
|
-
promise:
|
|
10055
|
+
promise: post4("/v1/traces", {
|
|
9740
10056
|
resourceSpans: [{ resource, scopeSpans: [{
|
|
9741
10057
|
scope: { name: "forgezero.agent" },
|
|
9742
10058
|
spans: currentSpans
|
|
@@ -9747,7 +10063,7 @@ function createAgentTelemetry(input, options = {}) {
|
|
|
9747
10063
|
exports.push({
|
|
9748
10064
|
kind: "metrics",
|
|
9749
10065
|
itemCount: metricPointCount,
|
|
9750
|
-
promise:
|
|
10066
|
+
promise: post4("/v1/metrics", {
|
|
9751
10067
|
resourceMetrics: [{ resource, scopeMetrics: [{
|
|
9752
10068
|
scope: { name: "forgezero.agent" },
|
|
9753
10069
|
metrics: metricInstruments
|
|
@@ -9758,7 +10074,7 @@ function createAgentTelemetry(input, options = {}) {
|
|
|
9758
10074
|
exports.push({
|
|
9759
10075
|
kind: "logs",
|
|
9760
10076
|
itemCount: currentLogs.length,
|
|
9761
|
-
promise:
|
|
10077
|
+
promise: post4("/v1/logs", {
|
|
9762
10078
|
resourceLogs: [{ resource, scopeLogs: [{
|
|
9763
10079
|
scope: { name: "forgezero.agent" },
|
|
9764
10080
|
logRecords: currentLogs
|
|
@@ -9881,7 +10197,7 @@ class AgentTelemetryRuntime {
|
|
|
9881
10197
|
// src/index.ts
|
|
9882
10198
|
function loadOrCreateSeed(path) {
|
|
9883
10199
|
if (existsSync13(path)) {
|
|
9884
|
-
const seed2 = new Uint8Array(Buffer.from(
|
|
10200
|
+
const seed2 = new Uint8Array(Buffer.from(readFileSync10(path, "utf8").trim(), "base64url"));
|
|
9885
10201
|
if (seed2.length < 32) {
|
|
9886
10202
|
throw new Error(`agent: the seed at ${path} is too short to derive a key from.`);
|
|
9887
10203
|
}
|
|
@@ -9889,8 +10205,8 @@ function loadOrCreateSeed(path) {
|
|
|
9889
10205
|
}
|
|
9890
10206
|
mkdirSync9(dirname7(path), { recursive: true });
|
|
9891
10207
|
const seed = new Uint8Array(randomBytes5(32));
|
|
9892
|
-
|
|
9893
|
-
|
|
10208
|
+
writeFileSync9(path, Buffer.from(seed).toString("base64url"), { mode: 384 });
|
|
10209
|
+
chmodSync13(path, 384);
|
|
9894
10210
|
return seed;
|
|
9895
10211
|
}
|
|
9896
10212
|
var DEFAULT_SOCKET_PATH = DEFAULT_SOCKET;
|
|
@@ -9921,7 +10237,7 @@ function loadSeedCredential(name = DEFAULT_SEED_CREDENTIAL, directory = process.
|
|
|
9921
10237
|
const path = `${directory}/${name}`;
|
|
9922
10238
|
if (!existsSync13(path))
|
|
9923
10239
|
throw new Error(`agent: the systemd credential ${name} is missing at ${path}.`);
|
|
9924
|
-
const seed = new Uint8Array(Buffer.from(
|
|
10240
|
+
const seed = new Uint8Array(Buffer.from(readFileSync10(path, "utf8").trim(), "base64url"));
|
|
9925
10241
|
if (seed.length < 32)
|
|
9926
10242
|
throw new Error(`agent: the systemd credential ${name} is too short to derive a key from.`);
|
|
9927
10243
|
return seed;
|
|
@@ -9931,7 +10247,7 @@ function loadTextCredential(name, directory = process.env.CREDENTIALS_DIRECTORY)
|
|
|
9931
10247
|
throw new Error("agent: CREDENTIALS_DIRECTORY is missing; systemd did not load the credential.");
|
|
9932
10248
|
if (!/^[A-Za-z0-9_.-]+$/.test(name))
|
|
9933
10249
|
throw new Error("agent: invalid systemd credential name.");
|
|
9934
|
-
const value =
|
|
10250
|
+
const value = readFileSync10(`${directory}/${name}`, "utf8").trim();
|
|
9935
10251
|
if (!value)
|
|
9936
10252
|
throw new Error(`agent: systemd credential ${name} is empty.`);
|
|
9937
10253
|
return value;
|
|
@@ -10054,7 +10370,7 @@ if (import.meta.main) {
|
|
|
10054
10370
|
keys: keys2,
|
|
10055
10371
|
label: process.env.FZ_NODE_LABEL,
|
|
10056
10372
|
edgeHostname: process.env.FZ_NODE_HOSTNAME,
|
|
10057
|
-
gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ?
|
|
10373
|
+
gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ? readFileSync10(process.env.FZ_GIT_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
|
|
10058
10374
|
privateNetworkAttachment: privateNetworkAttachmentFromEnvironment()
|
|
10059
10375
|
});
|
|
10060
10376
|
console.log(`[agent] enrolled ${binding2.computeReference} in project ${binding2.projectKey}/${binding2.environmentKey}`);
|
|
@@ -10087,7 +10403,7 @@ if (import.meta.main) {
|
|
|
10087
10403
|
const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
10088
10404
|
if (!profilePath)
|
|
10089
10405
|
throw new Error("metal-helper requires --profile=/absolute/path.json");
|
|
10090
|
-
const profile2 = JSON.parse(
|
|
10406
|
+
const profile2 = JSON.parse(readFileSync10(profilePath, "utf8"));
|
|
10091
10407
|
const helper = startMetalHelper({
|
|
10092
10408
|
profile: profile2,
|
|
10093
10409
|
socketPath: process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET
|
|
@@ -10205,7 +10521,7 @@ if (import.meta.main) {
|
|
|
10205
10521
|
const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
10206
10522
|
if (!profilePath)
|
|
10207
10523
|
throw new Error("metal-isolation requires --profile=/absolute/path.json");
|
|
10208
|
-
const profile2 = JSON.parse(
|
|
10524
|
+
const profile2 = JSON.parse(readFileSync10(profilePath, "utf8"));
|
|
10209
10525
|
await applyMetalIsolation(profile2);
|
|
10210
10526
|
console.log("[metal-isolation] host and guest cgroup boundaries active");
|
|
10211
10527
|
process.exit(0);
|
|
@@ -10250,7 +10566,7 @@ if (import.meta.main) {
|
|
|
10250
10566
|
if (claimArg !== "-" && !claimArg.startsWith("/")) {
|
|
10251
10567
|
throw new Error("metal-apply claim path must be absolute");
|
|
10252
10568
|
}
|
|
10253
|
-
const raw =
|
|
10569
|
+
const raw = readFileSync10(claimArg === "-" ? "/dev/stdin" : claimArg, "utf8");
|
|
10254
10570
|
if (Buffer.byteLength(raw) > 32 * 1024)
|
|
10255
10571
|
throw new Error("metal-apply claim exceeds 32 KiB");
|
|
10256
10572
|
const claim = JSON.parse(raw);
|
|
@@ -10396,7 +10712,7 @@ if (import.meta.main) {
|
|
|
10396
10712
|
keys,
|
|
10397
10713
|
label: process.env.FZ_NODE_LABEL,
|
|
10398
10714
|
edgeHostname: process.env.FZ_NODE_HOSTNAME,
|
|
10399
|
-
gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ?
|
|
10715
|
+
gitDeployPublicKey: process.env.FZ_GIT_PUBLIC_KEY_FILE ? readFileSync10(process.env.FZ_GIT_PUBLIC_KEY_FILE, "utf8").trim() : undefined,
|
|
10400
10716
|
privateNetworkAttachment: privateNetworkAttachmentFromEnvironment()
|
|
10401
10717
|
});
|
|
10402
10718
|
console.log(`[agent] enrolled ${binding.computeReference} in project ${binding.projectKey}/${binding.environmentKey}`);
|
|
@@ -10456,6 +10772,23 @@ if (import.meta.main) {
|
|
|
10456
10772
|
}) : undefined;
|
|
10457
10773
|
if (migrationPull)
|
|
10458
10774
|
console.log("[agent] PQ-authenticated outbound lifecycle claims enabled");
|
|
10775
|
+
const bootstrapEnabled = process.env.FZ_BOOTSTRAP_PULL === "true";
|
|
10776
|
+
const bootstrapCredential = process.env.FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL;
|
|
10777
|
+
const bootstrapKeyPath = bootstrapCredential && process.env.CREDENTIALS_DIRECTORY ? `${process.env.CREDENTIALS_DIRECTORY}/${bootstrapCredential}` : undefined;
|
|
10778
|
+
if (bootstrapEnabled && (!binding || !nodeApiUrl || !process.env.FZ_API || !bootstrapKeyPath || !existsSync13(bootstrapKeyPath) || !process.env.FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT)) {
|
|
10779
|
+
throw new Error("agent: bootstrap runner requires enrolment, API, local SSH credential and target OTLP coordinate");
|
|
10780
|
+
}
|
|
10781
|
+
const bootstrapPull = bootstrapEnabled ? startSshBootstrapPull({
|
|
10782
|
+
apiUrl: nodeApiUrl,
|
|
10783
|
+
platformApiUrl: process.env.FZ_API,
|
|
10784
|
+
nodeKey,
|
|
10785
|
+
keys,
|
|
10786
|
+
sshKeyPath: bootstrapKeyPath,
|
|
10787
|
+
targetTelemetryEndpoint: process.env.FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT,
|
|
10788
|
+
onEvent: (event) => console.log(`[agent] bootstrap ${event}`)
|
|
10789
|
+
}) : undefined;
|
|
10790
|
+
if (bootstrapPull)
|
|
10791
|
+
console.log("[agent] PQ-authenticated delegated SSH bootstrap claims enabled");
|
|
10459
10792
|
const systemdDeploymentSecrets = createSystemdDeploymentSecrets(process.env.FZ_DEPLOY_SYSTEMD_SECRETS);
|
|
10460
10793
|
const deploymentSecrets = secretCache ? {
|
|
10461
10794
|
async get(name) {
|
|
@@ -10493,10 +10826,10 @@ if (import.meta.main) {
|
|
|
10493
10826
|
return [name, process.env[name]];
|
|
10494
10827
|
})),
|
|
10495
10828
|
gitCredentialPath: process.env.CREDENTIALS_DIRECTORY ? `${process.env.CREDENTIALS_DIRECTORY}/git-deploy-key` : undefined,
|
|
10496
|
-
knownHostsPath: source.knownHosts ?
|
|
10829
|
+
knownHostsPath: source.knownHosts ? join7(root, "cache", `known-hosts-${key}`) : undefined,
|
|
10497
10830
|
knownHostsContent: source.knownHosts,
|
|
10498
10831
|
cache: deploymentSecrets,
|
|
10499
|
-
capacityEvidenceDirectory: process.env.FZ_CAPACITY_EVIDENCE_DIR ??
|
|
10832
|
+
capacityEvidenceDirectory: process.env.FZ_CAPACITY_EVIDENCE_DIR ?? join7(root, "cache", "capacity"),
|
|
10500
10833
|
ensureSoftware: (requirements) => requestSoftware(requirements, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
10501
10834
|
projectExec: process.env.FZ_DEPLOY_RUNNER_SOCKET ? (input) => requestDeploymentCommand(input, process.env.FZ_DEPLOY_RUNNER_SOCKET) : undefined
|
|
10502
10835
|
});
|
|
@@ -10545,7 +10878,8 @@ if (import.meta.main) {
|
|
|
10545
10878
|
try {
|
|
10546
10879
|
await Promise.all([
|
|
10547
10880
|
pull?.stop() ?? Promise.resolve(),
|
|
10548
|
-
migrationPull?.stop() ?? Promise.resolve()
|
|
10881
|
+
migrationPull?.stop() ?? Promise.resolve(),
|
|
10882
|
+
bootstrapPull?.stop() ?? Promise.resolve()
|
|
10549
10883
|
]);
|
|
10550
10884
|
const reports = await Promise.all([...new Set(managers.values())].map((manager) => manager.stop(Math.max(1, Number(process.env.FZ_DRAIN_DEADLINE_MS ?? 30000)))));
|
|
10551
10885
|
if (reports.some(({ timedOut }) => timedOut)) {
|
|
@@ -10583,9 +10917,10 @@ if (import.meta.main) {
|
|
|
10583
10917
|
const vaultDrain = vaultSync?.stop() ?? Promise.resolve();
|
|
10584
10918
|
const attestationDrain = attestationLoop?.stop() ?? Promise.resolve();
|
|
10585
10919
|
const migrationDrain = migrationPull?.stop() ?? Promise.resolve();
|
|
10920
|
+
const bootstrapDrain = bootstrapPull?.stop() ?? Promise.resolve();
|
|
10586
10921
|
const heartbeatDrain = heartbeat?.stop() ?? Promise.resolve();
|
|
10587
10922
|
const pullDrained = pull ? await settleWithin(pullDrain, remaining()) : true;
|
|
10588
|
-
const backgroundDrained = await settleWithin(Promise.all([vaultDrain, attestationDrain, migrationDrain, heartbeatDrain]), remaining());
|
|
10923
|
+
const backgroundDrained = await settleWithin(Promise.all([vaultDrain, attestationDrain, migrationDrain, bootstrapDrain, heartbeatDrain]), remaining());
|
|
10589
10924
|
const managerReports = await Promise.all([...new Set(managers.values())].map((manager) => manager.stop(remaining())));
|
|
10590
10925
|
const socketClosed = await closeServerWithin(server, remaining());
|
|
10591
10926
|
telemetry.event("agent.stopped");
|
|
@@ -10616,7 +10951,10 @@ if (import.meta.main) {
|
|
|
10616
10951
|
return;
|
|
10617
10952
|
updatePrepared = true;
|
|
10618
10953
|
try {
|
|
10619
|
-
await
|
|
10954
|
+
await Promise.all([
|
|
10955
|
+
migrationPull?.stop() ?? Promise.resolve(),
|
|
10956
|
+
bootstrapPull?.stop() ?? Promise.resolve()
|
|
10957
|
+
]);
|
|
10620
10958
|
telemetry.event("agent.update_prepared");
|
|
10621
10959
|
} catch (cause) {
|
|
10622
10960
|
updatePrepared = false;
|
|
@@ -10648,6 +10986,7 @@ if (import.meta.main) {
|
|
|
10648
10986
|
vaultSync?.stop() ?? Promise.resolve(),
|
|
10649
10987
|
attestationLoop?.stop() ?? Promise.resolve(),
|
|
10650
10988
|
migrationPull?.stop() ?? Promise.resolve(),
|
|
10989
|
+
bootstrapPull?.stop() ?? Promise.resolve(),
|
|
10651
10990
|
heartbeat?.stop() ?? Promise.resolve()
|
|
10652
10991
|
]), remaining());
|
|
10653
10992
|
const socketClosed = await closeServerWithin(server, remaining());
|
|
@@ -10670,6 +11009,7 @@ export {
|
|
|
10670
11009
|
systemdListenFd,
|
|
10671
11010
|
systemdAgentEgressDirectives,
|
|
10672
11011
|
superviseAgentEgressPolicy,
|
|
11012
|
+
startSshBootstrapPull,
|
|
10673
11013
|
startSoftwareHelper,
|
|
10674
11014
|
startProvisioningPull,
|
|
10675
11015
|
startNodeVaultSync,
|
|
@@ -10699,6 +11039,7 @@ export {
|
|
|
10699
11039
|
removeMetalGuest,
|
|
10700
11040
|
recoverInterruptedAgentUpdate,
|
|
10701
11041
|
readAgentUpdateReceipt,
|
|
11042
|
+
pullSshBootstrapOnce,
|
|
10702
11043
|
pullProvisioningOnce,
|
|
10703
11044
|
pullMigrationOnce,
|
|
10704
11045
|
pullDeploymentOnce,
|
|
@@ -10722,6 +11063,7 @@ export {
|
|
|
10722
11063
|
handleRequest,
|
|
10723
11064
|
handleApplicationRequest,
|
|
10724
11065
|
guestNameFor,
|
|
11066
|
+
executeSshBootstrap,
|
|
10725
11067
|
executeLifecycleAction,
|
|
10726
11068
|
ensureSoftwareRequirements,
|
|
10727
11069
|
enrolGuestIdentity,
|