"""Single-container inspect + lifecycle. Only containers that belong to a compose-managed stack (i.e. carry the ``com.docker.compose.project`` label) are exposed, so this never becomes a generic "control any container on the host" backdoor. """ from __future__ import annotations from docker_client import DockerError, get_client, safe_call COMPOSE_LABEL = "com.docker.compose.project" SERVICE_LABEL = "com.docker.compose.service" ACTIONS = {"start", "stop", "restart"} def _get_managed(container_id: str): client = get_client() container = safe_call(client.containers.get, container_id) if COMPOSE_LABEL not in (container.labels or {}): raise DockerError("not_managed", "container is not part of a managed stack") return container def inspect_container(container_id: str) -> dict: """Return a curated inspect view for a single managed container.""" c = _get_managed(container_id) attrs = c.attrs state = attrs.get("State", {}) or {} config = attrs.get("Config", {}) or {} health = (state.get("Health") or {}).get("Status") network_settings = attrs.get("NetworkSettings", {}) or {} networks = network_settings.get("Networks", {}) or {} mounts = [] for m in attrs.get("Mounts", []) or []: mounts.append( { "type": m.get("Type"), "source": m.get("Source") or m.get("Name"), "destination": m.get("Destination"), "mode": m.get("Mode"), "rw": m.get("RW"), } ) ports = [] for container_port, bindings in (network_settings.get("Ports") or {}).items(): if bindings: for b in bindings: ports.append( {"container": container_port, "host_ip": b.get("HostIp"), "host_port": b.get("HostPort")} ) else: ports.append({"container": container_port, "host_port": None}) return { "id": c.id, "name": c.name, "service": c.labels.get(SERVICE_LABEL, c.name), "stack": c.labels.get(COMPOSE_LABEL), "image": config.get("Image", "") or attrs.get("Image", ""), "command": config.get("Cmd"), "entrypoint": config.get("Entrypoint"), "state": state.get("Status", c.status), "status": state.get("Status", c.status), "health": health, "restart_count": attrs.get("RestartCount", 0), "exit_code": state.get("ExitCode"), "created": attrs.get("Created"), "started_at": state.get("StartedAt"), "finished_at": state.get("FinishedAt"), "env": config.get("Env") or [], "mounts": mounts, "ports": ports, "networks": sorted(networks.keys()), "labels": c.labels or {}, } def container_action(container_id: str, action: str) -> dict: """Start / stop / restart a single managed container.""" if action not in ACTIONS: raise DockerError("bad_action", f"unsupported action '{action}'") c = _get_managed(container_id) if action == "start": safe_call(c.start) elif action == "stop": safe_call(c.stop) else: safe_call(c.restart) return {"ok": True, "action": action, "id": c.id}