Files
pulse/docs/release-control
rcourtman 39b6007f5d Pin each manual-link side from its own identity (#2719)
Stacked on #2715 (its commit is this branch's parent; whichever lands
first, the other still merges).

## Problem

A manual link merges two resources for display, but
`PersistIdentityPins` pinned the merged identity. A link primary was
pinned with the folded resource's machine key, and the store's
one-owner-per-key rule deleted the folded resource's own pin.

For a Proxmox node with a qualified endpoint
(`https://pve1.example.test:8006`) linked to a host agent, the node's
pin records the endpoint, so the next rebuild completed the agent's
machine key from it, minted the agent's machine-derived ID and merged
the two by ID before the link applied. With the node as primary,
succession then rewrote the link into a self-link on the agent's ID; in
either direction, removing the link left the pair merged. Reproduced
through `MonitorAdapter.PopulateSnapshotAndSupplemental` with a
`MemoryStore` on #2715's head.

## Change

- `applyManualLinks` captures each side's own pin before the merge
(`recordLinkOwnPin`, first capture per rebuild wins), and
`PersistIdentityPins` writes those instead of the merged projection,
keeping a pinnable folded side pinned under its own canonical ID.
- Link sides write through a new
`ResourceStore.ReplaceResourceIdentityPins` (both stores). Like an
upsert the row keeps fields the side's own pin leaves empty, but it
drops a strong key another resource holds this rebuild, which is how a
key an earlier release borrowed from the merged identity leaves the row.
- A folded side drops a key a live resource or an earlier folded side
already holds instead of trading it every rebuild, and writes no pin
when that would cost it its machine evidence (a cluster-only row would
be open to cluster-slot succession).
- Proxmox auto-links (LinkedAgentID / identity matcher) merge before any
manual link applies, so their boot-window pins are unchanged
(`TestIssue1753*`, `TestMonitorAdapterRebuildPersistsIdentityPins`
pass).

Known limits, written into the contract: a node row an earlier release
already filled with the agent's key still merges the pair by ID while
the node can complete from it; two live resources sharing a strong key
still alternate, as on main; evidence a later record pass of the same
rebuild merges into an already-joined row reaches neither side's pin.

## Verification

- New tests in `registry_merge_policy_test.go` (standalone and clustered
node, both link directions, three rebuilds with no steady-state pin
writes, then unlink separates the pair; shared-key, kept-field,
hostname-guard, machine-loss and borrowed-key cases) and `store_test.go`
(replace vs upsert on memory and SQLite).
- Each new test fails against #2715's runtime files via `go test
-overlay`; each guard rule fails its own test when removed.
- `internal/unifiedresources` full package, `internal/monitoring`
link/identity tests, `internal/api/resourceapi`, `go build ./...` and
`go vet` pass.
- Contract: `unified-resources.md` clause 25 and "Operator links reach
record-ingested resources".
2026-10-08 00:45:22 +01:00
..