@forgezero/agent 0.1.40 → 0.1.42
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 +573 -424
- package/dist/agent-heartbeat.js +6 -3
- package/dist/agent-update-helper.js +5 -2
- package/dist/agent-update.js +5 -2
- package/dist/bootstrap.d.ts +17 -8
- package/dist/bootstrap.js +1504 -512
- package/dist/cli/agent-install.d.ts +6 -5
- package/dist/cli/cloudflare-bootstrap.d.ts +12 -1
- package/dist/cli/maintenance.d.ts +23 -0
- package/dist/cli/run.d.ts +3 -1
- package/dist/cli/session-store.d.ts +5 -0
- package/dist/cloudflare-bootstrap.d.ts +73 -35
- package/dist/cloudflare-bootstrap.js +587 -90
- package/dist/cloudflare-edge.d.ts +64 -12
- package/dist/cloudflare-edge.js +103 -8
- package/dist/community-rehearsal-host.d.ts +51 -0
- package/dist/community-rehearsal-host.js +272 -0
- package/dist/credential-schema.d.ts +54 -0
- package/dist/credential-schema.js +47 -0
- package/dist/definition.d.ts +31 -5
- package/dist/definition.js +271 -44
- package/dist/deploy-file.js +294 -68
- package/dist/deployment-runner.js +18 -5
- package/dist/deployment.d.ts +13 -1
- package/dist/fz-agent.js +9162 -7564
- package/dist/fz-git-ssh.js +122 -0
- package/dist/fz.js +5679 -5077
- package/dist/git-ssh.d.ts +5 -0
- package/dist/guest-enrolment.d.ts +2 -0
- package/dist/guest-enrolment.js +1 -0
- package/dist/host-maintenance.d.ts +39 -0
- package/dist/host-maintenance.js +135 -0
- package/dist/index.d.ts +4 -2
- package/dist/mesh-connector.d.ts +16 -0
- package/dist/mesh-connector.js +46 -0
- package/dist/metal-bootstrap.js +145 -7
- package/dist/metal-helper-socket.js +61 -31
- package/dist/metal-provision.d.ts +2 -2
- package/dist/metal-provision.js +62 -32
- package/dist/operator-bootstrap.d.ts +90 -0
- package/dist/operator-bootstrap.js +5704 -0
- package/dist/otel-collector.d.ts +18 -0
- package/dist/pipeline.d.ts +3 -2
- package/dist/pipeline.js +1 -1
- package/dist/platform-bootstrap-runtime.d.ts +39 -21
- package/dist/platform-bootstrap-runtime.js +182 -59
- package/dist/platform-fleet-verification.d.ts +19 -0
- package/dist/platform-fleet-verification.js +3873 -0
- package/dist/platform-genesis-config.d.ts +7 -0
- package/dist/platform-genesis.d.ts +17 -0
- package/dist/provision.d.ts +76 -3
- package/dist/provision.js +1061 -229
- package/dist/recovery-host.d.ts +7 -0
- package/dist/recovery-host.js +124 -0
- package/dist/service-supervisor.d.ts +42 -0
- package/dist/software-helper.d.ts +4 -0
- package/dist/software-helper.js +865 -63
- package/dist/software.d.ts +14 -3
- package/dist/software.js +163 -37
- package/dist/ssh-bootstrap.d.ts +97 -0
- package/dist/supervised-app.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/package.json +175 -164
- package/schema/{deploy-v2.json → deploy-v3.json} +53 -6
|
@@ -29,6 +29,8 @@ export interface GuestEnrolmentOptions {
|
|
|
29
29
|
keys: NodeKeyPair;
|
|
30
30
|
label?: string;
|
|
31
31
|
gitDeployPublicKey?: string;
|
|
32
|
+
/** Public half of the runner-only SSH identity; safe to show for authorized_keys. */
|
|
33
|
+
bootstrapSshPublicKey?: string;
|
|
32
34
|
privateNetworkAttachment?: GuestPrivateNetworkAttachment;
|
|
33
35
|
/** Stable API ingress identity; signed with the rest of the enrolment body. */
|
|
34
36
|
edgeHostname?: string;
|
package/dist/guest-enrolment.js
CHANGED
|
@@ -147,6 +147,7 @@ async function enrolGuestIdentity(options) {
|
|
|
147
147
|
token,
|
|
148
148
|
label: options.label,
|
|
149
149
|
gitDeployPublicKey: options.gitDeployPublicKey,
|
|
150
|
+
bootstrapSshPublicKey: options.bootstrapSshPublicKey,
|
|
150
151
|
privateNetworkAttachment: options.privateNetworkAttachment,
|
|
151
152
|
edgeHostname: options.edgeHostname?.trim() || undefined,
|
|
152
153
|
publicKeys: {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type HostMaintenanceOperation = 'transactions' | 'schema-plan' | 'schema-apply';
|
|
2
|
+
export interface HostMaintenanceRequest {
|
|
3
|
+
operation: HostMaintenanceOperation;
|
|
4
|
+
prepareBlankFzTest?: boolean;
|
|
5
|
+
confirmation?: string;
|
|
6
|
+
fenceEvidencePath?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface HostMaintenancePlan {
|
|
9
|
+
operation: HostMaintenanceOperation;
|
|
10
|
+
mutation: boolean;
|
|
11
|
+
activeSlot: 'blue' | 'green';
|
|
12
|
+
workingDirectory: string;
|
|
13
|
+
unit: string;
|
|
14
|
+
credentialNames: readonly string[];
|
|
15
|
+
argv: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
export interface HostMaintenanceResult {
|
|
18
|
+
plan: HostMaintenancePlan;
|
|
19
|
+
exitCode: number;
|
|
20
|
+
output: string;
|
|
21
|
+
}
|
|
22
|
+
export interface HostMaintenanceRuntime {
|
|
23
|
+
uid(): number;
|
|
24
|
+
read(path: string): string;
|
|
25
|
+
inspect(path: string): {
|
|
26
|
+
regular: boolean;
|
|
27
|
+
symbolic: boolean;
|
|
28
|
+
uid: number;
|
|
29
|
+
links: number;
|
|
30
|
+
mode: number;
|
|
31
|
+
size: number;
|
|
32
|
+
};
|
|
33
|
+
exec(argv: readonly string[]): Promise<{
|
|
34
|
+
exitCode: number;
|
|
35
|
+
output: string;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
export declare function planHostMaintenance(request: HostMaintenanceRequest, runtime?: HostMaintenanceRuntime): HostMaintenancePlan;
|
|
39
|
+
export declare function applyHostMaintenance(request: HostMaintenanceRequest, runtime?: HostMaintenanceRuntime): Promise<HostMaintenanceResult>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/host-maintenance.ts
|
|
3
|
+
import { lstatSync, readFileSync } from "fs";
|
|
4
|
+
var ROOT = "/opt/forgezero";
|
|
5
|
+
var SLOT_FILE = `${ROOT}/.forge-slot`;
|
|
6
|
+
var SHARED_ENV = `${ROOT}/shared/.env`;
|
|
7
|
+
var JWT = "/etc/forgezero/creds/arangodb-jwt.cred";
|
|
8
|
+
var FZ = "/usr/local/bin/fz";
|
|
9
|
+
var localRuntime = () => ({
|
|
10
|
+
uid: () => process.getuid?.() ?? -1,
|
|
11
|
+
read: (path) => readFileSync(path, "utf8"),
|
|
12
|
+
inspect(path) {
|
|
13
|
+
const value = lstatSync(path);
|
|
14
|
+
return {
|
|
15
|
+
regular: value.isFile(),
|
|
16
|
+
symbolic: value.isSymbolicLink(),
|
|
17
|
+
uid: value.uid,
|
|
18
|
+
links: value.nlink,
|
|
19
|
+
mode: value.mode,
|
|
20
|
+
size: value.size
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
async exec(argv) {
|
|
24
|
+
const child = Bun.spawn([...argv], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
25
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
26
|
+
new Response(child.stdout).text(),
|
|
27
|
+
new Response(child.stderr).text(),
|
|
28
|
+
child.exited
|
|
29
|
+
]);
|
|
30
|
+
return { exitCode, output: `${stdout}${stderr}`.slice(0, 256 * 1024) };
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
var trustedFile = (runtime, path, label, modeMask, max) => {
|
|
34
|
+
const metadata = runtime.inspect(path);
|
|
35
|
+
if (!metadata.regular || metadata.symbolic || metadata.uid !== 0 || metadata.links !== 1 || (metadata.mode & modeMask) !== 0 || metadata.size < 1 || metadata.size > max) {
|
|
36
|
+
throw new Error(`${label} is not a trusted root-owned regular file`);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function activeSlot(runtime) {
|
|
40
|
+
trustedFile(runtime, SLOT_FILE, "active slot marker", 18, 64);
|
|
41
|
+
const slot = runtime.read(SLOT_FILE).trim();
|
|
42
|
+
if (slot !== "blue" && slot !== "green")
|
|
43
|
+
throw new Error("active slot marker must be blue or green");
|
|
44
|
+
return slot;
|
|
45
|
+
}
|
|
46
|
+
var confirmation = (value) => {
|
|
47
|
+
if (!value || !/^apply-schema-consolidation-fz-[a-f0-9]{64}$/.test(value)) {
|
|
48
|
+
throw new Error("schema apply requires the exact fz plan confirmation");
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
};
|
|
52
|
+
function planHostMaintenance(request, runtime = localRuntime()) {
|
|
53
|
+
if (!["transactions", "schema-plan", "schema-apply"].includes(request.operation)) {
|
|
54
|
+
throw new Error("unsupported host maintenance operation");
|
|
55
|
+
}
|
|
56
|
+
const slot = activeSlot(runtime);
|
|
57
|
+
const workingDirectory = `${ROOT}/slots/${slot}`;
|
|
58
|
+
const common = [
|
|
59
|
+
"/usr/bin/systemd-run",
|
|
60
|
+
"--quiet",
|
|
61
|
+
"--wait",
|
|
62
|
+
"--pipe",
|
|
63
|
+
"--collect",
|
|
64
|
+
`--unit=forgezero-${request.operation}`,
|
|
65
|
+
"--property=User=forgezero",
|
|
66
|
+
`--property=WorkingDirectory=${workingDirectory}`,
|
|
67
|
+
`--property=EnvironmentFile=${SHARED_ENV}`,
|
|
68
|
+
"--property=Environment=NODE_ENV=production",
|
|
69
|
+
`--property=LoadCredentialEncrypted=arangodb-jwt:${JWT}`
|
|
70
|
+
];
|
|
71
|
+
let child;
|
|
72
|
+
const credentials = ["arangodb-jwt"];
|
|
73
|
+
if (request.operation === "transactions") {
|
|
74
|
+
if (request.confirmation || request.fenceEvidencePath)
|
|
75
|
+
throw new Error("transaction rehearsal does not accept schema coordinates");
|
|
76
|
+
child = [
|
|
77
|
+
"--property=Environment=FZ_DATABASE_MODE=test",
|
|
78
|
+
"--property=Environment=ARANGO_DB=fz_test",
|
|
79
|
+
FZ,
|
|
80
|
+
"rehearse",
|
|
81
|
+
"transactions",
|
|
82
|
+
"--root",
|
|
83
|
+
workingDirectory,
|
|
84
|
+
"--confirm-staging-fz-test",
|
|
85
|
+
...request.prepareBlankFzTest ? ["--prepare-blank-fz-test"] : []
|
|
86
|
+
];
|
|
87
|
+
} else if (request.operation === "schema-plan") {
|
|
88
|
+
if (request.prepareBlankFzTest || request.confirmation || request.fenceEvidencePath) {
|
|
89
|
+
throw new Error("schema plan accepts no mutation coordinates");
|
|
90
|
+
}
|
|
91
|
+
child = [FZ, "db", "consolidate", "--root", workingDirectory, "--json"];
|
|
92
|
+
} else {
|
|
93
|
+
if (request.prepareBlankFzTest)
|
|
94
|
+
throw new Error("schema apply does not accept staging preparation");
|
|
95
|
+
const evidence = request.fenceEvidencePath;
|
|
96
|
+
if (!evidence?.startsWith("/") || /[\r\n:]/.test(evidence))
|
|
97
|
+
throw new Error("schema apply requires an absolute fence-evidence path");
|
|
98
|
+
trustedFile(runtime, evidence, "schema fence evidence", 63, 2 * 1024 * 1024);
|
|
99
|
+
credentials.push("schema-fence-evidence");
|
|
100
|
+
child = [
|
|
101
|
+
"--property=Environment=FZ_RECOVERY_FLEET_FENCED=all-api-and-scheduler-replicas-are-stopped",
|
|
102
|
+
`--property=LoadCredential=schema-fence-evidence:${evidence}`,
|
|
103
|
+
FZ,
|
|
104
|
+
"db",
|
|
105
|
+
"consolidate",
|
|
106
|
+
"--root",
|
|
107
|
+
workingDirectory,
|
|
108
|
+
"--json",
|
|
109
|
+
"--apply",
|
|
110
|
+
`--confirm=${confirmation(request.confirmation)}`
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
operation: request.operation,
|
|
115
|
+
mutation: request.operation !== "schema-plan",
|
|
116
|
+
activeSlot: slot,
|
|
117
|
+
workingDirectory,
|
|
118
|
+
unit: `forgezero-${request.operation}`,
|
|
119
|
+
credentialNames: credentials,
|
|
120
|
+
argv: [...common, ...child]
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async function applyHostMaintenance(request, runtime = localRuntime()) {
|
|
124
|
+
if (runtime.uid() !== 0)
|
|
125
|
+
throw new Error("fz host --apply must run as root");
|
|
126
|
+
const plan = planHostMaintenance(request, runtime);
|
|
127
|
+
const result = await runtime.exec(plan.argv);
|
|
128
|
+
if (result.exitCode !== 0)
|
|
129
|
+
throw new Error(`host maintenance failed (${result.exitCode}): ${result.output.trim()}`);
|
|
130
|
+
return { plan, ...result };
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
planHostMaintenance,
|
|
134
|
+
applyHostMaintenance
|
|
135
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -18,8 +18,8 @@ export type { DeploymentPullOptions, RemoteDeploymentClaim, PullResult } from '.
|
|
|
18
18
|
export { pullProvisioningOnce, startProvisioningPull } from './provisioning-pull';
|
|
19
19
|
export type { CreateRemoteProvisionClaim, ProvisioningPullOptions, RemoteProvisionClaim, GuestAccess, ProvisionPullResult, ProvisionRunner, ProvisionResult } from './provisioning-pull';
|
|
20
20
|
export { pullMigrationOnce, startMigrationPull } from './migration-pull';
|
|
21
|
-
export { executeSshBootstrap, pullSshBootstrapOnce, startSshBootstrapPull } from './ssh-bootstrap';
|
|
22
|
-
export type { RemoteSshBootstrapClaim, SshBootstrapPullOptions, SshBootstrapResult } from './ssh-bootstrap';
|
|
21
|
+
export { executeMetalAdmission, executeSshBootstrap, pullMetalAdmissionOnce, pullSshBootstrapOnce, startMetalAdmissionPull, startSshBootstrapPull } from './ssh-bootstrap';
|
|
22
|
+
export type { MetalAdmissionProof, MetalAdmissionResult, RemoteMetalAdmissionClaim, RemoteSshBootstrapClaim, SshBootstrapPullOptions, SshBootstrapResult } from './ssh-bootstrap';
|
|
23
23
|
export type { MigrationAction, MigrationEvidence, MigrationNetwork, MigrationPullOptions, MigrationPullResult, RemoteMigrationClaim } from './migration-pull';
|
|
24
24
|
export { enrolGuestIdentity, loadGuestBinding } from './guest-enrolment';
|
|
25
25
|
export type { GuestBinding, GuestEnrolmentOptions } from './guest-enrolment';
|
|
@@ -32,6 +32,8 @@ export { DEFAULT_METAL_HELPER_SOCKET, requestMetalProvision, startMetalHelper }
|
|
|
32
32
|
export { DEFAULT_LIFECYCLE_HELPER_SOCKET, executeLifecycleAction, loadLifecycleProfile, requestLifecycleAction, startLifecycleHelper, validateLifecycleProfile } from './lifecycle-helper';
|
|
33
33
|
export type { LifecycleCommandResult, LifecycleExec, LifecycleProfile } from './lifecycle-helper';
|
|
34
34
|
export { materializeWarpMdm, renderWarpMdm } from './warp-config';
|
|
35
|
+
export { configureMeshConnector, readMeshConnectorCredential } from './mesh-connector';
|
|
36
|
+
export type { MeshConnectorCommandResult, MeshConnectorExec } from './mesh-connector';
|
|
35
37
|
export { compareVersions, restoreAgentRelease, selectAgentRelease, stageAgentRelease, validateAgentRelease, DEFAULT_AGENT_RELEASE_ROOT, DEFAULT_AGENT_UPDATE_SOCKET, MAX_AGENT_TARBALL_BYTES } from './agent-update';
|
|
36
38
|
export type { AgentRelease, StagedAgentRelease, UpdateCommand, UpdateCommandResult } from './agent-update';
|
|
37
39
|
export { activateAgentRelease, probeAgentSocket, readAgentUpdateReceipt, recoverInterruptedAgentUpdate, requestAgentUpdate, startAgentUpdateHelper, AGENT_UPDATE_GROUP, AGENT_UPDATE_HELPER_UNIT_PATH, AGENT_UPDATE_JOURNAL, AGENT_UPDATE_RECEIPT } from './agent-update-helper';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface MeshConnectorCommandResult {
|
|
2
|
+
exitCode: number;
|
|
3
|
+
output: string;
|
|
4
|
+
}
|
|
5
|
+
export type MeshConnectorExec = (argv: readonly string[]) => Promise<MeshConnectorCommandResult>;
|
|
6
|
+
/** Read one systemd credential without accepting an arbitrary filesystem path. */
|
|
7
|
+
export declare function readMeshConnectorCredential(name?: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Register and connect one Cloudflare Mesh node. The management API token is
|
|
10
|
+
* never present; this consumes only the connector-specific registration token
|
|
11
|
+
* returned by the attended bootstrap.
|
|
12
|
+
*/
|
|
13
|
+
export declare function configureMeshConnector(input: {
|
|
14
|
+
token: string;
|
|
15
|
+
exec: MeshConnectorExec;
|
|
16
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/mesh-connector.ts
|
|
2
|
+
import { constants, closeSync, fstatSync, openSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
var TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
|
|
5
|
+
function readMeshConnectorCredential(name = "CF_WARP_CONNECTOR_TOKEN") {
|
|
6
|
+
if (!/^[A-Z_][A-Z0-9_]{0,63}$/.test(name))
|
|
7
|
+
throw new Error("Mesh connector credential name is invalid");
|
|
8
|
+
const directory = process.env.CREDENTIALS_DIRECTORY;
|
|
9
|
+
if (!directory?.startsWith("/run/credentials/"))
|
|
10
|
+
throw new Error("Mesh connector requires a systemd credential directory");
|
|
11
|
+
const path = join(directory, name);
|
|
12
|
+
let descriptor;
|
|
13
|
+
try {
|
|
14
|
+
descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
15
|
+
const metadata = fstatSync(descriptor);
|
|
16
|
+
if (!metadata.isFile() || metadata.nlink !== 1 || metadata.size < 40 || metadata.size > 16384) {
|
|
17
|
+
throw new Error("Mesh connector systemd credential is malformed");
|
|
18
|
+
}
|
|
19
|
+
const token = readFileSync(descriptor, "utf8").trim();
|
|
20
|
+
if (!TOKEN.test(token))
|
|
21
|
+
throw new Error("Mesh connector systemd credential is malformed");
|
|
22
|
+
return token;
|
|
23
|
+
} finally {
|
|
24
|
+
if (descriptor !== undefined)
|
|
25
|
+
closeSync(descriptor);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function configureMeshConnector(input) {
|
|
29
|
+
if (!TOKEN.test(input.token))
|
|
30
|
+
throw new Error("Mesh connector token is malformed");
|
|
31
|
+
const registration = await input.exec(["/usr/bin/warp-cli", "--accept-tos", "connector", "new", input.token]);
|
|
32
|
+
if (registration.exitCode !== 0 && !/already|registered/i.test(registration.output)) {
|
|
33
|
+
throw new Error("Cloudflare Mesh connector registration failed");
|
|
34
|
+
}
|
|
35
|
+
const connected = await input.exec(["/usr/bin/warp-cli", "--accept-tos", "connect"]);
|
|
36
|
+
if (connected.exitCode !== 0)
|
|
37
|
+
throw new Error("Cloudflare Mesh connector connect failed");
|
|
38
|
+
const status = await input.exec(["/usr/bin/warp-cli", "--accept-tos", "status"]);
|
|
39
|
+
if (status.exitCode !== 0 || !/\bconnected\b/i.test(status.output) || /\bdisconnected\b/i.test(status.output)) {
|
|
40
|
+
throw new Error("Cloudflare Mesh connector did not become connected");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
readMeshConnectorCredential,
|
|
45
|
+
configureMeshConnector
|
|
46
|
+
};
|
package/dist/metal-bootstrap.js
CHANGED
|
@@ -233,7 +233,6 @@ async function applyMetalIsolation(profile, exec = defaultExec) {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
// src/egress-policy.ts
|
|
236
|
-
var AGENT_EGRESS_TABLE = "forgezero_agent_egress";
|
|
237
236
|
var SYSTEMD_RESOLVED_ADDRESS = "127.0.0.53";
|
|
238
237
|
var BLOCKED_IPV4 = [
|
|
239
238
|
"0.0.0.0/8",
|
|
@@ -293,7 +292,137 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
|
|
|
293
292
|
}
|
|
294
293
|
|
|
295
294
|
// src/version.ts
|
|
296
|
-
var VERSION = "0.1.
|
|
295
|
+
var VERSION = "0.1.42";
|
|
296
|
+
|
|
297
|
+
// src/otel-collector.ts
|
|
298
|
+
var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
|
|
299
|
+
var FORGEZERO_OTEL_COLLECTOR_VERSION = "0.157.0";
|
|
300
|
+
var FORGEZERO_OTEL_COLLECTOR_SHA256 = "2937cf24892af55b143c072fddece17862239cf78280620029276493eb81beae";
|
|
301
|
+
var ARCHIVE = "/run/forgezero-otelcol.tar.gz";
|
|
302
|
+
var EXTRACT = "/run/forgezero-otelcol-release";
|
|
303
|
+
var BINARY = "/usr/local/lib/forgezero/otelcol/otelcol";
|
|
304
|
+
var CONFIG = "/etc/forgezero/otelcol.yaml";
|
|
305
|
+
var UNIT = `/etc/systemd/system/${FORGEZERO_OTEL_COLLECTOR_UNIT}`;
|
|
306
|
+
var checked2 = async (host, argv) => {
|
|
307
|
+
const result = await host.exec(argv);
|
|
308
|
+
const output = `${result.output ?? result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
309
|
+
if (result.exitCode !== 0)
|
|
310
|
+
throw new Error(`OTLP collector operation failed: ${argv.join(" ")}${output.trim() ? `: ${output.trim()}` : ""}`);
|
|
311
|
+
return output.trim();
|
|
312
|
+
};
|
|
313
|
+
var destination = (value) => {
|
|
314
|
+
let url;
|
|
315
|
+
try {
|
|
316
|
+
url = new URL(value);
|
|
317
|
+
} catch {
|
|
318
|
+
throw new Error("OTLP collector export endpoint must be an absolute HTTPS URL");
|
|
319
|
+
}
|
|
320
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.port && url.port !== "443") {
|
|
321
|
+
throw new Error("OTLP collector export endpoint must be credential-free HTTPS");
|
|
322
|
+
}
|
|
323
|
+
return url.toString().replace(/\/$/, "");
|
|
324
|
+
};
|
|
325
|
+
function renderForgeZeroOtelCollector(exportEndpoint) {
|
|
326
|
+
const endpoint = destination(exportEndpoint);
|
|
327
|
+
return {
|
|
328
|
+
config: `receivers:
|
|
329
|
+
otlp:
|
|
330
|
+
protocols:
|
|
331
|
+
http:
|
|
332
|
+
endpoint: 127.0.0.1:4318
|
|
333
|
+
exporters:
|
|
334
|
+
otlphttp:
|
|
335
|
+
endpoint: ${JSON.stringify(endpoint)}
|
|
336
|
+
service:
|
|
337
|
+
telemetry:
|
|
338
|
+
metrics:
|
|
339
|
+
address: 127.0.0.1:8888
|
|
340
|
+
pipelines:
|
|
341
|
+
traces:
|
|
342
|
+
receivers: [otlp]
|
|
343
|
+
exporters: [otlphttp]
|
|
344
|
+
metrics:
|
|
345
|
+
receivers: [otlp]
|
|
346
|
+
exporters: [otlphttp]
|
|
347
|
+
logs:
|
|
348
|
+
receivers: [otlp]
|
|
349
|
+
exporters: [otlphttp]
|
|
350
|
+
`,
|
|
351
|
+
unit: `[Unit]
|
|
352
|
+
Description=ForgeZero independently supervised OpenTelemetry Collector
|
|
353
|
+
After=network-online.target
|
|
354
|
+
Wants=network-online.target
|
|
355
|
+
|
|
356
|
+
[Service]
|
|
357
|
+
Type=simple
|
|
358
|
+
User=forgezero-otel
|
|
359
|
+
Group=forgezero-otel
|
|
360
|
+
ExecStart=${BINARY} --config=${CONFIG}
|
|
361
|
+
Restart=always
|
|
362
|
+
RestartSec=2
|
|
363
|
+
LimitCORE=0
|
|
364
|
+
NoNewPrivileges=true
|
|
365
|
+
PrivateTmp=true
|
|
366
|
+
ProtectSystem=strict
|
|
367
|
+
ProtectHome=true
|
|
368
|
+
ProtectKernelTunables=true
|
|
369
|
+
ProtectKernelModules=true
|
|
370
|
+
ProtectControlGroups=true
|
|
371
|
+
RestrictSUIDSGID=true
|
|
372
|
+
RestrictRealtime=true
|
|
373
|
+
LockPersonality=true
|
|
374
|
+
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
|
375
|
+
|
|
376
|
+
[Install]
|
|
377
|
+
WantedBy=multi-user.target
|
|
378
|
+
`
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
|
|
382
|
+
const rendered = renderForgeZeroOtelCollector(exportEndpoint);
|
|
383
|
+
const observed = await host.exec([BINARY, "--version"]);
|
|
384
|
+
const versionOutput = `${observed.output ?? observed.stdout ?? ""}${observed.stderr ?? ""}`;
|
|
385
|
+
if (observed.exitCode !== 0 || !versionOutput.includes(`otelcol version ${FORGEZERO_OTEL_COLLECTOR_VERSION}`)) {
|
|
386
|
+
await checked2(host, [
|
|
387
|
+
"/usr/bin/curl",
|
|
388
|
+
"--fail",
|
|
389
|
+
"--silent",
|
|
390
|
+
"--show-error",
|
|
391
|
+
"--location",
|
|
392
|
+
"--proto",
|
|
393
|
+
"=https",
|
|
394
|
+
"--output",
|
|
395
|
+
ARCHIVE,
|
|
396
|
+
`https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${FORGEZERO_OTEL_COLLECTOR_VERSION}/otelcol_${FORGEZERO_OTEL_COLLECTOR_VERSION}_linux_amd64.tar.gz`
|
|
397
|
+
]);
|
|
398
|
+
const digest = (await checked2(host, ["/usr/bin/sha256sum", ARCHIVE])).split(/\s+/)[0];
|
|
399
|
+
if (digest !== FORGEZERO_OTEL_COLLECTOR_SHA256)
|
|
400
|
+
throw new Error("OTLP collector archive checksum mismatch");
|
|
401
|
+
await checked2(host, ["/usr/bin/install", "-d", "-m", "0755", "/usr/local/lib/forgezero/otelcol", EXTRACT]);
|
|
402
|
+
await checked2(host, ["/usr/bin/tar", "-xzf", ARCHIVE, "-C", EXTRACT, "otelcol"]);
|
|
403
|
+
await checked2(host, ["/usr/bin/install", "-m", "0755", `${EXTRACT}/otelcol`, BINARY]);
|
|
404
|
+
await checked2(host, ["/usr/bin/rm", "-f", ARCHIVE, `${EXTRACT}/otelcol`]);
|
|
405
|
+
}
|
|
406
|
+
if ((await host.exec(["/usr/bin/getent", "group", "forgezero-otel"])).exitCode !== 0) {
|
|
407
|
+
await checked2(host, ["/usr/sbin/groupadd", "--system", "forgezero-otel"]);
|
|
408
|
+
}
|
|
409
|
+
if ((await host.exec(["/usr/bin/id", "forgezero-otel"])).exitCode !== 0) {
|
|
410
|
+
await checked2(host, [
|
|
411
|
+
"/usr/sbin/useradd",
|
|
412
|
+
"--system",
|
|
413
|
+
"--no-create-home",
|
|
414
|
+
"--shell",
|
|
415
|
+
"/usr/sbin/nologin",
|
|
416
|
+
"--gid",
|
|
417
|
+
"forgezero-otel",
|
|
418
|
+
"forgezero-otel"
|
|
419
|
+
]);
|
|
420
|
+
}
|
|
421
|
+
host.write(CONFIG, rendered.config, 420);
|
|
422
|
+
host.write(UNIT, rendered.unit, 420);
|
|
423
|
+
await checked2(host, ["/usr/bin/systemctl", "daemon-reload"]);
|
|
424
|
+
await checked2(host, ["/usr/bin/systemctl", "enable", "--now", FORGEZERO_OTEL_COLLECTOR_UNIT]);
|
|
425
|
+
}
|
|
297
426
|
|
|
298
427
|
// src/metal-bootstrap.ts
|
|
299
428
|
var PROFILE_PATH = "/etc/forgezero/metal.json";
|
|
@@ -306,7 +435,7 @@ var HELPER_SOCKET = "/run/forgezero-metal/helper.sock";
|
|
|
306
435
|
var UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
|
|
307
436
|
var MAX_CONFIG_BYTES = 256 * 1024;
|
|
308
437
|
var SUPPORTED_BUN_VERSION = "1.3.14";
|
|
309
|
-
var
|
|
438
|
+
var SUPPORTED_BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
|
|
310
439
|
|
|
311
440
|
class MetalBootstrapError extends Error {
|
|
312
441
|
}
|
|
@@ -373,7 +502,7 @@ function validateMetalBootstrapConfig(config) {
|
|
|
373
502
|
"housekeepingCpus",
|
|
374
503
|
"housekeepingMemoryNodes",
|
|
375
504
|
"bunVersion",
|
|
376
|
-
"
|
|
505
|
+
"bunReleaseSha256",
|
|
377
506
|
"agentVersion",
|
|
378
507
|
"confidential"
|
|
379
508
|
], "metal profile");
|
|
@@ -389,7 +518,7 @@ function validateMetalBootstrapConfig(config) {
|
|
|
389
518
|
if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve(image.path) !== image.path || /[\0\r\n]/.test(image.path)) {
|
|
390
519
|
throw new MetalBootstrapError("the pinned guest image must use the fixed image directory");
|
|
391
520
|
}
|
|
392
|
-
if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.
|
|
521
|
+
if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION) {
|
|
393
522
|
throw new MetalBootstrapError("metal guest runtime must use the package-owned Bun and Agent release coordinates");
|
|
394
523
|
}
|
|
395
524
|
let api;
|
|
@@ -413,6 +542,9 @@ function validateMetalBootstrapConfig(config) {
|
|
|
413
542
|
throw new MetalBootstrapError("metal host OTLP must use exact loopback http://127.0.0.1:4318");
|
|
414
543
|
}
|
|
415
544
|
validUnit(config.hostTelemetryUnit);
|
|
545
|
+
if (config.hostTelemetryUnit !== FORGEZERO_OTEL_COLLECTOR_UNIT) {
|
|
546
|
+
throw new MetalBootstrapError(`metal bootstrap requires ${FORGEZERO_OTEL_COLLECTOR_UNIT}`);
|
|
547
|
+
}
|
|
416
548
|
if (config.agentSeedFile)
|
|
417
549
|
validateOwnerOnlyPath(config.agentSeedFile, false);
|
|
418
550
|
return config;
|
|
@@ -452,13 +584,14 @@ function planMetalBootstrap(config) {
|
|
|
452
584
|
metalHostname: config.metalHostname,
|
|
453
585
|
profilePath: PROFILE_PATH,
|
|
454
586
|
units: [
|
|
587
|
+
FORGEZERO_OTEL_COLLECTOR_UNIT,
|
|
455
588
|
"forgezero-metal-helper.service",
|
|
456
589
|
"forgezero-agent-update-helper.service",
|
|
457
590
|
"forgezero-metal-agent-egress.service",
|
|
458
591
|
"forgezero-metal-agent.service"
|
|
459
592
|
],
|
|
460
593
|
steps: [
|
|
461
|
-
"install fixed KVM/LVM/cloud-init/nftables prerequisites",
|
|
594
|
+
"install fixed KVM/LVM/cloud-init/nftables prerequisites and the checksum-verified OTLP collector",
|
|
462
595
|
"verify bridge, volume group, pinned guest image, KVM and optional SEV device",
|
|
463
596
|
"install the published Agent binary and root-owned immutable metal profile",
|
|
464
597
|
"seal or generate the metal identity seed as a systemd credential",
|
|
@@ -755,8 +888,13 @@ async function applyMetalBootstrap(config, options) {
|
|
|
755
888
|
"cloud-image-utils",
|
|
756
889
|
"lvm2",
|
|
757
890
|
"nftables",
|
|
758
|
-
"curl"
|
|
891
|
+
"curl",
|
|
892
|
+
"ca-certificates"
|
|
759
893
|
]);
|
|
894
|
+
await ensureForgeZeroOtelCollector({
|
|
895
|
+
write: atomicWrite,
|
|
896
|
+
exec
|
|
897
|
+
}, config.profile.agentTelemetryEndpoint);
|
|
760
898
|
await ensurePinnedGuestImage(config, exec);
|
|
761
899
|
await preflight(config, exec);
|
|
762
900
|
await ensureAccount(exec);
|
|
@@ -339,45 +339,70 @@ var yamlFile = (path, content, permissions) => ` - path: ${JSON.stringify(path)
|
|
|
339
339
|
encoding: b64
|
|
340
340
|
content: ${base64(content)}
|
|
341
341
|
`;
|
|
342
|
-
function
|
|
342
|
+
function guestBootstrapOperations(profile, attested = Boolean(profile.confidential), hasEnrolment = true, nodeLabel = "compute") {
|
|
343
343
|
validateMetalProfile(profile);
|
|
344
344
|
const agentBun = "/usr/local/lib/forgezero/bun";
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
345
|
+
return [
|
|
346
|
+
...attested ? [
|
|
347
|
+
["/usr/sbin/modprobe", "sev-guest"],
|
|
348
|
+
["/usr/bin/test", "-c", "/dev/sev-guest"]
|
|
349
|
+
] : [],
|
|
350
|
+
[
|
|
351
|
+
"/usr/bin/curl",
|
|
352
|
+
"--fail",
|
|
353
|
+
"--silent",
|
|
354
|
+
"--show-error",
|
|
355
|
+
"--location",
|
|
356
|
+
"https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64.zip",
|
|
357
|
+
"--output",
|
|
358
|
+
"/run/forgezero-bun.zip"
|
|
359
|
+
],
|
|
360
|
+
["/usr/bin/sha256sum", "--check", "/etc/forgezero/bun.sha256"],
|
|
361
|
+
["/usr/bin/unzip", "-q", "/run/forgezero-bun.zip", "-d", "/run/forgezero-bun"],
|
|
362
|
+
["/usr/bin/install", "-d", "-m", "0755", "/usr/local/lib/forgezero/runtime", agentBun],
|
|
363
|
+
["/usr/bin/install", "-m", "0755", "/run/forgezero-bun/bun-linux-x64/bun", "/usr/local/lib/forgezero/runtime/bun"],
|
|
364
|
+
["/usr/bin/ln", "-sfn", "/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun"],
|
|
365
|
+
[
|
|
366
|
+
"/usr/bin/env",
|
|
367
|
+
`BUN_INSTALL=${agentBun}`,
|
|
368
|
+
"/usr/local/bin/bun",
|
|
369
|
+
"add",
|
|
370
|
+
"-g",
|
|
371
|
+
"--no-cache",
|
|
372
|
+
"--force",
|
|
373
|
+
`@forgezero/agent@${profile.agentVersion}`
|
|
374
|
+
],
|
|
375
|
+
[
|
|
376
|
+
"/usr/bin/env",
|
|
377
|
+
`FZ_API=${profile.apiUrl}`,
|
|
378
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT=${profile.agentTelemetryEndpoint}`,
|
|
379
|
+
"FZ_AGENT_BIN=/usr/local/lib/forgezero/agent/fz-agent",
|
|
380
|
+
"FZ_AGENT_USER=forgezero-agent",
|
|
381
|
+
"FZ_SOCKET_PATH=/run/forgezero/vault.sock",
|
|
382
|
+
"FZ_SEED_CREDENTIAL_PATH=/etc/forgezero/creds/agent-seed.cred",
|
|
383
|
+
"FZ_GIT_CREDENTIAL_PATH=/etc/forgezero/creds/git-deploy-key.cred",
|
|
384
|
+
"FZ_GIT_PUBLIC_KEY_PATH=/etc/forgezero/git/deploy.pub",
|
|
385
|
+
"FZ_DEPLOY_ROOT=/opt/forgezero",
|
|
386
|
+
`FZ_DEPLOY_PULL=${hasEnrolment}`,
|
|
387
|
+
`FZ_AGENT_EGRESS_ENFORCE=${hasEnrolment}`,
|
|
388
|
+
`FZ_NODE_LABEL=${nodeLabel}`,
|
|
389
|
+
`${agentBun}/bin/fz`,
|
|
390
|
+
"agent",
|
|
391
|
+
"install",
|
|
392
|
+
"--apply",
|
|
393
|
+
...hasEnrolment ? ["--enrol"] : []
|
|
394
|
+
]
|
|
395
|
+
];
|
|
371
396
|
}
|
|
372
397
|
function cloudInit(profile, claim, manifest) {
|
|
373
398
|
const enrolled = Boolean(claim.enrolment);
|
|
374
|
-
const bootstrap =
|
|
399
|
+
const bootstrap = guestBootstrapOperations(profile, claim.spec.confidential, enrolled, manifest.name);
|
|
375
400
|
const access = claim.access;
|
|
376
401
|
const users = access ? `users:
|
|
377
402
|
- default
|
|
378
403
|
- name: ${access.sshUser}
|
|
379
404
|
groups: [sudo]
|
|
380
|
-
shell: /bin/
|
|
405
|
+
shell: /bin/dash
|
|
381
406
|
sudo: ALL=(ALL) NOPASSWD:ALL
|
|
382
407
|
ssh_authorized_keys:
|
|
383
408
|
${access.sshPublicKeys.map((key) => ` - ${JSON.stringify(key)}`).join(`
|
|
@@ -385,6 +410,10 @@ ${access.sshPublicKeys.map((key) => ` - ${JSON.stringify(key)}`).join(`
|
|
|
385
410
|
` : "";
|
|
386
411
|
const enrolmentFiles = enrolled ? yamlFile("/run/forgezero-enrol-token", `${claim.enrolment.token}
|
|
387
412
|
`, "0600") : "";
|
|
413
|
+
const attestationFile = claim.spec.confidential ? yamlFile("/etc/modules-load.d/sev-guest.conf", `sev-guest
|
|
414
|
+
`, "0644") : "";
|
|
415
|
+
const bunChecksum = yamlFile("/etc/forgezero/bun.sha256", `${profile.bunReleaseSha256} /run/forgezero-bun.zip
|
|
416
|
+
`, "0600");
|
|
388
417
|
return {
|
|
389
418
|
userData: `#cloud-config
|
|
390
419
|
package_update: true
|
|
@@ -393,8 +422,9 @@ ${users}disable_root: true
|
|
|
393
422
|
# every dynamically claimed repository must be cloneable on a clean image.
|
|
394
423
|
packages: [curl, ca-certificates, openssl, openssh-client, git, unzip${claim.spec.confidential ? ", python3" : ""}]
|
|
395
424
|
write_files:
|
|
396
|
-
${enrolmentFiles}${
|
|
397
|
-
|
|
425
|
+
${enrolmentFiles}${attestationFile}${bunChecksum}runcmd:
|
|
426
|
+
${bootstrap.map((argv) => ` - ${JSON.stringify(argv)}`).join(`
|
|
427
|
+
`)}
|
|
398
428
|
`,
|
|
399
429
|
metaData: `instance-id: ${manifest.name}-${claim.attempt}
|
|
400
430
|
local-hostname: ${manifest.name}
|
|
@@ -37,7 +37,7 @@ export interface MetalProvisionProfile {
|
|
|
37
37
|
/** NUMA nodes reserved for host allocations when the topology permits it. */
|
|
38
38
|
housekeepingMemoryNodes?: string;
|
|
39
39
|
bunVersion: string;
|
|
40
|
-
|
|
40
|
+
bunReleaseSha256: string;
|
|
41
41
|
agentVersion: string;
|
|
42
42
|
confidential?: {
|
|
43
43
|
cbitpos: number;
|
|
@@ -72,7 +72,7 @@ export declare function validateMetalProfile(profile: MetalProvisionProfile): vo
|
|
|
72
72
|
export declare function allocateAddress(profile: MetalProvisionProfile, computeKey: string, rows: GuestManifest[]): string;
|
|
73
73
|
/** Stable tight-fit allocation of one exclusive CPU/NUMA pool. */
|
|
74
74
|
export declare function allocateCpuPool(profile: MetalProvisionProfile, claim: Pick<RemoteProvisionClaim, 'computeKey' | 'spec'>, rows: GuestManifest[]): MetalCpuPool;
|
|
75
|
-
export declare function
|
|
75
|
+
export declare function guestBootstrapOperations(profile: MetalProvisionProfile, attested?: boolean, hasEnrolment?: boolean, nodeLabel?: string): readonly (readonly string[])[];
|
|
76
76
|
export declare function cloudInit(profile: MetalProvisionProfile, claim: CreateRemoteProvisionClaim, manifest: GuestManifest): {
|
|
77
77
|
userData: string;
|
|
78
78
|
metaData: string;
|