feat(post-install): moteur de profils + bootstrap + identité/réseau (tâche 2 SJ-8)

- templates custom/bootstrap-root + identity-network (sortie structurée parsable,
  sauvegardes, échec contrôlé, jamais de coupure réseau sans reconnexion)
- postInstall: registre de manifestes (champs typés + defaults/defaultFrom),
  validateProfileValues + maskSecretValues + buildPostInstallResult (TDD),
  renderProfile/previewProfile (masquage secrets), runPostInstall (SSH)
- execute: RunActionOpts.profileId/values + branche post_install (bloc postInstall)
- action_requests: post_install accepté, payload profileId/values transmis à approve
- routes: GET /profiles, POST .../preview (script masqué + validation),
  POST .../run (action_request si requiresConfirmation, sinon direct)

Champs = formulaire (pas de question SSH interactive) ; secrets jamais sérialisés ;
identity_network exige confirmation. tsc 0 · 101 tests · build OK · boot OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 08:02:32 +02:00
parent faa654c95a
commit e6f4ae470b
10 changed files with 566 additions and 3 deletions
+28
View File
@@ -33,6 +33,8 @@ const APT_ACTION_FILE: Partial<Record<ActionType, string>> = {
export interface RunActionOpts {
stackId?: string;
aggressive?: boolean; // docker_prune_images
profileId?: string; // post_install
values?: Record<string, string | number | boolean | undefined>;
}
/**
@@ -49,6 +51,7 @@ function archiveExecution(args: {
raw: string;
importantLines: string[];
docker?: ExecutionResult["docker"];
postInstall?: ExecutionResult["postInstall"];
errors?: ExecutionResult["errors"];
}): ExecutionResult {
const { machineId, machineName, executionId, action, startedAt, status, raw, importantLines } = args;
@@ -64,6 +67,7 @@ function archiveExecution(args: {
importantLogLines: importantLines,
rawLogRef: rawLogPath, reportRef: reportPath,
...(args.docker ? { docker: args.docker } : {}),
...(args.postInstall ? { postInstall: args.postInstall } : {}),
...(args.errors && args.errors.length ? { errors: args.errors } : {}),
};
writeFileSync(reportPath, buildReportMarkdown(result, machineName), "utf8");
@@ -333,6 +337,30 @@ export async function runAction(
}
}
// --- SJ-8 : post-install (profil + champs de formulaire) ---
if (action === "post_install") {
if (!opts?.profileId) throw new Error("post_install requiert un profileId");
const { runPostInstall } = await import("./postInstall.js");
try {
const o = await runPostInstall(machineId, opts.profileId, opts.values ?? {}, () => {});
const r = o.result;
const important = [
`post_install ${opts.profileId} : ${r.packagesInstalled.length} paquet(s), ${r.filesModified.length} fichier(s) modifié(s)${r.rebootsRequested ? " · reboot demandé" : ""}`,
...r.packagesInstalled.map((p) => ` + ${p}`),
...r.filesModified.map((f) => ` ~ ${f}`),
...(r.networkChange ? [` réseau : ${r.networkChange.oldEndpoint ?? "?"}${r.networkChange.newEndpoint ?? "?"} (reconnexion ${r.networkChange.reconnectHost ?? "?"})`] : []),
...(r.errors?.map((e) => ` [${e.kind}] ${e.message}`) ?? []),
];
outputHub.publish(machineId, `\n===SU:DONE status=${o.status}===\n`);
return archiveExecution({
machineId, machineName: m.name, executionId, action, startedAt, status: o.status, raw: o.raw,
importantLines: important, postInstall: r, errors: r.errors,
});
} catch (err) {
return archiveExecution({ machineId, machineName: m.name, executionId, action, startedAt, status: "error", raw: "", importantLines: [`[ERREUR] ${(err as Error).message}`] });
}
}
// --- SJ-7 : sonde machine (lecture seule) déléguée au service dédié ---
if (action === "machine_probe") {
const { runProbe } = await import("./machineProbe.js");