k8s.io/kubernetes@v1.29.3/pkg/scheduler/eventhandlers_test.go (about) 1 /* 2 Copyright 2019 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package scheduler 18 19 import ( 20 "context" 21 "reflect" 22 "testing" 23 "time" 24 25 "github.com/google/go-cmp/cmp" 26 appsv1 "k8s.io/api/apps/v1" 27 batchv1 "k8s.io/api/batch/v1" 28 v1 "k8s.io/api/core/v1" 29 storagev1 "k8s.io/api/storage/v1" 30 "k8s.io/apimachinery/pkg/api/resource" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/klog/v2/ktesting" 33 34 "k8s.io/apimachinery/pkg/runtime" 35 "k8s.io/apimachinery/pkg/runtime/schema" 36 "k8s.io/client-go/dynamic/dynamicinformer" 37 dyfake "k8s.io/client-go/dynamic/fake" 38 "k8s.io/client-go/informers" 39 "k8s.io/client-go/kubernetes/fake" 40 41 "k8s.io/kubernetes/pkg/scheduler/framework" 42 "k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodeaffinity" 43 "k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodename" 44 "k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodeports" 45 "k8s.io/kubernetes/pkg/scheduler/framework/plugins/noderesources" 46 "k8s.io/kubernetes/pkg/scheduler/internal/cache" 47 "k8s.io/kubernetes/pkg/scheduler/internal/queue" 48 st "k8s.io/kubernetes/pkg/scheduler/testing" 49 ) 50 51 func TestNodeAllocatableChanged(t *testing.T) { 52 newQuantity := func(value int64) resource.Quantity { 53 return *resource.NewQuantity(value, resource.BinarySI) 54 } 55 for _, test := range []struct { 56 Name string 57 Changed bool 58 OldAllocatable v1.ResourceList 59 NewAllocatable v1.ResourceList 60 }{ 61 { 62 Name: "no allocatable resources changed", 63 Changed: false, 64 OldAllocatable: v1.ResourceList{v1.ResourceMemory: newQuantity(1024)}, 65 NewAllocatable: v1.ResourceList{v1.ResourceMemory: newQuantity(1024)}, 66 }, 67 { 68 Name: "new node has more allocatable resources", 69 Changed: true, 70 OldAllocatable: v1.ResourceList{v1.ResourceMemory: newQuantity(1024)}, 71 NewAllocatable: v1.ResourceList{v1.ResourceMemory: newQuantity(1024), v1.ResourceStorage: newQuantity(1024)}, 72 }, 73 } { 74 t.Run(test.Name, func(t *testing.T) { 75 oldNode := &v1.Node{Status: v1.NodeStatus{Allocatable: test.OldAllocatable}} 76 newNode := &v1.Node{Status: v1.NodeStatus{Allocatable: test.NewAllocatable}} 77 changed := nodeAllocatableChanged(newNode, oldNode) 78 if changed != test.Changed { 79 t.Errorf("nodeAllocatableChanged should be %t, got %t", test.Changed, changed) 80 } 81 }) 82 } 83 } 84 85 func TestNodeLabelsChanged(t *testing.T) { 86 for _, test := range []struct { 87 Name string 88 Changed bool 89 OldLabels map[string]string 90 NewLabels map[string]string 91 }{ 92 { 93 Name: "no labels changed", 94 Changed: false, 95 OldLabels: map[string]string{"foo": "bar"}, 96 NewLabels: map[string]string{"foo": "bar"}, 97 }, 98 // Labels changed. 99 { 100 Name: "new node has more labels", 101 Changed: true, 102 OldLabels: map[string]string{"foo": "bar"}, 103 NewLabels: map[string]string{"foo": "bar", "test": "value"}, 104 }, 105 } { 106 t.Run(test.Name, func(t *testing.T) { 107 oldNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{Labels: test.OldLabels}} 108 newNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{Labels: test.NewLabels}} 109 changed := nodeLabelsChanged(newNode, oldNode) 110 if changed != test.Changed { 111 t.Errorf("Test case %q failed: should be %t, got %t", test.Name, test.Changed, changed) 112 } 113 }) 114 } 115 } 116 117 func TestNodeTaintsChanged(t *testing.T) { 118 for _, test := range []struct { 119 Name string 120 Changed bool 121 OldTaints []v1.Taint 122 NewTaints []v1.Taint 123 }{ 124 { 125 Name: "no taint changed", 126 Changed: false, 127 OldTaints: []v1.Taint{{Key: "key", Value: "value"}}, 128 NewTaints: []v1.Taint{{Key: "key", Value: "value"}}, 129 }, 130 { 131 Name: "taint value changed", 132 Changed: true, 133 OldTaints: []v1.Taint{{Key: "key", Value: "value1"}}, 134 NewTaints: []v1.Taint{{Key: "key", Value: "value2"}}, 135 }, 136 } { 137 t.Run(test.Name, func(t *testing.T) { 138 oldNode := &v1.Node{Spec: v1.NodeSpec{Taints: test.OldTaints}} 139 newNode := &v1.Node{Spec: v1.NodeSpec{Taints: test.NewTaints}} 140 changed := nodeTaintsChanged(newNode, oldNode) 141 if changed != test.Changed { 142 t.Errorf("Test case %q failed: should be %t, not %t", test.Name, test.Changed, changed) 143 } 144 }) 145 } 146 } 147 148 func TestNodeConditionsChanged(t *testing.T) { 149 nodeConditionType := reflect.TypeOf(v1.NodeCondition{}) 150 if nodeConditionType.NumField() != 6 { 151 t.Errorf("NodeCondition type has changed. The nodeConditionsChanged() function must be reevaluated.") 152 } 153 154 for _, test := range []struct { 155 Name string 156 Changed bool 157 OldConditions []v1.NodeCondition 158 NewConditions []v1.NodeCondition 159 }{ 160 { 161 Name: "no condition changed", 162 Changed: false, 163 OldConditions: []v1.NodeCondition{{Type: v1.NodeDiskPressure, Status: v1.ConditionTrue}}, 164 NewConditions: []v1.NodeCondition{{Type: v1.NodeDiskPressure, Status: v1.ConditionTrue}}, 165 }, 166 { 167 Name: "only LastHeartbeatTime changed", 168 Changed: false, 169 OldConditions: []v1.NodeCondition{{Type: v1.NodeDiskPressure, Status: v1.ConditionTrue, LastHeartbeatTime: metav1.Unix(1, 0)}}, 170 NewConditions: []v1.NodeCondition{{Type: v1.NodeDiskPressure, Status: v1.ConditionTrue, LastHeartbeatTime: metav1.Unix(2, 0)}}, 171 }, 172 { 173 Name: "new node has more healthy conditions", 174 Changed: true, 175 OldConditions: []v1.NodeCondition{}, 176 NewConditions: []v1.NodeCondition{{Type: v1.NodeReady, Status: v1.ConditionTrue}}, 177 }, 178 { 179 Name: "new node has less unhealthy conditions", 180 Changed: true, 181 OldConditions: []v1.NodeCondition{{Type: v1.NodeDiskPressure, Status: v1.ConditionTrue}}, 182 NewConditions: []v1.NodeCondition{}, 183 }, 184 { 185 Name: "condition status changed", 186 Changed: true, 187 OldConditions: []v1.NodeCondition{{Type: v1.NodeReady, Status: v1.ConditionFalse}}, 188 NewConditions: []v1.NodeCondition{{Type: v1.NodeReady, Status: v1.ConditionTrue}}, 189 }, 190 } { 191 t.Run(test.Name, func(t *testing.T) { 192 oldNode := &v1.Node{Status: v1.NodeStatus{Conditions: test.OldConditions}} 193 newNode := &v1.Node{Status: v1.NodeStatus{Conditions: test.NewConditions}} 194 changed := nodeConditionsChanged(newNode, oldNode) 195 if changed != test.Changed { 196 t.Errorf("Test case %q failed: should be %t, got %t", test.Name, test.Changed, changed) 197 } 198 }) 199 } 200 } 201 202 func TestUpdatePodInCache(t *testing.T) { 203 ttl := 10 * time.Second 204 nodeName := "node" 205 206 tests := []struct { 207 name string 208 oldObj interface{} 209 newObj interface{} 210 }{ 211 { 212 name: "pod updated with the same UID", 213 oldObj: withPodName(podWithPort("oldUID", nodeName, 80), "pod"), 214 newObj: withPodName(podWithPort("oldUID", nodeName, 8080), "pod"), 215 }, 216 { 217 name: "pod updated with different UIDs", 218 oldObj: withPodName(podWithPort("oldUID", nodeName, 80), "pod"), 219 newObj: withPodName(podWithPort("newUID", nodeName, 8080), "pod"), 220 }, 221 } 222 for _, tt := range tests { 223 t.Run(tt.name, func(t *testing.T) { 224 logger, ctx := ktesting.NewTestContext(t) 225 ctx, cancel := context.WithCancel(ctx) 226 defer cancel() 227 sched := &Scheduler{ 228 Cache: cache.New(ctx, ttl), 229 SchedulingQueue: queue.NewTestQueue(ctx, nil), 230 logger: logger, 231 } 232 sched.addPodToCache(tt.oldObj) 233 sched.updatePodInCache(tt.oldObj, tt.newObj) 234 235 if tt.oldObj.(*v1.Pod).UID != tt.newObj.(*v1.Pod).UID { 236 if pod, err := sched.Cache.GetPod(tt.oldObj.(*v1.Pod)); err == nil { 237 t.Errorf("Get pod UID %v from cache but it should not happen", pod.UID) 238 } 239 } 240 pod, err := sched.Cache.GetPod(tt.newObj.(*v1.Pod)) 241 if err != nil { 242 t.Errorf("Failed to get pod from scheduler: %v", err) 243 } 244 if pod.UID != tt.newObj.(*v1.Pod).UID { 245 t.Errorf("Want pod UID %v, got %v", tt.newObj.(*v1.Pod).UID, pod.UID) 246 } 247 }) 248 } 249 } 250 251 func withPodName(pod *v1.Pod, name string) *v1.Pod { 252 pod.Name = name 253 return pod 254 } 255 256 func TestPreCheckForNode(t *testing.T) { 257 cpu4 := map[v1.ResourceName]string{v1.ResourceCPU: "4"} 258 cpu8 := map[v1.ResourceName]string{v1.ResourceCPU: "8"} 259 cpu16 := map[v1.ResourceName]string{v1.ResourceCPU: "16"} 260 tests := []struct { 261 name string 262 nodeFn func() *v1.Node 263 existingPods, pods []*v1.Pod 264 want []bool 265 }{ 266 { 267 name: "regular node, pods with a single constraint", 268 nodeFn: func() *v1.Node { 269 return st.MakeNode().Name("fake-node").Label("hostname", "fake-node").Capacity(cpu8).Obj() 270 }, 271 existingPods: []*v1.Pod{ 272 st.MakePod().Name("p").HostPort(80).Obj(), 273 }, 274 pods: []*v1.Pod{ 275 st.MakePod().Name("p1").Req(cpu4).Obj(), 276 st.MakePod().Name("p2").Req(cpu16).Obj(), 277 st.MakePod().Name("p3").Req(cpu4).Req(cpu8).Obj(), 278 st.MakePod().Name("p4").NodeAffinityIn("hostname", []string{"fake-node"}).Obj(), 279 st.MakePod().Name("p5").NodeAffinityNotIn("hostname", []string{"fake-node"}).Obj(), 280 st.MakePod().Name("p6").Obj(), 281 st.MakePod().Name("p7").Node("invalid-node").Obj(), 282 st.MakePod().Name("p8").HostPort(8080).Obj(), 283 st.MakePod().Name("p9").HostPort(80).Obj(), 284 }, 285 want: []bool{true, false, false, true, false, true, false, true, false}, 286 }, 287 { 288 name: "tainted node, pods with a single constraint", 289 nodeFn: func() *v1.Node { 290 node := st.MakeNode().Name("fake-node").Obj() 291 node.Spec.Taints = []v1.Taint{ 292 {Key: "foo", Effect: v1.TaintEffectNoSchedule}, 293 {Key: "bar", Effect: v1.TaintEffectPreferNoSchedule}, 294 } 295 return node 296 }, 297 pods: []*v1.Pod{ 298 st.MakePod().Name("p1").Obj(), 299 st.MakePod().Name("p2").Toleration("foo").Obj(), 300 st.MakePod().Name("p3").Toleration("bar").Obj(), 301 st.MakePod().Name("p4").Toleration("bar").Toleration("foo").Obj(), 302 }, 303 want: []bool{false, true, false, true}, 304 }, 305 { 306 name: "regular node, pods with multiple constraints", 307 nodeFn: func() *v1.Node { 308 return st.MakeNode().Name("fake-node").Label("hostname", "fake-node").Capacity(cpu8).Obj() 309 }, 310 existingPods: []*v1.Pod{ 311 st.MakePod().Name("p").HostPort(80).Obj(), 312 }, 313 pods: []*v1.Pod{ 314 st.MakePod().Name("p1").Req(cpu4).NodeAffinityNotIn("hostname", []string{"fake-node"}).Obj(), 315 st.MakePod().Name("p2").Req(cpu16).NodeAffinityIn("hostname", []string{"fake-node"}).Obj(), 316 st.MakePod().Name("p3").Req(cpu8).NodeAffinityIn("hostname", []string{"fake-node"}).Obj(), 317 st.MakePod().Name("p4").HostPort(8080).Node("invalid-node").Obj(), 318 st.MakePod().Name("p5").Req(cpu4).NodeAffinityIn("hostname", []string{"fake-node"}).HostPort(80).Obj(), 319 }, 320 want: []bool{false, false, true, false, false}, 321 }, 322 { 323 name: "tainted node, pods with multiple constraints", 324 nodeFn: func() *v1.Node { 325 node := st.MakeNode().Name("fake-node").Label("hostname", "fake-node").Capacity(cpu8).Obj() 326 node.Spec.Taints = []v1.Taint{ 327 {Key: "foo", Effect: v1.TaintEffectNoSchedule}, 328 {Key: "bar", Effect: v1.TaintEffectPreferNoSchedule}, 329 } 330 return node 331 }, 332 pods: []*v1.Pod{ 333 st.MakePod().Name("p1").Req(cpu4).Toleration("bar").Obj(), 334 st.MakePod().Name("p2").Req(cpu4).Toleration("bar").Toleration("foo").Obj(), 335 st.MakePod().Name("p3").Req(cpu16).Toleration("foo").Obj(), 336 st.MakePod().Name("p3").Req(cpu16).Toleration("bar").Obj(), 337 }, 338 want: []bool{false, true, false, false}, 339 }, 340 } 341 342 for _, tt := range tests { 343 t.Run(tt.name, func(t *testing.T) { 344 nodeInfo := framework.NewNodeInfo(tt.existingPods...) 345 nodeInfo.SetNode(tt.nodeFn()) 346 preCheckFn := preCheckForNode(nodeInfo) 347 348 var got []bool 349 for _, pod := range tt.pods { 350 got = append(got, preCheckFn(pod)) 351 } 352 353 if diff := cmp.Diff(tt.want, got); diff != "" { 354 t.Errorf("Unexpected diff (-want, +got):\n%s", diff) 355 } 356 }) 357 } 358 } 359 360 // test for informers of resources we care about is registered 361 func TestAddAllEventHandlers(t *testing.T) { 362 tests := []struct { 363 name string 364 gvkMap map[framework.GVK]framework.ActionType 365 expectStaticInformers map[reflect.Type]bool 366 expectDynamicInformers map[schema.GroupVersionResource]bool 367 }{ 368 { 369 name: "default handlers in framework", 370 gvkMap: map[framework.GVK]framework.ActionType{}, 371 expectStaticInformers: map[reflect.Type]bool{ 372 reflect.TypeOf(&v1.Pod{}): true, 373 reflect.TypeOf(&v1.Node{}): true, 374 reflect.TypeOf(&v1.Namespace{}): true, 375 }, 376 expectDynamicInformers: map[schema.GroupVersionResource]bool{}, 377 }, 378 { 379 name: "add GVKs handlers defined in framework dynamically", 380 gvkMap: map[framework.GVK]framework.ActionType{ 381 "Pod": framework.Add | framework.Delete, 382 "PersistentVolume": framework.Delete, 383 "storage.k8s.io/CSIStorageCapacity": framework.Update, 384 }, 385 expectStaticInformers: map[reflect.Type]bool{ 386 reflect.TypeOf(&v1.Pod{}): true, 387 reflect.TypeOf(&v1.Node{}): true, 388 reflect.TypeOf(&v1.Namespace{}): true, 389 reflect.TypeOf(&v1.PersistentVolume{}): true, 390 reflect.TypeOf(&storagev1.CSIStorageCapacity{}): true, 391 }, 392 expectDynamicInformers: map[schema.GroupVersionResource]bool{}, 393 }, 394 { 395 name: "add GVKs handlers defined in plugins dynamically", 396 gvkMap: map[framework.GVK]framework.ActionType{ 397 "daemonsets.v1.apps": framework.Add | framework.Delete, 398 "cronjobs.v1.batch": framework.Delete, 399 }, 400 expectStaticInformers: map[reflect.Type]bool{ 401 reflect.TypeOf(&v1.Pod{}): true, 402 reflect.TypeOf(&v1.Node{}): true, 403 reflect.TypeOf(&v1.Namespace{}): true, 404 }, 405 expectDynamicInformers: map[schema.GroupVersionResource]bool{ 406 {Group: "apps", Version: "v1", Resource: "daemonsets"}: true, 407 {Group: "batch", Version: "v1", Resource: "cronjobs"}: true, 408 }, 409 }, 410 { 411 name: "add GVKs handlers defined in plugins dynamically, with one illegal GVK form", 412 gvkMap: map[framework.GVK]framework.ActionType{ 413 "daemonsets.v1.apps": framework.Add | framework.Delete, 414 "custommetrics.v1beta1": framework.Update, 415 }, 416 expectStaticInformers: map[reflect.Type]bool{ 417 reflect.TypeOf(&v1.Pod{}): true, 418 reflect.TypeOf(&v1.Node{}): true, 419 reflect.TypeOf(&v1.Namespace{}): true, 420 }, 421 expectDynamicInformers: map[schema.GroupVersionResource]bool{ 422 {Group: "apps", Version: "v1", Resource: "daemonsets"}: true, 423 }, 424 }, 425 } 426 427 scheme := runtime.NewScheme() 428 var localSchemeBuilder = runtime.SchemeBuilder{ 429 appsv1.AddToScheme, 430 batchv1.AddToScheme, 431 } 432 localSchemeBuilder.AddToScheme(scheme) 433 434 for _, tt := range tests { 435 t.Run(tt.name, func(t *testing.T) { 436 logger, ctx := ktesting.NewTestContext(t) 437 ctx, cancel := context.WithCancel(ctx) 438 defer cancel() 439 440 informerFactory := informers.NewSharedInformerFactory(fake.NewSimpleClientset(), 0) 441 schedulingQueue := queue.NewTestQueueWithInformerFactory(ctx, nil, informerFactory) 442 testSched := Scheduler{ 443 StopEverything: ctx.Done(), 444 SchedulingQueue: schedulingQueue, 445 logger: logger, 446 } 447 448 dynclient := dyfake.NewSimpleDynamicClient(scheme) 449 dynInformerFactory := dynamicinformer.NewDynamicSharedInformerFactory(dynclient, 0) 450 451 if err := addAllEventHandlers(&testSched, informerFactory, dynInformerFactory, tt.gvkMap); err != nil { 452 t.Fatalf("Add event handlers failed, error = %v", err) 453 } 454 455 informerFactory.Start(testSched.StopEverything) 456 dynInformerFactory.Start(testSched.StopEverything) 457 staticInformers := informerFactory.WaitForCacheSync(testSched.StopEverything) 458 dynamicInformers := dynInformerFactory.WaitForCacheSync(testSched.StopEverything) 459 460 if diff := cmp.Diff(tt.expectStaticInformers, staticInformers); diff != "" { 461 t.Errorf("Unexpected diff (-want, +got):\n%s", diff) 462 } 463 if diff := cmp.Diff(tt.expectDynamicInformers, dynamicInformers); diff != "" { 464 t.Errorf("Unexpected diff (-want, +got):\n%s", diff) 465 } 466 }) 467 } 468 } 469 470 func TestAdmissionCheck(t *testing.T) { 471 nodeaffinityError := AdmissionResult{Name: nodeaffinity.Name, Reason: nodeaffinity.ErrReasonPod} 472 nodenameError := AdmissionResult{Name: nodename.Name, Reason: nodename.ErrReason} 473 nodeportsError := AdmissionResult{Name: nodeports.Name, Reason: nodeports.ErrReason} 474 podOverheadError := AdmissionResult{InsufficientResource: &noderesources.InsufficientResource{ResourceName: v1.ResourceCPU, Reason: "Insufficient cpu", Requested: 2000, Used: 7000, Capacity: 8000}} 475 cpu := map[v1.ResourceName]string{v1.ResourceCPU: "8"} 476 tests := []struct { 477 name string 478 node *v1.Node 479 existingPods []*v1.Pod 480 pod *v1.Pod 481 wantAdmissionResults [][]AdmissionResult 482 }{ 483 { 484 name: "check nodeAffinity and nodeports, nodeAffinity need fail quickly if includeAllFailures is false", 485 node: st.MakeNode().Name("fake-node").Label("foo", "bar").Obj(), 486 pod: st.MakePod().Name("pod2").HostPort(80).NodeSelector(map[string]string{"foo": "bar1"}).Obj(), 487 existingPods: []*v1.Pod{ 488 st.MakePod().Name("pod1").HostPort(80).Obj(), 489 }, 490 wantAdmissionResults: [][]AdmissionResult{{nodeaffinityError, nodeportsError}, {nodeaffinityError}}, 491 }, 492 { 493 name: "check PodOverhead and nodeAffinity, PodOverhead need fail quickly if includeAllFailures is false", 494 node: st.MakeNode().Name("fake-node").Label("foo", "bar").Capacity(cpu).Obj(), 495 pod: st.MakePod().Name("pod2").Container("c").Overhead(v1.ResourceList{v1.ResourceCPU: resource.MustParse("1")}).Req(map[v1.ResourceName]string{v1.ResourceCPU: "1"}).NodeSelector(map[string]string{"foo": "bar1"}).Obj(), 496 existingPods: []*v1.Pod{ 497 st.MakePod().Name("pod1").Req(map[v1.ResourceName]string{v1.ResourceCPU: "7"}).Node("fake-node").Obj(), 498 }, 499 wantAdmissionResults: [][]AdmissionResult{{podOverheadError, nodeaffinityError}, {podOverheadError}}, 500 }, 501 { 502 name: "check nodename and nodeports, nodename need fail quickly if includeAllFailures is false", 503 node: st.MakeNode().Name("fake-node").Obj(), 504 pod: st.MakePod().Name("pod2").HostPort(80).Node("fake-node1").Obj(), 505 existingPods: []*v1.Pod{ 506 st.MakePod().Name("pod1").HostPort(80).Node("fake-node").Obj(), 507 }, 508 wantAdmissionResults: [][]AdmissionResult{{nodenameError, nodeportsError}, {nodenameError}}, 509 }, 510 } 511 for _, tt := range tests { 512 t.Run(tt.name, func(t *testing.T) { 513 nodeInfo := framework.NewNodeInfo(tt.existingPods...) 514 nodeInfo.SetNode(tt.node) 515 516 flags := []bool{true, false} 517 for i := range flags { 518 admissionResults := AdmissionCheck(tt.pod, nodeInfo, flags[i]) 519 520 if diff := cmp.Diff(tt.wantAdmissionResults[i], admissionResults); diff != "" { 521 t.Errorf("Unexpected admissionResults (-want, +got):\n%s", diff) 522 } 523 } 524 }) 525 } 526 }