feat: service refresh (check APT -> snapshot canonique)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
// server/services/refresh.test.ts
|
||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
|
||||||
|
// Mock all modules with side-effects at import time (DB, SSH, templates)
|
||||||
|
vi.mock("../db/client.js", () => ({ db: {}, schema: { machines: {}, snapshots: {} } }));
|
||||||
|
vi.mock("../env.js", () => ({ env: { requireMasterKey: vi.fn(), reportsDir: "/tmp" } }));
|
||||||
|
vi.mock("../ssh/client.js", () => ({ runScriptSudo: vi.fn() }));
|
||||||
|
vi.mock("../templates/render.js", () => ({ renderTemplate: vi.fn() }));
|
||||||
|
vi.mock("../templates/aptReduce.js", () => ({ reduceAptLines: vi.fn(() => []) }));
|
||||||
|
vi.mock("./machines.js", () => ({ getMachineRow: vi.fn(), getCreds: vi.fn() }));
|
||||||
|
vi.mock("../ws/outputHub.js", () => ({ outputHub: { clear: vi.fn(), publish: vi.fn() } }));
|
||||||
|
|
||||||
|
import { extractSection } from "./refresh.js";
|
||||||
|
|
||||||
|
const raw = "===SU:UPDATE===\nok\n===SU:SIMULATE===\nInst a [1] (2 X [amd64])\n===SU:REBOOT===\nREBOOT_REQUIRED=0\n===SU:END===";
|
||||||
|
|
||||||
|
describe("extractSection", () => {
|
||||||
|
it("extrait le contenu entre deux marqueurs", () => {
|
||||||
|
expect(extractSection(raw, "===SU:SIMULATE===", "===SU:REBOOT===")).toBe("Inst a [1] (2 X [amd64])");
|
||||||
|
});
|
||||||
|
it("retourne vide si marqueur absent", () => {
|
||||||
|
expect(extractSection(raw, "===SU:NOPE===", "===SU:END===")).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// server/services/refresh.ts
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db, schema } from "../db/client.js";
|
||||||
|
import { getMachineRow, getCreds } from "./machines.js";
|
||||||
|
import { renderTemplate } from "../templates/render.js";
|
||||||
|
import { reduceAptLines } from "../templates/aptReduce.js";
|
||||||
|
import { runScriptSudo } from "../ssh/client.js";
|
||||||
|
import { parseAptSimulate, parseRebootRequired } from "./aptParse.js";
|
||||||
|
import { outputHub } from "../ws/outputHub.js";
|
||||||
|
import type { UpdateSnapshot, MachineStatus } from "@shared/types.js";
|
||||||
|
|
||||||
|
/** Extrait la section entre deux marqueurs ===SU:X=== d'une sortie de script. */
|
||||||
|
export function extractSection(raw: string, start: string, end: string): string {
|
||||||
|
const s = raw.indexOf(start);
|
||||||
|
if (s === -1) return "";
|
||||||
|
const from = s + start.length;
|
||||||
|
const e = raw.indexOf(end, from);
|
||||||
|
return raw.slice(from, e === -1 ? undefined : e).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshMachine(machineId: string): Promise<UpdateSnapshot> {
|
||||||
|
const m = getMachineRow(machineId);
|
||||||
|
if (!m) throw new Error("Machine introuvable");
|
||||||
|
|
||||||
|
db.update(schema.machines).set({ status: "running" }).where(eq(schema.machines.id, machineId)).run();
|
||||||
|
outputHub.clear(machineId);
|
||||||
|
|
||||||
|
const proxy = m.aptProxyMode === "runtime" ? m.aptProxyUrl : null;
|
||||||
|
const script = renderTemplate("apt/check.sh.tpl", { aptProxy: proxy });
|
||||||
|
|
||||||
|
let raw = "";
|
||||||
|
try {
|
||||||
|
const res = await runScriptSudo(getCreds(m), script, (c) => {
|
||||||
|
raw += c;
|
||||||
|
outputHub.publish(machineId, c);
|
||||||
|
});
|
||||||
|
raw = res.stdout;
|
||||||
|
} catch (err) {
|
||||||
|
db.update(schema.machines).set({ status: "error" }).where(eq(schema.machines.id, machineId)).run();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const simulate = extractSection(raw, "===SU:SIMULATE===", "===SU:REBOOT===");
|
||||||
|
const rebootSection = extractSection(raw, "===SU:REBOOT===", "===SU:END===");
|
||||||
|
const packages = parseAptSimulate(simulate);
|
||||||
|
const rebootRequired = parseRebootRequired(rebootSection);
|
||||||
|
const status: MachineStatus = packages.length > 0 ? "updates_available" : "ok";
|
||||||
|
const checkedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
const snapshot: UpdateSnapshot = {
|
||||||
|
machineId,
|
||||||
|
hostname: m.hostname,
|
||||||
|
os: { family: m.osFamily as UpdateSnapshot["os"]["family"], version: "" },
|
||||||
|
checkedAt,
|
||||||
|
status,
|
||||||
|
apt: { enabled: true, count: packages.length, rebootRequired, packages },
|
||||||
|
rawHints: { logImportantLines: reduceAptLines(raw) },
|
||||||
|
};
|
||||||
|
|
||||||
|
db.insert(schema.snapshots).values({
|
||||||
|
id: randomUUID(),
|
||||||
|
machineId,
|
||||||
|
checkedAt,
|
||||||
|
status,
|
||||||
|
payloadJson: JSON.stringify(snapshot),
|
||||||
|
}).run();
|
||||||
|
db.update(schema.machines).set({ status, lastCheckedAt: checkedAt }).where(eq(schema.machines.id, machineId)).run();
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLatestSnapshot(machineId: string): UpdateSnapshot | null {
|
||||||
|
const row = db.select().from(schema.snapshots).where(eq(schema.snapshots.machineId, machineId)).all().at(-1);
|
||||||
|
return row ? (JSON.parse(row.payloadJson) as UpdateSnapshot) : null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user