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
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:
+107
-1
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user