mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-20 02:42:11 +00:00
5cde383b98
The v6.0.5 host-adoption fix (f85009913) only applies when a
re-registering agent matches a top-level instance host, so an agent on a
non-primary cluster member never benefited: its registration created a
standalone instance that ConsolidatePVEInstances folded back into the
cluster, and the fill-empty endpoint merge silently discarded the agent's
fresh address. The member row kept showing the corosync short-DNS host
rebuilt on every re-discovery (the "Install issues with V6" support
thread; a reinstall on v6.0.5 still showed the stale name).
Canonical auto-register now matches cluster member endpoints directly:
address identity against the agent's candidate list first, then an
unambiguous corosync node-name match. The Pulse-verified selected host is
adopted as the member's IPOverride, the durable field re-discovery
preserves and polling prefers, plus the fingerprint captured from that
address. An admin-managed override absent from the candidate list is
preserved, mirroring shouldPreserveExistingAutoRegisterHost. Credential
writes stay restricted to a same-token-identity secret refresh (reinstall
rotates the agent's token in place, so the stored secret is already
invalid) and full promotion onto a credential-less cluster; a member's
distinct per-node token never replaces working cluster credentials, and
no standalone instance is created for consolidation to discard.
171 lines
5.6 KiB
Go
171 lines
5.6 KiB
Go
package api
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
)
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClusterMemberOverrideIdentity(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value string
|
|
want string
|
|
}{
|
|
{name: "url with port", value: "https://192.168.1.12:8006", want: "192.168.1.12:8006"},
|
|
{name: "bare host defaults port", value: "192.168.1.12", want: "192.168.1.12:8006"},
|
|
{name: "host with port", value: "pve02:9000", want: "pve02:9000"},
|
|
{name: "hostname lowercased and port defaulted", value: "PVE02", want: "pve02:8006"},
|
|
{name: "empty", value: " ", want: ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := clusterMemberOverrideIdentity(tt.value); got != tt.want {
|
|
t.Fatalf("clusterMemberOverrideIdentity(%q) = %q, want %q", tt.value, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFindCanonicalAutoRegisterClusterMember(t *testing.T) {
|
|
instances := []config.PVEInstance{
|
|
{
|
|
Name: "clusterA", Host: "https://192.0.2.10:8006", IsCluster: true,
|
|
ClusterEndpoints: []config.ClusterEndpoint{
|
|
{NodeName: "pve01", Host: "https://pve01:8006", IP: "192.0.2.10"},
|
|
{NodeName: "pve02", Host: "https://pve02:8006", IP: "192.0.2.11"},
|
|
},
|
|
},
|
|
{
|
|
Name: "clusterB", Host: "https://198.51.100.10:8006", IsCluster: true,
|
|
ClusterEndpoints: []config.ClusterEndpoint{
|
|
{NodeName: "alpha", Host: "https://alpha:8006", IP: "198.51.100.11"},
|
|
},
|
|
},
|
|
}
|
|
|
|
t.Run("endpoint IP identity match wins", func(t *testing.T) {
|
|
i, j, ok := findCanonicalAutoRegisterClusterMember(instances, "unrelated-name", []string{"https://192.0.2.11:8006"})
|
|
if !ok || i != 0 || j != 1 {
|
|
t.Fatalf("got (%d, %d, %v), want (0, 1, true)", i, j, ok)
|
|
}
|
|
})
|
|
|
|
t.Run("unique node-name match", func(t *testing.T) {
|
|
i, j, ok := findCanonicalAutoRegisterClusterMember(instances, "pve02", []string{"https://203.0.113.5:8006"})
|
|
if !ok || i != 0 || j != 1 {
|
|
t.Fatalf("got (%d, %d, %v), want (0, 1, true)", i, j, ok)
|
|
}
|
|
})
|
|
|
|
t.Run("fqdn agent hostname matches short corosync name", func(t *testing.T) {
|
|
i, j, ok := findCanonicalAutoRegisterClusterMember(instances, "pve02.internal.example.com", []string{"https://203.0.113.5:8006"})
|
|
if !ok || i != 0 || j != 1 {
|
|
t.Fatalf("got (%d, %d, %v), want (0, 1, true)", i, j, ok)
|
|
}
|
|
})
|
|
|
|
t.Run("no match", func(t *testing.T) {
|
|
if _, _, ok := findCanonicalAutoRegisterClusterMember(instances, "unknown", []string{"https://203.0.113.5:8006"}); ok {
|
|
t.Fatal("expected no match")
|
|
}
|
|
})
|
|
|
|
t.Run("ambiguous node name across clusters is rejected", func(t *testing.T) {
|
|
ambiguous := append([]config.PVEInstance(nil), instances...)
|
|
ambiguous[1].ClusterEndpoints = []config.ClusterEndpoint{
|
|
{NodeName: "pve02", Host: "https://pve02.other:8006", IP: "198.51.100.12"},
|
|
}
|
|
if _, _, ok := findCanonicalAutoRegisterClusterMember(ambiguous, "pve02", []string{"https://203.0.113.5:8006"}); ok {
|
|
t.Fatal("expected ambiguous name match to be rejected")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestShouldAdoptClusterMemberAutoRegisterAddress(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
existingOverride string
|
|
candidateHosts []string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "empty override always adopts",
|
|
existingOverride: "",
|
|
candidateHosts: nil,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "override outranked in candidate list is replaced",
|
|
existingOverride: "192.168.1.12:8006",
|
|
candidateHosts: []string{"https://192.168.1.50:8006", "https://192.168.1.12:8006"},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "portless override matches default-port candidate",
|
|
existingOverride: "192.168.1.12",
|
|
candidateHosts: []string{"https://192.168.1.50:8006", "https://192.168.1.12:8006"},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "admin-managed override absent from candidates is preserved",
|
|
existingOverride: "10.99.0.5:8006",
|
|
candidateHosts: []string{"https://192.168.1.50:8006", "https://192.168.1.12:8006"},
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := shouldAdoptClusterMemberAutoRegisterAddress(tt.existingOverride, tt.candidateHosts); got != tt.want {
|
|
t.Fatalf("shouldAdoptClusterMemberAutoRegisterAddress(%q, %v) = %v, want %v",
|
|
tt.existingOverride, tt.candidateHosts, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|