generated from nathanwoodburn/python-webserver-template
153 lines
6.6 KiB
Python
153 lines
6.6 KiB
Python
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
|