package scheduling import "testing" func TestParseMessageDecomposesVerdictAndDropsPreemptionTail(t *testing.T) { message := "0/5 nodes are available: 2 Insufficient cpu, 3 node(s) had untolerated taint {dedicated: gpu}. " + "preemption: 0/5 nodes are available: 5 No preemption victims found for incoming pod." total, reasons := ParseMessage(message) if total != 5 { t.Fatalf("total nodes = %d, want 5", total) } if len(reasons) != 2 { t.Fatalf("reasons = %+v, want two non-preemption clauses", reasons) } if reasons[0] != (Reason{Class: InsufficientResource, NodeCount: 2, Resource: "cpu", Raw: "2 Insufficient cpu"}) { t.Errorf("CPU reason = %+v", reasons[0]) } if reasons[1] != (Reason{Class: UntoleratedTaint, NodeCount: 3, TaintKey: "dedicated", TaintValue: "gpu", Raw: "3 node(s) had untolerated taint {dedicated: gpu}"}) { t.Errorf("taint reason = %+v", reasons[1]) } } func TestParseMessageClasses(t *testing.T) { tests := []struct { name string clause string class ReasonClass resource string taintKey string taintValue string }{ {name: "CPU", clause: "3 Insufficient cpu", class: InsufficientResource, resource: "cpu"}, {name: "memory", clause: "3 Insufficient memory", class: InsufficientResource, resource: "memory"}, {name: "extended resource", clause: "3 Insufficient nvidia.com/gpu", class: InsufficientResource, resource: "nvidia.com/gpu"}, {name: "pod slots", clause: "3 Too many pods", class: InsufficientResource, resource: "pods"}, {name: "node affinity", clause: "3 node(s) didn't match Pod's node affinity/selector", class: NodeAffinitySelector}, {name: "pod affinity", clause: "3 node(s) didn't match pod affinity rules", class: PodAffinity}, {name: "pod anti-affinity", clause: "3 node(s) didn't match pod anti-affinity rules", class: PodAntiAffinity}, {name: "topology spread", clause: "3 node(s) didn't match pod topology spread constraints", class: TopologySpread}, {name: "volume node affinity", clause: "3 node(s) had volume node affinity conflict", class: VolumeNodeAffinity}, {name: "PersistentVolume node affinity", clause: "3 node(s) didn't match PersistentVolume's node affinity", class: VolumeNodeAffinity}, {name: "volume zone", clause: "3 node(s) had no available volume zone", class: VolumeNodeAffinity}, {name: "unbound PVC", clause: "pod has unbound immediate PersistentVolumeClaims", class: VolumeBinding}, {name: "volume count", clause: "3 node(s) exceed max volume count", class: VolumeCount}, {name: "host port", clause: "3 node(s) didn't have free ports for the requested pod ports", class: NoPorts}, {name: "cordoned", clause: "3 node(s) were unschedulable", class: NodeUnschedulable}, {name: "operator taint", clause: "3 node(s) had untolerated taint {dedicated: gpu}", class: UntoleratedTaint, taintKey: "dedicated", taintValue: "gpu"}, {name: "lifecycle taint", clause: "3 node(s) had untolerated taint {node.kubernetes.io/not-ready: }", class: NodeUnschedulable, taintKey: "node.kubernetes.io/not-ready"}, {name: "opaque", clause: "3 node(s) rejected by a future scheduler plugin", class: Other}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { _, reasons := ParseMessage("0/3 nodes are available: " + test.clause + ".") if len(reasons) != 1 { t.Fatalf("reasons = %+v, want one", reasons) } reason := reasons[0] if reason.Class != test.class || reason.Resource != test.resource || reason.TaintKey != test.taintKey || reason.TaintValue != test.taintValue { t.Errorf("reason = %+v", reason) } if reason.NodeCount != 3 && test.class != VolumeBinding { t.Errorf("node count = %d, want 3", reason.NodeCount) } }) } } // The duplicate_pvc rollout verdict: the PersistentVolume clause must not be // read as the Pod's own nodeSelector/affinity. func TestParseMessagePersistentVolumeAffinityWithAntiAffinity(t *testing.T) { _, reasons := ParseMessage("0/3 nodes are available: 1 node(s) didn't satisfy existing pods anti-affinity rules, " + "2 node(s) didn't match PersistentVolume's node affinity. preemption: 0/3 nodes are available: 1 No preemption victims found for incoming pod, 2 Preemption is not helpful for scheduling.") if len(reasons) != 2 || reasons[0].Class != PodAntiAffinity || reasons[1].Class != VolumeNodeAffinity || reasons[1].NodeCount != 2 { t.Fatalf("reasons = %+v, want pod anti-affinity then volume node affinity on 2 nodes", reasons) } } func TestParseMessageWholeEmptyAndIgnoredVariants(t *testing.T) { if total, reasons := ParseMessage("pod has unbound immediate PersistentVolumeClaims"); total != 0 || len(reasons) != 1 || reasons[0].Class != VolumeBinding { t.Fatalf("whole-message PVC = %d/%+v", total, reasons) } if total, reasons := ParseMessage(""); total != 0 || reasons != nil { t.Fatalf("empty message = %d/%+v", total, reasons) } if _, reasons := ParseMessage(" "); reasons != nil { t.Fatalf("whitespace reasons = %+v", reasons) } if total, reasons := ParseMessage("0/1 nodes are available: no new claims to deallocate."); total != 1 || len(reasons) != 0 { t.Fatalf("ignored DRA-only message = %d/%+v", total, reasons) } } // Volcano frames its verdict differently, and its gang-scheduling message is // not a list of node clauses. func TestParseMessageVolcano(t *testing.T) { total, reasons := ParseMessage("Node: 0/21 nodes are unavailable: 1 Insufficient cpu, 16 node(s) didn't match Pod's node affinity/selector, 4 Insufficient pods.") if total != 21 || len(reasons) != 3 { t.Fatalf("Volcano node verdict = %d/%+v, want 21 nodes and three reasons", total, reasons) } if reasons[0].Class != InsufficientResource || reasons[0].Resource != "cpu" || reasons[0].NodeCount != 1 { t.Errorf("first clause lost its count: %+v", reasons[0]) } if _, reasons := ParseMessage("2 Too many pods"); len(reasons) != 1 || reasons[0].Class != InsufficientResource || reasons[0].Resource != "pods" || reasons[0].NodeCount != 2 { t.Errorf("a single unframed clause should read as itself: %+v", reasons) } if _, reasons := ParseMessage("no new claims to deallocate, pod has unbound immediate PersistentVolumeClaims"); len(reasons) != 1 || reasons[0].Class != VolumeBinding { t.Errorf("the volume verdict should survive DRA filler: %+v", reasons) } gang := "pod group is not ready, 1 Pending, 1 minAvailable; Pending: 1 Unschedulable" if total, reasons := ParseMessage(gang); total != 0 || len(reasons) != 1 || reasons[0] != (Reason{Class: Other, Raw: gang}) { t.Errorf("gang message = %d/%+v, want it whole, as one unclassified reason", total, reasons) } } func TestParseTaint(t *testing.T) { tests := []struct { clause, key, value string }{ {clause: "3 node(s) had untolerated taint {dedicated: gpu}", key: "dedicated", value: "gpu"}, {clause: "3 node(s) had untolerated taint {workload}", key: "workload"}, {clause: "3 node(s) had untolerated taint {node.kubernetes.io/unreachable: }", key: "node.kubernetes.io/unreachable"}, {clause: "3 node(s) had untolerated taint"}, } for _, test := range tests { key, value := ParseTaint(test.clause) if key != test.key || value != test.value { t.Errorf("ParseTaint(%q) = %q,%q; want %q,%q", test.clause, key, value, test.key, test.value) } } }