104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from app.clients.beszel_client import BeszelClient
|
|
from app.clients.base import BaseHTTPClient
|
|
from app.config import Settings
|
|
from app.models.sources import DockerContainerSummary, DockerSnapshot
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DockerProxyClient(BaseHTTPClient):
|
|
def __init__(self, settings: Settings) -> None:
|
|
super().__init__(settings, "docker-proxy", settings.docker_proxy_base_url)
|
|
self.beszel_client = BeszelClient(settings)
|
|
|
|
async def fetch_containers(self) -> DockerSnapshot:
|
|
snapshot = DockerSnapshot()
|
|
payload = await self._request_json("GET", "/containers/json", params={"all": "true"})
|
|
if not isinstance(payload, list):
|
|
logger.warning("docker proxy returned non-list payload: %s", payload)
|
|
fallback = await self.beszel_client.fetch_container_snapshot()
|
|
if fallback.source_status == "online":
|
|
logger.info("docker proxy fallback to beszel containers succeeded")
|
|
return fallback
|
|
logger.warning(
|
|
"docker integration unavailable: docker proxy unreachable and beszel container fallback returned no usable data"
|
|
)
|
|
return snapshot
|
|
|
|
logger.info("docker proxy raw payload count: %s", len(payload))
|
|
logger.info("docker proxy raw payload sample: %s", payload[:3])
|
|
|
|
containers: list[DockerContainerSummary] = []
|
|
running = 0
|
|
stopped = 0
|
|
unhealthy = 0
|
|
|
|
for item in payload:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
|
|
state = self._normalize_state(item)
|
|
if state == "running":
|
|
running += 1
|
|
elif state == "unhealthy":
|
|
unhealthy += 1
|
|
else:
|
|
stopped += 1
|
|
|
|
containers.append(
|
|
DockerContainerSummary(
|
|
id=str(item.get("Id") or item.get("ID") or ""),
|
|
name=self._normalize_name(item.get("Names")),
|
|
state=state,
|
|
status_text=str(item.get("Status") or item.get("State") or "unknown"),
|
|
image=str(item.get("Image") or ""),
|
|
health=self._extract_health(item),
|
|
)
|
|
)
|
|
|
|
normalized = DockerSnapshot(
|
|
source_status="online",
|
|
running=running,
|
|
stopped=stopped,
|
|
unhealthy=unhealthy,
|
|
total=len(containers),
|
|
containers=containers,
|
|
)
|
|
logger.info("docker proxy normalized snapshot: %s", normalized.model_dump())
|
|
return normalized
|
|
|
|
@staticmethod
|
|
def _normalize_name(names: object) -> str:
|
|
if isinstance(names, list) and names:
|
|
return str(names[0]).lstrip("/")
|
|
return "unknown"
|
|
|
|
@classmethod
|
|
def _normalize_state(cls, item: dict) -> str:
|
|
status_text = str(item.get("Status") or "").lower()
|
|
state = str(item.get("State") or "").lower()
|
|
health = cls._extract_health(item)
|
|
|
|
if health == "unhealthy" or "unhealthy" in status_text:
|
|
return "unhealthy"
|
|
if state == "running":
|
|
return "running"
|
|
if state:
|
|
return "stopped"
|
|
return "unknown"
|
|
|
|
@staticmethod
|
|
def _extract_health(item: dict) -> str | None:
|
|
if isinstance(item.get("Health"), str):
|
|
return str(item["Health"]).lower()
|
|
if isinstance(item.get("State"), dict):
|
|
health = item["State"].get("Health")
|
|
if isinstance(health, dict):
|
|
return str(health.get("Status") or "").lower() or None
|
|
return None
|