Files
FabledSteward/tests/plugins/docker/test_timeline.py
T
bvandeusenandClaude Opus 5.5 2b97c36fd4
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m36s
CI / integration (push) Successful in 1m52s
CI / publish (push) Successful in 1m12s
feat(timeline): maintenance, reboots, deploys, crash-loops and reviewed anomalies on the timeline (#6349)
The host agent writes each maintenance job once it ends, and each stretch a
host was away (a reboot, a crash, lost contact, the agent restarting), kept up
to date until it is back. Docker judges each snapshot: a unit on a new image,
or a container recreated, is a deploy; deaths past the Thresholds crash-loop
count are one crash-loop entry that later deaths extend; single restarts are
recorded but hidden. An anomaly has one entry whose type follows its review:
only those judged real are shown by default.

A source whose record changes keeps one entry for it (record_or_update).
Plugins register the records their entries point to, so an aged-out job or
downtime reads "Details no longer kept". The first fill runs once per source
(timeline.filled), the host agent's jobs and downtime included.

get_anomaly's "what else was going on" reads the timeline for the host or
resource instead of querying alerts, runs, maintenance, downtime and container
deaths itself; Docker's own source keeps only starts, stops and health
changes. A new kind, Observed, is for what Steward saw happen with no rule or
person behind it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:21:44 -04:00

52 lines
2.8 KiB
Python

"""What a snapshot puts on the timeline as a deploy: a unit (a container, or
its swarm service) running an image it didn't, a container recreated under
its name, a new container; not a swarm task the scheduler moved here, and
not a container that only restarted."""
from types import SimpleNamespace as NS
from plugins.docker.timeline import _deploy_summary, deploys, short_image
def _row(name, image, cid, service=None):
return NS(name=name, image=image, container_id=cid, service_name=service)
def _c(name, image, cid, service=None, status="running"):
return {"name": name, "image": image, "container_id": cid, "service_name": service, "status": status}
def test_a_new_image_is_a_deploy_and_names_the_old_one():
[d] = deploys([_row("web", "nginx:1.27", "a1")], [_c("web", "nginx:1.28", "a2")])
assert (d["unit"], d["was"], d["recreated"]) == ("web", "nginx:1.27", False)
assert _deploy_summary(d, "h1") == "web on h1: nginx:1.27 → nginx:1.28"
def test_a_swarm_service_is_one_unit_and_a_new_build_of_the_same_tag_says_so():
old = [_row("app.1.aaa", "reg/steward:dev@sha256:1111aaaa2222bbbb", "x", "steward_app")]
new = [_c("app.1.bbb", "reg/steward:dev@sha256:3333cccc4444dddd", "y", "steward_app"),
_c("app.2.ccc", "reg/steward:dev@sha256:3333cccc4444dddd", "z", "steward_app")]
[d] = deploys(old, new) # one per service, not per task
assert d["unit"] == "steward_app" and d["swarm"]
assert _deploy_summary(d, "h1") == "steward_app on h1: new build of steward:dev (3333cccc4444)"
def test_a_recreated_container_and_a_new_one_are_deploys():
[d] = deploys([_row("db", "postgres:16", "c1")], [_c("db", "postgres:16", "c2")])
assert d["recreated"] and _deploy_summary(d, "h1") == "db on h1: recreated on postgres:16"
[d] = deploys([_row("db", "postgres:16", "c1")], [_c("db", "postgres:16", "c1"), _c("cache", "redis:7", "r1")])
assert _deploy_summary(d, "h1") == "cache on h1: started on redis:7"
def test_a_moved_swarm_task_a_restart_and_a_stopped_container_are_not():
assert deploys([], [_c("app.1.aaa", "img:1", "x", "svc")]) == [] # landed here, new to the host
assert deploys([_row("web", "nginx:1", "a1")], [_c("web", "nginx:1", "a1")]) == []
assert deploys([_row("web", "nginx:1", "a1")], [_c("web", "nginx:2", "a2", status="exited")]) == []
# A swarm task replaced on the same image (a crash, a reschedule) is not a deploy either.
assert deploys([_row("app.1.aaa", "img:1", "x", "svc")], [_c("app.1.bbb", "img:1", "y", "svc")]) == []
def test_short_image():
assert short_image("git.example.com/team/steward:dev@sha256:e12594c948a294693c") == "steward:dev (e12594c948a2)"
assert short_image("nginx:1.28") == "nginx:1.28"
assert short_image("") == "an unknown image"