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.
This commit is contained in:
rcourtman 2026-07-09 09:56:04 +01:00
parent c21693489a
commit f850099134
2 changed files with 66 additions and 2 deletions

View file

@ -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

View file

@ -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)
}
})
}
}