From ab15c4db8a9f84a59d5d2f74ae50da1793f031e3 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 04:29:10 +0100 Subject: [PATCH] Index only possible grouping owners and exercise real pass boundaries Maximum-priority roots cannot be better than any group in a snapshot, so leave them out of the temporary owner indexes while retaining every resource and group. Correct new fixtures to respect existing attachment semantics: differently ranked hosts may already merge, and an agent's first-pass IP attachment must remain ambiguous until its two owners merge. Cover equal-priority Proxmox cost before any timing run. Contract-Neutral: Candidate-only priority pruning and faithful regression fixtures preserve all stored/published resources, identity, strict attachment ambiguity, evidence precedence, public schemas and counted-system admission. Change-source: pulse-maintainer --- .../internal/subsystems/unified-resources.md | 5 ++-- .../resolved_host_set_test.go | 2 +- .../top_level_owner_index_test.go | 24 +++++++++++++-- .../unifiedresources/top_level_systems.go | 30 ++++++++++++++----- .../top_level_systems_test.go | 19 ++++++------ 5 files changed, 57 insertions(+), 23 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index c58a47f52e..143b32a627 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -5221,8 +5221,9 @@ short hostname and another reports the FQDN, but it must not collapse two distinct fully-qualified hosts that merely share the same short prefix across different domains. Fallback owner buckets are pass-local priority-ordered slices, rebuilt from -each complete group snapshot. Queries visit only strictly better-priority -owners; a second distinct qualifying root still rejects attachment even when +each complete group snapshot. A maximum-priority group cannot be better than +any query and is omitted only from the temporary candidate index. Queries visit +only strictly better-priority owners; a second distinct qualifying root still rejects attachment even when its priority or evidence kind differs. The unique root keeps lexical exact-host evidence before full short/FQDN evidence before lexical exact-IP evidence. Comparable/short-form keys only prune candidates: the full hostname equivalence diff --git a/internal/unifiedresources/resolved_host_set_test.go b/internal/unifiedresources/resolved_host_set_test.go index a6b3292a06..877c688285 100644 --- a/internal/unifiedresources/resolved_host_set_test.go +++ b/internal/unifiedresources/resolved_host_set_test.go @@ -697,7 +697,7 @@ func TestTopLevelOwnerIndexPreservesAmbiguityInMonitoredCountAndAdmission(t *tes t.Run(fmt.Sprintf("%d_targets", targetCount), func(t *testing.T) { resources := []Resource{ topLevelTestProxmoxNode("platform", "tower.lab", "pve-source", "https://tower.lab:8006"), - topLevelTestTrueNAS("nas", "tower.lab"), + topLevelTestProxmoxNode("platform-other", "tower.lab", "pve-source-other", "https://tower.lab:8006"), } registry := NewRegistry(nil) registry.IngestResources(resources[:targetCount]) diff --git a/internal/unifiedresources/top_level_owner_index_test.go b/internal/unifiedresources/top_level_owner_index_test.go index 81a9b60a67..f5edd5e1ca 100644 --- a/internal/unifiedresources/top_level_owner_index_test.go +++ b/internal/unifiedresources/top_level_owner_index_test.go @@ -168,12 +168,26 @@ func TestTopLevelOwnerIndexBucketsAreOrderedAndDeduplicated(t *testing.T) { } index := buildTopLevelSystemFallbackIndex(groups) for name, bucket := range map[string][]int{"host": index.hosts["alpha"], "ip": index.ips["192.0.2.8"], "form": index.hostForms["alpha"]} { - if !reflect.DeepEqual(bucket, []int{9, 3, 4, 1}) { + if !reflect.DeepEqual(bucket, []int{9, 3, 4}) { t.Fatalf("%s owners not priority/root ordered and deduplicated: %v", name, bucket) } } } +func TestTopLevelOwnerIndexEqualPriorityDoesNotBuildCandidateBuckets(t *testing.T) { + groups := map[int]topLevelSystemResolvedGroup{ + 0: ownerTestGroup(3, []string{"alpha", "alpha.lab"}, []string{"192.0.2.8"}), + 7: ownerTestGroup(3, []string{"alpha"}, []string{"192.0.2.8"}), + } + index := buildTopLevelSystemFallbackIndex(groups) + if len(index.hosts)+len(index.ips)+len(index.hostForms) != 0 { + t.Fatal("equal-priority roots cannot be candidates for any query") + } + for root := range groups { + assertOwnerTargetMatchesOracle(t, root, groups, index) + } +} + func TestTopLevelOwnerIndexEvidenceAndAmbiguity(t *testing.T) { tests := []struct { name string @@ -243,7 +257,7 @@ type topLevelOwnerIndexFixture struct { func topLevelOwnerIndexFixtures() []topLevelOwnerIndexFixture { fixtures := []topLevelOwnerIndexFixture{{name: "empty"}, {name: "one", resources: []Resource{topLevelTestAgent("agent", "tower.lab", "machine", "agent")}}} - for _, name := range []string{"spread-1220", "shared-exact-1220", "shared-short-1220", "ambiguous-1220", "different-fqdns-200"} { + for _, name := range []string{"spread-1220", "shared-exact-1220", "shared-short-1220", "ambiguous-1220", "different-fqdns-200", "equal-priority-proxmox-1220"} { count, nodes := 1220, 1 switch name { case "spread-1220": @@ -274,7 +288,11 @@ func topLevelOwnerIndexFixtures() []topLevelOwnerIndexFixture { case "different-fqdns-200": host = fmt.Sprintf("tower.other-%d", i) } - resources = append(resources, topLevelTestAgent(fmt.Sprintf("agent-%d", i), host, fmt.Sprintf("machine-%d", i), fmt.Sprintf("agent-id-%d", i))) + if name == "equal-priority-proxmox-1220" { + resources = append(resources, Resource{ID: fmt.Sprintf("vm-%d", i), Type: ResourceTypeVM, Name: fmt.Sprintf("workload-%d", i), Proxmox: &ProxmoxData{NodeName: host, VMID: i + 100}}) + } else { + resources = append(resources, topLevelTestAgent(fmt.Sprintf("agent-%d", i), host, fmt.Sprintf("machine-%d", i), fmt.Sprintf("agent-id-%d", i))) + } } fixtures = append(fixtures, topLevelOwnerIndexFixture{name: name, resources: resources}) } diff --git a/internal/unifiedresources/top_level_systems.go b/internal/unifiedresources/top_level_systems.go index bfb8d87b6f..6b1a8d9756 100644 --- a/internal/unifiedresources/top_level_systems.go +++ b/internal/unifiedresources/top_level_systems.go @@ -417,14 +417,25 @@ type topLevelSystemFallbackIndex struct { } func buildTopLevelSystemFallbackIndex(groups map[int]topLevelSystemResolvedGroup) topLevelSystemFallbackIndex { - index := topLevelSystemFallbackIndex{ - hosts: make(map[string][]int), - ips: make(map[string][]int), - hostForms: make(map[string][]int), + maxPriority := 0 + havePriority := false + for _, group := range groups { + if !havePriority || group.priority > maxPriority { + maxPriority = group.priority + havePriority = true + } } - roots := make([]int, 0, len(groups)) - for root := range groups { - roots = append(roots, root) + var roots []int + for root, group := range groups { + // A maximum-priority root cannot be better than any query in this + // snapshot. Omit it only from the temporary candidate index, never + // from the groups, complete inventory or output. + if group.priority < maxPriority { + roots = append(roots, root) + } + } + if len(roots) == 0 { + return topLevelSystemFallbackIndex{} } sort.Slice(roots, func(i, j int) bool { if groups[roots[i]].priority != groups[roots[j]].priority { @@ -432,6 +443,11 @@ func buildTopLevelSystemFallbackIndex(groups map[int]topLevelSystemResolvedGroup } return roots[i] < roots[j] }) + index := topLevelSystemFallbackIndex{ + hosts: make(map[string][]int), + ips: make(map[string][]int), + hostForms: make(map[string][]int), + } add := func(owners map[string][]int, key string, root int) { bucket := owners[key] // All aliases of one root are added together, so a last-entry check diff --git a/internal/unifiedresources/top_level_systems_test.go b/internal/unifiedresources/top_level_systems_test.go index 4a329e101c..02b635786c 100644 --- a/internal/unifiedresources/top_level_systems_test.go +++ b/internal/unifiedresources/top_level_systems_test.go @@ -437,18 +437,17 @@ func topLevelTestK8sCluster(id, clusterID, agentID, server string) Resource { } func TestResolveTopLevelSystemsRebuildsOwnerIndexAtEveryPassAndCall(t *testing.T) { - bridge := topLevelTestAgent("bridge", "alpha.lab", "bridge-machine", "bridge-agent") + platform := topLevelTestProxmoxNode("platform", "alpha.lab", "pve-source", "https://beta.lab:8006") + platform.Identity.IPAddresses = []string{"192.0.2.8"} + bridge := topLevelTestDockerHost("bridge", "beta.lab", "bridge-runtime", "") bridge.Identity.IPAddresses = []string{"192.0.2.8"} - leaf := topLevelTestDockerHost("leaf", "beta.lab", "leaf-runtime", "") + leaf := topLevelTestAgent("leaf", "gamma.lab", "leaf-machine", "leaf-agent") leaf.Identity.IPAddresses = []string{"192.0.2.8"} - resources := []Resource{ - topLevelTestProxmoxNode("platform", "alpha.lab", "pve-source", "https://alpha.lab:8006"), - bridge, leaf, - topLevelTestK8sCluster("cluster", "cluster-id", "", "https://alpha.lab:6443"), - } - // Docker has better priority than the unmerged agent, so it cannot attach - // by their shared IP in the first pass. Once the agent joins the platform, - // a fresh pass must admit the IP attachment while keeping the cluster apart. + resources := []Resource{platform, bridge, leaf, topLevelTestK8sCluster("cluster", "cluster-id", "", "https://alpha.lab:6443")} + // The agent's IP initially matches two distinct better-priority groups. + // Docker can attach to the platform, but that does not change the first + // pass's candidate snapshot. A fresh pass must then admit the agent's now + // unique IP attachment while keeping the Kubernetes cluster apart. resolver := ResolveTopLevelSystems(resources) if resolver.Count() != 2 { t.Fatalf("stale pass index or excluded cluster attachment: %d groups", resolver.Count())