[client] Skip peers without a PTR record in kubernetes commands (#8360)

* [client] Skip peers without a PTR record in kubernetes commands

getKubernetesClusters returned on the first failed reverse lookup, so a
single peer without a PTR record made `netbird kubernetes list` and
`write-kubeconfig` fail even when a netbird-kubeapi-proxy peer was
reachable later in the list. Log the failure at debug and keep scanning,
stopping only when the context is cancelled.

* [client] Return cancellation when the last peer's lookup is skipped

A failed lookup or fingerprint on the final peer was skipped and the
loop ended with a nil error, so a cancelled command reported no clusters
instead of the cancellation.

* [client] Report the lookup error when no peer resolves

Skipping every failed reverse lookup hid a broken resolver behind "No
Kubernetes clusters available" or "cluster not found", with the cause
only in the debug log. When no lookup succeeds, return the last lookup
error. One resolvable peer still means the resolver works, so per-peer
failures stay skipped.

* [client] Stop the kubernetes peer scan when the resolver is down

With a dead resolver every peer waited out the resolver timeout in turn
before the error was reported. A missing PTR record answers NXDOMAIN, so
a timeout or SERVFAIL before any peer resolved means the resolver itself
is failing: return it at once. After one peer resolved, later failures
are still skipped.

* [client] Extract kubernetes cluster probing from the peer scan

The lookup error handling pushed findKubernetesClusters past the
cognitive complexity limit. Move the FQDN match and fingerprint into
probeCluster so the scan loop only handles lookups and deduplication.
This commit is contained in:
Nicolas Frati
2026-10-26 16:02:26 +01:00
committed by GitHub
parent 08b924f739
commit afa6eec6a7
2 changed files with 169 additions and 20 deletions
+61 -20
View File
@@ -132,48 +132,89 @@ func getKubernetesClusters(ctx context.Context, peers []*proto.PeerState, nameFi
// https://github.com/golang/go/issues/17093
PreferGo: true,
}
return findKubernetesClusters(ctx, httpClient, resolver.LookupAddr, peers, nameFilter)
}
func findKubernetesClusters(
ctx context.Context,
httpClient *http.Client,
lookupAddr func(context.Context, string) ([]string, error),
peers []*proto.PeerState,
nameFilter string,
) ([]kubernetesCluster, error) {
kcs := []kubernetesCluster{}
attempted := map[string]struct{}{}
var lookupErr error
resolvedAny := false
for _, peer := range peers {
fqdns, err := resolver.LookupAddr(ctx, peer.IP)
if err != nil {
if err := ctx.Err(); err != nil {
return nil, err
}
// Peers without a DNS record have no PTR, which must not hide the remaining peers.
fqdns, err := lookupAddr(ctx, peer.IP)
if err != nil {
log.Debugf("reverse lookup of peer %s: %v", peer.IP, err)
// Before any success, a timeout or SERVFAIL means the resolver is down; waiting
// on each remaining peer would only repeat the timeout.
if !resolvedAny && !isNotFoundErr(err) {
return nil, fmt.Errorf("reverse lookup of peers: %w", err)
}
lookupErr = err
continue
}
resolvedAny = true
for _, fqdn := range fqdns {
if _, ok := attempted[fqdn]; ok {
continue
}
attempted[fqdn] = struct{}{}
comps := strings.Split(fqdn, ".")
if len(comps) < 2 {
kc, ok := probeCluster(ctx, httpClient, fqdn, nameFilter)
if !ok {
continue
}
if comps[1] != KubernetesDNSSuffix {
continue
}
if nameFilter != "" && nameFilter != comps[0] {
continue
}
clusterURL, clusterVersion, err := fingerprintClusters(ctx, httpClient, fqdn)
if err != nil {
log.Debugf("could not fingerprint Kubernetes cluster %s %q", fqdn, err)
continue
}
kc := kubernetesCluster{
name: comps[0],
url: clusterURL,
version: clusterVersion,
}
if nameFilter != "" {
return []kubernetesCluster{kc}, nil
}
kcs = append(kcs, kc)
}
}
// A skipped failure on the last peer would otherwise hide a cancellation.
if err := ctx.Err(); err != nil {
return nil, err
}
// Every lookup failing points at the resolver rather than at individual peers.
if !resolvedAny && lookupErr != nil {
return nil, fmt.Errorf("reverse lookup of peers: %w", lookupErr)
}
return kcs, nil
}
// probeCluster fingerprints fqdn when it names a netbird-kubeapi-proxy peer matching nameFilter.
func probeCluster(ctx context.Context, httpClient *http.Client, fqdn, nameFilter string) (kubernetesCluster, bool) {
comps := strings.Split(fqdn, ".")
if len(comps) < 2 || comps[1] != KubernetesDNSSuffix {
return kubernetesCluster{}, false
}
if nameFilter != "" && nameFilter != comps[0] {
return kubernetesCluster{}, false
}
clusterURL, clusterVersion, err := fingerprintClusters(ctx, httpClient, fqdn)
if err != nil {
log.Debugf("could not fingerprint Kubernetes cluster %s %q", fqdn, err)
return kubernetesCluster{}, false
}
return kubernetesCluster{
name: comps[0],
url: clusterURL,
version: clusterVersion,
}, true
}
func isNotFoundErr(err error) bool {
var dnsErr *net.DNSError
return errors.As(err, &dnsErr) && dnsErr.IsNotFound
}
func fingerprintClusters(ctx context.Context, httpClient *http.Client, fqdn string) (*url.URL, string, error) {
clusterURL, err := url.Parse("https://" + fqdn)
if err != nil {