From c1602d7ea8d8a5ef4928875b8026b08b0770e287 Mon Sep 17 00:00:00 2001 From: "Patrick W. Healy" Date: Wed, 16 Sep 2026 22:05:15 +0000 Subject: [PATCH] refactor(net): share exact status diagnostic counting rules Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d2243398-6c36-4c3d-969e-7ed7bfb5b459 --- internal/net/status/diagnostics.go | 60 +++++++++++++++++++ internal/net/status/diagnostics_test.go | 78 +++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 internal/net/status/diagnostics.go create mode 100644 internal/net/status/diagnostics_test.go diff --git a/internal/net/status/diagnostics.go b/internal/net/status/diagnostics.go new file mode 100644 index 000000000..04e86932c --- /dev/null +++ b/internal/net/status/diagnostics.go @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// SPDX-License-Identifier: Apache-2.0 + +package status + +import ( + "strings" + "time" + + "github.com/Azure/unbounded/internal/net/status/v1alpha1" +) + +// PeerLinkHealthyForDiagnostics preserves the problem list's normalized probe +// rules. Unlike PeerHealthyForOverview, a nonempty status enables probe checks +// even when Enabled is false. Each tunnel link is counted, not each node name. +func PeerLinkHealthyForDiagnostics(peer *v1alpha1.PeerStatus, now time.Time) bool { + checkStatus := "" + if peer.HealthCheck != nil { + checkStatus = strings.ToLower(strings.TrimSpace(peer.HealthCheck.Status)) + } + + if (peer.HealthCheck != nil && peer.HealthCheck.Enabled) || checkStatus != "" { + return checkStatus == "up" + } + + lastHandshake := peer.Tunnel.LastHandshake + + return !lastHandshake.IsZero() && now.Sub(lastHandshake) < 3*time.Minute +} + +// UnhealthyPeerLinkCount counts unhealthy links without deduplicating peer names. +func UnhealthyPeerLinkCount(peers []v1alpha1.PeerStatus, now time.Time) int { + count := 0 + + for i := range peers { + if !PeerLinkHealthyForDiagnostics(&peers[i], now) { + count++ + } + } + + return count +} + +// RouteMismatchCount counts every annotated expected/present next-hop mismatch. +func RouteMismatchCount(routes []v1alpha1.RouteEntry) int { + count := 0 + + for _, route := range routes { + for _, hop := range route.NextHops { + expected := hop.Expected != nil && *hop.Expected + + present := hop.Present != nil && *hop.Present + if expected != present { + count++ + } + } + } + + return count +} diff --git a/internal/net/status/diagnostics_test.go b/internal/net/status/diagnostics_test.go new file mode 100644 index 000000000..426377e28 --- /dev/null +++ b/internal/net/status/diagnostics_test.go @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// SPDX-License-Identifier: Apache-2.0 + +package status + +import ( + "testing" + "time" + + "github.com/Azure/unbounded/internal/net/status/v1alpha1" +) + +func TestPeerLinkHealthyForDiagnostics(t *testing.T) { + now := time.Unix(1000, 0) + for _, tc := range []struct { + name string + check *v1alpha1.HealthCheckPeerStatus + handshake time.Time + healthy bool + }{ + {"unknown", nil, time.Time{}, false}, + {"recent", nil, now.Add(-time.Minute), true}, + {"boundary", nil, now.Add(-3 * time.Minute), false}, + {"future", nil, now.Add(time.Minute), true}, + {"normalized up", &v1alpha1.HealthCheckPeerStatus{Enabled: true, Status: " UP "}, time.Time{}, true}, + {"disabled nonempty up", &v1alpha1.HealthCheckPeerStatus{Status: " up "}, time.Time{}, true}, + {"disabled nonempty down", &v1alpha1.HealthCheckPeerStatus{Status: " DOWN "}, now, false}, + {"enabled empty", &v1alpha1.HealthCheckPeerStatus{Enabled: true}, now, false}, + {"disabled whitespace uses handshake", &v1alpha1.HealthCheckPeerStatus{Status: " "}, now, true}, + } { + t.Run(tc.name, func(t *testing.T) { + peer := v1alpha1.PeerStatus{HealthCheck: tc.check, Tunnel: v1alpha1.PeerTunnelStatus{LastHandshake: tc.handshake}} + if got := PeerLinkHealthyForDiagnostics(&peer, now); got != tc.healthy { + t.Fatalf("healthy=%v, want %v", got, tc.healthy) + } + }) + } +} + +func TestUnhealthyPeerLinkCountKeepsDistinctLinks(t *testing.T) { + now := time.Unix(1000, 0) + + peers := []v1alpha1.PeerStatus{ + {Name: "same", HealthCheck: &v1alpha1.HealthCheckPeerStatus{Enabled: true, Status: "DOWN"}}, + {Name: "same", HealthCheck: &v1alpha1.HealthCheckPeerStatus{Status: " down "}, Tunnel: v1alpha1.PeerTunnelStatus{LastHandshake: now}}, + {Name: "same", HealthCheck: &v1alpha1.HealthCheckPeerStatus{Status: " UP "}}, + } + if got := UnhealthyPeerLinkCount(peers, now); got != 2 { + t.Fatalf("unhealthy links=%d, want 2", got) + } + + if got := UnhealthyPeerLinkCount(nil, now); got != 0 { + t.Fatalf("empty links=%d, want 0", got) + } +} + +func TestRouteMismatchCountIncludesEveryHop(t *testing.T) { + yes, no := true, false + + routes := []v1alpha1.RouteEntry{ + {NextHops: []v1alpha1.NextHop{ + {Expected: &yes}, + {Present: &yes}, + {Expected: &yes, Present: &no}, + {Expected: &yes, Present: &yes}, + {Expected: &no}, + {}, + }}, + {NextHops: []v1alpha1.NextHop{{Expected: &no, Present: &yes}}}, + } + if got := RouteMismatchCount(routes); got != 4 { + t.Fatalf("mismatched hops=%d, want 4", got) + } + + if got := RouteMismatchCount(nil); got != 0 { + t.Fatalf("empty routes=%d, want 0", got) + } +}