Bruno Charest 68a9b0f390
Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Remove Node.js cache files containing npm vulnerability data for vitest and vite packages
2025-12-15 20:36:06 -05:00

426 lines
14 KiB
Python

"""Docker management API routes."""
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.dependencies import get_db, verify_api_key, get_current_user, require_admin
from app.crud.docker_container import DockerContainerRepository
from app.crud.docker_image import DockerImageRepository
from app.crud.docker_volume import DockerVolumeRepository
from app.crud.docker_alert import DockerAlertRepository
from app.crud.host import HostRepository
from app.schemas.docker import (
DockerHostListResponse,
DockerHostInfo,
EnableDockerRequest,
DockerContainerListResponse,
DockerContainerResponse,
DockerImageListResponse,
DockerImageExtendedListResponse,
DockerImageExtendedResponse,
DockerVolumeListResponse,
DockerAlertListResponse,
DockerAlertAcknowledgeRequest,
ContainerActionResponse,
ContainerLogsRequest,
ContainerLogsResponse,
ContainerInspectResponse,
DockerCollectResponse,
DockerCollectAllResponse,
DockerStatsResponse,
ImageActionResponse,
)
from app.services.docker_service import docker_service
from app.services.docker_actions import docker_actions
from app.services.docker_alerts import docker_alerts_service
router = APIRouter()
# === Docker Hosts ===
@router.get("/hosts", response_model=DockerHostListResponse)
async def list_docker_hosts(
api_key_valid: bool = Depends(verify_api_key),
):
"""List all hosts with Docker information."""
hosts = await docker_service.get_docker_hosts()
enabled = sum(1 for h in hosts if h.get("docker_enabled"))
return DockerHostListResponse(
hosts=[DockerHostInfo(**h) for h in hosts],
total=len(hosts),
enabled=enabled
)
@router.post("/hosts/{host_id}/enable")
async def enable_docker_monitoring(
host_id: str,
request: EnableDockerRequest = None,
api_key_valid: bool = Depends(verify_api_key),
):
"""Enable or disable Docker monitoring on a host."""
enabled = request.enabled if request else True
success = await docker_service.enable_docker_monitoring(host_id, enabled)
if not success:
raise HTTPException(status_code=404, detail="Host not found")
action = "enabled" if enabled else "disabled"
return {"message": f"Docker monitoring {action} for host", "host_id": host_id}
@router.post("/hosts/{host_id}/collect", response_model=DockerCollectResponse)
async def collect_docker_now(
host_id: str,
api_key_valid: bool = Depends(verify_api_key),
):
"""Force an immediate Docker collection on a host."""
result = await docker_service.collect_docker_host(host_id)
return DockerCollectResponse(
success=result.get("success", False),
host_id=host_id,
host_name=result.get("host_name", ""),
message="Collection completed" if result.get("success") else "Collection failed",
docker_version=result.get("docker_version"),
containers_count=result.get("containers_count", 0),
images_count=result.get("images_count", 0),
volumes_count=result.get("volumes_count", 0),
duration_ms=result.get("duration_ms", 0),
error=result.get("error")
)
@router.post("/collect-all", response_model=DockerCollectAllResponse)
async def collect_all_docker_hosts(
api_key_valid: bool = Depends(verify_api_key),
):
"""Collect Docker info from all enabled hosts."""
result = await docker_service.collect_all_hosts()
normalized_results = []
for r in result.get("results", []):
if not isinstance(r, dict):
continue
normalized_results.append({
"success": bool(r.get("success", False)),
"host_id": r.get("host_id", ""),
"host_name": r.get("host_name", ""),
"message": r.get("message") or ("Collection completed" if r.get("success") else "Collection failed"),
"docker_version": r.get("docker_version"),
"containers_count": r.get("containers_count", 0),
"images_count": r.get("images_count", 0),
"volumes_count": r.get("volumes_count", 0),
"duration_ms": r.get("duration_ms", 0),
"error": r.get("error"),
})
return DockerCollectAllResponse(
success=result.get("success", False),
message=f"Collected from {result.get('successful', 0)}/{result.get('total_hosts', 0)} hosts",
total_hosts=result.get("total_hosts", 0),
successful=result.get("successful", 0),
failed=result.get("failed", 0),
results=[DockerCollectResponse(**r) for r in normalized_results]
)
# === Containers ===
@router.get("/hosts/{host_id}/containers", response_model=DockerContainerListResponse)
async def get_containers(
host_id: str,
state: Optional[str] = Query(None, description="Filter by state"),
compose_project: Optional[str] = Query(None, description="Filter by compose project"),
api_key_valid: bool = Depends(verify_api_key),
db_session: AsyncSession = Depends(get_db),
):
"""List containers for a host."""
container_repo = DockerContainerRepository(db_session)
containers = await container_repo.list_by_host(host_id, state=state, compose_project=compose_project)
counts = await container_repo.count_by_host(host_id)
return DockerContainerListResponse(
containers=[DockerContainerResponse(
id=c.id,
host_id=c.host_id,
container_id=c.container_id,
name=c.name,
image=c.image,
state=c.state,
status=c.status,
health=c.health,
ports=c.ports,
labels=c.labels,
compose_project=c.compose_project,
created_at=c.created_at,
last_update_at=c.last_update_at
) for c in containers],
total=counts.get("total", 0),
running=counts.get("running", 0),
stopped=counts.get("total", 0) - counts.get("running", 0)
)
@router.post("/containers/{host_id}/{container_id}/start", response_model=ContainerActionResponse)
async def start_container(
host_id: str,
container_id: str,
user: dict = Depends(get_current_user),
):
"""Start a stopped container."""
result = await docker_actions.start_container(host_id, container_id)
return ContainerActionResponse(**result)
@router.post("/containers/{host_id}/{container_id}/stop", response_model=ContainerActionResponse)
async def stop_container(
host_id: str,
container_id: str,
timeout: int = Query(10, ge=1, le=300),
user: dict = Depends(get_current_user),
):
"""Stop a running container."""
result = await docker_actions.stop_container(host_id, container_id, timeout=timeout)
return ContainerActionResponse(**result)
@router.post("/containers/{host_id}/{container_id}/restart", response_model=ContainerActionResponse)
async def restart_container(
host_id: str,
container_id: str,
timeout: int = Query(10, ge=1, le=300),
user: dict = Depends(get_current_user),
):
"""Restart a container."""
result = await docker_actions.restart_container(host_id, container_id, timeout=timeout)
return ContainerActionResponse(**result)
@router.post("/containers/{host_id}/{container_id}/remove", response_model=ContainerActionResponse)
async def remove_container(
host_id: str,
container_id: str,
force: bool = Query(False, description="Force remove running container"),
remove_volumes: bool = Query(False, description="Remove associated volumes"),
user: dict = Depends(require_admin),
):
"""Remove a container (admin only)."""
result = await docker_actions.remove_container(
host_id, container_id, force=force, remove_volumes=remove_volumes
)
return ContainerActionResponse(**result)
@router.post("/containers/{host_id}/{container_id}/redeploy", response_model=ContainerActionResponse)
async def redeploy_container(
host_id: str,
container_id: str,
user: dict = Depends(get_current_user),
):
"""Redeploy a container by pulling latest image."""
result = await docker_actions.redeploy_container(host_id, container_id)
return ContainerActionResponse(**result)
@router.get("/containers/{host_id}/{container_id}/logs", response_model=ContainerLogsResponse)
async def get_container_logs(
host_id: str,
container_id: str,
tail: int = Query(200, ge=1, le=5000),
timestamps: bool = Query(False),
since: Optional[str] = Query(None),
api_key_valid: bool = Depends(verify_api_key),
):
"""Get container logs."""
result = await docker_actions.get_container_logs(
host_id, container_id, tail=tail, timestamps=timestamps, since=since
)
return ContainerLogsResponse(**result)
@router.get("/containers/{host_id}/{container_id}/inspect", response_model=ContainerInspectResponse)
async def inspect_container(
host_id: str,
container_id: str,
api_key_valid: bool = Depends(verify_api_key),
):
"""Get detailed container information."""
result = await docker_actions.inspect_container(host_id, container_id)
return ContainerInspectResponse(**result)
# === Images ===
@router.get("/hosts/{host_id}/images", response_model=DockerImageExtendedListResponse)
async def get_images(
host_id: str,
api_key_valid: bool = Depends(verify_api_key),
db_session: AsyncSession = Depends(get_db),
):
"""List images for a host with usage information."""
image_repo = DockerImageRepository(db_session)
container_repo = DockerContainerRepository(db_session)
images = await image_repo.list_by_host(host_id)
counts = await image_repo.count_by_host(host_id)
# Get all container images to determine which images are in use
containers = await container_repo.list_by_host(host_id)
used_images = set()
for c in containers:
if c.image:
used_images.add(c.image)
# Also add the image without tag for matching
if ':' in c.image:
used_images.add(c.image.split(':')[0])
unused_count = 0
images_with_usage = []
for img in images:
# Check if image is in use by comparing repo_tags
in_use = False
if img.repo_tags:
for tag in img.repo_tags:
if tag in used_images:
in_use = True
break
# Check without tag
if ':' in tag and tag.split(':')[0] in used_images:
in_use = True
break
if not in_use:
unused_count += 1
images_with_usage.append({
"id": img.id,
"host_id": img.host_id,
"image_id": img.image_id,
"repo_tags": img.repo_tags,
"size": img.size,
"created": img.created,
"last_update_at": img.last_update_at,
"in_use": in_use
})
return DockerImageExtendedListResponse(
images=[DockerImageExtendedResponse(**img) for img in images_with_usage],
total=counts.get("total", 0),
total_size=counts.get("total_size", 0),
unused_count=unused_count
)
@router.delete("/images/{host_id}/{image_id}", response_model=ImageActionResponse)
async def remove_image(
host_id: str,
image_id: str,
force: bool = Query(False, description="Force remove even if image is in use"),
user: dict = Depends(require_admin),
):
"""Remove a Docker image (admin only)."""
result = await docker_actions.remove_image(host_id, image_id, force=force)
return ImageActionResponse(**result)
# === Volumes ===
@router.get("/hosts/{host_id}/volumes", response_model=DockerVolumeListResponse)
async def get_volumes(
host_id: str,
api_key_valid: bool = Depends(verify_api_key),
db_session: AsyncSession = Depends(get_db),
):
"""List volumes for a host."""
volume_repo = DockerVolumeRepository(db_session)
volumes = await volume_repo.list_by_host(host_id)
total = await volume_repo.count_by_host(host_id)
return DockerVolumeListResponse(
volumes=[{
"id": vol.id,
"host_id": vol.host_id,
"name": vol.name,
"driver": vol.driver,
"mountpoint": vol.mountpoint,
"scope": vol.scope,
"last_update_at": vol.last_update_at
} for vol in volumes],
total=total
)
# === Alerts ===
@router.get("/alerts", response_model=DockerAlertListResponse)
async def list_alerts(
host_id: Optional[str] = Query(None),
state: Optional[str] = Query("open", description="Filter by state: open, closed, acknowledged"),
severity: Optional[str] = Query(None, description="Filter by severity"),
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
api_key_valid: bool = Depends(verify_api_key),
):
"""List Docker alerts."""
result = await docker_alerts_service.get_alerts(
host_id=host_id,
state=state,
severity=severity,
limit=limit,
offset=offset
)
return DockerAlertListResponse(
alerts=result.get("alerts", []),
total=result.get("total", 0),
open_count=result.get("open_count", 0),
acknowledged_count=result.get("acknowledged_count", 0)
)
@router.post("/alerts/{alert_id}/acknowledge")
async def acknowledge_alert(
alert_id: int,
request: DockerAlertAcknowledgeRequest = None,
user: dict = Depends(get_current_user),
):
"""Acknowledge an alert."""
username = user.get("username", "unknown")
note = request.note if request else None
result = await docker_alerts_service.acknowledge_alert(alert_id, username, note)
if not result:
raise HTTPException(status_code=404, detail="Alert not found or already acknowledged")
return {"message": "Alert acknowledged", "alert": result}
@router.post("/alerts/{alert_id}/close")
async def close_alert(
alert_id: int,
user: dict = Depends(get_current_user),
):
"""Close an alert manually."""
result = await docker_alerts_service.close_alert(alert_id)
if not result:
raise HTTPException(status_code=404, detail="Alert not found or already closed")
return {"message": "Alert closed", "alert": result}
# === Stats ===
@router.get("/stats", response_model=DockerStatsResponse)
async def get_docker_stats(
api_key_valid: bool = Depends(verify_api_key),
):
"""Get global Docker statistics."""
stats = await docker_alerts_service.get_stats()
return DockerStatsResponse(**stats)