"""YakPanel - Docker API - list/start/stop containers via docker CLI""" from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from app.core.utils import exec_shell_sync from app.api.auth import get_current_user from app.models.user import User router = APIRouter(prefix="/docker", tags=["docker"]) class RunContainerRequest(BaseModel): image: str name: str = "" ports: str = "" cmd: str = "" @router.get("/containers") async def docker_containers(current_user: User = Depends(get_current_user)): """List Docker containers (docker ps -a)""" out, err = exec_shell_sync( 'docker ps -a --format "{{.ID}}\t{{.Image}}\t{{.Status}}\t{{.Names}}\t{{.Ports}}"', timeout=10, ) if err and "Cannot connect" in err: return {"containers": [], "error": "Docker not available"} containers = [] for line in out.strip().split("\n"): line = line.strip() if not line: continue parts = line.split("\t", 4) if len(parts) >= 5: containers.append({ "id": parts[0][:12], "id_full": parts[0], "image": parts[1], "status": parts[2], "names": parts[3], "ports": parts[4], }) elif len(parts) >= 4: containers.append({ "id": parts[0][:12], "id_full": parts[0], "image": parts[1], "status": parts[2], "names": parts[3], "ports": "", }) return {"containers": containers} @router.get("/images") async def docker_images(current_user: User = Depends(get_current_user)): """List Docker images (docker images)""" out, err = exec_shell_sync( 'docker images --format "{{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}"', timeout=10, ) if err and "Cannot connect" in err: return {"images": [], "error": "Docker not available"} images = [] for line in out.strip().split("\n"): line = line.strip() if not line: continue parts = line.split("\t", 3) if len(parts) >= 4: images.append({ "repository": parts[0], "tag": parts[1], "id": parts[2], "size": parts[3], }) return {"images": images} @router.post("/pull") async def docker_pull( image: str, current_user: User = Depends(get_current_user), ): """Pull Docker image (docker pull)""" if not image or " " in image or "'" in image or '"' in image: raise HTTPException(status_code=400, detail="Invalid image name") out, err = exec_shell_sync(f"docker pull {image}", timeout=600) if err and "error" in err.lower(): raise HTTPException(status_code=500, detail=err.strip() or out.strip()) return {"status": True, "msg": "Pulled"} @router.post("/run") async def docker_run( body: RunContainerRequest, current_user: User = Depends(get_current_user), ): """Run a new container (docker run -d)""" image = (body.image or "").strip() if not image: raise HTTPException(status_code=400, detail="Image required") if " " in image or "'" in image or '"' in image: raise HTTPException(status_code=400, detail="Invalid image name") cmd = f"docker run -d {image}" if body.name: name = body.name.strip().replace(" ", "-") if name and all(c.isalnum() or c in "-_" for c in name): cmd += f" --name {name}" if body.ports: for p in body.ports.replace(",", " ").split(): p = p.strip() if p: cmd += f" -p {p}" if ":" in p else f" -p {p}:{p}" if body.cmd: cmd += f" {body.cmd}" out, err = exec_shell_sync(cmd, timeout=60) if err and "error" in err.lower(): raise HTTPException(status_code=500, detail=err.strip() or out.strip()) return {"status": True, "msg": "Container started", "id": out.strip()[:12]} @router.post("/{container_id}/start") async def docker_start( container_id: str, current_user: User = Depends(get_current_user), ): """Start container""" if " " in container_id or "'" in container_id or '"' in container_id: raise HTTPException(status_code=400, detail="Invalid container ID") out, err = exec_shell_sync(f"docker start {container_id}", timeout=30) if err and "error" in err.lower(): raise HTTPException(status_code=500, detail=err.strip() or out.strip()) return {"status": True, "msg": "Started"} @router.post("/{container_id}/stop") async def docker_stop( container_id: str, current_user: User = Depends(get_current_user), ): """Stop container""" if " " in container_id or "'" in container_id or '"' in container_id: raise HTTPException(status_code=400, detail="Invalid container ID") out, err = exec_shell_sync(f"docker stop {container_id}", timeout=30) if err and "error" in err.lower(): raise HTTPException(status_code=500, detail=err.strip() or out.strip()) return {"status": True, "msg": "Stopped"} @router.post("/{container_id}/restart") async def docker_restart( container_id: str, current_user: User = Depends(get_current_user), ): """Restart container""" if " " in container_id or "'" in container_id or '"' in container_id: raise HTTPException(status_code=400, detail="Invalid container ID") out, err = exec_shell_sync(f"docker restart {container_id}", timeout=30) if err and "error" in err.lower(): raise HTTPException(status_code=500, detail=err.strip() or out.strip()) return {"status": True, "msg": "Restarted"}