feat(backend): image endpoints toegevoegd
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
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):
|
||||
# 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),
|
||||
):
|
||||
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)):
|
||||
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
|
||||
Reference in New Issue
Block a user