Ctrl K
ComunidadGuías…

pkg/gitops/fluxstatus.go

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

Archivo original ↗Preguntar a KiBot
package gitops

import (
	"strings"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// FluxState is the sync/health pair derived from a Flux object's conditions.
type FluxState struct {
	Sync   string
	Health string
	// Reconciling reports that a pass is executing right now. Activity, not
	// sync and not health — it never influences either. Carried so a wedged
	// controller can be surfaced next to an otherwise-correct Synced.
	Reconciling bool
}

// FluxStatus derives sync and health for any Flux object.
//
// Sync answers "is the declared state applied", which is Ready plus
// observedGeneration. Reconciling answers "is the controller mid-pass", which
// is neither: upstream the two are independent conditions (MarkReconciling
// deletes only Stalled, never Ready), so letting activity outvote readiness
// reported a healthy, fully-applied object as not synced — while kubectl and
// k9s, which read the CRD's printer columns, showed it Ready.
//
// Order:
//   - suspend                                   → Suspended (sync from Ready, unless drifted)
//   - Stalled=True                              → OutOfSync / Degraded
//   - Ready=True and no generation drift        → Synced / Healthy
//   - Ready=False with a failure reason about
//     the current generation                    → OutOfSync / Degraded
//   - drift, or Reconciling with Ready not True → Reconciling / Progressing
//   - Ready=False                               → OutOfSync / Degraded, or
//     Progressing for a retry/wait reason
//
// Shared by the insights and tree packages, which previously carried
// near-duplicate copies that had already drifted apart on suspend handling.
func FluxStatus(root *unstructured.Unstructured) FluxState {
	var ready, readyReason, healthy string
	var reconciling, stalled bool
	var readyGeneration int64
	var readyGenerationKnown bool

	for _, c := range fluxConditions(root) {
		switch c.typ {
		case "Ready":
			ready = c.status
			readyReason = c.reason
			readyGeneration, readyGenerationKnown = c.observedGeneration, c.observedGenerationKnown
		case "Healthy":
			healthy = c.status
		case "Reconciling":
			reconciling = reconciling || c.status == "True"
		case "Stalled":
			stalled = stalled || c.status == "True"
		}
	}

	drifted := fluxGenerationDrift(root)

	// Suspending does not un-apply what was applied, so a suspended object that
	// was Ready stays Synced. A spec change made while suspended will never be
	// picked up, though, so drift downgrades it.
	if suspended, _, _ := unstructured.NestedBool(root.Object, "spec", "suspend"); suspended {
		sync := "Unknown"
		if ready == "True" && !drifted {
			sync = "Synced"
		}
		return FluxState{Sync: sync, Health: "Suspended", Reconciling: reconciling}
	}

	// Terminal failure outranks readiness: the controller has given up,
	// whatever the last successful apply left behind.
	if stalled {
		return FluxState{Sync: "OutOfSync", Health: "Degraded", Reconciling: reconciling}
	}

	// Applied. Health comes from the Healthy condition where the kind has one —
	// the workloads can be unhealthy while the manifests are correctly applied,
	// and those are different facts. Source kinds carry no Healthy condition, so
	// absence means healthy rather than unknown.
	if ready == "True" && !drifted {
		health := "Healthy"
		if healthy == "False" {
			health = "Degraded"
		}
		return FluxState{Sync: "Synced", Health: health, Reconciling: reconciling}
	}

	// A failure the controller reported outranks activity too. A source that
	// can't clone, or a release whose chart can't resolve, keeps Reconciling=True
	// and never records observedGeneration while it retries; reading that as
	// "in progress" hid failures that never resolve on their own.
	// Only a failure about the current spec counts: after a fix, the old
	// Ready=False describes an older generation until the controller looks again.
	generation, _, _ := unstructured.NestedInt64(root.Object, "metadata", "generation")
	if ready == "False" && !isTransientFluxReason(readyReason) && readyGenerationKnown && readyGeneration == generation {
		return FluxState{Sync: "OutOfSync", Health: "Degraded", Reconciling: reconciling}
	}

	// Genuinely not applied: the controller has not observed the current spec,
	// or it is working and has not reached Ready.
	if drifted || reconciling {
		return FluxState{Sync: "Reconciling", Health: "Progressing", Reconciling: reconciling}
	}

	if ready == "False" {
		// Flux distinguishes a retry or remediation in flight (ProgressingWithRetry,
		// and the Wait/Retry reasons) from a settled failure. Both are out of sync,
		// but calling a retry Degraded overstates it.
		health := "Degraded"
		if isTransientFluxReason(readyReason) {
			health = "Progressing"
		}
		return FluxState{Sync: "OutOfSync", Health: health, Reconciling: reconciling}
	}

	return FluxState{Sync: "Unknown", Health: "Unknown", Reconciling: reconciling}
}

// fluxGenerationDrift reports that the controller has demonstrably not acted on
// the current spec.
//
// An absent observedGeneration is no signal, not drift: some Flux kinds and
// older controllers never set it, and reading absence as drift would mark every
// healthy object on them as out of date. A present-but-different value counts,
// including the -1 sentinel Flux writes on an object it has never reconciled.
func fluxGenerationDrift(root *unstructured.Unstructured) bool {
	gen, genOK, _ := unstructured.NestedInt64(root.Object, "metadata", "generation")
	obs, obsOK, _ := unstructured.NestedInt64(root.Object, "status", "observedGeneration")
	if !genOK || !obsOK {
		return false
	}
	return gen != obs
}

// isTransientFluxReason mirrors the frontend mapper's substring test so the two
// implementations agree on this row. Substring rather than an enumeration
// because Flux composes reasons (ProgressingWithRetry, HealthCheckFailed after
// a wait) and the set is not stable across controller versions.
func isTransientFluxReason(reason string) bool {
	for _, marker := range []string{"Progress", "Retry", "Wait"} {
		if strings.Contains(reason, marker) {
			return true
		}
	}
	return false
}

type fluxCondition struct {
	typ                     string
	status                  string
	reason                  string
	observedGeneration      int64
	observedGenerationKnown bool
}

func fluxConditions(root *unstructured.Unstructured) []fluxCondition {
	raw, ok, _ := unstructured.NestedSlice(root.Object, "status", "conditions")
	if !ok {
		return nil
	}
	out := make([]fluxCondition, 0, len(raw))
	for _, item := range raw {
		m, ok := item.(map[string]any)
		if !ok {
			continue
		}
		c := fluxCondition{
			typ:    StringValue(m["type"]),
			status: StringValue(m["status"]),
			reason: StringValue(m["reason"]),
		}
		switch g := m["observedGeneration"].(type) {
		case int64:
			c.observedGeneration, c.observedGenerationKnown = g, true
		case int:
			c.observedGeneration, c.observedGenerationKnown = int64(g), true
		case float64:
			c.observedGeneration, c.observedGenerationKnown = int64(g), true
		}
		out = append(out, c)
	}
	return out
}