pkg/gitops/fluxstatus_test.go
Rama main · réplica de lectura del repositorio original.
package gitops
import (
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func fluxObj(suspend bool, gen, obs any, conds ...map[string]any) *unstructured.Unstructured {
raw := make([]any, 0, len(conds))
for _, c := range conds {
raw = append(raw, c)
}
meta := map[string]any{"name": "k1", "namespace": "flux-system"}
if gen != nil {
meta["generation"] = gen
}
status := map[string]any{"conditions": raw}
if obs != nil {
status["observedGeneration"] = obs
}
obj := map[string]any{
"apiVersion": "kustomize.toolkit.fluxcd.io/v1",
"kind": "Kustomization",
"metadata": meta,
"status": status,
}
if suspend {
obj["spec"] = map[string]any{"suspend": true}
}
return &unstructured.Unstructured{Object: obj}
}
func cond(typ, status string) map[string]any {
return map[string]any{"type": typ, "status": status}
}
func condGen(c map[string]any, generation int64) map[string]any {
c["observedGeneration"] = generation
return c
}
func condReason(typ, status, reason string) map[string]any {
return map[string]any{"type": typ, "status": status, "reason": reason}
}
func TestFluxStatusTruthTable(t *testing.T) {
cases := []struct {
name string
obj *unstructured.Unstructured
wantSync string
wantHealth string
}{
{
// The reported bug. Ready and Reconciling are independent upstream, so a
// stale Reconciling sat on a fully-applied object and outvoted it, while
// kubectl and k9s read the printer columns and showed it Ready.
name: "stale Reconciling does not outvote an applied Ready",
obj: fluxObj(false, int64(1), int64(1), cond("Ready", "True"), cond("Healthy", "True"), cond("Reconciling", "True")),
wantSync: "Synced", wantHealth: "Healthy",
},
{
name: "ready and applied",
obj: fluxObj(false, int64(1), int64(1), cond("Ready", "True")),
wantSync: "Synced", wantHealth: "Healthy",
},
{
// Generation drift is the only state that genuinely means "not applied",
// and it holds even with Ready=True from the previous spec.
name: "generation drift outranks a stale Ready",
obj: fluxObj(false, int64(2), int64(1), cond("Ready", "True")),
wantSync: "Reconciling", wantHealth: "Progressing",
},
{
// Flux writes -1 on an object it has never reconciled. Present and
// different, so it counts. (A first reconcile reports Ready=Unknown; a
// reported failure is covered below and outranks drift.)
name: "the -1 sentinel counts as drift",
obj: fluxObj(false, int64(1), int64(-1), cond("Ready", "Unknown"), cond("Reconciling", "True")),
wantSync: "Reconciling", wantHealth: "Progressing",
},
{
// Absence is no signal. Reading it as drift would mark every healthy
// object on kinds that never set it as out of date.
name: "absent observedGeneration is not drift",
obj: fluxObj(false, int64(3), nil, cond("Ready", "True")),
wantSync: "Synced", wantHealth: "Healthy",
},
{
name: "absent generation is not drift",
obj: fluxObj(false, nil, int64(3), cond("Ready", "True")),
wantSync: "Synced", wantHealth: "Healthy",
},
{
// A behaviour change from the previous ordering, asserted rather than
// assumed: the controller has given up, whatever the last apply left.
name: "Stalled outranks Ready",
obj: fluxObj(false, int64(1), int64(1), cond("Ready", "True"), cond("Stalled", "True")),
wantSync: "OutOfSync", wantHealth: "Degraded",
},
{
name: "reconciling while not yet ready",
obj: fluxObj(false, int64(1), int64(1), cond("Ready", "Unknown"), cond("Reconciling", "True")),
wantSync: "Reconciling", wantHealth: "Progressing",
},
{
name: "not ready",
obj: fluxObj(false, int64(1), int64(1), cond("Ready", "False")),
wantSync: "OutOfSync", wantHealth: "Degraded",
},
{
// Applied but the workloads are unhealthy — two different facts, and
// the sync axis must not absorb the health one.
name: "unhealthy deployed resources are applied but degraded",
obj: fluxObj(false, int64(1), int64(1), cond("Ready", "True"), cond("Healthy", "False")),
wantSync: "Synced", wantHealth: "Degraded",
},
{
// Suspending does not un-apply what was applied.
name: "suspended and applied stays Synced",
obj: fluxObj(true, int64(1), int64(1), cond("Ready", "True")),
wantSync: "Synced", wantHealth: "Suspended",
},
{
// A spec change made while suspended will never be picked up.
name: "suspended with drift is not Synced",
obj: fluxObj(true, int64(2), int64(1), cond("Ready", "True")),
wantSync: "Unknown", wantHealth: "Suspended",
},
{
// A retry or remediation in flight is out of sync but not settled
// failure. Mirrors the frontend mapper, which has always drawn this
// distinction — the two must agree.
name: "a retrying Ready=False is progressing, not degraded",
obj: fluxObj(false, int64(1), int64(1), condReason("Ready", "False", "ProgressingWithRetry")),
wantSync: "OutOfSync", wantHealth: "Progressing",
},
{
name: "a settled Ready=False is degraded",
obj: fluxObj(false, int64(1), int64(1), condReason("Ready", "False", "BuildFailed")),
wantSync: "OutOfSync", wantHealth: "Degraded",
},
{
// Live shape of a GitRepository whose repository doesn't exist:
// never reconciled (-1) and retrying, but the clone failure is settled.
name: "a failure while retrying and never reconciled is degraded",
obj: fluxObj(false, int64(1), int64(-1),
condReason("Reconciling", "True", "ProgressingWithRetry"),
condGen(condReason("Ready", "False", "GitOperationFailed"), 1)),
wantSync: "OutOfSync", wantHealth: "Degraded",
},
{
// A source edited to a ref that doesn't exist: the new generation is
// never recorded while the fetch fails.
name: "a failure after a spec change is degraded",
obj: fluxObj(false, int64(2), int64(1), condGen(condReason("Ready", "False", "ArtifactFailed"), 2)),
wantSync: "OutOfSync", wantHealth: "Degraded",
},
{
// The spec was fixed (generation 3); the failure still describes
// generation 2, so it says nothing about the new spec yet.
name: "a failure about an older generation does not outrank a new spec",
obj: fluxObj(false, int64(3), int64(1), condGen(condReason("Ready", "False", "BuildFailed"), 2)),
wantSync: "Reconciling", wantHealth: "Progressing",
},
{
name: "a failure without a condition generation does not outrank drift",
obj: fluxObj(false, int64(2), int64(1), condReason("Ready", "False", "BuildFailed")),
wantSync: "Reconciling", wantHealth: "Progressing",
},
{
name: "a retry while reconciling stays progressing",
obj: fluxObj(false, int64(2), int64(1), condReason("Reconciling", "True", "Progressing"), condReason("Ready", "False", "ProgressingWithRetry")),
wantSync: "Reconciling", wantHealth: "Progressing",
},
{
name: "no conditions at all",
obj: fluxObj(false, int64(1), int64(1)),
wantSync: "Unknown", wantHealth: "Unknown",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := FluxStatus(tc.obj)
if got.Sync != tc.wantSync || got.Health != tc.wantHealth {
t.Errorf("FluxStatus = %s/%s, want %s/%s", got.Sync, got.Health, tc.wantSync, tc.wantHealth)
}
})
}
}
// The in-flight fact survives even when it no longer drives the status, so a
// wedged controller can be surfaced next to an otherwise-correct Synced.
func TestFluxStatusCarriesReconcilingActivity(t *testing.T) {
stuck := fluxObj(false, int64(1), int64(1), cond("Ready", "True"), cond("Reconciling", "True"))
got := FluxStatus(stuck)
if got.Sync != "Synced" {
t.Fatalf("Sync = %q, want Synced", got.Sync)
}
if !got.Reconciling {
t.Error("Reconciling activity was dropped; a wedged controller would be invisible")
}
quiet := FluxStatus(fluxObj(false, int64(1), int64(1), cond("Ready", "True")))
if quiet.Reconciling {
t.Error("Reconciling reported on an object with no Reconciling condition")
}
}
// Source kinds carry neither Healthy nor, in some versions, observedGeneration.
// Measured shapes: GitRepository/HelmRepository expose Ready + ArtifactInStorage,
// HelmRelease exposes Ready + Released.
func TestFluxStatusHandlesNonKustomizationKinds(t *testing.T) {
cases := []struct {
name string
conds []map[string]any
}{
{"GitRepository", []map[string]any{cond("Ready", "True"), cond("ArtifactInStorage", "True")}},
{"HelmRepository", []map[string]any{cond("Ready", "True"), cond("ArtifactInStorage", "True")}},
{"HelmRelease", []map[string]any{cond("Ready", "True"), cond("Released", "True")}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := FluxStatus(fluxObj(false, int64(1), int64(1), tc.conds...))
if got.Sync != "Synced" || got.Health != "Healthy" {
t.Errorf("FluxStatus = %s/%s, want Synced/Healthy", got.Sync, got.Health)
}
})
}
}