import docker from fastapi import APIRouter, Depends, HTTPException, Query from config import DOCKER_HOST from rbac import require_admin import time import logging logger = logging.getLogger(__name__) def docker_retry(fn, max_retries=3, base_delay=1): """Execute fn with exponential backoff. Returns (result, True) or (None, False).""" for attempt in range(max_retries): try: result = fn() return result except Exception as e: delay = base_delay * (2 ** attempt) logger.warning("Docker API error (attempt %d/%d): %s. Retrying in %ds...", attempt+1, max_retries, e, delay) if attempt < max_retries - 1: time.sleep(delay) logger.error("Docker API failed after %d attempts", max_retries) return None router = APIRouter() _client = None def get_docker_client(): """Get or create Docker client (lazy initialization).""" global _client if _client is None: _client = docker.DockerClient(base_url=DOCKER_HOST) return _client @router.get("/containers") def list_containers(): client = get_docker_client() result = docker_retry(lambda: client.containers.list(all=True)) if result is None: return [] return [ { "id": c.short_id, "name": c.name, "status": c.status, "health": (c.attrs.get("State", {}).get("Health", {}) or {}).get("Status", ""), "image": c.image.tags[0] if c.image.tags else str(c.image.id)[:20], "ports": c.ports, } for c in result ] @router.post("/containers/{container_id}/{action}", dependencies=[Depends(require_admin())]) def container_action(container_id: str, action: str): if action not in ("start", "stop", "restart"): raise HTTPException(status_code=400, detail="Invalid action. Must be one of: start, stop, restart") client = get_docker_client() def _do(): c = client.containers.get(container_id) getattr(c, action)() return True if docker_retry(_do) is None: raise HTTPException(status_code=503, detail="Docker API unavailable") return {"ok": True} @router.get("/containers/{container_id}/logs") def container_logs(container_id: str, tail: int = Query(200, le=10000)): client = get_docker_client() def _do(): c = client.containers.get(container_id) return {"logs": c.logs(tail=tail, timestamps=True).decode(errors="replace")} result = docker_retry(_do) if result is None: return {"logs": ""} return result