163 lines
5.6 KiB
Python
163 lines
5.6 KiB
Python
"""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"}
|