Ctrl K
ComunidadGuías…

pkg/k8score/object_warnings.go

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

Archivo original ↗Preguntar a KiBot
package k8score

import (
	"fmt"
	"strings"
	"time"

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

	"github.com/skyhook-io/radar/pkg/gitops"
	"github.com/skyhook-io/radar/pkg/resourceid"
)

// EnrichRuntimeObjectWarnings converts a typed K8s runtime.Object to
// *unstructured.Unstructured (if needed) and runs EnrichObjectWarnings.
// Returns nil on conversion error — best-effort, never blocks the caller.
func EnrichRuntimeObjectWarnings(obj runtime.Object) []string {
	return enrichRuntime(obj, true)
}

// EnrichRuntimeObjectAnsweredWarnings is EnrichRuntimeObjectWarnings for a
// caller that reports a GitOps manager answer beside the warnings: the answer
// weighs the Argo CD, Flux and Helm claims the metadata makes, so a warning
// naming one of those is left out. Metadata it doesn't weigh keeps its
// warning: another controller's app.kubernetes.io/managed-by label, or Helm
// named by labels without its release annotation.
func EnrichRuntimeObjectAnsweredWarnings(obj runtime.Object) []string {
	return enrichRuntime(obj, false)
}

func enrichRuntime(obj runtime.Object, withManager bool) []string {
	if obj == nil {
		return nil
	}
	if u, ok := obj.(*unstructured.Unstructured); ok {
		return enrich(u, withManager)
	}
	raw, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
	if err != nil {
		return nil
	}
	return enrich(&unstructured.Unstructured{Object: raw}, withManager)
}

// EnrichObjectWarnings derives advisory warnings purely from an object's own
// state — no API calls. Safe to invoke per-row on list responses. Surfaces
// state that an agent commonly skims past (deletionTimestamp, terminating
// namespace, never-healthy workloads, external reconciliation by Helm/Argo/Flux).
//
// All entries are self-contained factual sentences. Empty result means
// "nothing notable about this object's state."
func EnrichObjectWarnings(obj *unstructured.Unstructured) []string {
	return enrich(obj, true)
}

// enrich derives the warnings; without withManager, the caller reports a
// manager answer that covers the GitOps and Helm metadata.
func enrich(obj *unstructured.Unstructured, withManager bool) []string {
	if obj == nil {
		return nil
	}
	var out []string

	// Beside a manager answer, the first manager its claims don't already
	// cover: an independent controller label, or Helm named by labels alone.
	for _, m := range externalManagers(obj) {
		if withManager || !m.claim {
			out = append(out, formatExternalManagerWarning(m.name, m.instance))
			break
		}
	}

	if w := deletionTimestampWarning(obj); w != "" {
		out = append(out, w)
	}

	if w := namespaceTerminatingWarning(obj); w != "" {
		out = append(out, w)
	}

	if w := workloadHealthWarning(obj, time.Now()); w != "" {
		out = append(out, w)
	}

	if w := pvcPendingWarning(obj); w != "" {
		out = append(out, w)
	}

	return out
}

func deletionTimestampWarning(obj *unstructured.Unstructured) string {
	dt := obj.GetDeletionTimestamp()
	if dt == nil || dt.IsZero() {
		return ""
	}
	finalizers := obj.GetFinalizers()
	if len(finalizers) == 0 {
		return fmt.Sprintf("Resource is being deleted (deletionTimestamp: %s, no finalizers remaining); it may disappear shortly. Any edits will not persist.", dt.Format(time.RFC3339))
	}
	return fmt.Sprintf("Resource is being deleted (deletionTimestamp: %s) with %d finalizer(s) still active: %s. The resource is in cleanup; edits and reads can behave unexpectedly until finalizers are removed.", dt.Format(time.RFC3339), len(finalizers), strings.Join(finalizers, ", "))
}

func namespaceTerminatingWarning(obj *unstructured.Unstructured) string {
	if obj.GetKind() != "Namespace" {
		return ""
	}
	phase, _, _ := unstructured.NestedString(obj.Object, "status", "phase")
	if phase != "Terminating" {
		return ""
	}
	return "Namespace is in `Terminating` phase. The apiserver will reject new resource creates in this namespace until termination completes (or until a stuck finalizer is removed)."
}

// workloadHealthWarning surfaces (1) how long the workload's primary health
// condition has been False and (2) whether it went False at about the time the
// resource was created, always with the resource's age. Both forms are facts,
// not an interpretation: a failure present since creation is not evidence that
// it is old or unrelated to an incident, because a resource created seconds ago
// carries the same marker as one that has been broken for weeks. Only the age
// tells them apart.
func workloadHealthWarning(obj *unstructured.Unstructured, now time.Time) string {
	kind := obj.GetKind()
	var conditionType string
	switch kind {
	case "Deployment", "StatefulSet":
		conditionType = "Available"
	case "Pod":
		conditionType = "Ready"
	default:
		return ""
	}

	conds, _, _ := unstructured.NestedSlice(obj.Object, "status", "conditions")
	if len(conds) == 0 {
		return ""
	}

	var failingFor time.Duration
	var foundFailing bool
	for _, c := range conds {
		cm, ok := c.(map[string]any)
		if !ok {
			continue
		}
		cType, _ := cm["type"].(string)
		if cType != conditionType {
			continue
		}
		cStatus, _ := cm["status"].(string)
		if cStatus != "False" {
			return ""
		}
		foundFailing = true
		if ltt, _ := cm["lastTransitionTime"].(string); ltt != "" {
			if t, err := time.Parse(time.RFC3339, ltt); err == nil {
				failingFor = now.Sub(t)
			}
		}
		break
	}
	if !foundFailing {
		return ""
	}

	createdAt := obj.GetCreationTimestamp().Time
	if createdAt.IsZero() || failingFor <= 0 {
		return ""
	}
	age := now.Sub(createdAt)

	// The condition went False within 30s of creation. The slop accommodates the
	// brief moment after creation when conditions haven't been written yet; it
	// also absorbs a healthy window shorter than that, hence "about".
	if age-failingFor < 30*time.Second {
		return fmt.Sprintf("Condition `%s=False` for ~%s — since about when this resource was created (resource age: %s).", conditionType, durationShort(failingFor), durationShort(age))
	}
	return fmt.Sprintf("Condition `%s=False` for ~%s (resource age: %s).", conditionType, durationShort(failingFor), durationShort(age))
}

func pvcPendingWarning(obj *unstructured.Unstructured) string {
	if obj.GetKind() != "PersistentVolumeClaim" {
		return ""
	}
	phase, _, _ := unstructured.NestedString(obj.Object, "status", "phase")
	if phase != "Pending" {
		return ""
	}
	createdAt := obj.GetCreationTimestamp().Time
	if createdAt.IsZero() {
		return ""
	}
	age := time.Since(createdAt)
	if age < 30*time.Second {
		return ""
	}
	return fmt.Sprintf("PVC has been `Pending` for %s. Common causes: no matching StorageClass, `volumeBindingMode: WaitForFirstConsumer` with no consumer pod scheduled yet, or capacity exhaustion in the underlying provisioner. Check `status.conditions` and recent Warning events on this PVC.", durationShort(age))
}

func durationShort(d time.Duration) string {
	if d < 0 {
		d = 0
	}
	if d < time.Minute {
		return fmt.Sprintf("%ds", int(d.Seconds()))
	}
	if d < time.Hour {
		m := int(d.Minutes())
		s := int(d.Seconds()) - m*60
		if s == 0 {
			return fmt.Sprintf("%dm", m)
		}
		return fmt.Sprintf("%dm%ds", m, s)
	}
	if d < 24*time.Hour {
		h := int(d.Hours())
		m := int(d.Minutes()) - h*60
		if m == 0 {
			return fmt.Sprintf("%dh", h)
		}
		return fmt.Sprintf("%dh%dm", h, m)
	}
	days := int(d.Hours()) / 24
	return fmt.Sprintf("%dd", days)
}

// copiedTrackingID reports whether instance is an Argo CD tracking ID naming
// another object, as on a Secret copied with its source's annotations: Argo CD
// doesn't count such an object as its own.
func copiedTrackingID(obj *unstructured.Unstructured, instance string) bool {
	if instance == "" || instance != obj.GetAnnotations()[argoTrackingIDAnnotationKey] {
		return false
	}
	ref := resourceid.Ref{Group: resourceid.GroupFromAPIVersion(obj.GetAPIVersion()), Kind: obj.GetKind(), Namespace: obj.GetNamespace(), Name: obj.GetName()}
	for _, c := range gitops.Claims(ref, nil, obj.GetAnnotations()) {
		if c.Source == gitops.ClaimArgoTrackingID {
			return c.Copied
		}
	}
	return false
}

const argoTrackingIDAnnotationKey = "argocd.argoproj.io/tracking-id"