Skip to content

Commit a10998e

Browse files
committed
feat(docker): add cli fallback for docker connection checks
Implement a two-phase approach for checking Docker availability. The system now tries the Docker SDK first and falls back to `docker info` via CLI if the SDK fails. This improves support for remote Docker setups (like VirtualBox or DOCKER_HOST) where the SDK might not inherit the correct shell environment. - Update check_docker_connection to include subprocess fallback - Improve error messages to cover remote Docker host scenarios - Update unit tests to reflect new connection logic and messages
1 parent bae5bf4 commit a10998e

3 files changed

Lines changed: 57 additions & 13 deletions

File tree

apps/file-brain/file_brain/services/docker_manager.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def _get_friendly_error_message(self, error_msg: str) -> Optional[str]:
134134
)
135135

136136
if is_connection_error:
137-
return "Docker Desktop is likely not running. Please start Docker Desktop and try again."
137+
return "Docker is not reachable. If you are using Docker Desktop, please start it and try again."
138138

139139
if "permission denied" in error_msg_lower:
140140
return (
@@ -357,12 +357,22 @@ def check_docker_connection(self) -> Dict[str, any]:
357357
"""
358358
Check if we can actually connect to the Docker daemon.
359359
360+
Uses a two-phase approach:
361+
1. Try the Docker SDK (docker.from_env()) which reads DOCKER_HOST / Docker SDK env vars.
362+
2. If that fails, fall back to running `docker info` via CLI subprocess, which respects
363+
Docker Desktop contexts and system-level DOCKER_HOST configurations.
364+
365+
This handles users pointing the Docker CLI at a remote daemon (e.g. VirtualBox VM)
366+
where the SDK may not automatically pick up the active Docker context.
367+
360368
Returns:
361369
Dictionary with success and error message
362370
"""
363371
if not self.docker_cmd:
364372
return {"success": False, "error": "Docker not found"}
365373

374+
# Try Docker SDK
375+
sdk_error: Optional[str] = None
366376
try:
367377
import docker
368378

@@ -371,11 +381,38 @@ def check_docker_connection(self) -> Dict[str, any]:
371381
client.close()
372382
return {"success": True}
373383
except Exception as e:
374-
error_msg = str(e)
384+
sdk_error = str(e)
385+
logger.debug(f"Docker SDK connection failed: {sdk_error} — will try CLI fallback")
386+
387+
# CLI fallback via `docker info` — respects Docker contexts and system-level config
388+
try:
389+
result = subprocess.run(
390+
[self.docker_cmd, "info"],
391+
capture_output=True,
392+
text=True,
393+
timeout=10,
394+
)
395+
if result.returncode == 0:
396+
logger.info("Docker daemon reachable via CLI (SDK fallback succeeded)")
397+
return {"success": True}
398+
399+
# CLI also failed — build the best error message we can
400+
cli_error = result.stderr or result.stdout or sdk_error or "unknown error"
401+
friendly_msg = self._get_friendly_error_message(cli_error) or self._get_friendly_error_message(
402+
sdk_error or ""
403+
)
404+
return {
405+
"success": False,
406+
"error": friendly_msg or f"Failed to connect to Docker: {cli_error}",
407+
}
408+
except Exception as cli_exc:
409+
# Both SDK and CLI failed; surface the clearest error
410+
error_msg = sdk_error or str(cli_exc)
375411
friendly_msg = self._get_friendly_error_message(error_msg)
376-
if friendly_msg:
377-
return {"success": False, "error": friendly_msg}
378-
return {"success": False, "error": f"Failed to connect to Docker: {error_msg}"}
412+
return {
413+
"success": False,
414+
"error": friendly_msg or f"Failed to connect to Docker: {error_msg}",
415+
}
379416

380417
def _pull_images_docker_sdk(self, images: List[str], progress_callback=None):
381418
"""Pull images using Docker SDK with progress streaming (non-blocking)"""

apps/file-brain/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "file-brain"
3-
version = "0.1.26a1"
3+
version = "0.1.26a2"
44
description = "Smart local file search engine that understands your files"
55
authors = [{ name = "Hamza Abbad", email = "contact@file-brain.com" }]
66
readme = "README.md"

apps/file-brain/tests/unit/test_docker_manager_errors.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ def test_friendly_error_message(docker_manager):
2323
msg = docker_manager._get_friendly_error_message(
2424
"Error response from daemon: Dial pipe //./pipe/docker_engine: The system cannot find the file specified."
2525
)
26-
assert "Docker Desktop is likely not running" in msg
26+
assert "Docker is not reachable" in msg
2727

2828
# Test connection error (Mac/Linux connection refused)
2929
msg = docker_manager._get_friendly_error_message("Connection refused")
30-
assert "Docker Desktop is likely not running" in msg
30+
assert "Docker is not reachable" in msg
3131

3232
# Test permission error - now handled
3333
msg = docker_manager._get_friendly_error_message("permission denied while trying to connect to the Docker daemon")
@@ -65,17 +65,24 @@ def test_check_docker_connection_success(docker_manager):
6565

6666

6767
def test_check_docker_connection_failure(docker_manager):
68-
"""Test failed docker connection check."""
68+
"""Test failed docker connection check - both SDK and CLI fallback fail."""
6969
mock_docker_module = MagicMock()
7070
mock_docker_module.from_env.side_effect = Exception("Error while fetching server API version")
7171

72+
# Mock subprocess.run so the CLI fallback also fails
73+
mock_cli_result = MagicMock()
74+
mock_cli_result.returncode = 1
75+
mock_cli_result.stdout = ""
76+
mock_cli_result.stderr = "Error while fetching server API version"
77+
7278
with patch.dict(sys.modules, {"docker": mock_docker_module}):
73-
docker_manager.docker_cmd = "docker"
79+
with patch("subprocess.run", return_value=mock_cli_result):
80+
docker_manager.docker_cmd = "docker"
7481

75-
result = docker_manager.check_docker_connection()
82+
result = docker_manager.check_docker_connection()
7683

7784
assert result["success"] is False
78-
assert "Docker Desktop is likely not running" in result["error"]
85+
assert "Docker is not reachable" in result["error"]
7986

8087

8188
@patch("subprocess.run")
@@ -109,4 +116,4 @@ def test_start_services_connection_failure(mock_run, docker_manager):
109116
assert result["success"] is False
110117
# The start_services method uses logical OR: friendly_msg or f"Failed..."
111118
# So if friendly_msg is returned, it is used.
112-
assert "Docker Desktop is likely not running" in result["error"]
119+
assert "Docker is not reachable" in result["error"]

0 commit comments

Comments
 (0)