feat(backend): image endpoints toegevoegd
This commit is contained in:
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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