pkg/conditions/conditions.go
Rama main · réplica de lectura del repositorio original.
// 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]
}