"""The Docker page leads with what needs attention, ranked the way the MCP
tools rank it, and marks a container whose report is old as stale (#5123)."""
import re
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace as NS
from plugins.docker import service as svc
from plugins.docker.overview import DOCKER_STATES, NO_STACK, container_row, service_row
from plugins.docker.routes import _is_stale
from tests._templates import render_plugin_template
from tests.test_style_tokens import hex_colours
NOW = datetime.now(timezone.utc)
def _c(name, **kw):
base = dict(name=name, host_id="h1", image="nginx:1", status="running", health=None, exit_code=None,
oom_killed=False, restart_count=0, cpu_pct=5.0, mem_pct=10.0, compose_project=None,
service_name=None, ports_json=None, started_at=NOW - timedelta(hours=2),
scraped_at=NOW - timedelta(seconds=20))
base.update(kw)
return NS(**base)
def test_needs_attention_ranks_the_worst_problem_first():
ranked = [{"container": _c(n), "problems": p, "since": None, "replaced": False} for n, p in [
("fine", []), ("oom", ["oom_killed"]), ("sick", ["unhealthy"]),
("loop", ["crash_looping"]), ("exit", ["exited_nonzero"]), ("bounce", ["restarting", "crash_looping"])]]
assert [r["container"].name for r in svc.needs_attention(ranked)] == ["bounce", "loop", "sick", "oom", "exit"]
def test_stale_is_older_than_the_agents_window():
assert not _is_stale(_c("a"), NOW, 180)
assert _is_stale(_c("a", scraped_at=NOW - timedelta(minutes=10)), NOW, 180)
assert _is_stale(_c("a", scraped_at=None), NOW, 180)
# A fresh row on a host whose Docker stopped answering is stale too (#5152).
assert _is_stale(_c("a"), NOW, 180, {"h1": object()})
assert not _is_stale(_c("a"), NOW, 180, {"h2": object()})
assert svc.stale_after_seconds({"PLUGINS": {"host_agent": {"stale_after_seconds": 60}}}) == 60
assert svc.stale_after_seconds({}) == 180
_F = {"q": "", "stack": "", "host": "", "state": "", "sort": "status", "dir": "asc"}
async def _rows(app, **over):
"""docker/rows.html with what routes.rows() hands it (#5298–#5301),
nothing set unless given."""
ctx = dict(services=[], containers=[], services_total=0, containers_total=0, services_healthy=0,
failing_services=[], failures=[], docker_hosts=0, docker_down=[], attention=[],
says=svc.PROBLEM_SAYS, severity=svc.PROBLEM_SEVERITY, unhealthy=0, restarting=0,
stale=0, running=0, stopped=0, f=dict(_F), filtered=False,
sort_url=lambda key: f"/plugins/docker/?sort={key}",
page_url=lambda **kw: "/plugins/docker/?" + "&".join(f"{k}={v}" for k, v in kw.items()))
ctx.update(over)
async with app.test_request_context("/plugins/docker/rows"):
page = await render_plugin_template(app, "docker", "docker/rows.html", **ctx)
return re.sub(r">\s+<", "><", " ".join(page.split()))
def _crow(c, *, host="nas", stale=False, exit_failed=False, ports=()):
return container_row(c, label=svc.display_name(c), host=host, usage=svc.usage(c, 1, None),
ports=list(ports), stale=stale, exit_failed=exit_failed, now=NOW)
async def test_a_crash_loop_comes_first_and_a_stale_row_says_so(app):
loop = _c("api", status="restarting", exit_code=1, restart_count=7)
old = _c("cache", scraped_at=NOW - timedelta(minutes=30), cpu_pct=95.0)
attention = [{"container": loop, "label": "api", "problems": ["restarting", "crash_looping"], "replaced": False,
"since": datetime.now(timezone.utc) - timedelta(minutes=8), "stale": False}]
page = await _rows(app, attention=attention, restarting=1, stale=1, running=1, stopped=1, docker_hosts=1,
containers=[_crow(loop, exit_failed=True), _crow(old, stale=True)], containers_total=2)
# #5298: the verdict first; each problem section carries its own count (#5214).
assert page.index('class="page-verdict"') < page.index("Needs attention")
assert "1 needs attention" in page and "1 container running, 1 stopped" in page
assert " unhealthy" not in page and "Failed in the last 24h" not in page and "stat-strip" not in page
card = page[page.index("Needs attention"):page.index("")]
assert 'Restarting' in card
assert 'href="/plugins/docker/container/h1/api" title="api">api' in card
assert "is restarting and keeps dying · exit 1 · since " in card
assert 'href="/plugins/docker/container/h1/api/logs"' in card
assert "cache" not in card
assert page.index("Needs attention") < page.index("
1'
' · 1 restarting') in card
assert '">1 stale' in page[:page.index("")] # stale sits in the verdict
assert 'class="badge badge-warn" title="Last report' in page # the stale row
# CPU in cores, toned by its share of the host's CPUs through Settings → Thresholds.
assert ('0.95 cores') in page
assert "