mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-15 12:58:04 +00:00
fix(docker): make host Docker socket opt-in (#4902)
* fix(docker): make host socket compose opt-in * fix(cookbook): gate container Docker access * fix(docker): gate socket group setup on opt-in * fix(cookbook): gate generated docker exec serve commands * fix(cookbook): narrow generated docker exec forms
This commit is contained in:
committed by
GitHub
parent
1c1afe5dd1
commit
f38323c3a1
@@ -0,0 +1,339 @@
|
||||
import socket
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from fastapi import HTTPException
|
||||
from starlette.requests import Request
|
||||
|
||||
import routes.cookbook_routes as cookbook_routes
|
||||
from routes.cookbook_helpers import ServeRequest, _validate_serve_cmd
|
||||
from src.host_docker_access import HOST_DOCKER_ACCESS_HINT
|
||||
|
||||
|
||||
def _model_serve_endpoint():
|
||||
router = cookbook_routes.setup_cookbook_routes()
|
||||
for route in router.routes:
|
||||
if route.path == "/api/model/serve" and "POST" in route.methods:
|
||||
return route.endpoint
|
||||
raise AssertionError("POST /api/model/serve route not found")
|
||||
|
||||
|
||||
def _admin_request() -> Request:
|
||||
request = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"method": "POST",
|
||||
"path": "/api/model/serve",
|
||||
"headers": [],
|
||||
"state": {},
|
||||
}
|
||||
)
|
||||
request.state.current_user = "admin"
|
||||
return request
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_container_cli_only_is_rejected(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(cookbook_routes.shutil, "which", lambda binary: "/usr/bin/docker")
|
||||
|
||||
available = await cookbook_routes._binary_available(
|
||||
"docker",
|
||||
None,
|
||||
None,
|
||||
in_container=True,
|
||||
environ={},
|
||||
socket_path=str(tmp_path / "missing.sock"),
|
||||
)
|
||||
|
||||
assert available is False
|
||||
message = cookbook_routes._missing_binary_message(
|
||||
"docker",
|
||||
"local server",
|
||||
local_host_docker_blocked=True,
|
||||
)
|
||||
assert message == HOST_DOCKER_ACCESS_HINT
|
||||
assert "docker/host-docker.yml" in message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_container_opt_in_with_unix_socket_is_allowed(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(cookbook_routes.shutil, "which", lambda binary: "/usr/bin/docker")
|
||||
socket_path = tmp_path / "docker.sock"
|
||||
|
||||
with socket.socket(socket.AF_UNIX) as unix_socket:
|
||||
unix_socket.bind(str(socket_path))
|
||||
available = await cookbook_routes._binary_available(
|
||||
"docker",
|
||||
None,
|
||||
None,
|
||||
in_container=True,
|
||||
environ={"ODYSSEUS_ENABLE_HOST_DOCKER": "true"},
|
||||
socket_path=str(socket_path),
|
||||
)
|
||||
|
||||
assert available is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_native_local_docker_still_uses_cli_presence(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(cookbook_routes.shutil, "which", lambda binary: "/usr/bin/docker")
|
||||
|
||||
available = await cookbook_routes._binary_available(
|
||||
"docker",
|
||||
None,
|
||||
None,
|
||||
in_container=False,
|
||||
environ={},
|
||||
socket_path=str(tmp_path / "missing.sock"),
|
||||
)
|
||||
|
||||
assert available is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remote_docker_still_uses_ssh_probe(monkeypatch):
|
||||
remote_probe = AsyncMock(return_value=True)
|
||||
monkeypatch.setattr(cookbook_routes, "_remote_binary_available", remote_probe)
|
||||
monkeypatch.setattr(
|
||||
cookbook_routes.shutil,
|
||||
"which",
|
||||
lambda binary: pytest.fail("remote checks must not inspect the local CLI"),
|
||||
)
|
||||
|
||||
available = await cookbook_routes._binary_available(
|
||||
"docker",
|
||||
"gpu-server",
|
||||
"2222",
|
||||
windows=True,
|
||||
in_container=True,
|
||||
environ={},
|
||||
socket_path="/missing/docker.sock",
|
||||
)
|
||||
|
||||
assert available is True
|
||||
remote_probe.assert_awaited_once_with(
|
||||
"gpu-server",
|
||||
"2222",
|
||||
"docker",
|
||||
windows=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"cmd",
|
||||
[
|
||||
"docker exec ollama-test ollama-import example/model model 8192 model.gguf",
|
||||
"docker exec ollama-rocm ollama show llama3",
|
||||
],
|
||||
)
|
||||
async def test_local_container_serve_returns_host_docker_opt_in_hint(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
cmd,
|
||||
):
|
||||
async def binary_available(binary, remote, ssh_port, **kwargs):
|
||||
assert remote is None
|
||||
if binary == "tmux":
|
||||
return True
|
||||
assert cookbook_routes.shutil.which(binary) == "/usr/bin/docker"
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(cookbook_routes, "require_admin", lambda request: None)
|
||||
monkeypatch.setattr(cookbook_routes, "_binary_available", binary_available)
|
||||
monkeypatch.setattr(cookbook_routes, "running_in_container", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
cookbook_routes,
|
||||
"host_docker_access_enabled",
|
||||
lambda: False,
|
||||
)
|
||||
monkeypatch.setattr(cookbook_routes.shutil, "which", lambda binary: "/usr/bin/docker")
|
||||
monkeypatch.setattr(cookbook_routes, "TMUX_LOG_DIR", tmp_path)
|
||||
monkeypatch.setattr(
|
||||
cookbook_routes,
|
||||
"load_stored_hf_token",
|
||||
lambda **kwargs: "",
|
||||
)
|
||||
|
||||
response = await _model_serve_endpoint()(
|
||||
_admin_request(),
|
||||
ServeRequest(
|
||||
repo_id="example/model",
|
||||
cmd=cmd,
|
||||
),
|
||||
)
|
||||
|
||||
assert response["ok"] is False
|
||||
assert response["error"] == HOST_DOCKER_ACCESS_HINT
|
||||
assert "cmd binary 'docker' is not allowed" not in response["error"]
|
||||
assert "docker/host-docker.yml" in response["error"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_container_serve_allows_generated_docker_exec_when_enabled(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
):
|
||||
checked_binaries = []
|
||||
launched_commands = []
|
||||
|
||||
async def binary_available(binary, remote, ssh_port, **kwargs):
|
||||
checked_binaries.append(binary)
|
||||
if binary == "docker":
|
||||
assert cookbook_routes.running_in_container() is True
|
||||
assert cookbook_routes.host_docker_access_enabled() is True
|
||||
return True
|
||||
|
||||
class _Stderr:
|
||||
async def read(self):
|
||||
return b"mock launch stopped"
|
||||
|
||||
class _Process:
|
||||
returncode = 1
|
||||
stderr = _Stderr()
|
||||
|
||||
async def wait(self):
|
||||
return None
|
||||
|
||||
async def launch(command, **kwargs):
|
||||
launched_commands.append(command)
|
||||
return _Process()
|
||||
|
||||
monkeypatch.setattr(cookbook_routes, "require_admin", lambda request: None)
|
||||
monkeypatch.setattr(cookbook_routes, "_binary_available", binary_available)
|
||||
monkeypatch.setattr(cookbook_routes, "running_in_container", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
cookbook_routes,
|
||||
"host_docker_access_enabled",
|
||||
lambda: True,
|
||||
)
|
||||
monkeypatch.setattr(cookbook_routes, "TMUX_LOG_DIR", tmp_path)
|
||||
monkeypatch.setattr(
|
||||
cookbook_routes,
|
||||
"load_stored_hf_token",
|
||||
lambda **kwargs: "",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
cookbook_routes.asyncio,
|
||||
"create_subprocess_shell",
|
||||
launch,
|
||||
)
|
||||
|
||||
response = await _model_serve_endpoint()(
|
||||
_admin_request(),
|
||||
ServeRequest(
|
||||
repo_id="llama3",
|
||||
cmd="docker exec ollama-rocm ollama show llama3",
|
||||
),
|
||||
)
|
||||
|
||||
assert checked_binaries == ["tmux", "docker"]
|
||||
assert launched_commands
|
||||
assert response["error"] == "mock launch stopped"
|
||||
runner = next(tmp_path.glob("serve-*_run.sh")).read_text(encoding="utf-8")
|
||||
assert "docker exec ollama-rocm ollama show llama3" in runner
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cmd",
|
||||
[
|
||||
"docker run --rm alpine",
|
||||
"docker exec random-container ollama show llama3",
|
||||
"docker compose up",
|
||||
"docker exec ollama-rocm ollama rm llama3",
|
||||
"docker exec ollama-test ollama rm llama3",
|
||||
"docker exec ollama-rocm ollama pull llama3",
|
||||
"docker exec ollama-test ollama show llama3",
|
||||
"docker exec ollama-test sh -c 'ollama show llama3'",
|
||||
"docker exec ollama-test ollama show llama3; id",
|
||||
"docker exec ollama-rocm ollama show llama3 extra",
|
||||
"docker exec ollama-rocm ollama show llama3?",
|
||||
"docker exec ollama-test ollama-import org/model model many model.gguf",
|
||||
"docker exec ollama-test ollama-import org/model model 8192 path/model.gguf",
|
||||
"docker exec ollama-rocm ollama show $(id)",
|
||||
"docker exec ollama-rocm ollama show llama3 | cat",
|
||||
],
|
||||
)
|
||||
def test_arbitrary_docker_commands_stay_blocked(cmd):
|
||||
assert cookbook_routes._is_generated_ollama_docker_exec_cmd(cmd) is False
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
_validate_serve_cmd(cmd)
|
||||
|
||||
assert exc.value.status_code == 400
|
||||
|
||||
|
||||
def test_generated_ollama_import_shape_is_narrowly_allowed():
|
||||
assert cookbook_routes._is_generated_ollama_docker_exec_cmd(
|
||||
"docker exec ollama-test ollama-import org/model model 8192 model.gguf"
|
||||
)
|
||||
assert cookbook_routes._is_generated_ollama_docker_exec_cmd(
|
||||
"docker exec ollama-test ollama-import org/model model 8192"
|
||||
)
|
||||
assert not cookbook_routes._is_generated_ollama_docker_exec_cmd(
|
||||
"docker exec ollama-rocm ollama-import org/model model 8192 model.gguf"
|
||||
)
|
||||
|
||||
|
||||
def test_generated_ollama_show_shape_is_narrowly_allowed():
|
||||
assert cookbook_routes._is_generated_ollama_docker_exec_cmd(
|
||||
"docker exec ollama-rocm ollama show llama3:latest"
|
||||
)
|
||||
|
||||
|
||||
def test_local_ollama_docker_access_blocked_in_container_cli_only(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(cookbook_routes.shutil, "which", lambda binary: "/usr/bin/docker")
|
||||
|
||||
assert cookbook_routes._local_ollama_docker_access_blocked(
|
||||
in_container=True,
|
||||
environ={},
|
||||
socket_path=str(tmp_path / "missing.sock"),
|
||||
) is True
|
||||
|
||||
|
||||
def test_local_ollama_docker_access_not_blocked_for_native_cli(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(cookbook_routes.shutil, "which", lambda binary: "/usr/bin/docker")
|
||||
|
||||
assert cookbook_routes._local_ollama_docker_access_blocked(
|
||||
in_container=False,
|
||||
environ={},
|
||||
socket_path=str(tmp_path / "missing.sock"),
|
||||
) is False
|
||||
|
||||
|
||||
def test_local_ollama_download_probe_omits_docker_commands_when_blocked():
|
||||
lines = []
|
||||
|
||||
cookbook_routes._append_local_ollama_download_command_lines(
|
||||
lines,
|
||||
"ollama pull llama3:latest",
|
||||
docker_fallback_available=False,
|
||||
docker_fallback_blocked=True,
|
||||
)
|
||||
|
||||
rendered = "\n".join(lines)
|
||||
|
||||
assert "command -v docker" not in rendered
|
||||
assert "docker ps" not in rendered
|
||||
assert "docker exec" not in rendered
|
||||
assert "ODYSSEUS_OLLAMA_PULL_CMD" in rendered
|
||||
assert "docker/host-docker.yml" in rendered
|
||||
assert "exit 127" in rendered
|
||||
|
||||
|
||||
def test_local_ollama_download_probe_keeps_docker_fallback_when_allowed():
|
||||
lines = []
|
||||
|
||||
cookbook_routes._append_local_ollama_download_command_lines(
|
||||
lines,
|
||||
"ollama pull llama3:latest",
|
||||
docker_fallback_available=True,
|
||||
docker_fallback_blocked=False,
|
||||
)
|
||||
|
||||
rendered = "\n".join(lines)
|
||||
|
||||
assert "docker ps" in rendered
|
||||
assert "docker exec ${ODYSSEUS_OLLAMA_CONTAINER}" in rendered
|
||||
assert "ODYSSEUS_OLLAMA_PULL_CMD" in rendered
|
||||
@@ -17,6 +17,7 @@ COMPOSE_FILES = [
|
||||
ROOT / "docker-compose.gpu-nvidia.yml",
|
||||
ROOT / "docker-compose.gpu-amd.yml",
|
||||
]
|
||||
HOST_DOCKER_OVERLAY = ROOT / "docker" / "host-docker.yml"
|
||||
TEST_DOCS = [
|
||||
ROOT / "tests" / "README.md",
|
||||
ROOT / "tests" / "TESTING_STANDARD.md",
|
||||
@@ -54,6 +55,38 @@ def test_compose_files_forward_every_upload_limit_env_var():
|
||||
assert expected <= _compose_env_names(path), path.name
|
||||
|
||||
|
||||
def test_default_compose_files_do_not_mount_host_docker_socket():
|
||||
for path in COMPOSE_FILES:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
assert "/var/run/docker.sock" not in text, path.name
|
||||
|
||||
|
||||
def test_host_docker_overlay_mounts_socket_and_adds_docker_group():
|
||||
overlay = yaml.safe_load(HOST_DOCKER_OVERLAY.read_text(encoding="utf-8"))
|
||||
service = overlay["services"]["odysseus"]
|
||||
|
||||
assert "/var/run/docker.sock:/var/run/docker.sock" in service["volumes"]
|
||||
assert "${DOCKER_GID:-963}" in service["group_add"]
|
||||
assert "ODYSSEUS_ENABLE_HOST_DOCKER=true" in service["environment"]
|
||||
|
||||
|
||||
def test_docker_entrypoint_gates_socket_group_plumbing_on_explicit_opt_in():
|
||||
script = (ROOT / "docker" / "entrypoint.sh").read_text(encoding="utf-8")
|
||||
block_start = script.index("DOCKER_SOCK=\"${DOCKER_SOCK:-/var/run/docker.sock}\"")
|
||||
block_end = script.index("\nmount_root_for()", block_start)
|
||||
socket_group_block = script[block_start:block_end]
|
||||
|
||||
opt_in_check = socket_group_block.index(
|
||||
"[ \"${ODYSSEUS_ENABLE_HOST_DOCKER:-}\" = \"true\" ]"
|
||||
)
|
||||
socket_check = socket_group_block.index("[ -S \"$DOCKER_SOCK\" ]")
|
||||
stat_socket = socket_group_block.index("stat -c")
|
||||
add_group = socket_group_block.index("groupadd -g")
|
||||
add_user_group = socket_group_block.index("usermod -aG")
|
||||
|
||||
assert opt_in_check < socket_check < stat_socket < add_group < add_user_group
|
||||
|
||||
|
||||
def test_docker_entrypoint_does_not_resolve_root_commands_from_app_local_path():
|
||||
script = (ROOT / "docker" / "entrypoint.sh").read_text(encoding="utf-8")
|
||||
path_export = script.index('export PATH="/app/.local/bin:$PATH"')
|
||||
|
||||
@@ -20,6 +20,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
BASE = ROOT / "docker-compose.yml"
|
||||
NVIDIA_OVERLAY = ROOT / "docker" / "gpu.nvidia.yml"
|
||||
AMD_OVERLAY = ROOT / "docker" / "gpu.amd.yml"
|
||||
HOST_DOCKER_OVERLAY = ROOT / "docker" / "host-docker.yml"
|
||||
NVIDIA_STANDALONE = ROOT / "docker-compose.gpu-nvidia.yml"
|
||||
AMD_STANDALONE = ROOT / "docker-compose.gpu-amd.yml"
|
||||
|
||||
@@ -61,6 +62,13 @@ def _merge_overlay_into_base(base: dict, overlay: dict) -> dict:
|
||||
return expected
|
||||
|
||||
|
||||
def _merge_overlays_into_base(base: dict, *overlays: dict) -> dict:
|
||||
merged = copy.deepcopy(base)
|
||||
for overlay in overlays:
|
||||
merged = _merge_overlay_into_base(merged, overlay)
|
||||
return merged
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def base():
|
||||
return _load(BASE)
|
||||
@@ -124,9 +132,10 @@ def test_nvidia_odysseus_adds_only_overlay(base):
|
||||
{"driver": "nvidia", "count": "all", "capabilities": ["gpu"]}
|
||||
]
|
||||
|
||||
# Base Docker socket group is preserved; no AMD-only keys leaked in.
|
||||
# No Docker or AMD groups are added.
|
||||
assert "devices" not in svc
|
||||
assert svc["group_add"] == base_svc["group_add"]
|
||||
assert "group_add" not in base_svc
|
||||
assert "group_add" not in svc
|
||||
|
||||
|
||||
def test_amd_odysseus_adds_only_overlay(base):
|
||||
@@ -137,10 +146,66 @@ def test_amd_odysseus_adds_only_overlay(base):
|
||||
# Environment is unchanged from base for AMD.
|
||||
assert svc["environment"] == base_svc["environment"]
|
||||
|
||||
# devices are new; group_add preserves the base Docker group and appends AMD groups.
|
||||
# Devices and GPU-only groups are added.
|
||||
assert "devices" not in base_svc
|
||||
assert svc["devices"] == ["/dev/kfd", "/dev/dri"]
|
||||
assert svc["group_add"] == base_svc["group_add"] + ["video", "${RENDER_GID:-render}"]
|
||||
assert "group_add" not in base_svc
|
||||
assert svc["group_add"] == ["video", "${RENDER_GID:-render}"]
|
||||
|
||||
# No NVIDIA-only keys leaked in.
|
||||
assert "deploy" not in svc
|
||||
|
||||
|
||||
# --- Host Docker opt-in combinations ---------------------------------------
|
||||
|
||||
|
||||
def test_base_has_no_host_docker_access(base):
|
||||
service = base["services"][SERVICE]
|
||||
|
||||
assert "/var/run/docker.sock:/var/run/docker.sock" not in service["volumes"]
|
||||
assert "ODYSSEUS_ENABLE_HOST_DOCKER=true" not in service["environment"]
|
||||
assert "group_add" not in service
|
||||
|
||||
|
||||
def test_base_plus_host_docker_overlay_has_explicit_access(base):
|
||||
merged = _merge_overlays_into_base(base, _load(HOST_DOCKER_OVERLAY))
|
||||
service = merged["services"][SERVICE]
|
||||
|
||||
assert "/var/run/docker.sock:/var/run/docker.sock" in service["volumes"]
|
||||
assert "ODYSSEUS_ENABLE_HOST_DOCKER=true" in service["environment"]
|
||||
assert service["group_add"] == ["${DOCKER_GID:-963}"]
|
||||
|
||||
|
||||
def test_nvidia_plus_host_docker_preserves_gpu_and_docker_access(base):
|
||||
merged = _merge_overlays_into_base(
|
||||
base,
|
||||
_load(NVIDIA_OVERLAY),
|
||||
_load(HOST_DOCKER_OVERLAY),
|
||||
)
|
||||
service = merged["services"][SERVICE]
|
||||
|
||||
devices = service["deploy"]["resources"]["reservations"]["devices"]
|
||||
assert devices == [
|
||||
{"driver": "nvidia", "count": "all", "capabilities": ["gpu"]}
|
||||
]
|
||||
assert "/var/run/docker.sock:/var/run/docker.sock" in service["volumes"]
|
||||
assert "ODYSSEUS_ENABLE_HOST_DOCKER=true" in service["environment"]
|
||||
assert service["group_add"] == ["${DOCKER_GID:-963}"]
|
||||
|
||||
|
||||
def test_amd_plus_host_docker_preserves_gpu_and_docker_groups(base):
|
||||
merged = _merge_overlays_into_base(
|
||||
base,
|
||||
_load(AMD_OVERLAY),
|
||||
_load(HOST_DOCKER_OVERLAY),
|
||||
)
|
||||
service = merged["services"][SERVICE]
|
||||
|
||||
assert service["devices"] == ["/dev/kfd", "/dev/dri"]
|
||||
assert service["group_add"] == [
|
||||
"video",
|
||||
"${RENDER_GID:-render}",
|
||||
"${DOCKER_GID:-963}",
|
||||
]
|
||||
assert "/var/run/docker.sock:/var/run/docker.sock" in service["volumes"]
|
||||
assert "ODYSSEUS_ENABLE_HOST_DOCKER=true" in service["environment"]
|
||||
|
||||
@@ -5,6 +5,7 @@ import importlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
@@ -13,6 +14,7 @@ import pytest
|
||||
|
||||
from routes.shell_routes import (
|
||||
_find_line_break,
|
||||
_host_docker_access_enabled,
|
||||
_import_optional_dependency_for_status,
|
||||
_running_in_container,
|
||||
_docker_row_status,
|
||||
@@ -216,13 +218,24 @@ class TestDockerRowStatus:
|
||||
assert status.applicable is False
|
||||
assert status.install_hint == DOCKER_IN_CONTAINER_HINT
|
||||
|
||||
def test_in_container_but_present_is_applicable_with_default_hint(self):
|
||||
def test_in_container_cli_without_opt_in_is_not_applicable(self):
|
||||
status = _docker_row_status(
|
||||
on_remote=False,
|
||||
in_container=True,
|
||||
installed=True,
|
||||
default_hint=self.DEFAULT,
|
||||
)
|
||||
assert status.applicable is False
|
||||
assert status.install_hint == DOCKER_IN_CONTAINER_HINT
|
||||
|
||||
def test_in_container_opt_in_with_socket_is_applicable(self):
|
||||
status = _docker_row_status(
|
||||
on_remote=False,
|
||||
in_container=True,
|
||||
installed=True,
|
||||
default_hint=self.DEFAULT,
|
||||
host_docker_access=True,
|
||||
)
|
||||
assert status.applicable is True
|
||||
assert status.install_hint == self.DEFAULT
|
||||
|
||||
@@ -260,7 +273,51 @@ class TestDockerRowStatus:
|
||||
lowered = DOCKER_IN_CONTAINER_HINT.lower()
|
||||
assert "remote" in lowered
|
||||
assert "socket" in lowered
|
||||
assert "host-root" in lowered or "host root" in lowered
|
||||
assert "high-trust" in lowered
|
||||
assert "docker/host-docker.yml" in lowered
|
||||
|
||||
|
||||
class TestHostDockerAccess:
|
||||
def test_opt_in_without_socket_is_disabled(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("ODYSSEUS_ENABLE_HOST_DOCKER", "true")
|
||||
|
||||
assert _host_docker_access_enabled(str(tmp_path / "missing.sock")) is False
|
||||
|
||||
def test_regular_file_is_not_accepted(self, monkeypatch, tmp_path):
|
||||
socket_path = tmp_path / "docker.sock"
|
||||
socket_path.touch()
|
||||
monkeypatch.setenv("ODYSSEUS_ENABLE_HOST_DOCKER", "true")
|
||||
|
||||
assert _host_docker_access_enabled(str(socket_path)) is False
|
||||
|
||||
@pytest.mark.parametrize("flag", [None, "false"])
|
||||
def test_socket_without_explicit_opt_in_is_disabled(
|
||||
self,
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
flag,
|
||||
):
|
||||
socket_path = tmp_path / "docker.sock"
|
||||
with socket.socket(socket.AF_UNIX) as unix_socket:
|
||||
unix_socket.bind(str(socket_path))
|
||||
if flag is None:
|
||||
monkeypatch.delenv("ODYSSEUS_ENABLE_HOST_DOCKER", raising=False)
|
||||
else:
|
||||
monkeypatch.setenv("ODYSSEUS_ENABLE_HOST_DOCKER", flag)
|
||||
|
||||
assert _host_docker_access_enabled(str(socket_path)) is False
|
||||
|
||||
def test_explicit_opt_in_with_unix_socket_is_enabled(
|
||||
self,
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
):
|
||||
socket_path = tmp_path / "docker.sock"
|
||||
with socket.socket(socket.AF_UNIX) as unix_socket:
|
||||
unix_socket.bind(str(socket_path))
|
||||
monkeypatch.setenv("ODYSSEUS_ENABLE_HOST_DOCKER", "true")
|
||||
|
||||
assert _host_docker_access_enabled(str(socket_path)) is True
|
||||
|
||||
|
||||
class TestPackageProbeStatus:
|
||||
|
||||
Reference in New Issue
Block a user