From 2c2c4f36832bc8749e59f6aa39ee35f6e69c1c1f Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Sat, 30 May 2026 15:01:40 +0200 Subject: [PATCH] fix: exact container name matching in get_container_id/get_container_name docker ps --filter name= uses substring matching, so searching for code-server-headquarter-abc123 also matches tunnel-code-server-headquarter-abc123. This caused start_instance to store the tunnel container's ID instead of the tool container's ID, breaking tunnel connectivity and all container operations. Switched both helpers to docker inspect, which does exact name matching. Quality gates: ruff clean --- apps/api/src/api/tool_instances.py | 16 +++++++++++++-- apps/api/src/services/docker.py | 33 ++++++++++-------------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/apps/api/src/api/tool_instances.py b/apps/api/src/api/tool_instances.py index 59f702c..c68cd73 100644 --- a/apps/api/src/api/tool_instances.py +++ b/apps/api/src/api/tool_instances.py @@ -2554,11 +2554,23 @@ async def recreate_tunnel_endpoint( # Also probe from inside the API container directly to the target probe = subprocess.run( - ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "5", target_url], + [ + "curl", + "-s", + "-o", + "/dev/null", + "-w", + "%{http_code}", + "--max-time", + "5", + target_url, + ], capture_output=True, text=True, ) - logger.info("Direct probe from API to %s: HTTP %s", target_url, probe.stdout.strip()) + logger.info( + "Direct probe from API to %s: HTTP %s", target_url, probe.stdout.strip() + ) instance.tunnel_id = tunnel_info["container_name"] instance.public_url = tunnel_info["url"] diff --git a/apps/api/src/services/docker.py b/apps/api/src/services/docker.py index 8025ed1..edd965d 100644 --- a/apps/api/src/services/docker.py +++ b/apps/api/src/services/docker.py @@ -179,54 +179,43 @@ def execute_compose_command( def get_container_id(instance_name: str) -> str | None: """Get the container ID for a compose service. - Searches all containers including stopped/exited ones. + Uses exact name matching via docker inspect to avoid substring collisions + with tunnel containers (e.g. tunnel-code-server-... matching code-server-...). Args: - instance_name: The service name in compose + instance_name: The exact container name (case-insensitive for Docker). Returns: - Container ID or None if not found + Container ID or None if not found. """ - # Docker container names are lowercase internally; normalize to ensure match result = subprocess.run( - ["docker", "ps", "-a", "-q", "--filter", f"name={instance_name.lower()}"], + ["docker", "inspect", "-f", "{{.Id}}", instance_name.lower()], capture_output=True, text=True, ) - if result.returncode == 0 and result.stdout.strip(): - return result.stdout.strip().split("\n")[0] + return result.stdout.strip() return None def get_container_name(instance_name: str) -> str | None: """Get the full container name for a compose service. - Searches all containers including stopped/exited ones. + Uses exact name matching via docker inspect to avoid substring collisions. Args: - instance_name: The service name in compose + instance_name: The exact container name (case-insensitive for Docker). Returns: - Container name or None if not found + Container name or None if not found. """ - # Docker container names are lowercase internally; normalize to ensure match result = subprocess.run( - [ - "docker", - "ps", - "-a", - "--format", - "{{.Names}}", - "--filter", - f"name={instance_name.lower()}", - ], + ["docker", "inspect", "-f", "{{.Name}}", instance_name.lower()], capture_output=True, text=True, ) - if result.returncode == 0 and result.stdout.strip(): - return result.stdout.strip().split("\n")[0] + return result.stdout.strip().lstrip("/") return None