from typing import Dict, List import importlib import requests from config import AppConfig from .base import BaseCollector, CollectionResult class DockerHostsCollector(BaseCollector): source_name = "docker" def __init__(self, config: AppConfig): self.config = config def collect(self) -> CollectionResult: if not self.config.docker_enabled: return CollectionResult(source=self.source_name, assets=[], status="disabled") if not self.config.docker_hosts and not self.config.docker_agent_endpoints: return CollectionResult( source=self.source_name, assets=[], status="skipped", error="No DOCKER_HOSTS or DOCKER_AGENT_ENDPOINTS configured", ) assets: List[Dict] = [] errors: List[str] = [] headers = {"Accept": "application/json"} if self.config.docker_bearer_token: headers["Authorization"] = f"Bearer {self.config.docker_bearer_token}" agent_headers = {"Accept": "application/json"} if self.config.docker_agent_token: agent_headers["Authorization"] = f"Bearer {self.config.docker_agent_token}" for endpoint in self.config.docker_agent_endpoints: base = endpoint.rstrip("/") try: resp = requests.get( f"{base}/api/v1/containers", headers=agent_headers, timeout=self.config.request_timeout_seconds, ) resp.raise_for_status() payload = resp.json() containers = payload.get("containers", []) if isinstance(payload, dict) else payload for container in containers: assets.append( { "asset_type": "container", "external_id": container.get("id", "unknown-container"), "name": container.get("name", "unknown"), "hostname": container.get("name", "unknown"), "status": container.get("state", "unknown"), "ip_addresses": container.get("ip_addresses", []), "node": endpoint, "metadata": { "image": container.get("image", "unknown"), "ports": container.get("ports", []), "networks": container.get("networks", []), "labels": container.get("labels", {}), "collected_via": "docker-agent", }, } ) except Exception as exc: errors.append(f"{endpoint}: {exc}") for host in self.config.docker_hosts: if host.startswith("unix://") or host.startswith("tcp://"): try: assets.extend(self._collect_via_docker_sdk(host)) except Exception as exc: errors.append(f"{host}: {exc}") continue base = host.rstrip("/") try: resp = requests.get( f"{base}/containers/json?all=1", headers=headers, timeout=self.config.request_timeout_seconds, ) resp.raise_for_status() for container in resp.json(): ports = container.get("Ports", []) networks = list((container.get("NetworkSettings", {}) or {}).get("Networks", {}).keys()) assets.append( { "asset_type": "container", "external_id": container.get("Id", "unknown-container"), "name": (container.get("Names", ["unknown"])[0] or "unknown").lstrip("/"), "hostname": container.get("Names", ["unknown"])[0].lstrip("/"), "status": container.get("State", "unknown"), "ip_addresses": [], "node": host, "metadata": { "image": container.get("Image"), "ports": ports, "networks": networks, "collected_via": "docker-host-api", }, } ) except Exception as exc: errors.append(f"{host}: {exc}") if errors and not assets: return CollectionResult(source=self.source_name, assets=[], status="error", error=" | ".join(errors)) if errors: return CollectionResult(source=self.source_name, assets=assets, status="degraded", error=" | ".join(errors)) return CollectionResult(source=self.source_name, assets=assets, status="ok") def _collect_via_docker_sdk(self, host: str) -> List[Dict]: try: docker_sdk = importlib.import_module("docker") except Exception as exc: raise RuntimeError(f"Docker SDK unavailable: {exc}") from exc assets: List[Dict] = [] client = docker_sdk.DockerClient(base_url=host) try: for container in client.containers.list(all=True): ports = container.attrs.get("NetworkSettings", {}).get("Ports", {}) networks = list((container.attrs.get("NetworkSettings", {}).get("Networks", {}) or {}).keys()) state = container.attrs.get("State", {}).get("Status", "unknown") image_obj = container.image image_name = "unknown" if image_obj is not None: image_tags = image_obj.tags or [] image_name = image_tags[0] if image_tags else image_obj.id assets.append( { "asset_type": "container", "external_id": container.id, "name": container.name, "hostname": container.name, "status": state, "ip_addresses": [], "node": host, "metadata": { "image": image_name, "ports": ports, "networks": networks, "collected_via": "docker-sdk", }, } ) finally: client.close() return assets