mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 05:29:45 +00:00
Link unpinned cluster members to their Proxmox address
Under strict TLS a cluster member without its own certificate pin is dialed by its bare node name, which is what happens to a node that joins after the others were pinned. Its runtime link was left empty, so the frontend swapped the bare name for an address guessed from the node's interfaces and picked a bridge every member shares. A support report had the newest node's web interface link open 172.16.0.1 instead of the node's management address. When neither the member nor the connection has a link override and the dial URL is a bare label, use the address Proxmox reports for that member, keeping the dial scheme and port. Dialing is unchanged.
This commit is contained in:
@@ -8270,3 +8270,20 @@ are unchanged. This shapes synthetic readings only, not live measurements.
|
||||
a fixed seven-day point/series equivalence and byte-accounting regression, and
|
||||
host generation/tick checks across cache/database/backup/general role changes.
|
||||
The existing `TestBuildFixtureStateIncludesHostAgents` assertion stays intact.
|
||||
|
||||
### Unpinned cluster members link to their Proxmox address (24 October 2026)
|
||||
|
||||
With strict TLS, a cluster member that has no per-member certificate pin is
|
||||
dialed by its bare node name so the certificate can be validated. Members that
|
||||
join after the others were pinned land in that state. Their runtime node
|
||||
`GuestURL` was empty, so the frontend replaced the bare name with an address
|
||||
guessed from the node's interfaces and picked a bridge every member shares
|
||||
(a support report opened another node's internal address). When no member or
|
||||
connection link override exists and the dial URL is a bare label,
|
||||
`resolveNodeConnectionInfo` now sets the link to the address Proxmox reports
|
||||
for that member, keeping the dial scheme and port. Operator link overrides,
|
||||
FQDN and IP dial URLs, and the dial URL itself are unchanged.
|
||||
|
||||
`internal/monitoring/monitor_pve_cluster_refresh_test.go` registers the
|
||||
unpinned strict-member link, override precedence and the URL builder cases
|
||||
(custom port, IPv6, FQDN, IP and missing address).
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -25,6 +27,8 @@ func resolveNodeConnectionInfo(instanceCfg *config.PVEInstance, discoveryCfg con
|
||||
}
|
||||
if ep.GuestURL != "" {
|
||||
guestURL = ep.GuestURL
|
||||
} else if guestURL == "" {
|
||||
guestURL = clusterMemberBrowserURL(ep, connectionHost)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -34,6 +38,36 @@ func resolveNodeConnectionInfo(instanceCfg *config.PVEInstance, discoveryCfg con
|
||||
return connectionHost, guestURL
|
||||
}
|
||||
|
||||
// clusterMemberBrowserURL gives a cluster member's web interface link the
|
||||
// address Proxmox reports for that node when Pulse dials it by a bare
|
||||
// hostname. Strict TLS without a per-member pin dials by name so the
|
||||
// certificate can be validated, but a browser often cannot resolve that
|
||||
// label, and the frontend would otherwise guess an address from the node's
|
||||
// interfaces, which picks bridges every member shares.
|
||||
func clusterMemberBrowserURL(endpoint config.ClusterEndpoint, connectionHost string) string {
|
||||
ip := net.ParseIP(strings.TrimSpace(endpoint.EffectiveIP()))
|
||||
if ip == nil {
|
||||
return ""
|
||||
}
|
||||
parsed, err := url.Parse(connectionHost)
|
||||
if err != nil || parsed.Hostname() == "" {
|
||||
return ""
|
||||
}
|
||||
host := parsed.Hostname()
|
||||
if net.ParseIP(host) != nil || strings.Contains(host, ".") {
|
||||
return ""
|
||||
}
|
||||
port := parsed.Port()
|
||||
if port == "" {
|
||||
port = "8006"
|
||||
}
|
||||
scheme := parsed.Scheme
|
||||
if scheme == "" {
|
||||
scheme = "https"
|
||||
}
|
||||
return scheme + "://" + net.JoinHostPort(ip.String(), port)
|
||||
}
|
||||
|
||||
func (m *Monitor) determineNodeIDAndStatus(instanceName string, instanceCfg *config.PVEInstance, node proxmox.Node) (string, string) {
|
||||
// Apply grace period for node status to prevent flapping
|
||||
// The configured instance is the canonical provider identity. Normally a
|
||||
|
||||
@@ -472,3 +472,78 @@ func TestBuildClusterEndpointsRetainsDuplicatePinConflicts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A member that joined after the others had their certificates pinned is
|
||||
// dialed by its bare node name under strict TLS. Its web interface link must
|
||||
// use the address Proxmox reports for it, not leave the frontend to guess one
|
||||
// from interfaces every member shares.
|
||||
func TestResolveNodeConnectionInfoUnpinnedStrictMemberLinksToClusterIP(t *testing.T) {
|
||||
instance := &config.PVEInstance{
|
||||
Name: "nexus",
|
||||
Host: "https://192.168.13.5:8006",
|
||||
Fingerprint: "AA:BB",
|
||||
VerifySSL: true,
|
||||
IsCluster: true,
|
||||
ClusterEndpoints: []config.ClusterEndpoint{
|
||||
{NodeName: "aurora", Host: "https://aurora:8006", IP: "192.168.13.5", Fingerprint: "AA:BB"},
|
||||
{NodeName: "venus", Host: "https://venus:8006", IP: "192.168.13.8"},
|
||||
},
|
||||
}
|
||||
|
||||
host, guestURL := resolveNodeConnectionInfo(instance, config.DiscoveryConfig{}, "venus")
|
||||
if host != "https://venus:8006" {
|
||||
t.Fatalf("connection host = %q, want the hostname strict TLS validates", host)
|
||||
}
|
||||
if guestURL != "https://192.168.13.8:8006" {
|
||||
t.Fatalf("guest URL = %q, want the Proxmox-reported member address", guestURL)
|
||||
}
|
||||
|
||||
host, guestURL = resolveNodeConnectionInfo(instance, config.DiscoveryConfig{}, "aurora")
|
||||
if host != "https://192.168.13.5:8006" || guestURL != "" {
|
||||
t.Fatalf("pinned member = (%q, %q), want its IP dial URL and no derived link", host, guestURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveNodeConnectionInfoKeepsOperatorLinks(t *testing.T) {
|
||||
instance := &config.PVEInstance{
|
||||
Name: "nexus",
|
||||
Host: "https://192.168.13.5:8006",
|
||||
VerifySSL: true,
|
||||
IsCluster: true,
|
||||
ClusterEndpoints: []config.ClusterEndpoint{
|
||||
{NodeName: "venus", Host: "https://venus:8006", IP: "192.168.13.8", GuestURL: "https://venus.example.test"},
|
||||
},
|
||||
}
|
||||
if _, guestURL := resolveNodeConnectionInfo(instance, config.DiscoveryConfig{}, "venus"); guestURL != "https://venus.example.test" {
|
||||
t.Fatalf("member link override = %q", guestURL)
|
||||
}
|
||||
|
||||
instance.ClusterEndpoints[0].GuestURL = ""
|
||||
instance.GuestURL = "https://cluster.example.test"
|
||||
if _, guestURL := resolveNodeConnectionInfo(instance, config.DiscoveryConfig{}, "venus"); guestURL != "https://cluster.example.test" {
|
||||
t.Fatalf("connection link override = %q", guestURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterMemberBrowserURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
endpoint config.ClusterEndpoint
|
||||
host string
|
||||
want string
|
||||
}{
|
||||
{"bare name uses reported IP", config.ClusterEndpoint{IP: "192.168.13.8"}, "https://venus:8006", "https://192.168.13.8:8006"},
|
||||
{"custom port kept", config.ClusterEndpoint{IP: "10.0.0.2"}, "https://venus:9443", "https://10.0.0.2:9443"},
|
||||
{"IPv6 bracketed", config.ClusterEndpoint{IP: "fd00::8"}, "https://venus:8006", "https://[fd00::8]:8006"},
|
||||
{"FQDN left to the browser", config.ClusterEndpoint{IP: "192.168.13.8"}, "https://venus.lab.test:8006", ""},
|
||||
{"IP dial needs nothing", config.ClusterEndpoint{IP: "192.168.13.8"}, "https://192.168.13.8:8006", ""},
|
||||
{"no reported address", config.ClusterEndpoint{}, "https://venus:8006", ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := clusterMemberBrowserURL(tc.endpoint, tc.host); got != tc.want {
|
||||
t.Fatalf("clusterMemberBrowserURL = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user