feat: Initial code
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 1m4s
Build Docker / BuildImage (push) Successful in 1m26s

This commit is contained in:
2026-03-26 23:07:05 +11:00
parent d8ede00901
commit 0ce79935d7
24 changed files with 2527 additions and 143 deletions

152
collectors/docker_hosts.py Normal file
View File

@@ -0,0 +1,152 @@
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