generated from nathanwoodburn/python-webserver-template
feat: Initial code
This commit is contained in:
152
collectors/docker_hosts.py
Normal file
152
collectors/docker_hosts.py
Normal 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
|
||||
Reference in New Issue
Block a user