feat: Implement container customization and add Portainer installation/removal playbooks.
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

This commit is contained in:
2025-12-27 11:02:24 -05:00
parent 661d005fc7
commit 8affa0f8b7
30 changed files with 1767 additions and 22 deletions
+107 -1
View File
@@ -4,7 +4,14 @@ 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.core.dependencies import (
get_db,
verify_api_key,
get_current_user,
get_current_user_optional,
require_admin,
)
from app.crud.container_customization import ContainerCustomizationRepository
from app.crud.docker_container import DockerContainerRepository
from app.crud.docker_image import DockerImageRepository
from app.crud.docker_volume import DockerVolumeRepository
@@ -18,6 +25,9 @@ from app.schemas.docker import (
DockerContainerResponse,
DockerContainerAggregatedResponse,
DockerContainerAggregatedListResponse,
ContainerCustomizationListResponse,
ContainerCustomizationOut,
ContainerCustomizationUpsert,
DockerImageListResponse,
DockerImageExtendedListResponse,
DockerImageExtendedResponse,
@@ -40,6 +50,72 @@ from app.services.docker_alerts import docker_alerts_service
router = APIRouter()
def _resolve_user_id(user: dict) -> Optional[int]:
# For API-key auth, keep settings in a shared (user_id NULL) namespace.
if user.get("type") == "api_key":
return None
uid = user.get("user_id")
if uid is None:
return None
try:
return int(uid)
except Exception:
return None
@router.get("/container-customizations", response_model=ContainerCustomizationListResponse)
async def list_container_customizations(
user: dict = Depends(get_current_user),
db_session: AsyncSession = Depends(get_db),
):
user_id = _resolve_user_id(user)
repo = ContainerCustomizationRepository(db_session)
items = await repo.list_by_user(user_id)
return ContainerCustomizationListResponse(customizations=[ContainerCustomizationOut.model_validate(x) for x in items])
@router.put(
"/container-customizations/{host_id}/{container_id}",
response_model=ContainerCustomizationOut,
)
async def upsert_container_customization(
host_id: str,
container_id: str,
payload: ContainerCustomizationUpsert,
user: dict = Depends(get_current_user),
db_session: AsyncSession = Depends(get_db),
):
user_id = _resolve_user_id(user)
repo = ContainerCustomizationRepository(db_session)
item = await repo.upsert(
user_id=user_id,
host_id=host_id,
container_id=container_id,
icon_key=payload.icon_key,
icon_color=payload.icon_color,
bg_color=payload.bg_color,
)
await db_session.commit()
await db_session.refresh(item)
return ContainerCustomizationOut.model_validate(item)
@router.delete("/container-customizations/{host_id}/{container_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_container_customization(
host_id: str,
container_id: str,
user: dict = Depends(get_current_user),
db_session: AsyncSession = Depends(get_db),
):
user_id = _resolve_user_id(user)
repo = ContainerCustomizationRepository(db_session)
item = await repo.get_for_user(user_id=user_id, host_id=host_id, container_id=container_id)
if not item:
raise HTTPException(status_code=404, detail="Customisation introuvable")
await repo.delete(item)
await db_session.commit()
# === Docker Hosts ===
@router.get("/hosts", response_model=DockerHostListResponse)
@@ -139,6 +215,7 @@ async def get_all_containers(
health: Optional[str] = Query(None, description="Filter by health status"),
host_id: Optional[str] = Query(None, description="Filter by specific host"),
api_key_valid: bool = Depends(verify_api_key),
user: Optional[dict] = Depends(get_current_user_optional),
db_session: AsyncSession = Depends(get_db),
):
"""List all containers across all Docker hosts."""
@@ -161,6 +238,19 @@ async def get_all_containers(
hosts = await host_repo.list_all()
host_map = {h.id: {"name": h.name, "ip": h.ip_address} for h in hosts}
# Load container customizations for current user (or shared for api_key)
customization_repo = ContainerCustomizationRepository(db_session)
cust_map: dict[tuple[str, str], ContainerCustomizationOut] = {}
try:
user_id = _resolve_user_id(user or {})
customizations = await customization_repo.list_by_user(user_id)
cust_map = {
(c.host_id, c.container_id): ContainerCustomizationOut.model_validate(c)
for c in (customizations or [])
}
except Exception:
cust_map = {}
# Build response with host info
container_responses = []
for c in containers:
@@ -179,6 +269,7 @@ async def get_all_containers(
ports=c.ports,
labels=c.labels,
compose_project=c.compose_project,
customization=cust_map.get((c.host_id, c.container_id)),
created_at=c.created_at,
last_update_at=c.last_update_at
))
@@ -200,6 +291,7 @@ async def get_containers(
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),
user: Optional[dict] = Depends(get_current_user_optional),
db_session: AsyncSession = Depends(get_db),
):
"""List containers for a host."""
@@ -207,6 +299,19 @@ async def get_containers(
containers = await container_repo.list_by_host(host_id, state=state, compose_project=compose_project)
counts = await container_repo.count_by_host(host_id)
customization_repo = ContainerCustomizationRepository(db_session)
cust_map: dict[tuple[str, str], ContainerCustomizationOut] = {}
try:
user_id = _resolve_user_id(user or {})
customizations = await customization_repo.list_by_user(user_id)
cust_map = {
(c.host_id, c.container_id): ContainerCustomizationOut.model_validate(c)
for c in (customizations or [])
if c.host_id == host_id
}
except Exception:
cust_map = {}
return DockerContainerListResponse(
containers=[DockerContainerResponse(
id=c.id,
@@ -220,6 +325,7 @@ async def get_containers(
ports=c.ports,
labels=c.labels,
compose_project=c.compose_project,
customization=cust_map.get((c.host_id, c.container_id)),
created_at=c.created_at,
last_update_at=c.last_update_at
) for c in containers],