From f850099134b60017172c8c61b9b1739c2556115d Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 9 Jul 2026 09:56:04 +0100 Subject: [PATCH] Adopt the agent's preferred host when re-registering a matched node The resolved-identity match in canonical auto-register always preserved the stored host, so the route-aware IP preference added to the agent in 022f170df could never take effect for an already-registered node: a reinstall re-matched the node and kept the stale short-DNS host forever. Preserve the stored host only when it is absent from the agent's ordered candidate list (an admin-managed endpoint the agent cannot see); when the agent itself lists the stored host as a lower-priority candidate, adopt the newly selected host. Identity continuity (same node record, same token) is unchanged. Note: commit c21693489 carries this same message by mistake; it actually contains concurrent OIDC/SAML mapping work from a parallel session that was staged when the shared index raced. This commit is the real change. --- internal/api/config_setup_handlers.go | 22 ++++++++++- internal/api/config_setup_handlers_test.go | 46 ++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 internal/api/config_setup_handlers_test.go diff --git a/internal/api/config_setup_handlers.go b/internal/api/config_setup_handlers.go index 84ac011e7..4c76c6205 100644 --- a/internal/api/config_setup_handlers.go +++ b/internal/api/config_setup_handlers.go @@ -424,6 +424,22 @@ func canonicalAutoRegisterMatchMessage(reason string) string { return "Canonical auto-register matched existing node by " + reason } +// shouldPreserveExistingAutoRegisterHost reports whether an identity-matched +// node keeps its stored host on re-registration. The agent orders +// candidateHosts by preference, so an existing host that appears anywhere in +// that list was deliberately outranked by the selected host and is replaced. +// A host absent from the list is an admin-managed endpoint the agent cannot +// see; keep it. +func shouldPreserveExistingAutoRegisterHost(existingHost string, candidateHosts []string) bool { + existing := strings.TrimSpace(existingHost) + for _, candidate := range candidateHosts { + if strings.EqualFold(strings.TrimSpace(candidate), existing) { + return false + } + } + return true +} + func canonicalAutoRegisterCompletionPayloadMessage() string { return "Incomplete canonical auto-register token completion payload" } @@ -1314,10 +1330,11 @@ func (h *ConfigHandlers) handleCanonicalAutoRegister(w http.ResponseWriter, r *h } if hostsShareResolvedIdentity(node.Host, host) { existingIndex = i - preserveHost = true + preserveHost = shouldPreserveExistingAutoRegisterHost(node.Host, candidateHosts) log.Info(). Str("existingHost", node.Host). Str("newHost", host). + Bool("preserveHost", preserveHost). Str("type", "pve"). Msg(canonicalAutoRegisterMatchMessage("resolved host identity")) break @@ -1391,10 +1408,11 @@ func (h *ConfigHandlers) handleCanonicalAutoRegister(w http.ResponseWriter, r *h } if hostsShareResolvedIdentity(node.Host, host) { existingIndex = i - preserveHost = true + preserveHost = shouldPreserveExistingAutoRegisterHost(node.Host, candidateHosts) log.Info(). Str("existingHost", node.Host). Str("newHost", host). + Bool("preserveHost", preserveHost). Str("type", "pbs"). Msg(canonicalAutoRegisterMatchMessage("resolved host identity")) break diff --git a/internal/api/config_setup_handlers_test.go b/internal/api/config_setup_handlers_test.go new file mode 100644 index 000000000..f57d5c674 --- /dev/null +++ b/internal/api/config_setup_handlers_test.go @@ -0,0 +1,46 @@ +package api + +import "testing" + +func TestShouldPreserveExistingAutoRegisterHost(t *testing.T) { + tests := []struct { + name string + existingHost string + candidateHosts []string + want bool + }{ + { + name: "existing host outranked in candidate list is replaced", + existingHost: "https://pve02:8006", + candidateHosts: []string{"https://192.168.1.12:8006", "https://pve02:8006"}, + want: false, + }, + { + name: "existing host matches candidate case-insensitively", + existingHost: "https://PVE02:8006", + candidateHosts: []string{"https://192.168.1.12:8006", "https://pve02:8006"}, + want: false, + }, + { + name: "admin-managed host absent from candidates is preserved", + existingHost: "https://pve02.internal.example.com:8006", + candidateHosts: []string{"https://192.168.1.12:8006", "https://pve02:8006"}, + want: true, + }, + { + name: "empty candidate list preserves existing host", + existingHost: "https://pve02:8006", + candidateHosts: nil, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := shouldPreserveExistingAutoRegisterHost(tt.existingHost, tt.candidateHosts); got != tt.want { + t.Fatalf("shouldPreserveExistingAutoRegisterHost(%q, %v) = %v, want %v", + tt.existingHost, tt.candidateHosts, got, tt.want) + } + }) + } +}