Clarify Docker dependency status inside containers

* fix: show docker as N/A inside the container

* test: cover in-container docker detection

* fix: make the N/A dependency chip legible

* refactor: make remote docker applicability explicit and tested
This commit is contained in:
Daniel Grzelak
2026-06-01 09:56:42 +02:00
committed by GitHub
parent dea917b23f
commit 92c2392fd6
4 changed files with 144 additions and 14 deletions
+47 -7
View File
@@ -9,6 +9,7 @@ import shutil
import subprocess import subprocess
import uuid import uuid
import tempfile import tempfile
from collections import namedtuple
from pathlib import Path from pathlib import Path
from typing import Dict, Any from typing import Dict, Any
@@ -61,6 +62,36 @@ logger = logging.getLogger(__name__)
PTY_SUPPORTED = pty is not None and fcntl is not None and hasattr(os, "setsid") PTY_SUPPORTED = pty is not None and fcntl is not None and hasattr(os, "setsid")
DOCKER_IN_CONTAINER_HINT = (
"Not available inside the Odysseus container by design. The image ships no "
"docker CLI and no host socket is mounted. Run Docker-backed launches on a "
"remote server, where docker is checked over SSH. Mounting /var/run/docker.sock "
"into the container would grant it host-root access, so only do that if you "
"accept that risk."
)
def _running_in_container(dockerenv_path="/.dockerenv", cgroup_path="/proc/1/cgroup"):
if os.path.exists(dockerenv_path):
return True
try:
with open(cgroup_path, "r", encoding="utf-8") as fh:
contents = fh.read()
except OSError:
return False
return any(token in contents for token in ("docker", "containerd", "kubepods"))
DockerRowStatus = namedtuple("DockerRowStatus", ["applicable", "install_hint"])
def _docker_row_status(*, on_remote, in_container, installed, default_hint):
local_docker_unavailable = not on_remote and in_container and not installed
if local_docker_unavailable:
return DockerRowStatus(applicable=False, install_hint=DOCKER_IN_CONTAINER_HINT)
return DockerRowStatus(applicable=True, install_hint=default_hint)
def _find_line_break(buf): def _find_line_break(buf):
"""Find next line terminator in buffer. Returns (index, separator_length) or (-1, 0).""" """Find next line terminator in buffer. Returns (index, separator_length) or (-1, 0)."""
ni = buf.find(b"\n") ni = buf.find(b"\n")
@@ -702,20 +733,29 @@ def setup_shell_routes() -> APIRouter:
pass pass
for pkg in packages: for pkg in packages:
if host and pkg.get("target") == "remote": on_remote = bool(host and pkg.get("target") == "remote")
if on_remote:
pkg["installed"] = bool(remote_status.get(pkg["name"], False)) pkg["installed"] = bool(remote_status.get(pkg["name"], False))
continue elif pkg.get("kind") == "system":
if pkg.get("kind") == "system":
pkg["installed"] = shutil.which(pkg["name"]) is not None pkg["installed"] = shutil.which(pkg["name"]) is not None
continue elif pkg["name"] == "llama_cpp" and shutil.which("llama-server"):
try:
if pkg["name"] == "llama_cpp" and shutil.which("llama-server"):
pkg["installed"] = True pkg["installed"] = True
continue else:
try:
importlib.import_module(pkg["name"]) importlib.import_module(pkg["name"])
pkg["installed"] = True pkg["installed"] = True
except ImportError: except ImportError:
pkg["installed"] = False pkg["installed"] = False
if pkg["name"] == "docker":
status = _docker_row_status(
on_remote=on_remote,
in_container=_running_in_container() if not on_remote else False,
installed=pkg["installed"],
default_hint=pkg.get("install_hint"),
)
pkg["applicable"] = status.applicable
pkg["install_hint"] = status.install_hint
return {"packages": packages} return {"packages": packages}
@router.post("/api/cookbook/packages/install") @router.post("/api/cookbook/packages/install")
+5 -1
View File
@@ -542,7 +542,11 @@ async function _fetchDependencies() {
if (winBlocked) return `<span class="cookbook-dep-tag cookbook-dep-na">N/A</span>`; if (winBlocked) return `<span class="cookbook-dep-tag cookbook-dep-na">N/A</span>`;
if (pkg.installed && isSystemDep) return `<span class="cookbook-dep-tag cookbook-dep-installed" title="Found on selected server">Installed</span>`; if (pkg.installed && isSystemDep) return `<span class="cookbook-dep-tag cookbook-dep-installed" title="Found on selected server">Installed</span>`;
if (pkg.installed) return `<button class="cookbook-dep-tag cookbook-dep-installed cookbook-dep-installed-btn" title="Installed — click for actions"><span class="cookbook-dep-installed-label">Installed</span><span class="cookbook-dep-caret">&#9662;</span></button>`; if (pkg.installed) return `<button class="cookbook-dep-tag cookbook-dep-installed cookbook-dep-installed-btn" title="Installed — click for actions"><span class="cookbook-dep-installed-label">Installed</span><span class="cookbook-dep-caret">&#9662;</span></button>`;
if (isSystemDep) return `<span class="cookbook-dep-tag cookbook-dep-na" title="${esc(pkg.install_hint || 'Install this OS package on the selected server.')}">Missing</span>`; if (isSystemDep) {
const depTip = esc(pkg.install_hint || 'Install this OS package on the selected server.');
const depLabel = pkg.applicable === false ? 'N/A ?' : 'Missing';
return `<span class="cookbook-dep-tag cookbook-dep-na" title="${depTip}">${depLabel}</span>`;
}
return `<button class="cookbook-dep-tag cookbook-dep-install" data-dep-pip="${esc(pkg.pip)}" data-dep-target="${isLocal ? 'local' : 'remote'}">Install</button>`; return `<button class="cookbook-dep-tag cookbook-dep-install" data-dep-pip="${esc(pkg.pip)}" data-dep-target="${isLocal ? 'local' : 'remote'}">Install</button>`;
}; };
+4 -1
View File
@@ -18153,7 +18153,10 @@ body.gallery-selecting .gallery-dl-btn,
border: 1px solid color-mix(in srgb, var(--green, #50fa7b) 35%, transparent); border: 1px solid color-mix(in srgb, var(--green, #50fa7b) 35%, transparent);
} }
.cookbook-dep-na { .cookbook-dep-na {
color: color-mix(in srgb, var(--fg) 35%, transparent); background: color-mix(in srgb, var(--fg) 8%, transparent);
color: color-mix(in srgb, var(--fg) 60%, transparent);
border: 1px solid color-mix(in srgb, var(--fg) 16%, transparent);
cursor: help;
} }
.cookbook-dep-install { .cookbook-dep-install {
background: var(--accent, var(--red)); background: var(--accent, var(--red));
+84 -1
View File
@@ -7,7 +7,12 @@ import sys
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from routes.shell_routes import _find_line_break from routes.shell_routes import (
_find_line_break,
_running_in_container,
_docker_row_status,
DOCKER_IN_CONTAINER_HINT,
)
def test_shell_routes_import_without_posix_pty_modules(monkeypatch): def test_shell_routes_import_without_posix_pty_modules(monkeypatch):
@@ -99,3 +104,81 @@ class TestFindLineBreak:
def test_newline_before_cr(self): def test_newline_before_cr(self):
"""\\n comes before \\r — should return \\n.""" """\\n comes before \\r — should return \\n."""
assert _find_line_break(b"ab\ncd\r") == (2, 1) assert _find_line_break(b"ab\ncd\r") == (2, 1)
class TestRunningInContainer:
"""Detect whether the Odysseus process itself runs inside a container."""
def test_dockerenv_marker_present(self, tmp_path):
marker = tmp_path / ".dockerenv"
marker.write_text("")
assert _running_in_container(
dockerenv_path=str(marker), cgroup_path=str(tmp_path / "missing"),
) is True
def test_cgroup_names_a_container_runtime(self, tmp_path):
cgroup = tmp_path / "cgroup"
cgroup.write_text("12:devices:/docker/abcdef0123456789\n")
assert _running_in_container(
dockerenv_path=str(tmp_path / "no-marker"), cgroup_path=str(cgroup),
) is True
def test_bare_host_has_neither_signal(self, tmp_path):
cgroup = tmp_path / "cgroup"
cgroup.write_text("0::/user.slice/session-1.scope\n")
assert _running_in_container(
dockerenv_path=str(tmp_path / "no-marker"), cgroup_path=str(cgroup),
) is False
def test_missing_cgroup_file_is_not_a_container(self, tmp_path):
assert _running_in_container(
dockerenv_path=str(tmp_path / "no-marker"),
cgroup_path=str(tmp_path / "also-missing"),
) is False
class TestDockerRowStatus:
"""Applicability plus install hint for the docker dependency row."""
DEFAULT = "Install Docker on the selected server."
def test_in_container_and_absent_is_not_applicable_with_safe_default_hint(self):
status = _docker_row_status(
on_remote=False, in_container=True, installed=False, default_hint=self.DEFAULT,
)
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):
status = _docker_row_status(
on_remote=False, in_container=True, installed=True, default_hint=self.DEFAULT,
)
assert status.applicable is True
assert status.install_hint == self.DEFAULT
def test_on_host_and_absent_stays_applicable_with_default_hint(self):
status = _docker_row_status(
on_remote=False, in_container=False, installed=False, default_hint=self.DEFAULT,
)
assert status.applicable is True
assert status.install_hint == self.DEFAULT
def test_remote_server_is_always_applicable_even_when_absent(self):
status = _docker_row_status(
on_remote=True, in_container=False, installed=False, default_hint=self.DEFAULT,
)
assert status.applicable is True
assert status.install_hint == self.DEFAULT
def test_remote_server_ignores_local_container_status(self):
status = _docker_row_status(
on_remote=True, in_container=True, installed=False, default_hint=self.DEFAULT,
)
assert status.applicable is True
assert status.install_hint == self.DEFAULT
def test_container_hint_steers_to_remote_and_warns_on_socket(self):
lowered = DOCKER_IN_CONTAINER_HINT.lower()
assert "remote" in lowered
assert "socket" in lowered
assert "host-root" in lowered or "host root" in lowered