feat(backend): image endpoints toegevoegd

This commit is contained in:
kodi
2026-02-21 12:04:21 +01:00
parent acbf150e28
commit 1ed7699437
3 changed files with 162 additions and 1 deletions
+1
View File
@@ -3,4 +3,5 @@ WORKDIR /app
RUN apt-get update && apt-get install -y curl systemd && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y curl systemd && rm -rf /var/lib/apt/lists/*
RUN pip install fastapi uvicorn requests-unixsocket pyyaml pytest httpx RUN pip install fastapi uvicorn requests-unixsocket pyyaml pytest httpx
COPY app.py . COPY app.py .
COPY app_images.py .
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
+4 -1
View File
@@ -1,6 +1,7 @@
import os import os
import sys import sys
import subprocess import subprocess
from app_images import init_images_router
from fastapi import FastAPI, HTTPException, Query from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel from pydantic import BaseModel
import requests_unixsocket import requests_unixsocket
@@ -16,7 +17,9 @@ PODMAN_API_BASE = "http+unix://%2Frun%2Fuser%2F1000%2Fpodman%2Fpodman.sock/v5.4.
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(os.path.abspath(__file__))
ALLOWLIST_FILE = os.getenv("ALLOWLIST_FILE", os.path.join(BASE_DIR, "allowed_units.txt")) ALLOWLIST_FILE = os.getenv("ALLOWLIST_FILE", os.path.join(BASE_DIR, "allowed_units.txt"))
WORKLOADS_DIR = "/app/workloads" WORKLOADS_DIR = "/app/workloads"
# --- ROUTERS ---
# Images API lives in a dedicated module to keep this file from growing further.
app.include_router(init_images_router(SESSION, PODMAN_API_BASE))
# --- ADAPTERS (contract-neutral helpers) --- # --- ADAPTERS (contract-neutral helpers) ---
# Centralize Podman socket and systemctl invocation. # Centralize Podman socket and systemctl invocation.
+157
View File
@@ -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