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