"""Docker network management.""" from __future__ import annotations from typing import Optional from docker.errors import APIError from docker.types import IPAMConfig, IPAMPool from docker_client import DockerError, get_client, safe_call COMPOSE_PROJECT_LABEL = "com.docker.compose.project" DEFAULT_NETWORKS = {"bridge", "host", "none"} def _summary(net) -> dict: attrs = net.attrs ipam = (attrs.get("IPAM") or {}).get("Config") or [] subnet = ipam[0].get("Subnet") if ipam else None gateway = ipam[0].get("Gateway") if ipam else None containers = attrs.get("Containers") or {} names = [c.get("Name", cid[:12]) for cid, c in containers.items()] labels = attrs.get("Labels") or {} name = attrs.get("Name", net.name) return { "id": net.id[:12], "name": name, "driver": attrs.get("Driver"), "scope": attrs.get("Scope"), "internal": attrs.get("Internal", False), "attachable": attrs.get("Attachable", False), "subnet": subnet, "gateway": gateway, "containers": names, "in_use": bool(names), "stack": labels.get(COMPOSE_PROJECT_LABEL), "labels": labels, "created": attrs.get("Created"), "is_default": name in DEFAULT_NETWORKS, } def list_networks() -> list[dict]: client = get_client() nets = safe_call(client.networks.list) # list() entries are lightweight; reload for Containers/IPAM detail. out = [] for n in nets: try: n.reload() except APIError: pass out.append(_summary(n)) return sorted(out, key=lambda x: (x["is_default"] is False, x["name"])) def inspect_network(network_id: str) -> dict: client = get_client() net = safe_call(client.networks.get, network_id) return _summary(net) def create_network(spec: dict) -> dict: name = (spec.get("name") or "").strip() if not name: raise DockerError("invalid_request", "Network name is required") driver = spec.get("driver") or "bridge" ipam = None subnet = (spec.get("subnet") or "").strip() gateway = (spec.get("gateway") or "").strip() if subnet: pool = IPAMPool(subnet=subnet, gateway=gateway or None) ipam = IPAMConfig(pool_configs=[pool]) client = get_client() net = safe_call( client.networks.create, name=name, driver=driver, internal=bool(spec.get("internal")), attachable=bool(spec.get("attachable", True)), ipam=ipam, options=spec.get("options") or None, labels=spec.get("labels") or None, ) net.reload() return _summary(net) def delete_network(network_id: str) -> None: client = get_client() net = safe_call(client.networks.get, network_id) name = (net.attrs or {}).get("Name", net.name) if name in DEFAULT_NETWORKS: raise DockerError("forbidden", f"Cannot delete the default '{name}' network") safe_call(net.remove) def prune_networks() -> dict: client = get_client() return safe_call(client.networks.prune)