import os from fastapi import FastAPI, HTTPException from pydantic import BaseModel import requests_unixsocket import uvicorn app = FastAPI(title="Podman MVP Control Plane", root_path="/api") SESSION = requests_unixsocket.Session() PODMAN_API_BASE = "http+unix://%2Frun%2Fuser%2F1000%2Fpodman%2Fpodman.sock/v5.4.2" WORKLOADS_DIR = "/app/workloads" # --- MODELS --- class FileContent(BaseModel): content: str class FileCreate(BaseModel): path: str content: str @app.get("/workloads") def list_workloads(): yaml_files = [] for root, dirs, files in os.walk(WORKLOADS_DIR): for file in files: if file.endswith((".yaml", ".kube")): rel_path = os.path.relpath(os.path.join(root, file), WORKLOADS_DIR) yaml_files.append(rel_path) return {"workloads": sorted(yaml_files)} @app.get("/workloads/read/{filename:path}") def read_workload(filename: str): path = os.path.join(WORKLOADS_DIR, filename) if not os.path.exists(path): raise HTTPException(status_code=404) with open(path, 'r') as f: return {"filename": filename, "content": f.read()} @app.post("/workloads/save-file") def save_file(file_data: FileCreate): target_path = os.path.join(WORKLOADS_DIR, file_data.path) os.makedirs(os.path.dirname(target_path), exist_ok=True) with open(target_path, 'w') as f: f.write(file_data.content) return {"status": "success"} @app.post("/workloads/deploy/{filename:path}") def deploy_workload(filename: str): path = os.path.join(WORKLOADS_DIR, filename) with open(path, 'r') as f: yaml_content = f.read() url = f"{PODMAN_API_BASE}/libpod/kube/play" return SESSION.post(url, data=yaml_content).json() # --- PODS --- @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 SESSION.get(url).json() @app.post("/actions/{action}/{name}") def take_action(action: str, name: str): pod_name = f"pod{name}" if action == "stop": # Probeer de hele pod te stoppen res = SESSION.post(f"{PODMAN_API_BASE}/libpod/pods/{pod_name}/stop") if res.status_code == 204 or res.status_code == 200: return {"status": "stopped", "target": pod_name} # Fallback: als het geen pod is, stop individuele containers api_containers = SESSION.get(f"{PODMAN_API_BASE}/libpod/containers/json?all=true").json() matching = [c for c in api_containers if name in c['Names'][0]] for c in matching: SESSION.post(f"{PODMAN_API_BASE}/libpod/containers/{c['Id']}/stop") return {"status": "stop-attempted", "count": len(matching)} if action == "start": # Altijd eerst geforceerd de oude pod verwijderen SESSION.delete(f"{PODMAN_API_BASE}/libpod/pods/{pod_name}?force=true") target_path = None for root, dirs, files in os.walk(WORKLOADS_DIR): for f in files: if f.startswith(name) and f.endswith('.yaml'): target_path = os.path.join(root, f) break if target_path: with open(target_path, 'r') as file: yaml_content = file.read() url = f"{PODMAN_API_BASE}/libpod/kube/play" res = SESSION.post(url, data=yaml_content) return res.json() return {"status": "unknown_action"} # --- CONTAINERS --- @app.get("/containers") def list_containers(): # Ook hier ?all=true voor gestopte containers url = f"{PODMAN_API_BASE}/libpod/containers/json?all=true" return SESSION.get(url).json() @app.post("/containers/{action}/{name}") def container_action(action: str, name: str): # Podman API pad: /libpod/containers/{name}/{action} url = f"{PODMAN_API_BASE}/libpod/containers/{name}/{action}" res = SESSION.post(url) return {"status": "success", "code": res.status_code} @app.get("/dashboard") def get_dashboard(): try: api_containers = SESSION.get(f"{PODMAN_API_BASE}/libpod/containers/json?all=true").json() except: api_containers = [] dashboard = {} for root, dirs, files in os.walk(WORKLOADS_DIR): for f in files: if f.endswith(('.yaml', '.kube', '.container')): rel_path = os.path.relpath(os.path.join(root, f), WORKLOADS_DIR) # Gebruik de bestandsnaam zonder extensie als unieke sleutel name_base = f.split('.')[0] # Als we mediaserver.yaml EN mediaserver.kube hebben, zien we ze als 1 item if name_base not in dashboard: # Zoek containers die horen bij dit bestand (fuzzy match op naam) matching = [c for c in api_containers if name_base in c['Names'][0]] status = "stopped" ip = "-" container_count = len(matching) if matching: # Als er minstens één container draait, is de status 'running' status = "running" if any(c['State'] == 'running' for c in matching) else "exited" # Pak IP van de eerste container die een IP heeft for c in matching: nets = c.get('Networks', {}) first_net = next(iter(nets.values()), {}) if nets else {} if first_net.get('IPAddress'): ip = first_net['IPAddress'] break dashboard[name_base] = { "name": name_base, "path": rel_path, "status": status, "ip": ip, "containers": container_count } return list(dashboard.values()) @app.get("/test-hybrid") def test_hybrid(): # 1. Check bestanden op schijf try: files = os.listdir(WORKLOADS_DIR) except Exception as e: return {"error": f"Kan map {WORKLOADS_DIR} niet lezen: {str(e)}"} # 2. Check Podman API try: api_res = SESSION.get(f"{PODMAN_API_BASE}/libpod/containers/json?all=true") api_containers = api_res.json() except Exception as e: api_containers = f"API Fout: {str(e)}" return { "bestanden_gevonden": files, "api_containers_aantal": len(api_containers) if isinstance(api_containers, list) else 0, "api_raw_sample": api_containers[0] if isinstance(api_containers, list) and len(api_containers) > 0 else "Geen containers" } @app.get("/files/tree") def get_file_tree(): tree = [] # os.walk gaat door alle mappen heen for root, dirs, files in os.walk(WORKLOADS_DIR): # JUISTE SYNTAX: os.path.relpath relative_path = os.path.relpath(root, WORKLOADS_DIR) if root != WORKLOADS_DIR else "" # Filter alleen de bestanden die we willen zien valid_files = [f for f in files if f.endswith(('.yaml', '.container', '.kube'))] # Voeg de map toe aan de lijst tree.append({ "path": relative_path, "dirs": dirs, "files": valid_files }) return tree @app.get("/files/read") def read_file_managed(path: str): full_path = os.path.normpath(os.path.join(WORKLOADS_DIR, path)) if not full_path.startswith(os.path.abspath(WORKLOADS_DIR)): raise HTTPException(status_code=403) if not os.path.exists(full_path): raise HTTPException(status_code=404) with open(full_path, 'r') as f: return {"content": f.read()} @app.post("/files/save") def save_file_managed(path: str, data: FileContent): full_path = os.path.normpath(os.path.join(WORKLOADS_DIR, path)) if not full_path.startswith(os.path.abspath(WORKLOADS_DIR)): raise HTTPException(status_code=403) os.makedirs(os.path.dirname(full_path), exist_ok=True) with open(full_path, 'w') as f: f.write(data.content) return {"status": "success", "path": path} #@app.delete("/files/delete") #def delete_file_managed(path: str): # full_path = os.path.normpath(os.path.join(WORKLOADS_DIR, path)) # if not full_path.startswith(os.path.abspath(WORKLOADS_DIR)): raise HTTPException(status_code=403) # if os.path.exists(full_path): # os.remove(full_path) # return {"status": "deleted"} # raise HTTPException(status_code=404) @app.delete("/files/delete") def delete_item(path: str): full_path = os.path.normpath(os.path.join(WORKLOADS_DIR, path)) if not full_path.startswith(os.path.abspath(WORKLOADS_DIR)): raise HTTPException(status_code=403) if not os.path.exists(full_path): raise HTTPException(status_code=404) try: if os.path.isdir(full_path): # Verwijder een lege map os.rmdir(full_path) else: # Verwijder een bestand os.remove(full_path) return {"status": "deleted", "type": "folder" if os.path.isdir(full_path) else "file"} except OSError as e: # Dit gebeurt o.a. als een map niet leeg is raise HTTPException(status_code=400, detail=f"Kan niet verwijderen: {e.strerror}") @app.post("/files/mkdir") def create_directory(path: str): """Maakt een nieuwe map aan binnen de workloads map.""" full_path = os.path.normpath(os.path.join(WORKLOADS_DIR, path)) if not full_path.startswith(os.path.abspath(WORKLOADS_DIR)): raise HTTPException(status_code=403) os.makedirs(full_path, exist_ok=True) return {"status": "directory created"} if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)