Files
podman-mvp/control/app_images.py
T
kodi e469508570 feat (docs): voeg Swagger UI toe op /docs, lokaal gebundeld
- Swagger UI v5.32.1 lokaal in assets/swagger-ui/ (geen CDN, offline bruikbaar)
- webui/html/docs/index.html: custom pagina die /api/openapi.json laadt
  met requestInterceptor zodat Try it out via same-origin werkt
- Link toegevoegd aan dashboard "Snel acties": API docs ↗ (opent in nieuw tabblad)
- Docstrings toegevoegd aan destructieve endpoints (app_containers, app_images):
  container stop/restart, image remove (batch + single), image prune
  geven nu ⚠️-waarschuwingen in de Swagger UI beschrijving
- Backend rebuild nodig voor docstrings zichtbaar in spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 07:30:51 +01:00

161 lines
5.5 KiB
Python

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