diff --git a/docs/advanced/configuration.mdx b/docs/advanced/configuration.mdx index 2048eefe6..362ee2683 100644 --- a/docs/advanced/configuration.mdx +++ b/docs/advanced/configuration.mdx @@ -175,7 +175,11 @@ When remote vars are set, Strix dual-writes telemetry to both local JSONL and th - Docker daemon address, for example `unix:///var/run/docker.sock` or `tcp://10.0.0.5:2375`. When not set, Strix connects to the same daemon as the `docker` CLI: the current docker context (`docker context use`), then the default socket. Docker Desktop, OrbStack and Colima register their sockets as contexts, so no extra configuration is needed for them. + Docker daemon address, for example `unix:///var/run/docker.sock` or `tcp://10.0.0.5:2375`. When not set, Strix uses `DOCKER_CONTEXT`, then the saved current context (`docker context use`), then the default socket. Docker Desktop, OrbStack and Colima register their sockets as contexts, so no extra configuration is needed for them. + + + + Select a Docker context for this shell without changing the saved current context. Used when `DOCKER_HOST` is unset. A missing context is an error; Strix does not silently fall back to another daemon. Set to `default` to use the SDK's default connection settings. diff --git a/strix/runtime/docker_connection.py b/strix/runtime/docker_connection.py index 890d54b1e..bc3fcf7cc 100644 --- a/strix/runtime/docker_connection.py +++ b/strix/runtime/docker_connection.py @@ -44,13 +44,13 @@ def detail(self) -> str: def resolve_docker_endpoint(environ: dict[str, str] | None = None) -> DockerEndpoint: - """``DOCKER_HOST``, else the current docker context, else the SDK default.""" + """``DOCKER_HOST``, shell/saved context, then the SDK default.""" env = os.environ if environ is None else environ host = env.get("DOCKER_HOST", "").strip() if host: return DockerEndpoint(host, "DOCKER_HOST") - name = get_current_context_name() + name = env.get("DOCKER_CONTEXT", "").strip() or get_current_context_name() if name == DEFAULT_CONTEXT: return DockerEndpoint(None, "default socket") diff --git a/tests/test_docker_connection.py b/tests/test_docker_connection.py index 5ea73dc8e..11edcf799 100644 --- a/tests/test_docker_connection.py +++ b/tests/test_docker_connection.py @@ -2,9 +2,11 @@ from __future__ import annotations +import hashlib import importlib +import json from types import SimpleNamespace -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import pytest from docker.errors import DockerException @@ -19,6 +21,10 @@ ) +if TYPE_CHECKING: + from pathlib import Path + + cli_utils: Any = importlib.import_module("strix.interface.utils") @@ -34,7 +40,9 @@ def _sdk_error(inner: BaseException) -> DockerException: 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"}) + endpoint = resolve_docker_endpoint( + {"DOCKER_HOST": "tcp://10.0.0.5:2375", "DOCKER_CONTEXT": "selected"} + ) assert endpoint == DockerEndpoint("tcp://10.0.0.5:2375", "DOCKER_HOST") @@ -56,6 +64,61 @@ def test_default_context_uses_the_sdk_default(monkeypatch: pytest.MonkeyPatch) - assert resolve_docker_endpoint({}) == DockerEndpoint(None, "default socket") +@pytest.mark.parametrize( + ("selected", "expected_host"), + [ + ("selected", "unix:///tmp/strix-selected.sock"), + ("default", None), + ("", "unix:///tmp/strix-saved.sock"), + ], +) +def test_shell_context_overrides_the_saved_context( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + selected: str, + expected_host: str | None, +) -> None: + """Read a real Docker context store, without contacting any daemon.""" + (tmp_path / "config.json").write_text(json.dumps({"currentContext": "saved"})) + for name in ("saved", "selected"): + context_id = hashlib.sha256(name.encode()).hexdigest() + metadata = tmp_path / "contexts" / "meta" / context_id / "meta.json" + metadata.parent.mkdir(parents=True) + metadata.write_text( + json.dumps( + { + "Name": name, + "Metadata": {}, + "Endpoints": { + "docker": { + "Host": f"unix:///tmp/strix-{name}.sock", + "SkipTLSVerify": True, + } + }, + } + ) + ) + monkeypatch.setenv("DOCKER_CONFIG", str(tmp_path)) + monkeypatch.setenv("DOCKER_CONTEXT", selected) + monkeypatch.delenv("DOCKER_HOST", raising=False) + + assert resolve_docker_endpoint().host == expected_host + + +def test_missing_shell_context_does_not_fall_back_to_saved_context( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + (tmp_path / "config.json").write_text(json.dumps({"currentContext": "default"})) + monkeypatch.setenv("DOCKER_CONFIG", str(tmp_path)) + monkeypatch.setenv("DOCKER_CONTEXT", "missing") + monkeypatch.delenv("DOCKER_HOST", raising=False) + + with pytest.raises( + DockerConnectionError, match="docker context 'missing' is selected but does not exist" + ): + resolve_docker_endpoint() + + 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(