A leader-only runnable checks every configured provider concurrently under one timeout per round (first round at start, then every interval), keeps the latest results for the metrics collector, logs targets going down, changing reason or coming back, and publishes one ProviderHealth object per provider: created with a managed-by label when missing, then a status merge patch. Per-target lastTransitionTime survives rounds in which ready does not flip. Objects of providers that are no longer configured are deleted once at start; with checks turned off, all are. Reads go through the API reader, so no cluster-wide informer is needed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
136 lines
5.3 KiB
Go
136 lines
5.3 KiB
Go
package providerhealth
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
apimeta "k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
crawlv1alpha1 "gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/api/v1alpha1"
|
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
|
)
|
|
|
|
func readyAt(name string, at time.Time) crawlv1alpha1.ProviderTarget {
|
|
return crawlv1alpha1.ProviderTarget{Name: name, Ready: true, Reason: provider.ReasonReachable, LastTransitionTime: metav1.NewTime(at)}
|
|
}
|
|
|
|
func TestBuildStatus(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
prev crawlv1alpha1.ProviderHealthStatus
|
|
result Result
|
|
wantStatus metav1.ConditionStatus
|
|
wantReason string
|
|
wantMessage string
|
|
wantTargetsReady string
|
|
// wantTransitions is each target's lastTransitionTime, in order.
|
|
wantTransitions []time.Time
|
|
}{
|
|
{
|
|
name: "all ready",
|
|
result: Result{Type: "kvm", Supported: true, Targets: []provider.TargetStatus{up("nas1"), up("nas2")}, CheckedAt: t1},
|
|
wantStatus: metav1.ConditionTrue,
|
|
wantReason: reasonAllTargetsReady,
|
|
wantMessage: "2 of 2 targets ready",
|
|
wantTargetsReady: "2/2",
|
|
wantTransitions: []time.Time{t1, t1},
|
|
},
|
|
{
|
|
name: "one target down",
|
|
result: Result{Type: "kvm", Supported: true, Targets: []provider.TargetStatus{up("nas1"), down("nas2", provider.ReasonUnreachable)}, CheckedAt: t1},
|
|
wantStatus: metav1.ConditionFalse,
|
|
wantReason: reasonTargetsNotReady,
|
|
wantMessage: "nas2: Unreachable",
|
|
wantTargetsReady: "1/2",
|
|
wantTransitions: []time.Time{t1, t1},
|
|
},
|
|
{
|
|
name: "only a flip moves a target's transition time",
|
|
prev: crawlv1alpha1.ProviderHealthStatus{Targets: []crawlv1alpha1.ProviderTarget{readyAt("nas1", t0), readyAt("nas2", t0)}},
|
|
result: Result{Type: "kvm", Supported: true, CheckedAt: t1,
|
|
Targets: []provider.TargetStatus{up("nas1"), down("nas2", provider.ReasonTimeout)}},
|
|
wantStatus: metav1.ConditionFalse,
|
|
wantReason: reasonTargetsNotReady,
|
|
wantMessage: "nas2: Timeout",
|
|
wantTargetsReady: "1/2",
|
|
wantTransitions: []time.Time{t0, t1},
|
|
},
|
|
{
|
|
name: "no checker",
|
|
result: Result{Type: "gcp", CheckedAt: t1},
|
|
wantStatus: metav1.ConditionUnknown,
|
|
wantReason: reasonChecksNotImplemented,
|
|
},
|
|
{
|
|
name: "nothing to check",
|
|
result: Result{Type: "kvm", Supported: true, CheckedAt: t1},
|
|
wantStatus: metav1.ConditionUnknown,
|
|
wantReason: reasonNoTargets,
|
|
wantTargetsReady: "0/0",
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := buildStatus(tc.prev, tc.result)
|
|
|
|
cond := apimeta.FindStatusCondition(got.Conditions, crawlv1alpha1.ConditionReady)
|
|
if cond == nil || cond.Status != tc.wantStatus || cond.Reason != tc.wantReason {
|
|
t.Fatalf("Ready = %+v, want %s/%s", cond, tc.wantStatus, tc.wantReason)
|
|
}
|
|
if tc.wantMessage != "" && cond.Message != tc.wantMessage {
|
|
t.Errorf("Ready message = %q, want %q", cond.Message, tc.wantMessage)
|
|
}
|
|
if got.TargetsReady != tc.wantTargetsReady {
|
|
t.Errorf("targetsReady = %q, want %q", got.TargetsReady, tc.wantTargetsReady)
|
|
}
|
|
if got.Type != tc.result.Type || got.LastCheckTime == nil || !got.LastCheckTime.Time.Equal(t1) {
|
|
t.Errorf("type/lastCheckTime = %q/%v, want %q/%s", got.Type, got.LastCheckTime, tc.result.Type, t1)
|
|
}
|
|
if len(got.Targets) != len(tc.wantTransitions) {
|
|
t.Fatalf("got %d targets, want %d", len(got.Targets), len(tc.wantTransitions))
|
|
}
|
|
for i, want := range tc.wantTransitions {
|
|
if lt := got.Targets[i].LastTransitionTime.Time; !lt.Equal(want) {
|
|
t.Errorf("target %s lastTransitionTime = %s, want %s", got.Targets[i].Name, lt, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildStatus_conditionKeepsTransitionWhileUnchanged(t *testing.T) {
|
|
t.Parallel()
|
|
first := buildStatus(crawlv1alpha1.ProviderHealthStatus{},
|
|
Result{Type: "kvm", Supported: true, Targets: []provider.TargetStatus{up("nas1")}, CheckedAt: t0})
|
|
firstCond := apimeta.FindStatusCondition(first.Conditions, crawlv1alpha1.ConditionReady)
|
|
|
|
second := buildStatus(first, Result{Type: "kvm", Supported: true, Targets: []provider.TargetStatus{up("nas1")}, CheckedAt: t1})
|
|
|
|
cond := apimeta.FindStatusCondition(second.Conditions, crawlv1alpha1.ConditionReady)
|
|
if !cond.LastTransitionTime.Time.Equal(t0) {
|
|
t.Errorf("Ready lastTransitionTime = %s, want %s from the first round", cond.LastTransitionTime, t0)
|
|
}
|
|
if !firstCond.LastTransitionTime.Time.Equal(t0) || len(first.Conditions) != 1 {
|
|
t.Errorf("buildStatus modified the previous status: %+v", first.Conditions)
|
|
}
|
|
}
|
|
|
|
func TestBuildStatus_truncatesLongMessages(t *testing.T) {
|
|
t.Parallel()
|
|
long := strings.Repeat("é", 3000)
|
|
got := buildStatus(crawlv1alpha1.ProviderHealthStatus{}, Result{Type: "kvm", Supported: true, CheckedAt: t1,
|
|
Targets: []provider.TargetStatus{{Name: "nas1", Reason: provider.ReasonUnreachable, Message: long}}})
|
|
|
|
if n := utf8.RuneCountInString(got.Targets[0].Message); n != maxMessageLen {
|
|
t.Errorf("message has %d characters, want %d", n, maxMessageLen)
|
|
}
|
|
if !strings.HasSuffix(got.Targets[0].Message, "…") {
|
|
t.Error("truncated message does not end with an ellipsis")
|
|
}
|
|
}
|