pkg/health/readiness_gates_test.go
Rama main · réplica de lectura del repositorio original.
package health
import (
"encoding/json"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"os"
"testing"
"time"
)
func TestPodReadinessGateFailure(t *testing.T) {
now := time.Date(2026, 10, 25, 12, 0, 0, 0, time.UTC)
for _, tc := range []struct {
name, gate, reason, phase string
readyAge, containersAge, creationAge time.Duration
unknownTimes, noGates, notContainersReady, want, unknownOnset, deleting bool
}{
{name: "missing gate", want: true}, {name: "false gate", gate: "False", want: true}, {name: "passed gate", gate: "True"}, {name: "unknown gate", gate: "Unknown", want: true},
{name: "terminating", deleting: true}, {name: "no declaration", noGates: true}, {name: "other ready failure", reason: "NodeNotReady"}, {name: "containers unready", notContainersReady: true},
{name: "recent ready", readyAge: time.Minute}, {name: "long startup recently container ready", containersAge: time.Minute}, {name: "future containers ready", containersAge: -10 * time.Minute, want: true, unknownOnset: true},
{name: "future ready", readyAge: -10 * time.Minute, want: true, unknownOnset: true},
{name: "fresh transition with invalid other", readyAge: -10 * time.Minute, containersAge: time.Minute},
{name: "unknown transitions future creation", unknownTimes: true, creationAge: -10 * time.Minute, want: true, unknownOnset: true},
{name: "unknown transitions old creation", unknownTimes: true, want: true, unknownOnset: true},
{name: "unknown transitions young creation", unknownTimes: true, creationAge: time.Minute},
{name: "succeeded", phase: "Succeeded"}, {name: "pending", phase: "Pending"},
} {
t.Run(tc.name, func(t *testing.T) {
readyAge, containersAge, creationAge := tc.readyAge, tc.containersAge, tc.creationAge
if readyAge == 0 {
readyAge = 10 * time.Minute
}
if containersAge == 0 {
containersAge = 7 * time.Minute
}
if creationAge == 0 {
creationAge = time.Hour
}
reason := tc.reason
if reason == "" {
reason = "ReadinessGatesNotReady"
}
phase := tc.phase
if phase == "" {
phase = "Running"
}
p := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.NewTime(now.Add(-creationAge))}, Spec: corev1.PodSpec{ReadinessGates: []corev1.PodReadinessGate{{ConditionType: "example.com/ready"}}}, Status: corev1.PodStatus{Phase: corev1.PodPhase(phase), Conditions: []corev1.PodCondition{
{Type: corev1.PodReady, Status: corev1.ConditionFalse, Reason: reason, Message: "gate has not reported ready", LastTransitionTime: metav1.NewTime(now.Add(-readyAge))},
{Type: corev1.ContainersReady, Status: corev1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(-containersAge))},
}, ContainerStatuses: []corev1.ContainerStatus{{Name: "app", Ready: true, State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}}}}}}
if tc.gate != "" {
p.Status.Conditions = append(p.Status.Conditions, corev1.PodCondition{Type: "example.com/ready", Status: corev1.ConditionStatus(tc.gate)})
}
if tc.deleting {
t := metav1.NewTime(now.Add(-time.Minute))
p.DeletionTimestamp = &t
}
if tc.noGates {
p.Spec.ReadinessGates = nil
}
if tc.notContainersReady {
p.Status.Conditions[1].Status = corev1.ConditionFalse
}
if tc.unknownTimes {
for i := range p.Status.Conditions {
p.Status.Conditions[i].LastTransitionTime = metav1.Time{}
}
}
since, got := PodReadinessGateFailure(p, now)
if got != tc.want {
t.Fatalf("failure=%v want%v", got, tc.want)
}
if tc.deleting {
p.Status.ContainerStatuses[0].State = corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{Reason: "ImagePullBackOff"}}
if v := Pod(p, now); v.Level != LevelUnhealthy || v.Reason != "ImagePullBackOff" {
t.Fatalf("deletion masked hard failure: %+v", v)
}
}
if tc.want {
wantSince := now.Add(-7 * time.Minute)
if tc.unknownOnset {
wantSince = time.Time{}
}
if !since.Equal(wantSince) {
t.Errorf("onset=%v want%v", since, wantSince)
}
v := Pod(p, now)
if v.Level != LevelDegraded || v.Reason != "ReadinessGatesNotReady" || v.Message != "gate has not reported ready" {
t.Errorf("verdict %+v", v)
}
if tc.name == "missing gate" {
cs := &p.Status.ContainerStatuses[0]
cs.RestartCount = 1
cs.State = corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.NewTime(now.Add(-time.Minute))}}
cs.LastTerminationState = corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "OOMKilled", ExitCode: 137, FinishedAt: metav1.NewTime(now.Add(-2 * time.Minute))}}
if v := Pod(p, now); v.Level != LevelUnhealthy || v.Reason != "OOMKilled" {
t.Errorf("gate masked active OOM: %+v", v)
}
cs.RestartCount = 2
cs.LastTerminationState = corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{Reason: "Error", ExitCode: 1, FinishedAt: metav1.NewTime(now.Add(-10 * time.Second))}}
cs.State = corev1.ContainerState{Running: &corev1.ContainerStateRunning{StartedAt: metav1.NewTime(now.Add(-5 * time.Second))}}
if v := Pod(p, now); v.Reason != "CrashLoopBackOff" {
t.Errorf("gate masked stable crash loop: %+v", v)
}
cs.LastTerminationState = corev1.ContainerState{}
}
p.Status.ContainerStatuses[0].State = corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{Reason: "ImagePullBackOff"}}
if v := Pod(p, now); v.Level != LevelUnhealthy || v.Reason != "ImagePullBackOff" {
t.Errorf("masked fatal failure %+v", v)
}
}
})
}
}
func TestReadinessGateClockVectors(t *testing.T) {
raw, err := os.ReadFile("testdata/readiness_gate_clock.json")
if err != nil {
t.Fatal(err)
}
var file struct {
Now time.Time `json:"now"`
Vectors []struct {
Name string `json:"name"`
Blocked bool `json:"blocked"`
Object corev1.Pod `json:"object"`
} `json:"vectors"`
}
if err := json.Unmarshal(raw, &file); err != nil {
t.Fatal(err)
}
for _, v := range file.Vectors {
t.Run(v.Name, func(t *testing.T) {
since, got := PodReadinessGateFailure(&v.Object, file.Now)
if got != v.Blocked || !since.IsZero() {
t.Fatalf("blocked=%v onset=%v; want blocked=%v without known onset", got, since, v.Blocked)
}
})
}
}