refactor(dashboard): discover via docker compose ls
PRD 0018 chunk 5. The dashboard's operator-edit verbs
(`routes edit`, `pipelock edit`) enumerated running sidecars
via `docker ps --filter name=...` prefix scans. Switch to
`docker compose ls`-based discovery so the dashboard, cleanup
CLI, and launch step all agree on what's running.
Mechanics:
- `claude_bottle/backend/docker/compose.py` grows three shared
helpers: `list_compose_projects` (the JSON parse moved out
of cleanup), `slug_from_compose_project` (inverse of
`compose_project_name`), and `list_active_slugs` (sugar over
the first two for the common "what's running?" question).
- cleanup.py drops its private `_list_compose_projects` +
`_PROJECT_PREFIX` in favor of the shared ones; `list_active`
simplifies (one compose-ls call, not two).
- dashboard.py's `_discover_sidecar_slugs` becomes
`_discover_active_with_service`: cross-references the active
slug list with a label-filtered `docker ps` so only bottles
whose given service container is actually up surface in the
edit menu. Bottles without an egress sidecar (no
bottle.egress.routes) no longer appear for `routes edit`.
3 new unit tests cover the slug ↔ compose-project naming
contract; manual probe with a fake compose project confirms
both `discover_egress_slugs` and `discover_pipelock_slugs`
return the expected slug.
This commit is contained in:
@@ -27,6 +27,10 @@ from ..backend.docker.capability_apply import (
|
||||
CapabilityApplyError,
|
||||
apply_capability_change,
|
||||
)
|
||||
from ..backend.docker.compose import (
|
||||
COMPOSE_PROJECT_PREFIX,
|
||||
list_active_slugs,
|
||||
)
|
||||
from ..backend.docker.egress_apply import (
|
||||
EgressApplyError,
|
||||
add_route,
|
||||
@@ -79,15 +83,23 @@ class QueuedProposal:
|
||||
queue_dir: Path
|
||||
|
||||
|
||||
def _discover_sidecar_slugs(name_prefix: str) -> list[str]:
|
||||
"""Slugs of bottles whose sidecar container names start with
|
||||
`name_prefix`. Empty list if docker isn't reachable or not
|
||||
installed."""
|
||||
def _discover_active_with_service(service: str) -> list[str]:
|
||||
"""Slugs of bottles whose compose project is up AND has the
|
||||
named service container running. PRD 0018 chunk 5 grounded the
|
||||
discovery on `docker compose ls` so all the dashboard verbs
|
||||
agree with the cleanup CLI about what's running. A second
|
||||
`docker ps` filter narrows by service label — a bottle without
|
||||
egress routes has no egress service, and the operator-edit
|
||||
flow shouldn't offer it for routes editing."""
|
||||
slugs = list_active_slugs()
|
||||
if not slugs:
|
||||
return []
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[
|
||||
"docker", "ps",
|
||||
"--filter", f"name=^{name_prefix}",
|
||||
"--filter", f"label=com.docker.compose.service={service}",
|
||||
"--filter", f"name=^{COMPOSE_PROJECT_PREFIX}{service}-",
|
||||
"--format", "{{.Names}}",
|
||||
],
|
||||
capture_output=True, text=True, check=False,
|
||||
@@ -96,24 +108,27 @@ def _discover_sidecar_slugs(name_prefix: str) -> list[str]:
|
||||
return []
|
||||
if r.returncode != 0:
|
||||
return []
|
||||
prefix = f"{COMPOSE_PROJECT_PREFIX}{service}-"
|
||||
out: list[str] = []
|
||||
for line in (r.stdout or "").splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith(name_prefix):
|
||||
out.append(line[len(name_prefix):])
|
||||
return sorted(out)
|
||||
if line.startswith(prefix):
|
||||
slug = line[len(prefix):]
|
||||
if slug in slugs:
|
||||
out.append(slug)
|
||||
return sorted(set(out))
|
||||
|
||||
|
||||
def discover_egress_slugs() -> list[str]:
|
||||
"""Slugs of bottles with a running egress sidecar. Used by
|
||||
the operator-initiated `routes edit` verb."""
|
||||
return _discover_sidecar_slugs("claude-bottle-egress-")
|
||||
return _discover_active_with_service("egress")
|
||||
|
||||
|
||||
def discover_pipelock_slugs() -> list[str]:
|
||||
"""Slugs of bottles with a running pipelock sidecar. Used by
|
||||
the operator-initiated `pipelock edit` verb."""
|
||||
return _discover_sidecar_slugs("claude-bottle-pipelock-")
|
||||
return _discover_active_with_service("pipelock")
|
||||
|
||||
|
||||
def _approval_status(qp: QueuedProposal, verb: str) -> str:
|
||||
|
||||
Reference in New Issue
Block a user