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();