Ctrl K
ComunidadGuías…

pkg/scheduling/message.go

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

Archivo original ↗Preguntar a KiBot
package scheduling

import (
	"regexp"
	"strconv"
	"strings"
)

type ReasonClass string

const (
	InsufficientResource ReasonClass = "InsufficientResource"
	UntoleratedTaint     ReasonClass = "UntoleratedTaint"
	NodeAffinitySelector ReasonClass = "NodeAffinitySelector"
	PodAffinity          ReasonClass = "PodAffinity"
	PodAntiAffinity      ReasonClass = "PodAntiAffinity"
	TopologySpread       ReasonClass = "TopologySpread"
	VolumeNodeAffinity   ReasonClass = "VolumeNodeAffinity"
	VolumeBinding        ReasonClass = "VolumeBinding"
	VolumeCount          ReasonClass = "VolumeCount"
	NoPorts              ReasonClass = "NoPorts"
	NodeUnschedulable    ReasonClass = "NodeUnschedulable"
	Other                ReasonClass = "Other"
)

type Reason struct {
	Class      ReasonClass
	NodeCount  int
	Resource   string
	TaintKey   string
	TaintValue string
	Raw        string
}

var (
	// kube-scheduler says "0/5 nodes are available:"; Volcano says
	// "Node: 0/5 nodes are unavailable:".
	nodesAvailablePattern = regexp.MustCompile(`(\d+)/(\d+)\s+nodes? are (?:un)?available`)
	leadingCountPattern   = regexp.MustCompile(`^\s*(\d+)\s+`)
	insufficientPattern   = regexp.MustCompile(`Insufficient\s+([A-Za-z0-9./_-]+)`)
	taintPattern          = regexp.MustCompile(`\{([^}]*)\}`)
)

func ParseMessage(message string) (totalNodes int, reasons []Reason) {
	message = strings.TrimSpace(message)
	if message == "" {
		return 0, nil
	}

	if before, _, ok := strings.Cut(message, ". preemption:"); ok {
		message = before
	} else if before, _, ok := strings.Cut(message, " preemption:"); ok {
		message = before
	}

	match := nodesAvailablePattern.FindStringSubmatchIndex(message)
	if match == nil {
		whole := NormalizeClause(message)
		// A single clause without the per-node frame ("2 Too many pods", the
		// unbound-volume verdict) reads as itself.
		if !strings.ContainsAny(whole, ",;:") {
			if reason, ok := classifyClause(whole); ok {
				return 0, []Reason{reason}
			}
			return 0, nil
		}
		// Longer text without the frame is one statement, not a list of node
		// clauses: Volcano's gang-scheduling "pod group is not ready, 1 Pending,
		// …; Pending: 1 Unschedulable" says nothing about nodes.
		if reason, ok := classifyClause(whole); ok && reason.Class == VolumeBinding {
			return 0, []Reason{reason}
		}
		if IsIgnoredClause(whole) {
			return 0, nil
		}
		return 0, []Reason{{Class: Other, Raw: whole}}
	}
	totalNodes, _ = strconv.Atoi(message[match[4]:match[5]])

	clauses := message[match[1]:]
	clauses = strings.TrimPrefix(strings.TrimSpace(clauses), ":")
	clauses = strings.TrimRight(strings.TrimSpace(clauses), ".")
	if clauses == "" {
		return totalNodes, nil
	}

	for clause := range strings.SplitSeq(clauses, ", ") {
		clause = NormalizeClause(clause)
		if clause == "" {
			continue
		}
		if reason, ok := classifyClause(clause); ok {
			reasons = append(reasons, reason)
		}
	}
	return totalNodes, reasons
}

func NormalizeClause(clause string) string {
	return strings.TrimSpace(strings.TrimRight(strings.TrimSpace(clause), ",."))
}

func ParseTaint(clause string) (key, value string) {
	match := taintPattern.FindStringSubmatch(clause)
	if match == nil {
		return "", ""
	}
	inner := strings.TrimSpace(match[1])
	if inner == "" {
		return "", ""
	}
	if key, value, ok := strings.Cut(inner, ":"); ok {
		return strings.TrimSpace(key), strings.TrimSpace(value)
	}
	return inner, ""
}

func IsNodeLifecycleTaint(key string) bool {
	return strings.HasPrefix(key, "node.kubernetes.io/") ||
		strings.HasPrefix(key, "node-role.kubernetes.io/") ||
		strings.HasPrefix(key, "node.cloudprovider.kubernetes.io/")
}

func IsIgnoredClause(clause string) bool {
	return strings.Contains(strings.ToLower(NormalizeClause(clause)), "no new claims to deallocate")
}

func classifyClause(clause string) (Reason, bool) {
	clause = NormalizeClause(clause)
	if clause == "" {
		return Reason{}, false
	}
	reason := Reason{Raw: clause}
	if match := leadingCountPattern.FindStringSubmatch(clause); match != nil {
		reason.NodeCount, _ = strconv.Atoi(match[1])
	}

	lower := strings.ToLower(clause)
	switch {
	case strings.Contains(clause, "Insufficient"):
		reason.Class = InsufficientResource
		if match := insufficientPattern.FindStringSubmatch(clause); match != nil {
			// A sentence break inside the clause ("Insufficient memory. no new claims
			// to deallocate") would otherwise end the resource name with a dot.
			reason.Resource = strings.TrimRight(match[1], ".")
		}
	case strings.Contains(lower, "too many pods"):
		reason.Class = InsufficientResource
		reason.Resource = "pods"
	case strings.Contains(lower, "untolerated taint"):
		reason.Class = UntoleratedTaint
		reason.TaintKey, reason.TaintValue = ParseTaint(clause)
		if IsNodeLifecycleTaint(reason.TaintKey) {
			reason.Class = NodeUnschedulable
		}
	// Volume topology must match before the generic "node affinity" case:
	// kube-scheduler's VolumeBinding plugin says "didn't match
	// PersistentVolume's node affinity" (older releases: "had volume node
	// affinity conflict"), and VolumeZone says "had no available volume zone".
	// All mean the Pod's bound volume can't attach on those nodes, not that the
	// Pod's own nodeSelector/affinity is wrong.
	case strings.Contains(lower, "volume node affinity"),
		strings.Contains(lower, "persistentvolume") && strings.Contains(lower, "node affinity"),
		strings.Contains(lower, "volume zone"):
		reason.Class = VolumeNodeAffinity
	case strings.Contains(lower, "anti-affinity"):
		reason.Class = PodAntiAffinity
	case strings.Contains(lower, "node affinity") || strings.Contains(lower, "node selector"):
		reason.Class = NodeAffinitySelector
	case strings.Contains(lower, "pod affinity"):
		reason.Class = PodAffinity
	case strings.Contains(lower, "topology spread"):
		reason.Class = TopologySpread
	case strings.Contains(lower, "max volume count"):
		reason.Class = VolumeCount
	case strings.Contains(lower, "free ports"):
		reason.Class = NoPorts
	case strings.Contains(lower, "unbound") && strings.Contains(lower, "persistentvolumeclaim"),
		strings.Contains(lower, "persistent volumes to bind"):
		reason.Class = VolumeBinding
	case IsIgnoredClause(clause):
		return Reason{}, false
	case strings.Contains(lower, "unschedulable"), strings.Contains(lower, "were not ready"):
		reason.Class = NodeUnschedulable
	default:
		reason.Class = Other
	}
	return reason, true
}