Record a relinked node/agent row as a manual-link fold

Main replaced the registry's linkMergedIDs with ManualLinkFold records
on the holder row (#2721), which canonical-ID succession, reference
resolution and report-merge all read. The relink of a split node and
its agent still wrote the removed map, so the branch no longer built
against main.

When a relink's absent side is one of a joined node+agent row's own
IDs, applyManualLinks now records it as a fold on that row, with the
side's own source. Pin succession then keeps the link instead of
re-keying it onto the joined ID, and a later report-merge of the joined
row names the relinked pair, which replaces the link.
This commit is contained in:
rcourtman
2026-10-08 09:49:18 +01:00
parent fe51ee2e67
commit 5b5026fafd
4 changed files with 39 additions and 23 deletions
@@ -7066,10 +7066,11 @@ another pair than the exclusions do, as a relink of the two rows a
report-merge left does (a registry now keeps each exclusion's `CreatedAt`);
a tie keeps the pair apart. Once a relink rejoins them, the declared link
folds one of the rows it names into the other's ID, and `applyManualLinks`
records that ID with the IDs manual links fold (it recognises the node's own
IDs from the joined row's Proxmox facet, and the agent's source-specific
ID), so pin succession keeps the link instead of re-keying it onto the
joined ID and handing the decision back to the older exclusions. The paths
records that as a `ManualLinkFold` on the joined row (it recognises the
node's own IDs from the row's Proxmox facet, and the agent's source-specific
ID), so pin succession treats the folded ID as observed and keeps the link
instead of re-keying it onto the joined ID and handing the decision back to
the older exclusions, and report-merge of the joined row names that pair. The paths
that join rows after ingest apply the same decision: `applyManualLinks`
skips a link between a node row and an agent row split after it, and the
presentation filter (`ListForPresentation` and the broadcast) keeps such
+11 -10
View File
@@ -202,22 +202,23 @@ func pinBelongsToAgent(pin ResourceIdentityPin, agent ResourceIdentity) bool {
return true
}
// joinedPairOwnsIDLocked reports whether id is an ID one side of a joined
// Proxmox node+agent row holds on its own: the node's (derived from the
// row's Proxmox facet, not its merged identity, whose first hostname can be
// the agent's) or the agent's source-specific ID, which an agent reporting
// no machine key holds when split.
func (rr *ResourceRegistry) joinedPairOwnsIDLocked(joined *Resource, id string) bool {
// joinedPairSideLocked reports whether id is an ID one side of a joined
// Proxmox node+agent row holds on its own, and that side's source: the
// node's own IDs (derived from the row's Proxmox facet, not its merged
// identity, whose first hostname can be the agent's) or the agent's
// source-specific ID, which an agent reporting no machine key holds when
// split.
func (rr *ResourceRegistry) joinedPairSideLocked(joined *Resource, id string) (DataSource, bool) {
if joined == nil || joined.Proxmox == nil || joined.Agent == nil || CanonicalResourceType(joined.Type) != ResourceTypeAgent {
return false
return "", false
}
id = CanonicalResourceID(id)
if agentID := normalizeSourceID(joined.Agent.AgentID); agentID != "" && id == rr.sourceSpecificID(ResourceTypeAgent, SourceAgent, agentID) {
return true
return SourceAgent, true
}
sourceID := normalizeSourceID(joined.Proxmox.SourceID)
if sourceID == "" {
return false
return "", false
}
_, identity := resourceFromProxmoxNode(models.Node{
ID: sourceID,
@@ -226,7 +227,7 @@ func (rr *ResourceRegistry) joinedPairOwnsIDLocked(joined *Resource, id string)
Instance: joined.Proxmox.Instance,
ClusterName: joined.Proxmox.ClusterName,
}, nil)
return slices.Contains(rr.proxmoxNodeOwnIDs(sourceID, identity), id)
return SourceProxmox, slices.Contains(rr.proxmoxNodeOwnIDs(sourceID, identity), id)
}
// nodeAgentRowsSplit reports whether two listed rows are a Proxmox node and
+10 -9
View File
@@ -4950,16 +4950,17 @@ func (rr *ResourceRegistry) applyManualLinks(thresholds map[DataSource]time.Dura
otherID = link.ResourceA
}
other := rr.resources[otherID]
if other == nil && otherID != primaryID && rr.joinedPairOwnsIDLocked(primary, otherID) {
// A relink of a split node and its agent names the two rows,
// and the declared link folds one of them into the other's ID
// once the relink rejoins them. Treat the ID as folded by this
// link so pin succession keeps the link rather than re-keying
// it onto the joined ID, which would let the split decide again.
if rr.linkMergedIDs == nil {
rr.linkMergedIDs = make(map[string]struct{})
if other == nil && otherID != primaryID {
if source, ok := rr.joinedPairSideLocked(primary, otherID); ok {
// A relink of a split node and its agent names the two rows,
// and the declared link folds one of them into the other's
// ID once the relink rejoins them. Record the fold so pin
// succession keeps the link rather than re-keying it onto
// the joined ID, which would let the split decide again,
// and so report-merge of the joined row names this pair.
recordManualLinkFold(primary, primaryID, &Resource{Sources: []DataSource{source}}, otherID)
rr.indexLinkFoldsLocked(primary)
}
rr.linkMergedIDs[otherID] = struct{}{}
}
if other == nil || otherID == primaryID {
continue
@@ -350,6 +350,19 @@ func TestOperatorSplitOverridesProxmoxNodeAgentLink(t *testing.T) {
t.Fatal(err)
}
assertViews("after relink", 2, nodeAgentRows{joined: merged.joined})
// The joined row records the relinked row it took in, so a
// report-merge of it names the relink's own pair.
folded := want.node
if folded == merged.joined {
folded = want.agent
}
recorded := false
for _, fold := range adapter.currentRegistry().ManualLinkFolds(merged.joined) {
recorded = recorded || (fold.HolderID == merged.joined && fold.FoldedID == folded)
}
if !recorded {
t.Fatalf("joined %s records no fold of relinked %s: %+v", merged.joined, folded, adapter.currentRegistry().ManualLinkFolds(merged.joined))
}
// A rebuild that meets the agent first orders the joined row's
// hostnames the agent's way; persisting its pins must not
// re-key the relink onto the joined ID either.