diff --git a/strix/interface/utils.py b/strix/interface/utils.py index 7ab55d8ee..beefbce68 100644 --- a/strix/interface/utils.py +++ b/strix/interface/utils.py @@ -25,6 +25,14 @@ from strix.utils.api_spec import detect_spec_format logger = logging.getLogger(__name__) +DOCKER_PERMISSION_ERROR_MARKERS = ("permission denied", "operation not permitted") +DOCKER_LOCAL_SOCKET_SCHEME = "unix://" +DOCKER_SOCKET_ACCESS_HINT = ( + "Docker is installed, but Strix cannot access the Docker socket. " + "Run Strix with a user that can access Docker, or add your user to the docker group " + "and restart your shell before trying again.\n" +) + def get_severity_color(severity: str) -> str: severity_colors = { @@ -1598,6 +1606,24 @@ def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None) ) from e +def _is_docker_permission_error(exc: BaseException) -> bool: + message = str(exc).lower() + return any(marker in message for marker in DOCKER_PERMISSION_ERROR_MARKERS) + + +def _is_docker_socket_permission_error(exc: Any) -> bool: + """A permission error against the *local* Docker socket. + + The docker-group remedy only applies to a Unix-socket daemon. A permission + failure against a remote DOCKER_HOST (TCP/SSH) endpoint is not fixed by + group membership, so the socket hint must not be shown for it. + """ + if not _is_docker_permission_error(exc): + return False + host = exc.endpoint.host + return host is None or host.startswith(DOCKER_LOCAL_SOCKET_SCHEME) + + def check_docker_connection() -> Any: from strix.runtime.docker_connection import DockerConnectionError, connect_docker @@ -1611,6 +1637,8 @@ def check_docker_connection() -> Any: error_text.append("\n\n", style="white") error_text.append(f"Cannot connect to Docker at {exc.endpoint.label}.\n", style="white") error_text.append(f"{exc.detail}\n\n", style="dim red") + if _is_docker_socket_permission_error(exc): + error_text.append(DOCKER_SOCKET_ACCESS_HINT, style="white") error_text.append( "Make sure Docker is running. If `docker info` works in this shell, strix uses " "the same daemon. Otherwise set DOCKER_HOST.\n", diff --git a/tests/test_docker_connection_message.py b/tests/test_docker_connection_message.py new file mode 100644 index 000000000..07158e787 --- /dev/null +++ b/tests/test_docker_connection_message.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import io + +import pytest +from docker.errors import DockerException +from rich.console import Console + +from strix.interface import utils +from strix.runtime import docker_connection +from strix.runtime.docker_connection import DockerConnectionError, DockerEndpoint + + +DOCKER_PERMISSION_ERROR = "permission denied while trying to connect to the Docker daemon socket" +DOCKER_SOCKET_HINT = "Docker socket" +DOCKER_GROUP_HINT = "docker group" + + +def test_docker_connection_permission_error_mentions_socket_access( + monkeypatch: pytest.MonkeyPatch, +) -> None: + output = io.StringIO() + + def fake_console() -> Console: + return Console(file=output, color_system=None, force_terminal=False) + + def fail_connect() -> None: + endpoint = DockerEndpoint(None, "default socket") + raise DockerConnectionError(endpoint, DockerException(DOCKER_PERMISSION_ERROR)) + + monkeypatch.setattr(utils, "Console", fake_console) + monkeypatch.setattr(docker_connection, "connect_docker", fail_connect) + + with pytest.raises(SystemExit): + utils.check_docker_connection() + + rendered = output.getvalue() + assert DOCKER_SOCKET_HINT in rendered + assert DOCKER_GROUP_HINT in rendered + + +def test_docker_connection_permission_error_on_remote_host_skips_socket_hint( + monkeypatch: pytest.MonkeyPatch, +) -> None: + output = io.StringIO() + + def fake_console() -> Console: + return Console(file=output, color_system=None, force_terminal=False) + + def fail_connect() -> None: + endpoint = DockerEndpoint("tcp://10.0.0.5:2376", "DOCKER_HOST") + raise DockerConnectionError(endpoint, DockerException(DOCKER_PERMISSION_ERROR)) + + monkeypatch.setattr(utils, "Console", fake_console) + monkeypatch.setattr(docker_connection, "connect_docker", fail_connect) + + with pytest.raises(SystemExit): + utils.check_docker_connection() + + rendered = output.getvalue() + assert DOCKER_SOCKET_HINT not in rendered + assert DOCKER_GROUP_HINT not in rendered