pkg/k8score/object_warnings_test.go
Rama main · réplica de lectura del repositorio original.
package k8score
import (
"strings"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestDurationShort(t *testing.T) {
tests := []struct {
d time.Duration
want string
}{
{0, "0s"},
{-5 * time.Second, "0s"},
{59 * time.Second, "59s"},
{60 * time.Second, "1m"},
{90 * time.Second, "1m30s"},
{59*time.Minute + 59*time.Second, "59m59s"},
{time.Hour, "1h"},
{time.Hour + time.Minute, "1h1m"},
{23*time.Hour + 59*time.Minute, "23h59m"},
{24 * time.Hour, "1d"},
{49 * time.Hour, "2d"},
}
for _, tt := range tests {
if got := durationShort(tt.d); got != tt.want {
t.Errorf("durationShort(%v) = %q, want %q", tt.d, got, tt.want)
}
}
}
func mkHealthObj(kind, condType, condStatus string, created, ltt time.Time) *unstructured.Unstructured {
obj := &unstructured.Unstructured{Object: map[string]any{
"kind": kind,
"metadata": map[string]any{"name": "w"},
"status": map[string]any{
"conditions": []any{
map[string]any{
"type": condType,
"status": condStatus,
"lastTransitionTime": ltt.UTC().Format(time.RFC3339),
},
},
},
}}
obj.SetCreationTimestamp(metav1.NewTime(created))
return obj
}
func TestWorkloadHealthWarning(t *testing.T) {
now := time.Now()
t.Run("never healthy since creation", func(t *testing.T) {
// Condition went False essentially at creation → since-creation phrasing.
obj := mkHealthObj("Deployment", "Available", "False", now.Add(-2*time.Minute), now.Add(-2*time.Minute+time.Second))
w := workloadHealthWarning(obj, now)
if !strings.Contains(w, "since about when this resource was created (resource age: ") {
t.Errorf("want since-creation phrasing, got %q", w)
}
})
t.Run("regressed long after creation", func(t *testing.T) {
obj := mkHealthObj("Deployment", "Available", "False", now.Add(-time.Hour), now.Add(-5*time.Minute))
w := workloadHealthWarning(obj, now)
if strings.Contains(w, "since about when this resource was created") {
t.Errorf("should not use since-creation phrasing, got %q", w)
}
if !strings.Contains(w, "(resource age:") {
t.Errorf("want regressed phrasing with resource age, got %q", w)
}
})
// A failure present since creation reads the same on a resource created
// seconds ago and on one broken for weeks; only the age tells them apart,
// so every form states it and none offers an interpretation of what the
// timing means for an incident.
t.Run("every form states the age and nothing more", func(t *testing.T) {
cases := []struct {
name string
obj *unstructured.Unstructured
want string
}{
{
name: "young pod failing since creation",
obj: mkHealthObj("Pod", "Ready", "False", now.Add(-20*time.Second), now.Add(-20*time.Second)),
want: "Condition `Ready=False` for ~20s — since about when this resource was created (resource age: 20s).",
},
{
name: "young deployment failing since creation",
obj: mkHealthObj("Deployment", "Available", "False", now.Add(-11*time.Second), now.Add(-11*time.Second)),
want: "Condition `Available=False` for ~11s — since about when this resource was created (resource age: 11s).",
},
{
name: "deployment failing since creation 30 days ago",
obj: mkHealthObj("Deployment", "Available", "False", now.Add(-30*24*time.Hour), now.Add(-30*24*time.Hour)),
want: "Condition `Available=False` for ~30d — since about when this resource was created (resource age: 30d).",
},
{
// The 30s slop absorbs a healthy window this short, which is
// why the since-creation form says "about".
name: "pod ready for 20s then failing",
obj: mkHealthObj("Pod", "Ready", "False", now.Add(-25*time.Second), now.Add(-5*time.Second)),
want: "Condition `Ready=False` for ~5s — since about when this resource was created (resource age: 25s).",
},
{
name: "deployment regressed after a healthy hour",
obj: mkHealthObj("Deployment", "Available", "False", now.Add(-time.Hour), now.Add(-5*time.Minute)),
want: "Condition `Available=False` for ~5m (resource age: 1h).",
},
}
for _, tc := range cases {
if got := workloadHealthWarning(tc.obj, now); got != tc.want {
t.Errorf("%s:\n got %q\nwant %q", tc.name, got, tc.want)
}
}
})
t.Run("available true yields nothing", func(t *testing.T) {
obj := mkHealthObj("Deployment", "Available", "True", now.Add(-time.Hour), now.Add(-time.Hour))
if w := workloadHealthWarning(obj, now); w != "" {
t.Errorf("healthy workload should yield no warning, got %q", w)
}
})
t.Run("pod uses Ready condition", func(t *testing.T) {
obj := mkHealthObj("Pod", "Ready", "False", now.Add(-time.Hour), now.Add(-5*time.Minute))
if w := workloadHealthWarning(obj, now); !strings.Contains(w, "`Ready=False`") {
t.Errorf("pod should report Ready, got %q", w)
}
// A Pod's Available condition is irrelevant — only Ready drives the warning.
other := mkHealthObj("Pod", "Available", "False", now.Add(-time.Hour), now.Add(-5*time.Minute))
if w := workloadHealthWarning(other, now); w != "" {
t.Errorf("pod with no Ready condition should yield nothing, got %q", w)
}
})
t.Run("unhandled kind yields nothing", func(t *testing.T) {
obj := mkHealthObj("DaemonSet", "Available", "False", now.Add(-time.Hour), now.Add(-5*time.Minute))
if w := workloadHealthWarning(obj, now); w != "" {
t.Errorf("DaemonSet has no Available condition; want nothing, got %q", w)
}
})
t.Run("future lastTransitionTime is guarded", func(t *testing.T) {
// Clock skew: condition stamped in the future → negative failingFor must
// not produce a "~-Xs" warning.
obj := mkHealthObj("StatefulSet", "Available", "False", now.Add(-time.Hour), now.Add(5*time.Minute))
if w := workloadHealthWarning(obj, now); w != "" {
t.Errorf("future lastTransitionTime should yield nothing, got %q", w)
}
})
t.Run("no conditions yields nothing", func(t *testing.T) {
obj := &unstructured.Unstructured{Object: map[string]any{"kind": "Deployment", "metadata": map[string]any{"name": "w"}}}
if w := workloadHealthWarning(obj, now); w != "" {
t.Errorf("no conditions should yield nothing, got %q", w)
}
})
}
// A tracking ID copied from another object names that object's manager, not
// this one's: no warning. A controller the manager answer doesn't model keeps
// its warning beside one.
func TestManagerWarningSkipsCopiesAndKeepsUnmodeledControllers(t *testing.T) {
secret := func(labels, annotations map[string]any) *unstructured.Unstructured {
return &unstructured.Unstructured{Object: map[string]any{"apiVersion": "v1", "kind": "Secret",
"metadata": map[string]any{"namespace": "backup", "name": "copy", "labels": labels, "annotations": annotations}}}
}
copied := secret(nil, map[string]any{"argocd.argoproj.io/tracking-id": "source:/Secret:shop/original"})
if got := EnrichObjectWarnings(copied); len(got) != 0 {
t.Errorf("copied tracking ID warned: %q", got)
}
own := secret(nil, map[string]any{"argocd.argoproj.io/tracking-id": "source:/Secret:backup/copy"})
if got := EnrichObjectWarnings(own); len(got) != 1 {
t.Errorf("own tracking ID: %q; want one warning", got)
}
if got := EnrichRuntimeObjectAnsweredWarnings(own); len(got) != 0 {
t.Errorf("answered Argo CD warning kept: %q", got)
}
certManager := secret(map[string]any{"app.kubernetes.io/managed-by": "cert-manager"}, nil)
if got := EnrichRuntimeObjectAnsweredWarnings(certManager); len(got) != 1 {
t.Errorf("cert-manager warning beside an answer: %q; want it kept", got)
}
// A copied tracking ID doesn't hide the controller the object's own label names.
both := secret(map[string]any{"app.kubernetes.io/managed-by": "cert-manager"}, map[string]any{"argocd.argoproj.io/tracking-id": "source:/Secret:shop/original"})
for name, got := range map[string][]string{"plain": EnrichObjectWarnings(both), "answered": EnrichRuntimeObjectAnsweredWarnings(both)} {
if len(got) != 1 || !strings.Contains(got[0], "cert-manager") {
t.Errorf("%s: copied tracking ID beside a cert-manager label: %q; want the cert-manager warning", name, got)
}
}
// Beside the answer, a claim it weighs doesn't hide what it doesn't.
ownWithCertManager := secret(map[string]any{"app.kubernetes.io/managed-by": "cert-manager"}, map[string]any{"argocd.argoproj.io/tracking-id": "source:/Secret:backup/copy"})
if got := EnrichRuntimeObjectAnsweredWarnings(ownWithCertManager); len(got) != 1 || !strings.Contains(got[0], "cert-manager") {
t.Errorf("own tracking ID beside a cert-manager label, answered: %q; want the cert-manager warning", got)
}
helmLabels := secret(map[string]any{"app.kubernetes.io/managed-by": "Helm", "app.kubernetes.io/instance": "chart"}, nil)
if got := EnrichRuntimeObjectAnsweredWarnings(helmLabels); len(got) != 1 || !strings.Contains(got[0], "Helm") {
t.Errorf("Helm labels without a release annotation, answered: %q; want the Helm warning", got)
}
}