@forgezero/agent 0.1.26 → 0.1.28
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 +5 -3
- package/dist/agent-heartbeat.d.ts +45 -0
- package/dist/agent-heartbeat.js +511 -0
- package/dist/agent-update-helper.d.ts +45 -0
- package/dist/agent-update-helper.js +366 -0
- package/dist/agent-update.d.ts +46 -0
- package/dist/agent-update.js +184 -0
- package/dist/definition.d.ts +1 -7
- package/dist/definition.js +92 -23
- package/dist/deployment-pull.d.ts +2 -0
- package/dist/deployment.d.ts +3 -0
- package/dist/fz-agent.js +7865 -2617
- package/dist/fz.js +8287 -38
- package/dist/guest-enrolment.js +14 -1
- package/dist/index.d.ts +13 -0
- package/dist/metal-helper-socket.js +23 -4
- package/dist/metal-provision.js +23 -4
- package/dist/migration-pull.js +14 -1
- package/dist/node-vault.js +15 -2
- package/dist/provision.d.ts +13 -0
- package/dist/provision.js +685 -6
- package/dist/provisioning-pull.js +14 -1
- package/dist/signed-node-http.d.ts +10 -0
- package/dist/socket.d.ts +2 -0
- package/dist/software-helper.d.ts +14 -0
- package/dist/software-helper.js +203 -0
- package/dist/software.d.ts +27 -0
- package/dist/software.js +95 -0
- package/dist/version.d.ts +1 -1
- package/package.json +24 -4
package/dist/provision.js
CHANGED
|
@@ -1,3 +1,561 @@
|
|
|
1
|
+
// src/agent-update.ts
|
|
2
|
+
import { createHash, timingSafeEqual, randomUUID } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readlinkSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
symlinkSync,
|
|
12
|
+
writeFileSync
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
|
|
16
|
+
var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
|
|
17
|
+
var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
|
|
18
|
+
var VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
19
|
+
var REGISTRY = "registry.npmjs.org";
|
|
20
|
+
function validateAgentRelease(release) {
|
|
21
|
+
if (release?.package !== "@forgezero/agent")
|
|
22
|
+
throw new Error("agent update package is fixed");
|
|
23
|
+
if (!VERSION.test(release.version))
|
|
24
|
+
throw new Error("agent update version must be exact semver");
|
|
25
|
+
const expectedTarball = `/@forgezero/agent/-/agent-${release.version}.tgz`;
|
|
26
|
+
let url;
|
|
27
|
+
try {
|
|
28
|
+
url = new URL(release.tarball);
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error("agent update tarball URL is malformed");
|
|
31
|
+
}
|
|
32
|
+
if (url.protocol !== "https:" || url.hostname !== REGISTRY || url.port || url.username || url.password || url.search || url.hash || url.pathname !== expectedTarball)
|
|
33
|
+
throw new Error("agent update tarball must be the exact official npm artifact");
|
|
34
|
+
const match = /^sha512-([A-Za-z0-9+/]+={0,2})$/.exec(release.integrity);
|
|
35
|
+
if (!match || Buffer.from(match[1], "base64").length !== 64) {
|
|
36
|
+
throw new Error("agent update requires one sha512 npm integrity");
|
|
37
|
+
}
|
|
38
|
+
return release;
|
|
39
|
+
}
|
|
40
|
+
function compareVersions(left, right) {
|
|
41
|
+
if (!VERSION.test(left) || !VERSION.test(right))
|
|
42
|
+
throw new Error("agent version must be exact semver");
|
|
43
|
+
const a = left.split(".").map(Number);
|
|
44
|
+
const b = right.split(".").map(Number);
|
|
45
|
+
for (let index = 0;index < 3; index += 1) {
|
|
46
|
+
if (a[index] > b[index])
|
|
47
|
+
return 1;
|
|
48
|
+
if (a[index] < b[index])
|
|
49
|
+
return -1;
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
var command = async (input) => {
|
|
54
|
+
const child = Bun.spawn([input.command, ...input.args], {
|
|
55
|
+
cwd: input.cwd,
|
|
56
|
+
stdout: "pipe",
|
|
57
|
+
stderr: "pipe",
|
|
58
|
+
env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }
|
|
59
|
+
});
|
|
60
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
61
|
+
new Response(child.stdout).text(),
|
|
62
|
+
new Response(child.stderr).text(),
|
|
63
|
+
child.exited
|
|
64
|
+
]);
|
|
65
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
66
|
+
};
|
|
67
|
+
var checked = async (run, input, label) => {
|
|
68
|
+
const result = await run(input);
|
|
69
|
+
if (result.exitCode !== 0)
|
|
70
|
+
throw new Error(`${label} failed: ${result.output.trim()}`);
|
|
71
|
+
return result;
|
|
72
|
+
};
|
|
73
|
+
async function validateReleaseDirectory(directory, release, run) {
|
|
74
|
+
const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
75
|
+
if (manifest.name !== release.package || manifest.version !== release.version) {
|
|
76
|
+
throw new Error("agent update manifest does not match the selected release");
|
|
77
|
+
}
|
|
78
|
+
const agent = join(directory, "dist", "fz-agent.js");
|
|
79
|
+
const cli = join(directory, "dist", "fz.js");
|
|
80
|
+
for (const binary of [agent, cli]) {
|
|
81
|
+
if (!readFileSync(binary, "utf8").startsWith(`#!/usr/bin/env bun
|
|
82
|
+
`)) {
|
|
83
|
+
throw new Error("agent update artifact is not a self-contained Bun executable");
|
|
84
|
+
}
|
|
85
|
+
chmodSync(binary, 493);
|
|
86
|
+
}
|
|
87
|
+
const version = (await checked(run, { command: agent, args: ["--version"] }, "agent update smoke test")).output.trim();
|
|
88
|
+
if (version !== release.version)
|
|
89
|
+
throw new Error(`agent update binary reports ${version}`);
|
|
90
|
+
}
|
|
91
|
+
async function stageAgentRelease(releaseInput, options) {
|
|
92
|
+
const release = validateAgentRelease(releaseInput);
|
|
93
|
+
if (compareVersions(release.version, options.currentVersion) <= 0) {
|
|
94
|
+
throw new Error(`agent update ${release.version} is not newer than ${options.currentVersion}`);
|
|
95
|
+
}
|
|
96
|
+
const root = resolve(options.root ?? DEFAULT_AGENT_RELEASE_ROOT);
|
|
97
|
+
const versions = join(root, "versions");
|
|
98
|
+
const finalDirectory = join(versions, release.version);
|
|
99
|
+
const currentLink = join(root, "current");
|
|
100
|
+
const stage = join(versions, `.${release.version}.${randomUUID()}.staging`);
|
|
101
|
+
const archive = join(stage, "agent.tgz");
|
|
102
|
+
const unpacked = join(stage, "unpacked");
|
|
103
|
+
const run = options.run ?? command;
|
|
104
|
+
mkdirSync(unpacked, { recursive: true, mode: 448 });
|
|
105
|
+
try {
|
|
106
|
+
const response = await (options.fetch ?? globalThis.fetch)(release.tarball, {
|
|
107
|
+
redirect: "error",
|
|
108
|
+
signal: AbortSignal.timeout(30000)
|
|
109
|
+
});
|
|
110
|
+
if (!response.ok)
|
|
111
|
+
throw new Error(`npm returned HTTP ${response.status}`);
|
|
112
|
+
const declared = Number(response.headers.get("content-length") ?? "0");
|
|
113
|
+
if (declared > MAX_AGENT_TARBALL_BYTES)
|
|
114
|
+
throw new Error("agent update tarball exceeds the size limit");
|
|
115
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
116
|
+
if (bytes.length === 0 || bytes.length > MAX_AGENT_TARBALL_BYTES) {
|
|
117
|
+
throw new Error("agent update tarball is empty or exceeds the size limit");
|
|
118
|
+
}
|
|
119
|
+
const expected = Buffer.from(release.integrity.slice("sha512-".length), "base64");
|
|
120
|
+
const actual = createHash("sha512").update(bytes).digest();
|
|
121
|
+
if (!timingSafeEqual(actual, expected))
|
|
122
|
+
throw new Error("agent update integrity mismatch");
|
|
123
|
+
writeFileSync(archive, bytes, { mode: 384, flag: "wx" });
|
|
124
|
+
await checked(run, {
|
|
125
|
+
command: "/usr/bin/tar",
|
|
126
|
+
args: [
|
|
127
|
+
"-xzf",
|
|
128
|
+
archive,
|
|
129
|
+
"-C",
|
|
130
|
+
unpacked,
|
|
131
|
+
"--strip-components=1",
|
|
132
|
+
"package/package.json",
|
|
133
|
+
"package/dist/fz-agent.js",
|
|
134
|
+
"package/dist/fz.js"
|
|
135
|
+
]
|
|
136
|
+
}, "agent update extraction");
|
|
137
|
+
await validateReleaseDirectory(unpacked, release, run);
|
|
138
|
+
if (!existsSync(finalDirectory))
|
|
139
|
+
renameSync(unpacked, finalDirectory);
|
|
140
|
+
else
|
|
141
|
+
await validateReleaseDirectory(finalDirectory, release, run);
|
|
142
|
+
if (!existsSync(currentLink)) {
|
|
143
|
+
throw new Error("agent update requires an active immutable release to roll back to");
|
|
144
|
+
}
|
|
145
|
+
const previousTarget = readlinkSync(currentLink);
|
|
146
|
+
return {
|
|
147
|
+
version: release.version,
|
|
148
|
+
directory: finalDirectory,
|
|
149
|
+
previousTarget,
|
|
150
|
+
nextTarget: join("versions", release.version),
|
|
151
|
+
currentLink
|
|
152
|
+
};
|
|
153
|
+
} finally {
|
|
154
|
+
rmSync(stage, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function selectAgentRelease(staged) {
|
|
158
|
+
const next = join(dirname(staged.currentLink), `.current.${randomUUID()}.next`);
|
|
159
|
+
try {
|
|
160
|
+
symlinkSync(staged.nextTarget, next);
|
|
161
|
+
renameSync(next, staged.currentLink);
|
|
162
|
+
} finally {
|
|
163
|
+
rmSync(next, { force: true });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function restoreAgentRelease(staged) {
|
|
167
|
+
const next = join(dirname(staged.currentLink), `.current.${randomUUID()}.rollback`);
|
|
168
|
+
try {
|
|
169
|
+
symlinkSync(staged.previousTarget, next);
|
|
170
|
+
renameSync(next, staged.currentLink);
|
|
171
|
+
} finally {
|
|
172
|
+
rmSync(next, { force: true });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/agent-update-helper.ts
|
|
177
|
+
import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
178
|
+
import { connect, createServer } from "node:net";
|
|
179
|
+
import { dirname as dirname2 } from "node:path";
|
|
180
|
+
import { DEFAULT_SOCKET } from "@forgezero/vault";
|
|
181
|
+
var AGENT_UPDATE_GROUP = "forgezero-update";
|
|
182
|
+
var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-helper.service";
|
|
183
|
+
var AGENT_UPDATE_RECEIPT = "/var/lib/forgezero/agent-update.json";
|
|
184
|
+
var MAX_REQUEST_BYTES = 8 * 1024;
|
|
185
|
+
var COMPUTE_HELPER_UNITS = [
|
|
186
|
+
"forgezero-deploy-runner.service",
|
|
187
|
+
"forgezero-lifecycle-helper.service",
|
|
188
|
+
"forgezero-software-helper.service"
|
|
189
|
+
];
|
|
190
|
+
var runCommand = async (input) => {
|
|
191
|
+
const child = Bun.spawn([input.command, ...input.args], {
|
|
192
|
+
cwd: input.cwd,
|
|
193
|
+
stdout: "pipe",
|
|
194
|
+
stderr: "pipe",
|
|
195
|
+
env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }
|
|
196
|
+
});
|
|
197
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
198
|
+
new Response(child.stdout).text(),
|
|
199
|
+
new Response(child.stderr).text(),
|
|
200
|
+
child.exited
|
|
201
|
+
]);
|
|
202
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
203
|
+
};
|
|
204
|
+
var runOk = async (run, command2, args) => (await run({ command: command2, args })).exitCode === 0;
|
|
205
|
+
function probeAgentSocket(socketPath = DEFAULT_SOCKET, timeoutMs = 5000) {
|
|
206
|
+
return new Promise((resolve2) => {
|
|
207
|
+
const socket = connect(socketPath);
|
|
208
|
+
let settled = false;
|
|
209
|
+
let buffer = "";
|
|
210
|
+
const finish = (value) => {
|
|
211
|
+
if (settled)
|
|
212
|
+
return;
|
|
213
|
+
settled = true;
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
socket.destroy();
|
|
216
|
+
resolve2(value);
|
|
217
|
+
};
|
|
218
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
219
|
+
socket.on("connect", () => socket.write(`{"op":"identity"}
|
|
220
|
+
`));
|
|
221
|
+
socket.on("data", (chunk) => {
|
|
222
|
+
buffer += chunk.toString("utf8");
|
|
223
|
+
const newline = buffer.indexOf(`
|
|
224
|
+
`);
|
|
225
|
+
if (newline < 0)
|
|
226
|
+
return;
|
|
227
|
+
try {
|
|
228
|
+
const response = JSON.parse(buffer.slice(0, newline));
|
|
229
|
+
finish(response.ok === false && response.error?.code === "APP_OPERATION_REFUSED");
|
|
230
|
+
} catch {
|
|
231
|
+
finish(false);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
socket.on("error", () => finish(false));
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async function activateAgentRelease(staged, options = {}) {
|
|
238
|
+
const run = options.run ?? runCommand;
|
|
239
|
+
const target = options.target ?? "compute";
|
|
240
|
+
const probe = options.probe ?? (target === "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"]));
|
|
241
|
+
const restart = async () => {
|
|
242
|
+
const helpers = target === "compute" ? COMPUTE_HELPER_UNITS : ["forgezero-metal-helper.service"];
|
|
243
|
+
for (const unit of helpers) {
|
|
244
|
+
await run({ command: "/usr/bin/systemctl", args: ["try-restart", unit] });
|
|
245
|
+
}
|
|
246
|
+
const service = target === "compute" ? "forgezero-agent.service" : "forgezero-metal-agent.service";
|
|
247
|
+
const restarted = await runOk(run, "/usr/bin/systemctl", ["restart", service]);
|
|
248
|
+
if (!restarted)
|
|
249
|
+
throw new Error(`systemd could not restart ${service}`);
|
|
250
|
+
};
|
|
251
|
+
let selected = false;
|
|
252
|
+
try {
|
|
253
|
+
selectAgentRelease(staged);
|
|
254
|
+
selected = true;
|
|
255
|
+
await restart();
|
|
256
|
+
if (!await probe())
|
|
257
|
+
throw new Error("the replacement Agent did not answer its retained Vault socket");
|
|
258
|
+
const receipt = options.receiptPath ?? AGENT_UPDATE_RECEIPT;
|
|
259
|
+
mkdirSync2(dirname2(receipt), { recursive: true, mode: 493 });
|
|
260
|
+
const next = `${receipt}.next`;
|
|
261
|
+
writeFileSync2(next, JSON.stringify({
|
|
262
|
+
version: staged.version,
|
|
263
|
+
outcome: "active",
|
|
264
|
+
updatedAtTs: (options.now ?? Date.now)()
|
|
265
|
+
}) + `
|
|
266
|
+
`, { mode: 420 });
|
|
267
|
+
renameSync2(next, receipt);
|
|
268
|
+
run({
|
|
269
|
+
command: "/usr/bin/systemctl",
|
|
270
|
+
args: ["try-restart", "--no-block", "forgezero-agent-update-helper.service"]
|
|
271
|
+
});
|
|
272
|
+
return { ok: true, version: staged.version };
|
|
273
|
+
} catch (cause) {
|
|
274
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
275
|
+
if (selected) {
|
|
276
|
+
restoreAgentRelease(staged);
|
|
277
|
+
await restart();
|
|
278
|
+
}
|
|
279
|
+
return { ok: false, rolledBack: selected, reason };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function startAgentUpdateHelper(options = {}) {
|
|
283
|
+
const socketPath = options.socketPath ?? DEFAULT_AGENT_UPDATE_SOCKET;
|
|
284
|
+
if (existsSync2(socketPath))
|
|
285
|
+
unlinkSync(socketPath);
|
|
286
|
+
mkdirSync2(dirname2(socketPath), { recursive: true, mode: 488 });
|
|
287
|
+
const setTimer = options.setTimer ?? ((callback, ms) => setTimeout(callback, ms));
|
|
288
|
+
const activate = options.activate ?? ((staged, target) => activateAgentRelease(staged, { target }));
|
|
289
|
+
const server = createServer((socket) => {
|
|
290
|
+
let buffer = "";
|
|
291
|
+
socket.on("data", (chunk) => {
|
|
292
|
+
buffer += chunk.toString("utf8");
|
|
293
|
+
if (Buffer.byteLength(buffer) > MAX_REQUEST_BYTES) {
|
|
294
|
+
socket.end(`${JSON.stringify({ ok: false, error: { code: "TOO_LARGE", message: "update request too large" } })}
|
|
295
|
+
`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const newline = buffer.indexOf(`
|
|
299
|
+
`);
|
|
300
|
+
if (newline < 0)
|
|
301
|
+
return;
|
|
302
|
+
const line = buffer.slice(0, newline);
|
|
303
|
+
buffer = "";
|
|
304
|
+
Promise.resolve().then(() => JSON.parse(line)).then(async (request) => {
|
|
305
|
+
if (request.op !== "apply")
|
|
306
|
+
throw new Error("unknown update operation");
|
|
307
|
+
if (request.target !== "compute" && request.target !== "metal") {
|
|
308
|
+
throw new Error("agent update target is invalid");
|
|
309
|
+
}
|
|
310
|
+
const staged = await stageAgentRelease(request.release, {
|
|
311
|
+
currentVersion: request.currentVersion,
|
|
312
|
+
root: options.root ?? DEFAULT_AGENT_RELEASE_ROOT
|
|
313
|
+
});
|
|
314
|
+
const response = { ok: true, status: "staged", version: staged.version };
|
|
315
|
+
socket.end(`${JSON.stringify(response)}
|
|
316
|
+
`, () => {
|
|
317
|
+
setTimer(() => void activate(staged, request.target), 100);
|
|
318
|
+
});
|
|
319
|
+
}).catch((cause) => {
|
|
320
|
+
const response = {
|
|
321
|
+
ok: false,
|
|
322
|
+
error: { code: "UPDATE_REFUSED", message: cause instanceof Error ? cause.message : String(cause) }
|
|
323
|
+
};
|
|
324
|
+
socket.end(`${JSON.stringify(response)}
|
|
325
|
+
`);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
socket.on("error", () => socket.destroy());
|
|
329
|
+
});
|
|
330
|
+
server.listen(socketPath, () => chmodSync2(socketPath, 432));
|
|
331
|
+
return server;
|
|
332
|
+
}
|
|
333
|
+
function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, timeoutMs = 90000) {
|
|
334
|
+
return new Promise((resolve2, reject) => {
|
|
335
|
+
const socket = connect(socketPath, () => socket.write(`${JSON.stringify(request)}
|
|
336
|
+
`));
|
|
337
|
+
let buffer = "";
|
|
338
|
+
socket.setTimeout(timeoutMs, () => {
|
|
339
|
+
socket.destroy();
|
|
340
|
+
reject(new Error("agent update helper did not answer before its deadline"));
|
|
341
|
+
});
|
|
342
|
+
socket.on("data", (chunk) => {
|
|
343
|
+
buffer += chunk.toString("utf8");
|
|
344
|
+
const newline = buffer.indexOf(`
|
|
345
|
+
`);
|
|
346
|
+
if (newline < 0)
|
|
347
|
+
return;
|
|
348
|
+
socket.end();
|
|
349
|
+
try {
|
|
350
|
+
resolve2(JSON.parse(buffer.slice(0, newline)));
|
|
351
|
+
} catch (cause) {
|
|
352
|
+
reject(cause);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
socket.on("error", reject);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/software.ts
|
|
360
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
361
|
+
var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
|
|
362
|
+
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
363
|
+
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
364
|
+
var UBUNTU_2604_X64 = [
|
|
365
|
+
{
|
|
366
|
+
requirement: { id: "bun", version: "1.3.14" },
|
|
367
|
+
check: 'test "$(/usr/local/bin/bun --version 2>/dev/null)" = 1.3.14',
|
|
368
|
+
install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL https://bun.sh/install -o "$tmp/install"; ` + `echo "${BUN_INSTALLER_SHA256} $tmp/install" | sha256sum -c -; ` + `BUN_INSTALL="$tmp/bun" BUN_VERSION=1.3.14 bash "$tmp/install" >/dev/null; ` + `install -d -m 0755 /usr/local/lib/forgezero/runtime; ` + `install -m 0755 "$tmp/bun/bin/bun" /usr/local/lib/forgezero/runtime/bun.next; ` + `mv -Tf /usr/local/lib/forgezero/runtime/bun.next /usr/local/lib/forgezero/runtime/bun; ` + `ln -sfn /usr/local/lib/forgezero/runtime/bun /usr/local/bin/bun`
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
requirement: { id: "nginx", version: "ubuntu-26.04" },
|
|
372
|
+
check: "command -v nginx >/dev/null && systemctl is-active --quiet nginx",
|
|
373
|
+
install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y nginx && systemctl enable --now nginx"
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
requirement: { id: "arangodb", version: "3.11.14" },
|
|
377
|
+
check: `arangod --version 2>/dev/null | head -1 | grep -q '3.11.14'`,
|
|
378
|
+
install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL 'https://download.arangodb.com/arangodb311/DEBIAN/amd64/arangodb3_3.11.14-1_amd64.deb' -o "$tmp/arangodb.deb"; ` + `echo "${ARANGO_SHA256} $tmp/arangodb.deb" | sha256sum -c -; ` + `DEBIAN_FRONTEND=noninteractive dpkg -i "$tmp/arangodb.deb" >/dev/null 2>&1 || ` + `DEBIAN_FRONTEND=noninteractive apt-get -y -f install`
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
requirement: { id: "cloudflared", version: "2026.7.3" },
|
|
382
|
+
check: `cloudflared --version 2>/dev/null | grep -q '2026.7.3'`,
|
|
383
|
+
install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL 'https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64' -o "$tmp/cloudflared"; ` + `echo "${CLOUDFLARED_SHA256} $tmp/cloudflared" | sha256sum -c -; ` + `install -m 0755 "$tmp/cloudflared" /usr/local/bin/cloudflared`
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
requirement: { id: "ufw", version: "ubuntu-26.04" },
|
|
387
|
+
check: "command -v ufw >/dev/null",
|
|
388
|
+
install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
|
|
389
|
+
}
|
|
390
|
+
];
|
|
391
|
+
function observeSoftwareHost(osRelease = readFileSync2("/etc/os-release", "utf8"), architecture = process.arch) {
|
|
392
|
+
const values = Object.fromEntries(osRelease.split(`
|
|
393
|
+
`).flatMap((line) => {
|
|
394
|
+
const separator = line.indexOf("=");
|
|
395
|
+
return separator > 0 ? [[line.slice(0, separator), line.slice(separator + 1).replace(/^['"]|['"]$/g, "")]] : [];
|
|
396
|
+
}));
|
|
397
|
+
return {
|
|
398
|
+
os: { id: (values.ID ?? "unknown").toLowerCase(), versionId: values.VERSION_ID ?? "unknown" },
|
|
399
|
+
architecture
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function validateSoftwareRequirements(value) {
|
|
403
|
+
if (!Array.isArray(value) || value.length > 32)
|
|
404
|
+
throw new Error("software requirements must be an array of at most 32 entries");
|
|
405
|
+
const seen = new Set;
|
|
406
|
+
return value.map((item) => {
|
|
407
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
408
|
+
throw new Error("software requirement must be an object");
|
|
409
|
+
const row = item;
|
|
410
|
+
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
411
|
+
throw new Error("software requirement contains an unknown field");
|
|
412
|
+
}
|
|
413
|
+
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)) {
|
|
414
|
+
throw new Error("software requirement coordinate is invalid");
|
|
415
|
+
}
|
|
416
|
+
const requirement = { id: row.id, version: row.version };
|
|
417
|
+
if (seen.has(requirement.id))
|
|
418
|
+
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
419
|
+
seen.add(requirement.id);
|
|
420
|
+
return requirement;
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
424
|
+
const requirements = validateSoftwareRequirements(requirementsInput);
|
|
425
|
+
const observation = options.observation ?? observeSoftwareHost();
|
|
426
|
+
if (observation.os.id !== "ubuntu" || observation.os.versionId !== "26.04" || observation.architecture !== "x64") {
|
|
427
|
+
throw new Error(`unsupported software strategy: ${observation.os.id} ${observation.os.versionId} ${observation.architecture}`);
|
|
428
|
+
}
|
|
429
|
+
const results = [];
|
|
430
|
+
for (const requirement of requirements) {
|
|
431
|
+
const strategy = UBUNTU_2604_X64.find(({ requirement: candidate }) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
432
|
+
if (!strategy)
|
|
433
|
+
throw new Error(`unsupported software requirement: ${requirement.id}@${requirement.version}`);
|
|
434
|
+
const before = await options.exec(strategy.check);
|
|
435
|
+
if (before.exitCode === 0) {
|
|
436
|
+
results.push({ ...requirement, changed: false });
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const installed = await options.exec(strategy.install);
|
|
440
|
+
if (installed.exitCode !== 0)
|
|
441
|
+
throw new Error(`could not install ${requirement.id}@${requirement.version}: ${installed.output.trim()}`);
|
|
442
|
+
const after = await options.exec(strategy.check);
|
|
443
|
+
if (after.exitCode !== 0)
|
|
444
|
+
throw new Error(`${requirement.id}@${requirement.version} did not pass its post-install check`);
|
|
445
|
+
results.push({ ...requirement, changed: true });
|
|
446
|
+
}
|
|
447
|
+
return results;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/software-helper.ts
|
|
451
|
+
import { chmodSync as chmodSync3, existsSync as existsSync3, mkdirSync as mkdirSync3, unlinkSync as unlinkSync2 } from "node:fs";
|
|
452
|
+
import { connect as connect2, createServer as createServer2 } from "node:net";
|
|
453
|
+
import { dirname as dirname3 } from "node:path";
|
|
454
|
+
var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
|
|
455
|
+
var SOFTWARE_HELPER_GROUP = "forgezero-software";
|
|
456
|
+
var SOFTWARE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-software-helper.service";
|
|
457
|
+
var MAX_REQUEST_BYTES2 = 8 * 1024;
|
|
458
|
+
var MAX_PENDING_REQUESTS = 128;
|
|
459
|
+
var execute = async (command2) => {
|
|
460
|
+
const child = Bun.spawn(["/bin/bash", "-Eeuo", "pipefail", "-c", command2], {
|
|
461
|
+
stdout: "pipe",
|
|
462
|
+
stderr: "pipe",
|
|
463
|
+
env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }
|
|
464
|
+
});
|
|
465
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
466
|
+
new Response(child.stdout).text(),
|
|
467
|
+
new Response(child.stderr).text(),
|
|
468
|
+
child.exited
|
|
469
|
+
]);
|
|
470
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
471
|
+
};
|
|
472
|
+
function startSoftwareHelper(options = {}) {
|
|
473
|
+
const socketPath = options.socketPath ?? DEFAULT_SOFTWARE_HELPER_SOCKET;
|
|
474
|
+
if (existsSync3(socketPath))
|
|
475
|
+
unlinkSync2(socketPath);
|
|
476
|
+
mkdirSync3(dirname3(socketPath), { recursive: true, mode: 488 });
|
|
477
|
+
const ensure = options.ensure ?? ensureSoftwareRequirements;
|
|
478
|
+
let tail = Promise.resolve();
|
|
479
|
+
let pending = 0;
|
|
480
|
+
const server = createServer2((socket) => {
|
|
481
|
+
let buffer = "";
|
|
482
|
+
socket.on("data", (chunk) => {
|
|
483
|
+
buffer += chunk.toString("utf8");
|
|
484
|
+
if (Buffer.byteLength(buffer) > MAX_REQUEST_BYTES2)
|
|
485
|
+
return socket.destroy();
|
|
486
|
+
const newline = buffer.indexOf(`
|
|
487
|
+
`);
|
|
488
|
+
if (newline < 0)
|
|
489
|
+
return;
|
|
490
|
+
const line = buffer.slice(0, newline);
|
|
491
|
+
buffer = "";
|
|
492
|
+
if (pending >= MAX_PENDING_REQUESTS) {
|
|
493
|
+
socket.end(`${JSON.stringify({
|
|
494
|
+
ok: false,
|
|
495
|
+
error: { code: "SOFTWARE_BUSY", message: "software helper queue is full" }
|
|
496
|
+
})}
|
|
497
|
+
`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
pending += 1;
|
|
501
|
+
const work = tail.then(() => Promise.resolve().then(() => JSON.parse(line)).then(async (request) => {
|
|
502
|
+
if (request.op !== "ensure")
|
|
503
|
+
throw new Error("unknown software helper operation");
|
|
504
|
+
const requirements = validateSoftwareRequirements(request.requirements);
|
|
505
|
+
const results = await ensure(requirements, { exec: execute });
|
|
506
|
+
socket.end(`${JSON.stringify({ ok: true, results })}
|
|
507
|
+
`);
|
|
508
|
+
}).catch((cause) => socket.end(`${JSON.stringify({
|
|
509
|
+
ok: false,
|
|
510
|
+
error: { code: "SOFTWARE_REFUSED", message: cause instanceof Error ? cause.message : String(cause) }
|
|
511
|
+
})}
|
|
512
|
+
`)).finally(() => {
|
|
513
|
+
pending -= 1;
|
|
514
|
+
}));
|
|
515
|
+
tail = work.then(() => {
|
|
516
|
+
return;
|
|
517
|
+
}, () => {
|
|
518
|
+
return;
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
socket.on("error", () => socket.destroy());
|
|
522
|
+
});
|
|
523
|
+
server.listen(socketPath, () => chmodSync3(socketPath, 432));
|
|
524
|
+
return server;
|
|
525
|
+
}
|
|
526
|
+
function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCKET, timeoutMs = 15 * 60000) {
|
|
527
|
+
validateSoftwareRequirements(requirements);
|
|
528
|
+
return new Promise((resolve2, reject) => {
|
|
529
|
+
const socket = connect2(socketPath, () => socket.write(`${JSON.stringify({ op: "ensure", requirements })}
|
|
530
|
+
`));
|
|
531
|
+
let buffer = "";
|
|
532
|
+
socket.setTimeout(timeoutMs, () => {
|
|
533
|
+
socket.destroy();
|
|
534
|
+
reject(new Error("software helper did not answer before its deadline"));
|
|
535
|
+
});
|
|
536
|
+
socket.on("data", (chunk) => {
|
|
537
|
+
buffer += chunk.toString("utf8");
|
|
538
|
+
const newline = buffer.indexOf(`
|
|
539
|
+
`);
|
|
540
|
+
if (newline < 0)
|
|
541
|
+
return;
|
|
542
|
+
socket.end();
|
|
543
|
+
try {
|
|
544
|
+
const response = JSON.parse(buffer.slice(0, newline));
|
|
545
|
+
if (!response.ok || !response.results)
|
|
546
|
+
throw new Error(response.error?.message ?? "software helper refused the request");
|
|
547
|
+
resolve2(response.results);
|
|
548
|
+
} catch (cause) {
|
|
549
|
+
reject(cause);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
socket.on("error", reject);
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/version.ts
|
|
557
|
+
var VERSION2 = "0.1.28";
|
|
558
|
+
|
|
1
559
|
// src/provision.ts
|
|
2
560
|
function atLeast(version, floor) {
|
|
3
561
|
const parse = (value) => (value.trim().replace(/^v/, "").match(/\d+/g) ?? []).slice(0, 3).map(Number);
|
|
@@ -44,12 +602,102 @@ var DEPLOYMENT_GROUP = "forgezero-deploy";
|
|
|
44
602
|
var VAULT_GROUP = "forgezero-vault";
|
|
45
603
|
var LIFECYCLE_GROUP = "forgezero-lifecycle";
|
|
46
604
|
var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
|
|
605
|
+
var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
|
|
47
606
|
var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
|
|
48
607
|
var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
|
|
49
608
|
var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
|
|
50
609
|
var LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
|
|
51
610
|
var WARP_CONFIG_UNIT_PATH = "/etc/systemd/system/forgezero-warp-config.service";
|
|
52
611
|
var WARP_SERVICE_DROP_IN_PATH = "/etc/systemd/system/warp-svc.service.d/forgezero.conf";
|
|
612
|
+
function softwareHelperUnit(options) {
|
|
613
|
+
const bin = options.binPath ?? "fz-agent";
|
|
614
|
+
return `[Unit]
|
|
615
|
+
Description=ForgeZero declarative software strategy helper
|
|
616
|
+
Documentation=https://www.forgezero.net/docs/agent
|
|
617
|
+
After=network-online.target
|
|
618
|
+
Wants=network-online.target
|
|
619
|
+
|
|
620
|
+
[Service]
|
|
621
|
+
Type=simple
|
|
622
|
+
User=root
|
|
623
|
+
Group=${SOFTWARE_HELPER_GROUP}
|
|
624
|
+
Environment=FZ_SOFTWARE_HELPER_SOCKET=${DEFAULT_SOFTWARE_HELPER_SOCKET}
|
|
625
|
+
ExecStart=${bin} software-helper
|
|
626
|
+
Restart=always
|
|
627
|
+
RestartSec=2
|
|
628
|
+
RuntimeDirectory=forgezero-software
|
|
629
|
+
RuntimeDirectoryMode=0750
|
|
630
|
+
UMask=0007
|
|
631
|
+
LimitCORE=0
|
|
632
|
+
PrivateTmp=true
|
|
633
|
+
ProtectHome=true
|
|
634
|
+
ProtectKernelTunables=true
|
|
635
|
+
ProtectKernelModules=true
|
|
636
|
+
ProtectControlGroups=true
|
|
637
|
+
RestrictRealtime=true
|
|
638
|
+
LockPersonality=true
|
|
639
|
+
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
|
640
|
+
|
|
641
|
+
[Install]
|
|
642
|
+
WantedBy=multi-user.target
|
|
643
|
+
`;
|
|
644
|
+
}
|
|
645
|
+
function agentUpdateHelperUnit(options) {
|
|
646
|
+
const bin = options.binPath ?? "fz-agent";
|
|
647
|
+
return `[Unit]
|
|
648
|
+
Description=ForgeZero verified Agent update helper
|
|
649
|
+
Documentation=https://www.forgezero.net/docs/agent
|
|
650
|
+
After=network-online.target
|
|
651
|
+
Wants=network-online.target
|
|
652
|
+
|
|
653
|
+
[Service]
|
|
654
|
+
Type=simple
|
|
655
|
+
User=root
|
|
656
|
+
Group=${AGENT_UPDATE_GROUP}
|
|
657
|
+
Environment=FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}
|
|
658
|
+
ExecStart=${bin} update-helper
|
|
659
|
+
Restart=always
|
|
660
|
+
RestartSec=2
|
|
661
|
+
RuntimeDirectory=forgezero-update
|
|
662
|
+
RuntimeDirectoryMode=0750
|
|
663
|
+
UMask=0007
|
|
664
|
+
LimitCORE=0
|
|
665
|
+
NoNewPrivileges=true
|
|
666
|
+
PrivateTmp=true
|
|
667
|
+
ProtectSystem=strict
|
|
668
|
+
ProtectHome=true
|
|
669
|
+
ProtectKernelTunables=true
|
|
670
|
+
ProtectKernelModules=true
|
|
671
|
+
ProtectControlGroups=true
|
|
672
|
+
RestrictSUIDSGID=true
|
|
673
|
+
RestrictRealtime=true
|
|
674
|
+
LockPersonality=true
|
|
675
|
+
ReadWritePaths=${DEFAULT_AGENT_RELEASE_ROOT} /var/lib/forgezero
|
|
676
|
+
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
|
677
|
+
|
|
678
|
+
[Install]
|
|
679
|
+
WantedBy=multi-user.target
|
|
680
|
+
`;
|
|
681
|
+
}
|
|
682
|
+
function agentSocketUnit(options) {
|
|
683
|
+
const socket = systemdPath(options.socketPath, "agent socket");
|
|
684
|
+
return `[Unit]
|
|
685
|
+
Description=ForgeZero application Vault socket
|
|
686
|
+
Documentation=https://www.forgezero.net/docs/agent
|
|
687
|
+
|
|
688
|
+
[Socket]
|
|
689
|
+
ListenStream=${socket}
|
|
690
|
+
SocketUser=root
|
|
691
|
+
SocketGroup=${VAULT_GROUP}
|
|
692
|
+
SocketMode=0660
|
|
693
|
+
DirectoryMode=0750
|
|
694
|
+
RemoveOnStop=true
|
|
695
|
+
Service=forgezero-agent.service
|
|
696
|
+
|
|
697
|
+
[Install]
|
|
698
|
+
WantedBy=sockets.target
|
|
699
|
+
`;
|
|
700
|
+
}
|
|
53
701
|
var systemdPath = (value, label) => {
|
|
54
702
|
if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
|
|
55
703
|
throw new Error(`invalid ${label} path`);
|
|
@@ -310,12 +958,14 @@ function agentUnit(options) {
|
|
|
310
958
|
options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
|
|
311
959
|
deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
|
|
312
960
|
deploymentEnabled ? `FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}` : null,
|
|
961
|
+
deploymentEnabled ? `FZ_SOFTWARE_HELPER_SOCKET=${DEFAULT_SOFTWARE_HELPER_SOCKET}` : null,
|
|
313
962
|
Object.keys(deploymentCredentials).length > 0 ? `FZ_DEPLOY_SYSTEMD_SECRETS=${Object.keys(deploymentCredentials).join(",")}` : null,
|
|
314
963
|
Object.keys(deploymentEnvironment).length > 0 ? `FZ_DEPLOY_ENV_NAMES=${Object.keys(deploymentEnvironment).join(",")}` : null,
|
|
315
964
|
...Object.entries(deploymentEnvironment).map(([name, value]) => `${name}=${value}`),
|
|
316
965
|
options.publicApiUrl ? `FZ_PUBLIC_API_URL=${options.publicApiUrl}` : null,
|
|
317
966
|
options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null,
|
|
318
967
|
options.pullMigrations ? "FZ_MIGRATION_PULL=true" : null,
|
|
968
|
+
`FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}`,
|
|
319
969
|
options.pullMigrations ? `FZ_LIFECYCLE_HELPER_SOCKET=${lifecycleHelperSocketPath}` : null
|
|
320
970
|
].filter((line) => line !== null);
|
|
321
971
|
if (deploymentEnabled) {
|
|
@@ -327,19 +977,25 @@ function agentUnit(options) {
|
|
|
327
977
|
`);
|
|
328
978
|
const deploymentWrites = deploymentEnabled ? `ReadWritePaths=${deployRoot}/releases ${deployRoot}/agent-home ${deployRoot}/cache` : "";
|
|
329
979
|
const supplementaryGroups = [
|
|
980
|
+
AGENT_UPDATE_GROUP,
|
|
330
981
|
deploymentEnabled ? DEPLOYMENT_GROUP : null,
|
|
982
|
+
deploymentEnabled ? SOFTWARE_HELPER_GROUP : null,
|
|
331
983
|
lifecycleEnabled ? LIFECYCLE_GROUP : null
|
|
332
984
|
].filter((value) => value !== null);
|
|
333
985
|
const deploymentGroup = supplementaryGroups.length > 0 ? `SupplementaryGroups=${supplementaryGroups.join(" ")}` : "";
|
|
334
986
|
const after = [
|
|
335
987
|
"network-online.target",
|
|
988
|
+
"forgezero-agent-update-helper.service",
|
|
336
989
|
deploymentEnabled ? "forgezero-deploy-runner.service" : null,
|
|
990
|
+
deploymentEnabled ? "forgezero-software-helper.service" : null,
|
|
337
991
|
lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
|
|
338
992
|
warpEnabled ? "warp-svc.service" : null,
|
|
339
993
|
enrolmentEnabled ? "forgezero-agent-enrol.service" : null
|
|
340
994
|
].filter((value) => value !== null);
|
|
341
995
|
const requires = [
|
|
996
|
+
"forgezero-agent-update-helper.service",
|
|
342
997
|
deploymentEnabled ? "forgezero-deploy-runner.service" : null,
|
|
998
|
+
deploymentEnabled ? "forgezero-software-helper.service" : null,
|
|
343
999
|
lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
|
|
344
1000
|
warpEnabled ? "warp-svc.service" : null,
|
|
345
1001
|
enrolmentEnabled ? "forgezero-agent-enrol.service" : null
|
|
@@ -365,6 +1021,7 @@ Type=simple
|
|
|
365
1021
|
User=${user}
|
|
366
1022
|
Group=${VAULT_GROUP}
|
|
367
1023
|
${deploymentGroup}
|
|
1024
|
+
Sockets=forgezero-agent.socket
|
|
368
1025
|
LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
|
|
369
1026
|
${gitCredential}${projectCredentials}${projectCredentials ? `
|
|
370
1027
|
` : ""}${snpPrepare}ExecStart=${bin}
|
|
@@ -383,6 +1040,7 @@ LimitCORE=0
|
|
|
383
1040
|
# than wherever the process happened to have write access.
|
|
384
1041
|
RuntimeDirectory=forgezero
|
|
385
1042
|
RuntimeDirectoryMode=0750
|
|
1043
|
+
RuntimeDirectoryPreserve=yes
|
|
386
1044
|
UMask=0007
|
|
387
1045
|
|
|
388
1046
|
# Tenant-controlled commands execute in forgezero-deploy-runner.service. This
|
|
@@ -450,8 +1108,11 @@ function planProvision(options) {
|
|
|
450
1108
|
unitPath: UNIT_PATH,
|
|
451
1109
|
unit: agentUnit({ ...options, mode, lifecycleProfilePath, lifecycleHelperSocketPath }),
|
|
452
1110
|
auxiliaryUnits: [
|
|
1111
|
+
{ path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
|
|
1112
|
+
{ path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
|
|
453
1113
|
...deploymentEnabled ? [
|
|
454
|
-
{ path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) }
|
|
1114
|
+
{ path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) },
|
|
1115
|
+
{ path: SOFTWARE_HELPER_UNIT_PATH, unit: softwareHelperUnit(options) }
|
|
455
1116
|
] : [],
|
|
456
1117
|
...enrolmentEnabled ? [
|
|
457
1118
|
{ path: ENROLMENT_UNIT_PATH, unit: agentEnrolmentUnit(options) }
|
|
@@ -479,9 +1140,13 @@ function planProvision(options) {
|
|
|
479
1140
|
label: "vault socket access group",
|
|
480
1141
|
command: `groupadd --system ${VAULT_GROUP} || true`
|
|
481
1142
|
},
|
|
1143
|
+
{
|
|
1144
|
+
label: "Agent update helper access group",
|
|
1145
|
+
command: `groupadd --system ${AGENT_UPDATE_GROUP} || true`
|
|
1146
|
+
},
|
|
482
1147
|
...sourceBinPath && binPath ? [{
|
|
483
1148
|
label: "root-owned agent runtime",
|
|
484
|
-
command: `install -d -o root -g root -m 0755 ${binPath.replace(/\/[^/]+$/, "")}; ` + `install -o root -g root -m 0755 ${sourceBinPath} ${binPath}`
|
|
1149
|
+
command: `install -d -o root -g root -m 0755 ${binPath.replace(/\/[^/]+$/, "")} ` + `${DEFAULT_AGENT_RELEASE_ROOT}/versions/${VERSION2}/dist; ` + `install -o root -g root -m 0755 ${sourceBinPath} ` + `${DEFAULT_AGENT_RELEASE_ROOT}/versions/${VERSION2}/dist/fz-agent.js; ` + `ln -sfn versions/${VERSION2} ${DEFAULT_AGENT_RELEASE_ROOT}/current.next; ` + `mv -Tf ${DEFAULT_AGENT_RELEASE_ROOT}/current.next ${DEFAULT_AGENT_RELEASE_ROOT}/current; ` + `rm -f ${binPath}; ln -s ${DEFAULT_AGENT_RELEASE_ROOT}/current/dist/fz-agent.js ${binPath}`
|
|
485
1150
|
}] : [],
|
|
486
1151
|
...warpEnabled ? [{
|
|
487
1152
|
label: "Cloudflare One client for Ubuntu 26.04",
|
|
@@ -489,7 +1154,7 @@ function planProvision(options) {
|
|
|
489
1154
|
}] : [],
|
|
490
1155
|
...deploymentEnabled ? [{
|
|
491
1156
|
label: "deployment isolation group",
|
|
492
|
-
command: `groupadd --system ${DEPLOYMENT_GROUP} || true`
|
|
1157
|
+
command: `groupadd --system ${DEPLOYMENT_GROUP} || true; groupadd --system ${SOFTWARE_HELPER_GROUP} || true`
|
|
493
1158
|
}] : [],
|
|
494
1159
|
...lifecycleEnabled ? [{
|
|
495
1160
|
label: "lifecycle helper access group",
|
|
@@ -503,13 +1168,17 @@ function planProvision(options) {
|
|
|
503
1168
|
label: "bind service account to vault group",
|
|
504
1169
|
command: `usermod -g ${VAULT_GROUP} ${user}`
|
|
505
1170
|
},
|
|
1171
|
+
{
|
|
1172
|
+
label: "grant verified Agent update access",
|
|
1173
|
+
command: `usermod -a -G ${AGENT_UPDATE_GROUP} ${user}`
|
|
1174
|
+
},
|
|
506
1175
|
...lifecycleEnabled ? [{
|
|
507
1176
|
label: "grant lifecycle helper socket access",
|
|
508
1177
|
command: `usermod -a -G ${LIFECYCLE_GROUP} ${user}`
|
|
509
1178
|
}] : [],
|
|
510
1179
|
...deploymentEnabled ? [{
|
|
511
1180
|
label: "credential-free deployment account",
|
|
512
|
-
command: `useradd --system --no-create-home --shell /usr/sbin/nologin --gid ${DEPLOYMENT_GROUP} ${DEPLOYMENT_RUNNER_USER} || true; ` + `usermod -a -G ${DEPLOYMENT_GROUP} ${user}`
|
|
1181
|
+
command: `useradd --system --no-create-home --shell /usr/sbin/nologin --gid ${DEPLOYMENT_GROUP} ${DEPLOYMENT_RUNNER_USER} || true; ` + `usermod -a -G ${DEPLOYMENT_GROUP},${SOFTWARE_HELPER_GROUP} ${user}`
|
|
513
1182
|
}] : [],
|
|
514
1183
|
{
|
|
515
1184
|
label: "credential directory",
|
|
@@ -551,7 +1220,9 @@ function planProvision(options) {
|
|
|
551
1220
|
{
|
|
552
1221
|
label: "enable and start",
|
|
553
1222
|
command: `systemctl enable --now ${[
|
|
554
|
-
|
|
1223
|
+
"forgezero-agent.socket",
|
|
1224
|
+
"forgezero-agent-update-helper.service",
|
|
1225
|
+
...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
|
|
555
1226
|
...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
|
|
556
1227
|
...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
|
|
557
1228
|
...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
|
|
@@ -564,9 +1235,13 @@ function planProvision(options) {
|
|
|
564
1235
|
}] : [],
|
|
565
1236
|
{ label: "prove it is running", command: "systemctl is-active forgezero-agent.service" },
|
|
566
1237
|
{ label: "prove the vault socket exists", command: `test -S ${options.socketPath}` },
|
|
1238
|
+
{ label: "prove the Agent update helper exists", command: `test -S ${DEFAULT_AGENT_UPDATE_SOCKET}` },
|
|
567
1239
|
...deploymentEnabled ? [{
|
|
568
1240
|
label: "prove the deployment runner socket exists",
|
|
569
1241
|
command: `test -S ${DEPLOYMENT_RUNNER_SOCKET}`
|
|
1242
|
+
}, {
|
|
1243
|
+
label: "prove the software strategy helper socket exists",
|
|
1244
|
+
command: `test -S ${DEFAULT_SOFTWARE_HELPER_SOCKET}`
|
|
570
1245
|
}] : [],
|
|
571
1246
|
...lifecycleEnabled ? [{
|
|
572
1247
|
label: "prove the lifecycle helper socket exists",
|
|
@@ -586,13 +1261,16 @@ function planProvision(options) {
|
|
|
586
1261
|
export {
|
|
587
1262
|
warpServiceDropIn,
|
|
588
1263
|
warpConfigUnit,
|
|
1264
|
+
softwareHelperUnit,
|
|
589
1265
|
reasonFor,
|
|
590
1266
|
planProvision,
|
|
591
1267
|
modeFor,
|
|
592
1268
|
lifecycleHelperUnit,
|
|
593
1269
|
deploymentRunnerUnit,
|
|
594
1270
|
atLeast,
|
|
1271
|
+
agentUpdateHelperUnit,
|
|
595
1272
|
agentUnit,
|
|
1273
|
+
agentSocketUnit,
|
|
596
1274
|
agentEnrolmentUnit,
|
|
597
1275
|
WARP_SERVICE_DROP_IN_PATH,
|
|
598
1276
|
WARP_CONFIG_UNIT_PATH,
|
|
@@ -606,5 +1284,6 @@ export {
|
|
|
606
1284
|
DEPLOYMENT_RUNNER_UNIT_PATH,
|
|
607
1285
|
DEPLOYMENT_RUNNER_SOCKET,
|
|
608
1286
|
DEPLOYMENT_GROUP,
|
|
609
|
-
CAPABILITY_CHECKS
|
|
1287
|
+
CAPABILITY_CHECKS,
|
|
1288
|
+
AGENT_SOCKET_UNIT_PATH
|
|
610
1289
|
};
|