Files
pulse/docs/release-control
rcourtman f4ddeeb0e5 Give Docker containers and services without IDs their own alert refs (#2629)
Follow-up to #2620, which documented both conflations below as
residuals.

## Problem

Two Docker alert producers wrote names into identity slots:

- A container reported without an ID alerted under its Docker host's own
reference, `docker:<host>`. Its alerts showed on the host, joined the
host's resource history (which #2620 binds to the host), were never
cleared when the container left the report (cleanup matches
`docker:<host>/`), and Patrol registered `docker:<host>` on both the
host and the container, so the host's scope alias was ambiguous.
- A Swarm service without an ID put its normalized name in the
`/service/<ID>` slot, so a name equal to another service's Swarm ID
shared that service's alerts and history. #2620's resolver had to treat
every service in such a cluster as a conflict.

Pulse's own agent always sends Docker and Swarm IDs, so this needs a
malformed or third-party report. The registry still shows those
containers and services.

## Change

- `internal/alerts`: `DockerHostResourceID`,
`DockerContainerResourceID(host, id, name)` and
`DockerServiceResourceID` are the only builders. An ID-less container
alerts under `docker:<host>/name:<name>` and an ID-less service under
`docker:<host>/service/name:<normalized name>`. Docker and Swarm IDs
never contain a colon, so neither can equal a host or ID reference. One
with neither an ID nor a name raises no alert.
- The container and service policy switches classify by the first path
segment after the host, so a container name containing `/service/` stays
a container.
- Report cleanup, host removal and the host's offline alert also clear a
pre-upgrade container alert left on exactly `docker:<host>` (recorded
host must match, container ID empty). A pending image update recorded
there keeps its age when the first report still shows it pending.
- Canonical state IDs were split at the first `::`, but colliding Docker
hosts get IDs like `<base>::<suffix>`. Every container alert of such a
host was rewritten to `docker:<base>`, the other host's reference: it
joined that host's history and missed its own host's cleanup.
`splitCanonicalStateID` now splits before the alert's recorded spec ID,
else after its resource ID, so alerts saved the old way also recover on
restore.
- Patrol registers the same builders' output as scope aliases, with the
container's name.
- History: a service slot containing `:` is a name reference (never
binds, never retried), and the cluster-wide conflict for ID-less
services is gone. A real service ID reference now binds even when its
cluster also has an ID-less service.
- Contract notes in alerts, ai-runtime, monitoring and
unified-resources.

Remaining legacy boundary (in the unified-resources contract): rows
journaled before this change keep their old reference. An ID-less
container's old rows stay in its host's history, and an ID-less
service's old rows bind only to a service whose ID equals its normalized
name.

## Proof

- `TestDockerAlertsWithoutIDsUseTheirOwnReferences` (real
`CheckDockerHost`: fired and active references, removal cleanup,
pre-upgrade leftover on report and offline, config-save
container/service switches).
- `TestDockerHostIDWithSeparatorKeepsItsAlertReferences`,
`TestDockerAlertsRestoredWithCutShortReferencesRecover` (writes
`active-alerts.json`, restores through `LoadActiveAlerts`) and
`TestDockerContainerWithoutIDKeepsPendingUpdateAgeAcrossUpgrade`.
- `TestOwnerAlertTimelinesUseCanonicalHistoryIdentity`: real producers
into the SQLite history. The ID-less container and the service named
like `web`'s Swarm ID stay out of the host's and `web`'s histories.
- `TestResolvePatrolScopeResolvesDockerAlertResourceIDs`: the host's
alias stays unambiguous, and the ID-less container's name reference
resolves to the container in the unified read state.
-
`TestHistoryIdentityMonitorAdapterResolvesDockerHostAndServiceReferences`:
the ID reference binds before and after the service leaves inventory,
and name references never bind or retry.
- Mutation check: putting back each old behavior fails at least one of
these tests. Covered: host fallback, unmarked service name, no skip for
unidentifiable containers or services, each legacy clause and its host
check, the update-age carry-over, first-`::` splitting with and without
the spec rule, substring classification, the Patrol alias without the
name, and the resolver accepting a `name:` slot.
2026-10-07 02:52:45 +01:00
..