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