Ctrl K
ComunidadGuías…

pkg/karpenter/karpenter.go

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

Archivo original ↗Preguntar a KiBot
// Package karpenter normalizes Karpenter resources and their relationships.
package karpenter

import (
	"strconv"
	"strings"

	corev1 "k8s.io/api/core/v1"
	apiMeta "k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

const (
	Group                = "karpenter.sh"
	NodePoolKind         = "NodePool"
	NodeClaimKind        = "NodeClaim"
	NodePoolLabelKey     = "karpenter.sh/nodepool"
	CapacityTypeLabelKey = "karpenter.sh/capacity-type"
	APIVersionV1         = "karpenter.sh/v1"
	APIVersionV1Beta1    = "karpenter.sh/v1beta1"

	NodeRegistrationHealthyConditionType = "NodeRegistrationHealthy"
)

type NodeClassRef struct {
	Group      string
	Version    string
	Kind       string
	Name       string
	APIVersion string
}

type Readiness string

const (
	ReadinessReady    Readiness = "ready"
	ReadinessNotReady Readiness = "not_ready"
	ReadinessUnknown  Readiness = "unknown"
)

type Requirement struct {
	Key       string
	Operator  corev1.NodeSelectorOperator
	Values    []string
	MinValues *int64
}

type DisruptionBudget struct {
	Nodes    string
	Schedule string
	Duration string
	Reasons  []string
}

type Disruption struct {
	ConsolidationPolicy string
	ConsolidateAfter    string
	Budgets             []DisruptionBudget
}

type NodePoolSpec struct {
	Replicas               *int64
	Weight                 *int64
	Limits                 corev1.ResourceList
	TemplateLabels         map[string]string
	NodeClassRef           *NodeClassRef
	Requirements           []Requirement
	Taints                 []corev1.Taint
	StartupTaints          []corev1.Taint
	ExpireAfter            string
	TerminationGracePeriod string
	Disruption             Disruption
}

type RelationshipSource string

const (
	RelationshipNone     RelationshipSource = ""
	RelationshipOwnerRef RelationshipSource = "owner_ref"
	RelationshipLabel    RelationshipSource = "label"
)

func NodeClassRefForNodePool(pool *unstructured.Unstructured) (NodeClassRef, bool) {
	if pool == nil {
		return NodeClassRef{}, false
	}
	return nodeClassRef(pool, "spec", "template", "spec", "nodeClassRef")
}

func NodeClassRefForNodeClaim(claim *unstructured.Unstructured) (NodeClassRef, bool) {
	if claim == nil {
		return NodeClassRef{}, false
	}
	return nodeClassRef(claim, "spec", "nodeClassRef")
}

func nodeClassRef(obj *unstructured.Unstructured, fields ...string) (NodeClassRef, bool) {
	ref, found, err := unstructured.NestedMap(obj.Object, fields...)
	if err != nil || !found {
		return NodeClassRef{}, false
	}
	kind, _ := ref["kind"].(string)
	name, _ := ref["name"].(string)
	if kind == "" || name == "" {
		return NodeClassRef{}, false
	}

	switch obj.GetAPIVersion() {
	case APIVersionV1:
		group, _ := ref["group"].(string)
		if group == "" {
			return NodeClassRef{}, false
		}
		return NodeClassRef{Group: group, Kind: kind, Name: name}, true
	case APIVersionV1Beta1:
		apiVersion, _ := ref["apiVersion"].(string)
		gv, err := schema.ParseGroupVersion(apiVersion)
		if err != nil || gv.Group == "" || gv.Version == "" {
			return NodeClassRef{}, false
		}
		return NodeClassRef{
			Group:      gv.Group,
			Version:    gv.Version,
			Kind:       kind,
			Name:       name,
			APIVersion: apiVersion,
		}, true
	default:
		return NodeClassRef{}, false
	}
}

func Conditions(obj *unstructured.Unstructured) []metav1.Condition {
	if obj == nil {
		return nil
	}
	raw, found, err := unstructured.NestedSlice(obj.Object, "status", "conditions")
	if err != nil || !found {
		return nil
	}
	conditions := make([]metav1.Condition, 0, len(raw))
	for _, item := range raw {
		conditionMap, ok := item.(map[string]any)
		if !ok {
			continue
		}
		var condition metav1.Condition
		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(conditionMap, &condition); err != nil || condition.Type == "" {
			continue
		}
		conditions = append(conditions, condition)
	}
	return conditions
}

func ReadyCondition(obj *unstructured.Unstructured) *metav1.Condition {
	return apiMeta.FindStatusCondition(Conditions(obj), "Ready")
}

func ResourceReadiness(obj *unstructured.Unstructured) Readiness {
	ready := ReadyCondition(obj)
	if ready == nil {
		return ReadinessUnknown
	}
	if obj.GetGeneration() > 0 && ready.ObservedGeneration > 0 && ready.ObservedGeneration < obj.GetGeneration() {
		return ReadinessUnknown
	}
	switch ready.Status {
	case metav1.ConditionTrue:
		return ReadinessReady
	case metav1.ConditionFalse:
		return ReadinessNotReady
	default:
		return ReadinessUnknown
	}
}

func NodePoolConditions(pool *unstructured.Unstructured) []metav1.Condition {
	return Conditions(pool)
}

func NodePoolCondition(pool *unstructured.Unstructured, conditionType string) *metav1.Condition {
	return apiMeta.FindStatusCondition(Conditions(pool), conditionType)
}

func NodePoolReadyCondition(pool *unstructured.Unstructured) *metav1.Condition {
	return ReadyCondition(pool)
}

func NodePoolReadiness(pool *unstructured.Unstructured) Readiness {
	return ResourceReadiness(pool)
}

func NodeClaimConditions(claim *unstructured.Unstructured) []metav1.Condition {
	return Conditions(claim)
}

// nodeClaimFailureReasons are condition reasons that mark a lifecycle stage as
// failed rather than in progress. Karpenter keeps a failing stage at
// status=Unknown (e.g. Launched=Unknown/LaunchFailed) and reserves
// status=False for hard invariant failures (Registered=False/MultipleNodesFound),
// so status alone cannot distinguish "still working on it" from "broken".
var nodeClaimFailureReasons = map[string]bool{
	"LaunchFailed":              true,
	"CreateError":               true,
	"InsufficientCapacityError": true,
	"NodeClassNotReady":         true,
	"MultipleNodesFound":        true,
	"UnregisteredTaintMissing":  true,
	// Cloud providers write their error mapping verbatim into
	// Launched=Unknown — the AWS provider's vocabulary carries no
	// fail/error substring for most of them. This mirrors the full set the
	// AWS provider's ToReasonMessage can return; the ones it produces that DO
	// carry the substring (SpotSLRCreationFailed, AMIAuthorizationFailure,
	// InternalError, LaunchFailed) are matched either way.
	"Unauthorized":                      true,
	"LaunchTemplateNotFound":            true,
	"SecurityGroupSubnetVPCMismatch":    true,
	"InstanceProfileNameInvalid":        true,
	"RequestLimitExceeded":              true,
	"VCPULimitExceeded":                 true,
	"InsufficientInstanceCapacity":      true,
	"InsufficientFreeAddressesInSubnet": true,
	"InvalidAMIID":                      true,
	"AMINotFound":                       true,
	"FreeTierIneligible":                true,
	"FleetQuotaExceeded":                true,
	"AccountPendingVerification":        true,
	"SpotQuotaExceeded":                 true,
}

// IsFailedLifecycleCondition reports whether a NodeClaim lifecycle condition
// (Launched/Registered/Initialized/Ready) records a failed stage. False always
// counts; Unknown counts only with a failure reason — other Unknown reasons
// (NodeNotFound, StartupTaintsExist, ...) are normal in-progress states.
func IsFailedLifecycleCondition(condition metav1.Condition) bool {
	switch condition.Status {
	case metav1.ConditionFalse:
		return true
	case metav1.ConditionUnknown:
		return IsFailureReason(condition.Reason)
	default:
		return false
	}
}

// IsFailedLifecycleConditionForVersion applies the API version's condition
// dialect. v1 reserves False for hard invariant failures, but v1beta1 used
// False for ordinary unmet stages (Initialized=False/NotInitialized is a
// claim still provisioning), so on v1beta1 a False only reads as failed when
// its reason carries failure vocabulary.
func IsFailedLifecycleConditionForVersion(apiVersion string, condition metav1.Condition) bool {
	if condition.Status == metav1.ConditionFalse && apiVersion == APIVersionV1Beta1 {
		return IsFailureReason(condition.Reason)
	}
	return IsFailedLifecycleCondition(condition)
}

// IsFailureReason reports whether a condition reason carries failure
// vocabulary — a known NodeClaim failure reason or generic fail/error text.
// Callers use it where a bare False is NOT evidence of failure (the Ready
// rollup, which is False for every claim still provisioning).
func IsFailureReason(reason string) bool {
	if nodeClaimFailureReasons[reason] {
		return true
	}
	lowered := strings.ToLower(reason)
	return strings.Contains(lowered, "fail") || strings.Contains(lowered, "error")
}

func NodeClaimReadyCondition(claim *unstructured.Unstructured) *metav1.Condition {
	return ReadyCondition(claim)
}

func NodeClaimReadiness(claim *unstructured.Unstructured) Readiness {
	return ResourceReadiness(claim)
}

func NodePoolStatusResources(pool *unstructured.Unstructured) corev1.ResourceList {
	if pool == nil {
		return nil
	}
	return resourceList(pool.Object, "status", "resources")
}

func NodeClaimCapacity(claim *unstructured.Unstructured) corev1.ResourceList {
	if claim == nil {
		return nil
	}
	return resourceList(claim.Object, "status", "capacity")
}

// NodeClaimAllocatable is what the node will actually offer the scheduler once
// it registers — capacity minus the kubelet/system reservations. karpenter.sh/v1
// publishes it; it is absent on claims that have not been launched yet.
func NodeClaimAllocatable(claim *unstructured.Unstructured) corev1.ResourceList {
	if claim == nil {
		return nil
	}
	return resourceList(claim.Object, "status", "allocatable")
}

func NormalizeNodePoolSpec(pool *unstructured.Unstructured) NodePoolSpec {
	if pool == nil {
		return NodePoolSpec{}
	}
	normalized := NodePoolSpec{
		Replicas:       nestedInt64(pool.Object, "spec", "replicas"),
		Weight:         nestedInt64(pool.Object, "spec", "weight"),
		Limits:         resourceList(pool.Object, "spec", "limits"),
		TemplateLabels: nestedStringMap(pool.Object, "spec", "template", "metadata", "labels"),
		Requirements:   requirements(pool.Object),
		Taints:         taints(pool.Object, "spec", "template", "spec", "taints"),
		StartupTaints:  taints(pool.Object, "spec", "template", "spec", "startupTaints"),
		Disruption: Disruption{
			ConsolidationPolicy: nestedString(pool.Object, "spec", "disruption", "consolidationPolicy"),
			ConsolidateAfter:    nestedString(pool.Object, "spec", "disruption", "consolidateAfter"),
			Budgets:             disruptionBudgets(pool.Object),
		},
	}
	if ref, ok := NodeClassRefForNodePool(pool); ok {
		normalized.NodeClassRef = &ref
	}
	// Karpenter moved expiry from disruption in v1beta1 to the node template in v1.
	switch pool.GetAPIVersion() {
	case APIVersionV1:
		normalized.ExpireAfter = nestedString(pool.Object, "spec", "template", "spec", "expireAfter")
		normalized.TerminationGracePeriod = nestedString(pool.Object, "spec", "template", "spec", "terminationGracePeriod")
	case APIVersionV1Beta1:
		normalized.ExpireAfter = nestedString(pool.Object, "spec", "disruption", "expireAfter")
	}
	return normalized
}

func ResolveNodePoolForClaim(claim metav1.Object, poolsByName map[string]*unstructured.Unstructured) (*unstructured.Unstructured, RelationshipSource) {
	if claim == nil {
		return nil, RelationshipNone
	}
	for _, owner := range claim.GetOwnerReferences() {
		if !isNodePoolOwner(owner) {
			continue
		}
		pool := poolsByName[owner.Name]
		if pool == nil {
			return nil, RelationshipNone
		}
		if owner.UID != "" && owner.UID != pool.GetUID() {
			// A same-name NodePool may have been recreated; the label must not relink
			// a claim whose authoritative owner UID identifies the deleted instance.
			return nil, RelationshipNone
		}
		return pool, RelationshipOwnerRef
	}

	poolName := claim.GetLabels()[NodePoolLabelKey]
	if poolName == "" {
		return nil, RelationshipNone
	}
	pool := poolsByName[poolName]
	if pool == nil {
		return nil, RelationshipNone
	}
	return pool, RelationshipLabel
}

func ClaimNodeName(claim *unstructured.Unstructured) string {
	if claim == nil {
		return ""
	}
	return nestedString(claim.Object, "status", "nodeName")
}

func ClaimProviderID(claim *unstructured.Unstructured) string {
	if claim == nil {
		return ""
	}
	return nestedString(claim.Object, "status", "providerID")
}

func isNodePoolOwner(owner metav1.OwnerReference) bool {
	if owner.Kind != NodePoolKind {
		return false
	}
	gv, err := schema.ParseGroupVersion(owner.APIVersion)
	return err == nil && gv.Group == Group
}

func nestedString(obj map[string]any, fields ...string) string {
	value, found, err := unstructured.NestedString(obj, fields...)
	if err != nil || !found {
		return ""
	}
	return value
}

func nestedInt64(obj map[string]any, fields ...string) *int64 {
	value, found, err := unstructured.NestedInt64(obj, fields...)
	if err != nil || !found {
		return nil
	}
	return &value
}

func nestedStringMap(obj map[string]any, fields ...string) map[string]string {
	value, found, err := unstructured.NestedStringMap(obj, fields...)
	if err != nil || !found {
		return nil
	}
	return value
}

func resourceList(obj map[string]any, fields ...string) corev1.ResourceList {
	raw, found, err := unstructured.NestedMap(obj, fields...)
	if err != nil || !found {
		return nil
	}
	result := make(corev1.ResourceList, len(raw))
	for name, value := range raw {
		text, ok := quantityText(value)
		if !ok {
			continue
		}
		quantity, err := resource.ParseQuantity(text)
		if err != nil {
			continue
		}
		result[corev1.ResourceName(name)] = quantity
	}
	return result
}

func quantityText(value any) (string, bool) {
	switch value := value.(type) {
	case string:
		return value, value != ""
	case int64:
		return strconv.FormatInt(value, 10), true
	case int32:
		return strconv.FormatInt(int64(value), 10), true
	case int:
		return strconv.Itoa(value), true
	case float64:
		return strconv.FormatFloat(value, 'g', -1, 64), true
	default:
		return "", false
	}
}

func requirements(obj map[string]any) []Requirement {
	raw, found, err := unstructured.NestedSlice(obj, "spec", "template", "spec", "requirements")
	if err != nil || !found {
		return nil
	}
	result := make([]Requirement, 0, len(raw))
	for _, item := range raw {
		entry, ok := item.(map[string]any)
		if !ok {
			continue
		}
		key := nestedString(entry, "key")
		operator := nestedString(entry, "operator")
		if key == "" || operator == "" {
			continue
		}
		values, _, _ := unstructured.NestedStringSlice(entry, "values")
		result = append(result, Requirement{
			Key:       key,
			Operator:  corev1.NodeSelectorOperator(operator),
			Values:    values,
			MinValues: nestedInt64(entry, "minValues"),
		})
	}
	return result
}

func taints(obj map[string]any, fields ...string) []corev1.Taint {
	raw, found, err := unstructured.NestedSlice(obj, fields...)
	if err != nil || !found {
		return nil
	}
	result := make([]corev1.Taint, 0, len(raw))
	for _, item := range raw {
		entry, ok := item.(map[string]any)
		if !ok {
			continue
		}
		key := nestedString(entry, "key")
		if key == "" {
			continue
		}
		result = append(result, corev1.Taint{
			Key:    key,
			Value:  nestedString(entry, "value"),
			Effect: corev1.TaintEffect(nestedString(entry, "effect")),
		})
	}
	return result
}

func disruptionBudgets(obj map[string]any) []DisruptionBudget {
	raw, found, err := unstructured.NestedSlice(obj, "spec", "disruption", "budgets")
	if err != nil || !found {
		return nil
	}
	result := make([]DisruptionBudget, 0, len(raw))
	for _, item := range raw {
		entry, ok := item.(map[string]any)
		if !ok {
			continue
		}
		reasons, _, _ := unstructured.NestedStringSlice(entry, "reasons")
		nodes := nestedString(entry, "nodes")
		if numeric := nestedInt64(entry, "nodes"); nodes == "" && numeric != nil {
			nodes = strconv.FormatInt(*numeric, 10)
		}
		result = append(result, DisruptionBudget{
			Nodes:    nodes,
			Schedule: nestedString(entry, "schedule"),
			Duration: nestedString(entry, "duration"),
			Reasons:  reasons,
		})
	}
	return result
}