Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions hermes_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -865,11 +865,14 @@ def is_container() -> bool:
Kubernetes/k3s) were previously missed. To cover those, also check:
* ``KUBERNETES_SERVICE_HOST`` env var — set in every Kubernetes pod.
* ``kubepods`` / ``containerd`` / ``crio`` markers in ``/proc/1/cgroup``.
* the same markers in ``/proc/self/mountinfo`` (cgroup-v2 fallback).
* the same markers on the **root** mount (``/``) in
``/proc/self/mountinfo`` (cgroup-v2 fallback). Only the root mount is
checked, so a host running its own containers isn't misread as one
(see #58135).

Result is cached for the process lifetime. Import-safe — no heavy deps.

See: NousResearch/hermes-agent#47111
See: NousResearch/hermes-agent#47111, #58135
"""
global _container_detected
if _container_detected is not None:
Expand All @@ -893,15 +896,21 @@ def is_container() -> bool:
return True
except OSError:
pass
# cgroup v2: /proc/1/cgroup is just "0::/" with no marker. The container
# runtime still shows up in the mount table (overlay rootfs, runtime mount
# paths), so scan mountinfo as a last resort.
# cgroup v2 collapses /proc/1/cgroup to "0::/", so fall back to mountinfo —
# but only the root mount ("/") reflects our own rootfs (a runtime overlay
# in a container, a plain block device on a host). Containers the *host*
# runs put containerd/crio in other mounts' lowerdir= options; scanning the
# whole table matched those and misclassified the host (#58135).
try:
with open("/proc/self/mountinfo", "r", encoding="utf-8") as f:
mountinfo = f.read()
if any(marker in mountinfo for marker in ("kubepods", "containerd", "crio")):
_container_detected = True
return True
for line in f:
# 5th field (index 4) is the mount point; only "/" is our rootfs.
fields = line.split()
if len(fields) > 4 and fields[4] == "/" and any(
marker in line for marker in ("kubepods", "containerd", "crio")
):
_container_detected = True
return True
except OSError:
pass
_container_detected = False
Expand Down
56 changes: 56 additions & 0 deletions tests/test_hermes_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,62 @@ def _fake_open(p, *a, **kw):
monkeypatch.setattr("builtins.open", _fake_open)
assert is_container() is True

def test_host_running_containers_not_flagged(self, monkeypatch, tmp_path):
"""A host that merely runs containers is not itself a container (#58135)."""
import builtins
self._reset_cache(monkeypatch)
monkeypatch.delenv("KUBERNETES_SERVICE_HOST", raising=False)
monkeypatch.setattr(os.path, "exists", lambda p: False)
cgroup_file = tmp_path / "cgroup"
cgroup_file.write_text("0::/\n") # cgroup v2 — no runtime marker
mountinfo_file = tmp_path / "mountinfo"
mountinfo_file.write_text(
# host root: a real block device, no marker
"25 30 259:2 / / rw,relatime shared:1 - ext4 /dev/nvme0n1p2 rw\n"
# a running container's overlay: mount point is not "/", marker only
# in the lowerdir= options
"469 554 0:94 / /var/lib/docker/rootfs/overlayfs/7dda83 rw,relatime "
"shared:247 - overlay overlay rw,lowerdir=/var/lib/containerd/"
"io.containerd.snapshotter.v1.overlayfs/snapshots/33509/fs\n"
)
_real_open = builtins.open

def _fake_open(p, *a, **kw):
if p == "/proc/1/cgroup":
return _real_open(str(cgroup_file), *a, **kw)
if p == "/proc/self/mountinfo":
return _real_open(str(mountinfo_file), *a, **kw)
return _real_open(p, *a, **kw)

monkeypatch.setattr("builtins.open", _fake_open)
assert is_container() is False

def test_detects_containerd_root_overlay_lowerdir(self, monkeypatch, tmp_path):
"""Marker on the process's own root ("/") overlay still counts (#58135)."""
import builtins
self._reset_cache(monkeypatch)
monkeypatch.delenv("KUBERNETES_SERVICE_HOST", raising=False)
monkeypatch.setattr(os.path, "exists", lambda p: False)
cgroup_file = tmp_path / "cgroup"
cgroup_file.write_text("0::/\n")
mountinfo_file = tmp_path / "mountinfo"
mountinfo_file.write_text(
"1543 1542 0:158 / / rw,relatime - overlay overlay "
"rw,lowerdir=/var/lib/containerd/io.containerd.snapshotter.v1."
"overlayfs/snapshots/42/fs,upperdir=/run/containerd/x/fs\n"
)
_real_open = builtins.open

def _fake_open(p, *a, **kw):
if p == "/proc/1/cgroup":
return _real_open(str(cgroup_file), *a, **kw)
if p == "/proc/self/mountinfo":
return _real_open(str(mountinfo_file), *a, **kw)
return _real_open(p, *a, **kw)

monkeypatch.setattr("builtins.open", _fake_open)
assert is_container() is True

def test_caches_result(self, monkeypatch):
"""Second call uses cached value without re-probing."""
monkeypatch.setattr(hermes_constants, "_container_detected", True)
Expand Down