From 2c15b8c06bea0f2e3d50d7424df916f0bff08582 Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Sat, 6 Jun 2026 06:24:43 +0200 Subject: [PATCH] feat(docker): routes de gestion des stacks (settings/roots/scan/list/enable) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rend le flux Docker déclenchable via l'API (prérequis SJ-5/SJ-6) : - GET /machines/:id/docker/settings — settings + racines Compose - POST /machines/:id/docker/roots — déclare/active les racines à scanner - POST /machines/:id/docker/scan — scan passif (background, WS) - GET /machines/:id/docker/stacks — liste stacks + services - PATCH /machines/:id/docker/stacks/:stackId — cycle candidate→enabled→ignored dockerScan: getDockerSettings, listStacks, setStackStatus. Les actions pull-check/apply/down restent réservées aux stacks enabled. Co-Authored-By: Claude Opus 4.8 --- server/routes/docker.ts | 45 ++++++++++++++++++++++++ server/routes/index.ts | 2 ++ server/services/dockerScan.ts | 65 +++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 server/routes/docker.ts diff --git a/server/routes/docker.ts b/server/routes/docker.ts new file mode 100644 index 0000000..a479c28 --- /dev/null +++ b/server/routes/docker.ts @@ -0,0 +1,45 @@ +// server/routes/docker.ts +import { Hono } from "hono"; +import { runAction } from "../services/execute.js"; +import { + getDockerSettings, + setDockerRoots, + listStacks, + setStackStatus, + type StackStatus, +} from "../services/dockerScan.js"; + +export const dockerRoutes = new Hono(); + +// Paramètres Docker (settings + racines Compose déclarées). +dockerRoutes.get("/:id/docker/settings", (c) => c.json(getDockerSettings(c.req.param("id")))); + +// Déclare/active les racines Compose à scanner. +dockerRoutes.post("/:id/docker/roots", async (c) => { + const body = (await c.req.json()) as { paths?: string[]; scanDepth?: number }; + if (!Array.isArray(body.paths)) return c.json({ error: "paths[] requis" }, 400); + setDockerRoots(c.req.param("id"), body.paths, body.scanDepth ?? 4); + return c.json(getDockerSettings(c.req.param("id")), 201); +}); + +// Déclenche un scan (passif) en arrière-plan ; suivi via WebSocket. +dockerRoutes.post("/:id/docker/scan", (c) => { + runAction(c.req.param("id"), "docker_scan").catch((err) => + console.error("[docker_scan]", (err as Error).message), + ); + return c.json({ ok: true, action: "docker_scan" }, 202); +}); + +// Liste les stacks détectés (+ services). +dockerRoutes.get("/:id/docker/stacks", (c) => c.json(listStacks(c.req.param("id")))); + +// Cycle de vie d'un stack : candidate → enabled (validé) → ignored… +dockerRoutes.patch("/:id/docker/stacks/:stackId", async (c) => { + const body = (await c.req.json()) as { status?: StackStatus }; + if (!body.status) return c.json({ error: "status requis" }, 400); + try { + return c.json(setStackStatus(c.req.param("id"), c.req.param("stackId"), body.status)); + } catch (err) { + return c.json({ error: (err as Error).message }, 400); + } +}); diff --git a/server/routes/index.ts b/server/routes/index.ts index 10e8102..2840714 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -3,6 +3,7 @@ import { Hono } from "hono"; import { machinesRoutes } from "./machines.js"; import { actionsRoutes } from "./actions.js"; import { actionRequestsRoutes } from "./actionRequests.js"; +import { dockerRoutes } from "./docker.js"; import { dbRoutes } from "./db.js"; import { getServerCapabilities } from "../services/capabilities.js"; import { getSystemMetrics, getSystemStatus } from "../services/system.js"; @@ -14,4 +15,5 @@ api.get("/system/metrics", (c) => c.json(getSystemMetrics())); api.route("/system/db", dbRoutes); api.route("/machines", machinesRoutes); api.route("/machines", actionsRoutes); +api.route("/machines", dockerRoutes); api.route("/", actionRequestsRoutes); diff --git a/server/services/dockerScan.ts b/server/services/dockerScan.ts index f925af5..283714a 100644 --- a/server/services/dockerScan.ts +++ b/server/services/dockerScan.ts @@ -49,6 +49,71 @@ export function getComposeRoots(machineId: string): string[] { .map((r) => r.path); } +/** Paramètres Docker + racines déclarées d'une machine. */ +export function getDockerSettings(machineId: string) { + const settings = db + .select() + .from(schema.dockerSettings) + .where(eq(schema.dockerSettings.machineId, machineId)) + .get(); + const roots = db + .select() + .from(schema.dockerComposeRoots) + .where(eq(schema.dockerComposeRoots.machineId, machineId)) + .all(); + return { settings: settings ?? null, roots }; +} + +/** Liste les stacks d'une machine avec leurs services. */ +export function listStacks(machineId: string) { + const stacks = db + .select() + .from(schema.dockerComposeStacks) + .where(eq(schema.dockerComposeStacks.machineId, machineId)) + .all(); + return stacks.map((s) => ({ + ...s, + composeFiles: safeParseArray(s.composeFilesJson), + services: db + .select() + .from(schema.dockerStackServices) + .where(eq(schema.dockerStackServices.stackId, s.id)) + .all(), + })); +} + +const STACK_STATUSES = ["candidate", "enabled", "ignored", "error"] as const; +export type StackStatus = (typeof STACK_STATUSES)[number]; + +/** Change le cycle de vie d'un stack (candidate → enabled → …). */ +export function setStackStatus(machineId: string, stackId: string, status: StackStatus) { + if (!STACK_STATUSES.includes(status)) throw new Error(`Statut invalide : ${status}`); + const stack = db + .select() + .from(schema.dockerComposeStacks) + .where(eq(schema.dockerComposeStacks.id, stackId)) + .get(); + if (!stack || stack.machineId !== machineId) throw new Error("Stack introuvable"); + db.update(schema.dockerComposeStacks) + .set({ status, updatedAt: new Date().toISOString() }) + .where(eq(schema.dockerComposeStacks.id, stackId)) + .run(); + return db + .select() + .from(schema.dockerComposeStacks) + .where(eq(schema.dockerComposeStacks.id, stackId)) + .get(); +} + +function safeParseArray(json: string): string[] { + try { + const v = JSON.parse(json); + return Array.isArray(v) ? (v as string[]) : []; + } catch { + return []; + } +} + /** Déclare/active Docker pour une machine + ses racines Compose (idempotent). */ export function setDockerRoots(machineId: string, paths: string[], scanDepth = 4): void { const now = new Date().toISOString();