Ctrl K
ComunidadGuías…

pkg/health/workload_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package health

import (
	"testing"
	"time"

	appsv1 "k8s.io/api/apps/v1"
	batchv1 "k8s.io/api/batch/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func ptr32(v int32) *int32 { return &v }
func ptrBool(b bool) *bool { return &b }

func TestWorkloadGoldenVectors(t *testing.T) {
	now := time.Date(2026, 6, 25, 12, 0, 0, 0, time.UTC)

	cases := []struct {
		name       string
		obj        any
		wantLevel  Level
		wantReason string
	}{
		// Deployments (require available).
		{"deployment fully ready", &appsv1.Deployment{
			Spec:   appsv1.DeploymentSpec{Replicas: ptr32(3)},
			Status: appsv1.DeploymentStatus{ReadyReplicas: 3, AvailableReplicas: 3},
		}, LevelHealthy, ""},
		{"deployment scaled to zero is neutral", &appsv1.Deployment{
			Spec: appsv1.DeploymentSpec{Replicas: ptr32(0)},
		}, LevelNeutral, "ScaledToZero"},
		{"deployment partially ready is degraded", &appsv1.Deployment{
			Spec:   appsv1.DeploymentSpec{Replicas: ptr32(3)},
			Status: appsv1.DeploymentStatus{ReadyReplicas: 1, AvailableReplicas: 1},
		}, LevelDegraded, "PartiallyReady"},
		{"deployment none ready is unhealthy", &appsv1.Deployment{
			Spec:   appsv1.DeploymentSpec{Replicas: ptr32(2)},
			Status: appsv1.DeploymentStatus{ReadyReplicas: 0},
		}, LevelUnhealthy, "NoneReady"},
		{"deployment ready-but-not-available is degraded", &appsv1.Deployment{
			Spec:   appsv1.DeploymentSpec{Replicas: ptr32(2)},
			Status: appsv1.DeploymentStatus{ReadyReplicas: 2, AvailableReplicas: 1},
		}, LevelDegraded, "PartiallyReady"},

		// StatefulSet / ReplicaSet (no availability requirement).
		{"statefulset fully ready", &appsv1.StatefulSet{
			Spec:   appsv1.StatefulSetSpec{Replicas: ptr32(2)},
			Status: appsv1.StatefulSetStatus{ReadyReplicas: 2},
		}, LevelHealthy, ""},
		{"replicaset scaled to zero is neutral", &appsv1.ReplicaSet{
			Spec: appsv1.ReplicaSetSpec{Replicas: ptr32(0)},
		}, LevelNeutral, "ScaledToZero"},

		// DaemonSet (desired from DesiredNumberScheduled).
		{"daemonset all ready", &appsv1.DaemonSet{
			Status: appsv1.DaemonSetStatus{DesiredNumberScheduled: 4, NumberReady: 4},
		}, LevelHealthy, ""},
		{"daemonset no matching nodes is neutral", &appsv1.DaemonSet{
			Status: appsv1.DaemonSetStatus{DesiredNumberScheduled: 0},
		}, LevelNeutral, "ScaledToZero"},
		{"daemonset partially ready is degraded", &appsv1.DaemonSet{
			Status: appsv1.DaemonSetStatus{DesiredNumberScheduled: 4, NumberReady: 2},
		}, LevelDegraded, "PartiallyReady"},

		// Jobs.
		{"job complete is neutral", &batchv1.Job{
			Status: batchv1.JobStatus{Conditions: []batchv1.JobCondition{{Type: batchv1.JobComplete, Status: corev1.ConditionTrue}}},
		}, LevelNeutral, "Completed"},
		{"job failed is unhealthy", &batchv1.Job{
			Status: batchv1.JobStatus{Conditions: []batchv1.JobCondition{{Type: batchv1.JobFailed, Status: corev1.ConditionTrue}}},
		}, LevelUnhealthy, "Failed"},
		{"job suspended is neutral", &batchv1.Job{
			Spec: batchv1.JobSpec{Suspend: ptrBool(true)},
		}, LevelNeutral, "Suspended"},
		{"suspended job with terminal failure is still unhealthy", &batchv1.Job{
			Spec:   batchv1.JobSpec{Suspend: ptrBool(true)},
			Status: batchv1.JobStatus{Conditions: []batchv1.JobCondition{{Type: batchv1.JobFailed, Status: corev1.ConditionTrue}}},
		}, LevelUnhealthy, "Failed"},
		{"job running is neutral", &batchv1.Job{
			Status: batchv1.JobStatus{Active: 1},
		}, LevelNeutral, "Running"},
		{"job succeeded count meets completions is neutral", &batchv1.Job{
			Spec:   batchv1.JobSpec{Completions: ptr32(1)},
			Status: batchv1.JobStatus{Succeeded: 1},
		}, LevelNeutral, "Completed"},
		{"explicit zero-completions job is complete (K8s semantics)", &batchv1.Job{
			Spec:   batchv1.JobSpec{Completions: ptr32(0)},
			Status: batchv1.JobStatus{Succeeded: 0},
		}, LevelNeutral, "Completed"},
		{"job failed with nothing active/succeeded is unhealthy", &batchv1.Job{
			Status: batchv1.JobStatus{Failed: 3},
		}, LevelUnhealthy, "Failed"},

		// CronJobs.
		{"cronjob suspended is neutral", &batchv1.CronJob{
			Spec: batchv1.CronJobSpec{Suspend: ptrBool(true), Schedule: "0 * * * *"},
		}, LevelNeutral, "Suspended"},
		{"cronjob recent scheduling is neutral configuration evidence", &batchv1.CronJob{
			Spec:   batchv1.CronJobSpec{Schedule: "*/5 * * * *"},
			Status: batchv1.CronJobStatus{LastScheduleTime: &metav1.Time{Time: now.Add(-2 * time.Minute)}},
		}, LevelNeutral, ""},
		{"cronjob stale past cadence is degraded", &batchv1.CronJob{
			Spec:   batchv1.CronJobSpec{Schedule: "*/5 * * * *"},
			Status: batchv1.CronJobStatus{LastScheduleTime: &metav1.Time{Time: now.Add(-48 * time.Hour)}},
		}, LevelDegraded, "Stale"},
		{"cronjob freshly created, never run, is neutral", &batchv1.CronJob{
			ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Time{Time: now.Add(-1 * time.Minute)}},
			Spec:       batchv1.CronJobSpec{Schedule: "0 0 * * *"},
		}, LevelNeutral, "NeverRun"},
		{"cronjob old and never scheduled is degraded", &batchv1.CronJob{
			ObjectMeta: metav1.ObjectMeta{CreationTimestamp: metav1.Time{Time: now.Add(-72 * time.Hour)}},
			Spec:       batchv1.CronJobSpec{Schedule: "0 0 * * *"},
		}, LevelDegraded, "NeverScheduled"},

		// PVCs.
		{"pvc bound is healthy", &corev1.PersistentVolumeClaim{
			Status: corev1.PersistentVolumeClaimStatus{Phase: corev1.ClaimBound},
		}, LevelHealthy, ""},
		{"pvc pending is neutral", &corev1.PersistentVolumeClaim{
			Status: corev1.PersistentVolumeClaimStatus{Phase: corev1.ClaimPending},
		}, LevelNeutral, "Pending"},
		{"pvc lost is unhealthy", &corev1.PersistentVolumeClaim{
			Status: corev1.PersistentVolumeClaimStatus{Phase: corev1.ClaimLost},
		}, LevelUnhealthy, "Lost"},

		// Unhandled kind falls back to unknown.
		{"unknown kind", &corev1.Pod{}, LevelUnknown, ""},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			got := Workload(c.obj, now)
			if got.Level != c.wantLevel {
				t.Errorf("Workload().Level = %q, want %q", got.Level, c.wantLevel)
			}
			if c.wantReason != "" && got.Reason != c.wantReason {
				t.Errorf("Workload().Reason = %q, want %q", got.Reason, c.wantReason)
			}
		})
	}
}

// TestWorkloadConvergenceGrace covers the window between a spec change and the
// controller acting on it. Without it, replicaVerdict graded purely on
// ready-vs-desired, so a Deployment scaled 3->10 read NoneReady/PartiallyReady
// the instant the spec changed — before the controller had created a single
// pod. That is controller lag, not an outage.
//
// The contract is Neutral, never Healthy: radar declines to call it broken
// without claiming it is fine.
func TestWorkloadConvergenceGrace(t *testing.T) {
	now := time.Date(2026, 8, 9, 12, 0, 0, 0, time.UTC)
	old := metav1.NewTime(now.Add(-30 * 24 * time.Hour))
	fresh := metav1.NewTime(now.Add(-30 * time.Second))

	tests := []struct {
		name      string
		dep       *appsv1.Deployment
		wantLevel Level
	}{
		{
			name: "long-lived deployment scaled up, controller has not observed the new spec",
			dep: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{CreationTimestamp: old, Generation: 7},
				Spec:       appsv1.DeploymentSpec{Replicas: ptr32(10)},
				Status: appsv1.DeploymentStatus{
					ObservedGeneration: 6, // one behind: spec moved, controller hasn't
					ReadyReplicas:      3, AvailableReplicas: 3,
				},
			},
			wantLevel: LevelNeutral,
		},
		{
			// Age alone must NOT buy grace. A deploy that is broken on arrival is
			// still broken, and softening it is worst exactly when it matters
			// most: an earlier revision granted every workload a 5-minute window
			// and reported an unschedulable Deployment as neutral while that same
			// object carried a critical workload_degraded issue.
			name: "freshly created but controller caught up is graded normally",
			dep: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{CreationTimestamp: fresh, Generation: 1},
				Spec:       appsv1.DeploymentSpec{Replicas: ptr32(3)},
				Status:     appsv1.DeploymentStatus{ObservedGeneration: 1},
			},
			wantLevel: LevelUnhealthy,
		},
		{
			// Past the window with the controller caught up and still nothing
			// ready — this is a real outage and must not be softened.
			name: "settled deployment with zero ready is unhealthy",
			dep: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{CreationTimestamp: old, Generation: 7},
				Spec:       appsv1.DeploymentSpec{Replicas: ptr32(3)},
				Status:     appsv1.DeploymentStatus{ObservedGeneration: 7},
			},
			wantLevel: LevelUnhealthy,
		},
		{
			// Converged: grace must never downgrade a genuinely healthy result.
			name: "settled and fully ready is healthy",
			dep: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{CreationTimestamp: old, Generation: 7},
				Spec:       appsv1.DeploymentSpec{Replicas: ptr32(3)},
				Status: appsv1.DeploymentStatus{
					ObservedGeneration: 7, ReadyReplicas: 3, AvailableReplicas: 3,
				},
			},
			wantLevel: LevelHealthy,
		},
		{
			name: "surge replicas do not degrade serving health",
			dep: &appsv1.Deployment{
				ObjectMeta: metav1.ObjectMeta{CreationTimestamp: old, Generation: 8},
				Spec:       appsv1.DeploymentSpec{Replicas: ptr32(1)},
				Status: appsv1.DeploymentStatus{
					ObservedGeneration: 8, ReadyReplicas: 2, AvailableReplicas: 1,
				},
			},
			wantLevel: LevelHealthy,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Workload(tt.dep, now).Level; got != tt.wantLevel {
				t.Errorf("Level = %v, want %v", got, tt.wantLevel)
			}
		})
	}
}