257 lines
9.8 KiB
Python
257 lines
9.8 KiB
Python
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_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):
|
|
# We dwingen af dat als het pad niet met systemd begint, we het toevoegen
|
|
if not path.startswith("systemd"):
|
|
path = os.path.join("systemd", path)
|
|
|
|
full_path = os.path.normpath(os.path.join(WORKLOADS_DIR, path))
|
|
# Veiligheidscheck blijft gelijk
|
|
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", "path": path}
|
|
|
|
@app.get("/containers/logs/{name}")
|
|
def get_container_logs(name: str):
|
|
# We vragen de laatste 100 regels op (tail=100)
|
|
res = SESSION.get(f"{PODMAN_API_BASE}/libpod/containers/{name}/logs?stdout=true&stderr=true&tail=100")
|
|
# Podman logs komen vaak met wat binaire metadata, we decoden dit als tekst
|
|
return {"logs": res.text}
|
|
|
|
@app.get("/containers/inspect/{name}")
|
|
def inspect_container(name: str):
|
|
res = SESSION.get(f"{PODMAN_API_BASE}/libpod/containers/{name}/json")
|
|
return res.json()
|
|
|
|
if __name__ == "__main__":
|
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|