mirror of
https://github.com/usestrix/strix.git
synced 2026-10-11 03:37:54 +00:00
205 lines
7.5 KiB
Python
205 lines
7.5 KiB
Python
"""Docker endpoint resolution and the panel printed when the daemon is unreachable."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
from types import SimpleNamespace
|
|
from typing import Any, cast
|
|
|
|
import pytest
|
|
from docker.errors import DockerException
|
|
from requests.exceptions import ConnectionError as RequestsConnectionError
|
|
from urllib3.exceptions import ProtocolError
|
|
|
|
from strix.runtime import backends, docker_connection
|
|
from strix.runtime.docker_connection import (
|
|
DockerConnectionError,
|
|
DockerEndpoint,
|
|
resolve_docker_endpoint,
|
|
)
|
|
|
|
|
|
cli_utils: Any = importlib.import_module("strix.interface.utils")
|
|
|
|
|
|
def _sdk_error(inner: BaseException) -> DockerException:
|
|
"""Build the exception exactly as docker-py raises it for a dead daemon."""
|
|
protocol = ProtocolError("Connection aborted.", inner)
|
|
requests_exc = RequestsConnectionError(protocol)
|
|
requests_exc.__cause__ = protocol
|
|
sdk_exc = DockerException(f"Error while fetching server API version: {requests_exc}")
|
|
sdk_exc.__cause__ = requests_exc
|
|
return sdk_exc
|
|
|
|
|
|
def test_docker_host_wins_over_context(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(docker_connection, "get_current_context_name", lambda: "desktop-linux")
|
|
endpoint = resolve_docker_endpoint({"DOCKER_HOST": "tcp://10.0.0.5:2375"})
|
|
assert endpoint == DockerEndpoint("tcp://10.0.0.5:2375", "DOCKER_HOST")
|
|
|
|
|
|
def test_current_context_is_used_like_the_cli(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(docker_connection, "get_current_context_name", lambda: "desktop-linux")
|
|
monkeypatch.setattr(
|
|
"strix.runtime.docker_connection.ContextAPI.get_context",
|
|
lambda _name: SimpleNamespace(
|
|
Host="unix:///Users/me/.docker/run/docker.sock", TLSConfig=None
|
|
),
|
|
)
|
|
endpoint = resolve_docker_endpoint({})
|
|
assert endpoint.host == "unix:///Users/me/.docker/run/docker.sock"
|
|
assert endpoint.source == "docker context 'desktop-linux'"
|
|
|
|
|
|
def test_default_context_uses_the_sdk_default(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(docker_connection, "get_current_context_name", lambda: "default")
|
|
assert resolve_docker_endpoint({}) == DockerEndpoint(None, "default socket")
|
|
|
|
|
|
def test_missing_or_broken_current_context_is_an_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(docker_connection, "get_current_context_name", lambda: "gone")
|
|
monkeypatch.setattr(
|
|
"strix.runtime.docker_connection.ContextAPI.get_context", lambda _name: None
|
|
)
|
|
with pytest.raises(
|
|
DockerConnectionError, match="docker context 'gone' is selected but does not exist"
|
|
):
|
|
resolve_docker_endpoint({})
|
|
|
|
def boom(_name: str) -> None:
|
|
raise DockerException("bad meta.json")
|
|
|
|
monkeypatch.setattr("strix.runtime.docker_connection.ContextAPI.get_context", boom)
|
|
with pytest.raises(DockerConnectionError, match="bad meta") as info:
|
|
resolve_docker_endpoint({})
|
|
assert info.value.endpoint.label == "docker context 'gone'"
|
|
|
|
|
|
def test_docker_host_goes_through_from_env_so_tls_settings_apply(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
docker_connection,
|
|
"resolve_docker_endpoint",
|
|
lambda _environ=None: DockerEndpoint("tcp://10.0.0.5:2376", "DOCKER_HOST"),
|
|
)
|
|
client = object()
|
|
monkeypatch.setattr("strix.runtime.docker_connection.docker.from_env", lambda: client)
|
|
|
|
def unexpected(**_kwargs: Any) -> None:
|
|
raise AssertionError("DOCKER_HOST must not bypass from_env")
|
|
|
|
monkeypatch.setattr("strix.runtime.docker_connection.docker.DockerClient", unexpected)
|
|
assert docker_connection.connect_docker() is client
|
|
|
|
|
|
def test_context_endpoint_keeps_its_tls_config(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
tls = object()
|
|
monkeypatch.setattr(
|
|
docker_connection,
|
|
"resolve_docker_endpoint",
|
|
lambda _environ=None: DockerEndpoint("tcp://remote:2376", "docker context 'remote'", tls),
|
|
)
|
|
seen: dict[str, Any] = {}
|
|
|
|
def client(**kwargs: Any) -> str:
|
|
seen.update(kwargs)
|
|
return "client"
|
|
|
|
monkeypatch.setattr("strix.runtime.docker_connection.docker.DockerClient", client)
|
|
assert docker_connection.connect_docker() == "client"
|
|
assert seen == {"base_url": "tcp://remote:2376", "tls": tls}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"inner",
|
|
[
|
|
FileNotFoundError(2, "No such file or directory"),
|
|
PermissionError(13, "Permission denied"),
|
|
ConnectionRefusedError(111, "Connection refused"),
|
|
],
|
|
)
|
|
def test_connect_docker_surfaces_the_root_cause(
|
|
monkeypatch: pytest.MonkeyPatch, inner: OSError
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
docker_connection,
|
|
"resolve_docker_endpoint",
|
|
lambda _environ=None: DockerEndpoint("unix:///nope.sock", "docker context 'dead'"),
|
|
)
|
|
|
|
def dead_client(**_kwargs: Any) -> None:
|
|
raise _sdk_error(inner)
|
|
|
|
monkeypatch.setattr("strix.runtime.docker_connection.docker.DockerClient", dead_client)
|
|
|
|
with pytest.raises(DockerConnectionError) as info:
|
|
docker_connection.connect_docker()
|
|
|
|
assert info.value.cause is inner
|
|
assert info.value.detail.startswith(type(inner).__name__)
|
|
assert info.value.endpoint.host == "unix:///nope.sock"
|
|
|
|
|
|
def test_check_docker_connection_prints_endpoint_and_error_then_exits(
|
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
reported: list[tuple[str, type | None]] = []
|
|
monkeypatch.setattr(
|
|
cli_utils, "report_error", lambda name, exc=None: reported.append((name, type(exc)))
|
|
)
|
|
error = DockerConnectionError(
|
|
DockerEndpoint(None, "default socket"), _sdk_error(PermissionError(13, "Permission denied"))
|
|
)
|
|
|
|
def failing_connect() -> None:
|
|
raise error
|
|
|
|
monkeypatch.setattr(docker_connection, "connect_docker", failing_connect)
|
|
|
|
with pytest.raises(SystemExit) as exit_info:
|
|
cli_utils.check_docker_connection()
|
|
|
|
out = capsys.readouterr().out
|
|
assert exit_info.value.code == 1
|
|
assert reported == [("docker_unavailable", PermissionError)]
|
|
assert "DOCKER NOT AVAILABLE" in out
|
|
assert "Cannot connect to Docker at default socket." in out
|
|
assert "PermissionError: [Errno 13] Permission denied" in out
|
|
assert "docker info" in out
|
|
|
|
|
|
def test_check_docker_connection_returns_the_client(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
client = object()
|
|
monkeypatch.setattr(docker_connection, "connect_docker", lambda: client)
|
|
assert cli_utils.check_docker_connection() is client
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sandbox_backend_uses_the_same_endpoint(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
resolved = object()
|
|
monkeypatch.setattr(docker_connection, "connect_docker", lambda: resolved)
|
|
captured: dict[str, Any] = {}
|
|
|
|
class FakeSession:
|
|
async def start(self) -> None:
|
|
captured["started"] = True
|
|
|
|
class FakeClient:
|
|
def __init__(self, docker_client: Any) -> None:
|
|
captured["docker_client"] = docker_client
|
|
|
|
async def create(self, *, options: Any, **_kwargs: Any) -> FakeSession:
|
|
captured["image"] = options.image
|
|
return FakeSession()
|
|
|
|
monkeypatch.setattr("strix.runtime.docker_client.StrixDockerSandboxClient", FakeClient)
|
|
|
|
client, session = await backends._docker_backend(
|
|
image="img:1", manifest=cast("Any", SimpleNamespace()), exposed_ports=(8080,)
|
|
)
|
|
|
|
assert captured["docker_client"] is resolved
|
|
assert captured == {"docker_client": resolved, "image": "img:1", "started": True}
|
|
assert isinstance(client, FakeClient)
|
|
assert isinstance(session, FakeSession)
|