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>
52 lines
2.8 KiB
Python
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"
|