Ctrl K
ComunidadGuías…

pkg/conditions/conditions.go

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

Archivo original ↗Preguntar a KiBot
// Package conditions holds the neutral controller-condition-state vocabulary
// shared across the platform: which condition reasons mean "still reconciling"
// (transient) vs. which look transient but are genuine stuck failures. It is a
// dependency-free leaf — package inventory (pkg/packages), the GitOps detector
// (internal/k8s), and the issues classifier (internal/issues) all depend on it
// rather than on each other, so the two interpretations can't drift.
package conditions

import (
	"time"

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

// State is the structured value of one Kubernetes status condition. Status is
// intentionally kept as a string so callers can distinguish True, False,
// Unknown, and malformed values without this neutral package assigning product
// semantics to them.
type State struct {
	Status  string
	Reason  string
	Message string
}

type FalseCondition struct {
	Type                  string
	ObservedGeneration    int64
	Reason                string
	Message               string
	LastTransitionTime    time.Time
	HasLastTransitionTime bool
}

// Find returns the first condition with the requested type. The v1beta2 shape
// is checked first to match FindFalseCondition's established precedence.
func Find(obj *unstructured.Unstructured, condType string) (State, bool) {
	if obj == nil || condType == "" {
		return State{}, false
	}
	condSlices := [][]any{}
	if v1b2, ok, _ := unstructured.NestedSlice(obj.Object, "status", "v1beta2", "conditions"); ok {
		condSlices = append(condSlices, v1b2)
	}
	if v1b1, ok, _ := unstructured.NestedSlice(obj.Object, "status", "conditions"); ok {
		condSlices = append(condSlices, v1b1)
	}
	for _, conds := range condSlices {
		for _, raw := range conds {
			condition, ok := raw.(map[string]any)
			if !ok {
				continue
			}
			conditionType, _ := condition["type"].(string)
			if conditionType != condType {
				continue
			}
			status, _ := condition["status"].(string)
			reason, _ := condition["reason"].(string)
			message, _ := condition["message"].(string)
			return State{Status: status, Reason: reason, Message: message}, true
		}
	}
	return State{}, false
}

// DefaultFalseConditionTypes is the set of "is this resource healthy?" condition
// types the generic CRD fallback flags when False. Selection follows the
// object's condition order, not this list's order. Curated across the integration matrix: Argo
// (Synced/Healthy), Flux (Ready/Reconciled/Released), cert-manager/Knative/KEDA/
// Crossplane/CNPG (Ready/Synced/Active/…), Cluster API (Ready/…, though CAPI has
// its own curated checker).
var DefaultFalseConditionTypes = []string{
	"Ready", "Available", "Reconciled", "Healthy", "Synced", "Released",
}

// FindFalseCondition walks an unstructured object's status.conditions (with a
// status.v1beta2.conditions fallback) and returns the first entry whose type is
// in condTypes (or DefaultFalseConditionTypes when none given) AND whose status
// is "False". Returns the type, reason, message, age-since-lastTransitionTime,
// and whether one was found. The single shared reader for every condition-state
// consumer (issues generic fallback, the CAPI detector, the Flux detector) so
// they can't drift.
func FindFalseCondition(obj *unstructured.Unstructured, condTypes ...string) (condType, reason, message string, since time.Duration, found bool) {
	condition, found := FindFalseConditionWithTime(obj, condTypes...)
	if !found {
		return "", "", "", 0, false
	}
	if condition.HasLastTransitionTime {
		since = time.Since(condition.LastTransitionTime)
	}
	return condition.Type, condition.Reason, condition.Message, since, true
}

func FindFalseConditionWithTime(obj *unstructured.Unstructured, condTypes ...string) (FalseCondition, bool) {
	if obj == nil {
		return FalseCondition{}, false
	}
	if len(condTypes) == 0 {
		condTypes = DefaultFalseConditionTypes
	}
	condSlices := [][]any{}
	if v1b2, ok, _ := unstructured.NestedSlice(obj.Object, "status", "v1beta2", "conditions"); ok {
		condSlices = append(condSlices, v1b2)
	}
	if v1b1, ok, _ := unstructured.NestedSlice(obj.Object, "status", "conditions"); ok {
		condSlices = append(condSlices, v1b1)
	}
	for _, conds := range condSlices {
		for _, c := range conds {
			cond, ok := c.(map[string]any)
			if !ok {
				continue
			}
			ct, _ := cond["type"].(string)
			if status, _ := cond["status"].(string); status != "False" {
				continue
			}
			for _, wanted := range condTypes {
				if ct == wanted {
					r, _ := cond["reason"].(string)
					m, _ := cond["message"].(string)
					observedGeneration, _ := cond["observedGeneration"].(int64)
					result := FalseCondition{Type: ct, Reason: r, Message: m, ObservedGeneration: observedGeneration}
					if ts, _ := cond["lastTransitionTime"].(string); ts != "" {
						if t, err := time.Parse(time.RFC3339, ts); err == nil {
							result.LastTransitionTime = t
							result.HasLastTransitionTime = true
						}
					}
					return result, true
				}
			}
		}
	}
	return FalseCondition{}, false
}

// transientConditionReasons is the canonical set of controller condition reasons
// that mean "still reconciling / not done yet", NOT "failed". A False
// Ready/Available condition carrying one of these is in-progress, not broken —
// this is the CRD-condition noise floor. Curated across the controller families
// Radar integrates with:
//
//   - Flux:         Progressing, DependencyNotReady, ReconciliationInProgress,
//     ChartNotReady, ArtifactFailed
//   - Argo/Crossplane: Reconciling, Creating
//   - cert-manager: Issuing, Pending
//   - generic:      InProgress, Initializing, Waiting (NOT "Unknown" — ambiguous,
//     not in-progress; it stays loud/unhealthy)
var transientConditionReasons = map[string]bool{
	"Progressing":              true,
	"DependencyNotReady":       true,
	"ReconciliationInProgress": true,
	"ChartNotReady":            true,
	"ArtifactFailed":           true,
	"Reconciling":              true,
	"Creating":                 true,
	"Issuing":                  true,
	"Pending":                  true,
	"InProgress":               true,
	"Initializing":             true,
	"Waiting":                  true,
}

// genuineFailureReason holds reasons that APPEAR in transientConditionReasons
// (the health-display path softens them to "degraded", still visible) but are
// actually persistent stuck failures the live issue queue must surface, never
// suppress: a Flux source reporting ArtifactFailed can't produce an artifact;
// ChartNotReady can't resolve a chart. The issue detectors subtract these from
// the transient set; the health badge may keep them transient.
var genuineFailureReason = map[string]bool{
	"ArtifactFailed": true,
	"ChartNotReady":  true,
}

// IsTransientConditionReason reports whether a reason denotes an in-progress /
// not-yet-settled state rather than a genuine failure. Used by the GitOps
// health mapping and (minus the genuine-failure carve-out) the issue detectors.
func IsTransientConditionReason(r string) bool {
	return transientConditionReasons[r]
}

// IsGenuineFailureReason reports whether a reason is a stuck failure that looks
// transient but must NOT be suppressed from the live issue stream.
func IsGenuineFailureReason(r string) bool {
	return genuineFailureReason[r]
}

// IsInProgressForIssues is the NARROW "still reconciling" predicate the issue
// detectors use: transient MINUS the genuine-failure reasons, so ArtifactFailed/
// ChartNotReady surface as issues instead of being softened away.
func IsInProgressForIssues(r string) bool {
	return transientConditionReasons[r] && !genuineFailureReason[r]
}