feat(docker): pull-check + comparaison déterministe par stack (tâche 2 SJ-5)
- template docker/pull-check.sh.tpl (pull sans up, inspect before/after) - dockerPull: parseDockerPullCheck + buildDockerPullResult (TDD) — compare image id/digest/label OCI → services up_to_date|updates_available|error, changes operation=pulled ; erreurs registry nettoyées (URL/token/password) - dockerDedupKey (digests prioritaires, fallback image ids) + DockerImageChange.dedupKey - pullCheckStack: SSH + upsert docker_stack_services, refuse stack non enabled, refresh Docker séparé (hors refreshMachine, pas de pull auto) - execute: runAction(opts.stackId), branche docker_pull_check, injection stackDir (corrige docker_inspect_current) ; route: allowlist Docker passifs + pull_check, destructives toujours hors API jusqu'à action_requests (SJ-6) Pas de migration (schéma SJ-4 suffisant). tsc 0 erreur · 85 tests · build OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,15 @@ const TEMPLATE_FOR: Partial<Record<ActionType, string>> = {
|
||||
docker_inspect_current: "docker/inspect-compose.sh.tpl",
|
||||
};
|
||||
|
||||
export async function runAction(machineId: string, action: ActionType): Promise<ExecutionResult> {
|
||||
export interface RunActionOpts {
|
||||
stackId?: string;
|
||||
}
|
||||
|
||||
export async function runAction(
|
||||
machineId: string,
|
||||
action: ActionType,
|
||||
opts?: RunActionOpts,
|
||||
): Promise<ExecutionResult> {
|
||||
const m = getMachineRow(machineId);
|
||||
if (!m) throw new Error("Machine introuvable");
|
||||
|
||||
@@ -112,10 +120,97 @@ export async function runAction(machineId: string, action: ActionType): Promise<
|
||||
return resultDocker;
|
||||
}
|
||||
|
||||
// --- SJ-5 : docker_pull_check délégué au service dédié (pull + comparaison + persistance) ---
|
||||
if (action === "docker_pull_check") {
|
||||
if (!opts?.stackId) throw new Error("docker_pull_check requiert un stackId");
|
||||
const { pullCheckStack, dockerDedupKey } = await import("./dockerPull.js");
|
||||
let rawPull = "";
|
||||
let pullStatus: ExecutionStatus = "ok";
|
||||
let importantPull: string[] = [];
|
||||
let dockerExec: ExecutionResult["docker"];
|
||||
try {
|
||||
const outcome = await pullCheckStack(machineId, opts.stackId, (c) => {
|
||||
rawPull += c;
|
||||
});
|
||||
rawPull = outcome.raw;
|
||||
const r = outcome.result;
|
||||
pullStatus = r.status === "error" ? "error" : r.status === "warning" ? "warning" : "ok";
|
||||
const changes = r.changes.map((ch) => ({
|
||||
...ch,
|
||||
dedupKey: dockerDedupKey(ch.imageRef ?? ch.stack, ch.fromDigest ?? null, ch.toDigest ?? null, ch.fromImageId ?? null, ch.toImageId ?? null),
|
||||
}));
|
||||
dockerExec = { pull: { changes, ...(r.errors.length ? { errors: r.errors } : {}) } };
|
||||
importantPull = [
|
||||
`docker_pull_check ${outcome.stackName} : ${r.changes.length} image(s) mise(s) à jour (${r.services.length} service(s))`,
|
||||
...r.services.map((s) => ` ${s.status} ${s.image}`),
|
||||
...r.errors.map((e) => ` [${e.kind}] ${e.message}`),
|
||||
];
|
||||
outputHub.publish(machineId, `\n===SU:DONE status=${pullStatus} changes=${r.changes.length}===\n`);
|
||||
} catch (err) {
|
||||
pullStatus = "error";
|
||||
importantPull = [`[ERREUR] ${(err as Error).message}`];
|
||||
rawPull += `\n[ERREUR] ${(err as Error).message}\n`;
|
||||
outputHub.publish(machineId, `\n[ERREUR] ${(err as Error).message}\n`);
|
||||
}
|
||||
const finishedAtPull = new Date().toISOString();
|
||||
const dirPull = join(env.reportsDir, machineId);
|
||||
mkdirSync(dirPull, { recursive: true });
|
||||
const rawLogPathPull = join(dirPull, `${executionId}.log`);
|
||||
const reportPathPull = join(dirPull, `${executionId}.md`);
|
||||
writeFileSync(rawLogPathPull, rawPull || importantPull.join("\n") + "\n", "utf8");
|
||||
const resultPull: ExecutionResult = {
|
||||
executionId, machineId, startedAt, finishedAt: finishedAtPull,
|
||||
mode: "manual", action, status: pullStatus,
|
||||
rebootRequiredAfterRun: false,
|
||||
importantLogLines: importantPull,
|
||||
rawLogRef: rawLogPathPull, reportRef: reportPathPull,
|
||||
...(dockerExec ? { docker: dockerExec } : {}),
|
||||
};
|
||||
writeFileSync(reportPathPull, buildReportMarkdown(resultPull, m.name), "utf8");
|
||||
const reportIdPull = randomUUID();
|
||||
db.update(schema.executions).set({
|
||||
finishedAt: finishedAtPull, status: pullStatus, schemaVersion: 1,
|
||||
resultJson: JSON.stringify(resultPull), importantJson: JSON.stringify(importantPull),
|
||||
reportPath: reportPathPull, rawLogPath: rawLogPathPull, reportId: reportIdPull,
|
||||
exitCode: pullStatus === "ok" ? 0 : 1,
|
||||
errorKind: pullStatus === "error" ? "execution_failed" : null,
|
||||
errorMessage: pullStatus === "error" ? (importantPull.at(-1) ?? null) : null,
|
||||
}).where(eq(schema.executions.id, executionId)).run();
|
||||
db.update(schema.machines).set({ status: pullStatus === "error" ? "error" : "unknown" })
|
||||
.where(eq(schema.machines.id, machineId)).run();
|
||||
db.insert(schema.reports).values({
|
||||
id: reportIdPull, machineId, executionId, kind: "machine",
|
||||
title: `${m.name} — docker_pull_check`, path: reportPathPull, createdAt: finishedAtPull,
|
||||
}).run();
|
||||
db.insert(schema.rawArtifacts).values({
|
||||
id: randomUUID(), machineId, kind: "raw_log", path: rawLogPathPull,
|
||||
bytes: statSync(rawLogPathPull).size, createdAt: finishedAtPull,
|
||||
retentionPolicy: pullStatus === "error" ? "failed" : "default",
|
||||
}).run();
|
||||
upsertMachineState(machineId, {
|
||||
status: pullStatus === "error" ? "error" : "unknown",
|
||||
runningJobId: null,
|
||||
lastErrorKind: pullStatus === "error" ? "execution_failed" : null,
|
||||
lastErrorMessage: pullStatus === "error" ? (importantPull.at(-1) ?? null) : null,
|
||||
});
|
||||
recordEvent({
|
||||
machineId, eventType: "action_docker_pull_check",
|
||||
severity: pullStatus === "error" ? "error" : "info",
|
||||
executionId, message: `Action docker_pull_check : ${pullStatus}`,
|
||||
});
|
||||
return resultPull;
|
||||
}
|
||||
|
||||
const proxy = m.aptProxyMode === "runtime" ? m.aptProxyUrl : null;
|
||||
const rel = TEMPLATE_FOR[action];
|
||||
if (!rel) throw new Error("Action sans template: " + action);
|
||||
const script = renderTemplate(rel, { aptProxy: proxy });
|
||||
// Templates Docker par-stack (inspect) : injecter stackDir ; ignoré par les templates APT.
|
||||
let stackDir: string | null = null;
|
||||
if (opts?.stackId) {
|
||||
const st = db.select().from(schema.dockerComposeStacks).where(eq(schema.dockerComposeStacks.id, opts.stackId)).get();
|
||||
stackDir = st?.workingDir ?? null;
|
||||
}
|
||||
const script = renderTemplate(rel, { aptProxy: proxy, stackDir });
|
||||
|
||||
const inactivity = action === "reboot" ? 0 : 600000;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user