from __future__ import annotations import io import os import tarfile import tempfile from pathlib import Path from typing import List from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel class ImageRemoveRequest(BaseModel): images: List[str] force: bool = False ignore: bool = False class ImageBuildRequest(BaseModel): # paden RELATIEF t.o.v. /app/workloads (Files tab) context_dir: str dockerfile: str # bv "Dockerfile" of "subdir/Dockerfile" binnen context tag: str # bv "localhost/testimg:latest" pull: bool = False nocache: bool = False ## Helpers ## def _safe_join(root: Path, rel: str) -> Path: p = (root / rel).resolve() root_resolved = root.resolve() if root_resolved not in p.parents and p != root_resolved: raise HTTPException(status_code=400, detail="Path escapes workloads root") return p def _create_context_tar(context_dir: Path) -> str: # Maak tar in /tmp om niet alles in RAM te houden tmp = tempfile.NamedTemporaryFile(prefix="podman-mvp-buildctx-", suffix=".tar", delete=False) tmp_path = tmp.name tmp.close() with tarfile.open(tmp_path, "w") as tf: # Voeg alles toe uit context_dir for root, dirs, files in os.walk(context_dir): root_path = Path(root) for name in files: fp = root_path / name # tar-path moet relatief zijn aan context_dir arcname = fp.relative_to(context_dir) tf.add(fp, arcname=str(arcname)) return tmp_path ## Einde Helpers def _raise_on_error(resp): if 200 <= resp.status_code < 300: return # Podman API geeft vaak JSON error-body; maar text is altijd safe raise HTTPException(status_code=resp.status_code, detail=resp.text) def init_images_router(session, podman_api_base: str) -> APIRouter: router = APIRouter(prefix="/images", tags=["images"]) @router.get("") def list_images(): url = f"{podman_api_base}/libpod/images/json" resp = session.get(url) _raise_on_error(resp) return resp.json() # --- STAP 2: remove selected (batch) --- @router.post("/remove") def remove_images(req: ImageRemoveRequest): """⚠️ Destructief: verwijdert één of meerdere images permanent. Niet terug te draaien.""" # Libpod heeft batch remove via query params (images=...). url = f"{podman_api_base}/libpod/images/remove" params = { "images": req.images, "force": str(req.force).lower(), "ignore": str(req.ignore).lower(), } resp = session.delete(url, params=params) _raise_on_error(resp) return resp.json() # Convenience: delete single image (handig voor UI per-row) @router.delete("/{image_ref:path}") def remove_image( image_ref: str, force: bool = Query(False), ignore: bool = Query(False), ): """⚠️ Destructief: verwijdert één image permanent op basis van naam of ID.""" url = f"{podman_api_base}/libpod/images/remove" params = { "images": [image_ref], "force": str(force).lower(), "ignore": str(ignore).lower(), } resp = session.delete(url, params=params) _raise_on_error(resp) return resp.json() # --- STAP 2: prune (dangling default, all=true => unused) --- @router.post("/prune") def prune_images(all: bool = Query(False)): """⚠️ Destructief: verwijdert dangling images (standaard) of alle ongebruikte images (`all=true`).""" url = f"{podman_api_base}/libpod/images/prune" params = {"all": str(all).lower()} resp = session.post(url, params=params) _raise_on_error(resp) return resp.json() @router.post("/build") def build_image(req: ImageBuildRequest): if not req.context_dir.startswith("systemd/"): raise HTTPException(status_code=400, detail="context_dir must start with systemd/") workloads_root = Path("/app/workloads") context_dir = _safe_join(workloads_root, req.context_dir) if not context_dir.is_dir(): raise HTTPException(status_code=400, detail="context_dir is not a directory") dockerfile_path = (context_dir / req.dockerfile).resolve() if context_dir.resolve() not in dockerfile_path.parents: raise HTTPException(status_code=400, detail="dockerfile must be inside context_dir") if not dockerfile_path.is_file(): raise HTTPException(status_code=400, detail="dockerfile not found") tar_path = _create_context_tar(context_dir) try: url = f"{podman_api_base}/build" params = { "dockerfile": str(Path(req.dockerfile)), "t": req.tag, "pull": str(req.pull).lower(), "nocache": str(req.nocache).lower(), } with open(tar_path, "rb") as f: resp = session.post( url, params=params, data=f, headers={"Content-Type": "application/x-tar"}, ) _raise_on_error(resp) # Build API geeft doorgaans JSON-lines/stream tekst terug; voor MVP geven we raw text terug. return {"ok": True, "output": resp.text} finally: try: os.unlink(tar_path) except OSError: pass return router