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:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user