From efd4fe46d7fa20d536e82a3b576436a986e9e194 Mon Sep 17 00:00:00 2001 From: kodi Date: Fri, 27 Feb 2026 15:02:53 +0100 Subject: [PATCH] refactor(api): move pods endpoints into app_pods router --- control/app.py | 163 +++++--------------------------------------- control/app_pods.py | 160 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 147 deletions(-) create mode 100644 control/app_pods.py diff --git a/control/app.py b/control/app.py index fb5d726..60bebae 100644 --- a/control/app.py +++ b/control/app.py @@ -3,6 +3,7 @@ import sys import subprocess from app_images import init_images_router from app_files import init_files_router +from app_pods import init_pods_router from app_networks import init_networks_router from fastapi import FastAPI, HTTPException, Query from pydantic import BaseModel @@ -166,62 +167,6 @@ def health(): return {"ok": ok, "podman": {"ok": podman_ok}, "systemd_user": {"reachable": systemd_reachable}} -# --- PODS / CONTAINERS --- -@app.get("/pods") -def list_pods(): - # Cruciaal: ?all=true zorgt dat EXIT_STATE pods ook getoond worden - url = f"{PODMAN_API_BASE}/libpod/pods/json?all=true" - return _podman_get_json(url) - - -@app.post("/actions/{action}/{name}") -def take_action(action: str, name: str): - # Legacy endpoint (keep behavior) - possible_names = [name, f"pod{name}", f"pod-{name}"] - - if action == "start": - # STAP 1: Probeer direct de pod te starten (de 'Cockpit' methode) - for target in possible_names: - res = _podman_post(f"{PODMAN_API_BASE}/libpod/pods/{target}/start") - if res.status_code in (200, 204): - return {"status": "started", "target": target, "method": "direct"} - - # STAP 2: Als direct starten faalt, probeer dan YAML opnieuw te deployen - target_path = None - for ext in (".yaml", ".yml"): - cand = os.path.join(WORKLOADS_DIR, f"{name}{ext}") - if os.path.exists(cand): - target_path = cand - break - - if target_path: - with open(target_path, 'r') as file: - yaml_content = file.read() - res = _podman_post(f"{PODMAN_API_BASE}/libpod/kube/play", data=yaml_content) - - # SPECIALE CASE: Pod bestaat al, forceer dan restart - if res.status_code == 500 and "already exists" in res.text: - print(f"DEBUG: Forceer herstart voor {name} wegens conflict") - for target in possible_names: - _podman_delete(f"{PODMAN_API_BASE}/libpod/pods/{target}?force=true") - # Probeer het nu opnieuw - retry_res = _podman_post(f"{PODMAN_API_BASE}/libpod/kube/play", data=yaml_content) - return retry_res.json() - - return res.json() - - return {"status": "unknown", "method": "no_yaml_found"} - - if action == "stop": - for target in possible_names: - res = _podman_post(f"{PODMAN_API_BASE}/libpod/pods/{target}/stop") - if res.status_code in (200, 204): - return {"status": "stopped", "target": target} - return {"status": "not found"} - - return {"status": "unknown"} - - # --- DASHBOARD HELPERS (contract-neutral, no ordering/sorting changes) --- def _build_pod_to_containers_map(containers: list): @@ -247,42 +192,6 @@ def _map_pod_to_unit(podname: str) -> str | None: return f"{podname}.service" -def _append_podman_pods_dashboard_rows(dashboard: list, api_pods: list, pod_to_containers: dict): - # preserves original api_pods iteration order - for p in api_pods: - name = p.get("Name") - status = p.get("Status", "unknown") - unit = _map_pod_to_unit(name) if name else "" - dashboard.append({ - "Name": name, - "Status": status, - "Containers": pod_to_containers.get(name, []), - "Unit": unit, - "Source": "podman", - }) - - -def _append_defined_pods_dashboard_rows(dashboard: list, by_name: dict, root_dir: str): - # preserves original os.walk order and file iteration order - for root, _, files in os.walk(root_dir): - for f in files: - if f.endswith((".yaml", ".yml")): - base = os.path.splitext(os.path.basename(f))[0] - pod_name = f"pod{base}" - unit_name = _map_pod_to_unit(pod_name) - - if pod_name not in by_name: - code, out = _systemctl(["systemctl", "--user", "is-active", unit_name]) - status = (out or "").strip() or ("active" if code == 0 else "inactive") - dashboard.append({ - "Name": pod_name, - "Status": status, - "Containers": [], - "Unit": unit_name, - "Source": "systemd", - }) - - def _ensure_container_status_field(container: dict): # keep exact existing defaulting behavior if "Status" not in container: @@ -318,27 +227,6 @@ def _legacy_dashboard_item_from_container(c: dict): } -@app.get("/pods-dashboard") -def pods_dashboard(): - dashboard = [] - - # 0) Bouw mapping: pod_name -> [container_names...] - containers = _podman_get_json(f"{PODMAN_API_BASE}/libpod/containers/json?all=true") - pod_to_containers = _build_pod_to_containers_map(containers) - - # 1) A) echte pods - api_pods = _podman_get_json(f"{PODMAN_API_BASE}/libpod/pods/json?all=true") - by_name = {p.get("Name"): p for p in api_pods} - - _append_podman_pods_dashboard_rows(dashboard, api_pods, pod_to_containers) - - # 1) B) defined pods via workloads scan - # Based on YAML files in WORKLOADS_DIR; show even if not running. - _append_defined_pods_dashboard_rows(dashboard, by_name, WORKLOADS_DIR) - - return dashboard - - def _systemd_then_podman(systemd_callable, podman_callable): systemd_res = systemd_callable() if systemd_res is not None: @@ -348,40 +236,6 @@ def _systemd_then_podman(systemd_callable, podman_callable): return podman_callable(None) -def try_systemd_pod_action(action: str, podname: str): - # If systemd unit exists/allowed, prefer it. - unit = _map_pod_to_unit(podname) - if not unit: - return None - code, out = _systemctl(["systemctl", "--user", action, unit]) - return { - "method": "systemd", - "pod": podname, - "unit": unit, - "cmd": f"systemctl --user {action} {unit}", - "exit": code, - "output": out, - } - - -@app.post("/pods/actions/{action}/{podname}") -def pod_action_prefer_systemd(action: str, podname: str): - if action not in ("start", "stop", "restart"): - return {"error": "Invalid action"}, 400 - - def _systemd_call(): - return try_systemd_pod_action(action, podname) - - def _podman_call(systemd_res): - if systemd_res: - note = "systemd failed; falling back to podman" - podman = _podman_action_post("pods", podname, action).json() - return {"method": "systemd_then_podman", "note": note, "systemd": systemd_res, "podman": podman} - return {"method": "podman", "result": _podman_action_post("pods", podname, action).json()} - - return _systemd_then_podman(_systemd_call, _podman_call) - - def find_defined_containers(): defined = {} for root, _, files in os.walk(os.path.join(WORKLOADS_DIR, "systemd")): @@ -625,6 +479,21 @@ def api_daemon_reload(): raise HTTPException(status_code=500, detail=str(e)) +app.include_router(init_pods_router( + SESSION, + PODMAN_API_BASE, + WORKLOADS_DIR, + _podman_get_json, + _podman_post, + _podman_delete, + _systemctl, + _podman_action_post, + _map_pod_to_unit, + _systemd_then_podman, + _build_pod_to_containers_map, +)) + + @app.post("/{action}/{unit}") def api_action(action: str, unit: str): if action not in ("status", "start", "stop", "restart"): diff --git a/control/app_pods.py b/control/app_pods.py new file mode 100644 index 0000000..eb70e3c --- /dev/null +++ b/control/app_pods.py @@ -0,0 +1,160 @@ +import os + +from fastapi import APIRouter + + +def init_pods_router( + session, + podman_api_base: str, + workloads_dir: str, + podman_get_json, + podman_post, + podman_delete, + systemctl_func, + podman_action_post, + map_pod_to_unit, + systemd_then_podman, + build_pod_to_containers_map, +) -> APIRouter: + router = APIRouter(tags=["pods"]) + + def _append_podman_pods_dashboard_rows(dashboard: list, api_pods: list, pod_to_containers: dict): + # preserves original api_pods iteration order + for p in api_pods: + name = p.get("Name") + status = p.get("Status", "unknown") + unit = map_pod_to_unit(name) if name else "" + dashboard.append({ + "Name": name, + "Status": status, + "Containers": pod_to_containers.get(name, []), + "Unit": unit, + "Source": "podman", + }) + + def _append_defined_pods_dashboard_rows(dashboard: list, by_name: dict, root_dir: str): + # preserves original os.walk order and file iteration order + for root, _, files in os.walk(root_dir): + for f in files: + if f.endswith((".yaml", ".yml")): + base = os.path.splitext(os.path.basename(f))[0] + pod_name = f"pod{base}" + unit_name = map_pod_to_unit(pod_name) + + if pod_name not in by_name: + code, out = systemctl_func(["systemctl", "--user", "is-active", unit_name]) + status = (out or "").strip() or ("active" if code == 0 else "inactive") + dashboard.append({ + "Name": pod_name, + "Status": status, + "Containers": [], + "Unit": unit_name, + "Source": "systemd", + }) + + def try_systemd_pod_action(action: str, podname: str): + # If systemd unit exists/allowed, prefer it. + unit = map_pod_to_unit(podname) + if not unit: + return None + code, out = systemctl_func(["systemctl", "--user", action, unit]) + return { + "method": "systemd", + "pod": podname, + "unit": unit, + "cmd": f"systemctl --user {action} {unit}", + "exit": code, + "output": out, + } + + @router.get("/pods") + def list_pods(): + # Cruciaal: ?all=true zorgt dat EXIT_STATE pods ook getoond worden + url = f"{podman_api_base}/libpod/pods/json?all=true" + return podman_get_json(url) + + @router.post("/actions/{action}/{name}") + def take_action(action: str, name: str): + # Legacy endpoint (keep behavior) + possible_names = [name, f"pod{name}", f"pod-{name}"] + + if action == "start": + # STAP 1: Probeer direct de pod te starten (de 'Cockpit' methode) + for target in possible_names: + res = podman_post(f"{podman_api_base}/libpod/pods/{target}/start") + if res.status_code in (200, 204): + return {"status": "started", "target": target, "method": "direct"} + + # STAP 2: Als direct starten faalt, probeer dan YAML opnieuw te deployen + target_path = None + for ext in (".yaml", ".yml"): + cand = os.path.join(workloads_dir, f"{name}{ext}") + if os.path.exists(cand): + target_path = cand + break + + if target_path: + with open(target_path, 'r') as file: + yaml_content = file.read() + res = podman_post(f"{podman_api_base}/libpod/kube/play", data=yaml_content) + + # SPECIALE CASE: Pod bestaat al, forceer dan restart + if res.status_code == 500 and "already exists" in res.text: + print(f"DEBUG: Forceer herstart voor {name} wegens conflict") + for target in possible_names: + podman_delete(f"{podman_api_base}/libpod/pods/{target}?force=true") + # Probeer het nu opnieuw + retry_res = podman_post(f"{podman_api_base}/libpod/kube/play", data=yaml_content) + return retry_res.json() + + return res.json() + + return {"status": "unknown", "method": "no_yaml_found"} + + if action == "stop": + for target in possible_names: + res = podman_post(f"{podman_api_base}/libpod/pods/{target}/stop") + if res.status_code in (200, 204): + return {"status": "stopped", "target": target} + return {"status": "not found"} + + return {"status": "unknown"} + + @router.get("/pods-dashboard") + def pods_dashboard(): + dashboard = [] + + # 0) Bouw mapping: pod_name -> [container_names...] + containers = podman_get_json(f"{podman_api_base}/libpod/containers/json?all=true") + pod_to_containers = build_pod_to_containers_map(containers) + + # 1) A) echte pods + api_pods = podman_get_json(f"{podman_api_base}/libpod/pods/json?all=true") + by_name = {p.get("Name"): p for p in api_pods} + + _append_podman_pods_dashboard_rows(dashboard, api_pods, pod_to_containers) + + # 1) B) defined pods via workloads scan + # Based on YAML files in WORKLOADS_DIR; show even if not running. + _append_defined_pods_dashboard_rows(dashboard, by_name, workloads_dir) + + return dashboard + + @router.post("/pods/actions/{action}/{podname}") + def pod_action_prefer_systemd(action: str, podname: str): + if action not in ("start", "stop", "restart"): + return {"error": "Invalid action"}, 400 + + def _systemd_call(): + return try_systemd_pod_action(action, podname) + + def _podman_call(systemd_res): + if systemd_res: + note = "systemd failed; falling back to podman" + podman = podman_action_post("pods", podname, action).json() + return {"method": "systemd_then_podman", "note": note, "systemd": systemd_res, "podman": podman} + return {"method": "podman", "result": podman_action_post("pods", podname, action).json()} + + return systemd_then_podman(_systemd_call, _podman_call) + + return router