diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 31e4da09fa..434aa7392d 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -684,7 +684,11 @@ but registry IDs, merge keys, parent links, metrics targets, source-native guest IDs, external URLs, discovery targets, and actions must not. Children such as VMs, containers, and storage inherit the parent's presentation and native aliases so Overview, search, API/websocket/mobile, storage, backups, -and filters agree. A linked agent may enrich the canonical node but cannot +and filters agree. That inheritance stays in the child's `ProxmoxData`: only +the node itself lists its node identity, native name, and native-name aliases +as canonical aliases, because a child carrying them would answer to the node's +alert resource ID and every guest would show each node alert as its own. +A linked agent may enrich the canonical node but cannot replace a configured override with its hostname; same-name cluster identity remains provider scoped even after a native rename. Physical-disk resources own cross-source disk identity. When Proxmox inventory diff --git a/internal/unifiedresources/canonical_identity.go b/internal/unifiedresources/canonical_identity.go index 838def2176..cdafee8cd2 100644 --- a/internal/unifiedresources/canonical_identity.go +++ b/internal/unifiedresources/canonical_identity.go @@ -111,12 +111,18 @@ func canonicalAliases(resource Resource, primaryID, platformID, hostname string) values = append(values, "agent:"+agentID) } if resource.Proxmox != nil { - values = append( - values, - strings.TrimSpace(resource.Proxmox.NodeIdentity), - strings.TrimSpace(resource.Proxmox.NodeName), - ) - values = append(values, resource.Proxmox.NodeAliases...) + // Only the node owns its node identity. Guests and storage inherit + // the parent's NodeIdentity and NodeAliases for presentation and + // search, but as aliases they would make every child answer to the + // node's alert resource ID, so one node alert painted every guest. + if CanonicalResourceType(resource.Type) == ResourceTypeAgent { + values = append( + values, + strings.TrimSpace(resource.Proxmox.NodeIdentity), + strings.TrimSpace(resource.Proxmox.NodeName), + ) + values = append(values, resource.Proxmox.NodeAliases...) + } // PVE disk alerts predate the unified resource registry and retain a // path-shaped resource reference. Resolve it to this exact physical // disk so a UI mute reaches both new and restored occurrences (#2112). diff --git a/internal/unifiedresources/canonical_identity_test.go b/internal/unifiedresources/canonical_identity_test.go index 3418d614e1..ad7f95816d 100644 --- a/internal/unifiedresources/canonical_identity_test.go +++ b/internal/unifiedresources/canonical_identity_test.go @@ -176,6 +176,42 @@ func TestRefreshCanonicalIdentityKeepsProxmoxPresentationSeparateFromNativeAlias } } +func TestRefreshCanonicalIdentityKeepsParentNodeIdentityOffProxmoxChildren(t *testing.T) { + parentNode := &ProxmoxData{ + Instance: "homelab", + NodeIdentity: "homelab-minipc", + NodeName: "minipc", + NodeAliases: []string{"minipc-old"}, + } + for _, resourceType := range []ResourceType{ResourceTypeVM, ResourceTypeSystemContainer, ResourceTypeStorage} { + resource := Resource{ + ID: string(resourceType) + "-1", + Type: resourceType, + Name: "cloudflared", + Proxmox: &ProxmoxData{ + SourceID: "homelab:minipc:104", + Instance: parentNode.Instance, + NodeIdentity: parentNode.NodeIdentity, + NodeName: parentNode.NodeName, + NodeAliases: append([]string(nil), parentNode.NodeAliases...), + }, + MetricsTarget: &MetricsTarget{ResourceType: string(resourceType), ResourceID: "homelab:minipc:104"}, + } + + RefreshCanonicalIdentity(&resource) + + if resource.Canonical == nil { + t.Fatalf("%s: canonical identity missing", resourceType) + } + for _, alias := range resource.Canonical.Aliases { + if alias == "homelab-minipc" || alias == "minipc-old" { + t.Fatalf("%s carries parent node identity %q as an alias, so node alerts attach to it: %v", + resourceType, alias, resource.Canonical.Aliases) + } + } + } +} + func TestProxmoxDiskAlertAliasIsScopedAndNotPrimaryIdentity(t *testing.T) { resource := Resource{ ID: "physical-disk-1", Type: ResourceTypePhysicalDisk,