Phase 20: per-container inspect + start/stop/restart, local + agent (0.26.0)
Stack Overview now renders each service as an expandable ContainerCard with a
curated single-container inspect view and admin start/stop/restart buttons,
both for local stacks (GET/POST /api/containers/{id}[/{action}]) and remote
stacks (proxied via /api/agents/{id}/containers/* to the agent's new
/agent/containers/* endpoints). Only compose-managed containers are exposed.
Also bumps version 0.23.0 -> 0.26.0 (the bumps for the already-committed
Phase 18 image-prune / Phase 19 compose-validate were missed) and backfills
README sections for Phase 18/19/20.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9c4d319f8f
commit
2f63247fc1
@@ -0,0 +1,92 @@
|
||||
"""Single-container inspect + lifecycle — shared by the central app and agent.
|
||||
|
||||
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}
|
||||
Reference in New Issue
Block a user