k8s.io/kubernetes@v1.29.3/pkg/scheduler/extender_test.go (about) 1 /* 2 Copyright 2015 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 v1 "k8s.io/api/core/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/util/sets" 28 "k8s.io/client-go/informers" 29 clientsetfake "k8s.io/client-go/kubernetes/fake" 30 "k8s.io/klog/v2/ktesting" 31 extenderv1 "k8s.io/kube-scheduler/extender/v1" 32 schedulerapi "k8s.io/kubernetes/pkg/scheduler/apis/config" 33 "k8s.io/kubernetes/pkg/scheduler/framework" 34 "k8s.io/kubernetes/pkg/scheduler/framework/plugins/defaultbinder" 35 "k8s.io/kubernetes/pkg/scheduler/framework/plugins/queuesort" 36 "k8s.io/kubernetes/pkg/scheduler/framework/runtime" 37 internalcache "k8s.io/kubernetes/pkg/scheduler/internal/cache" 38 internalqueue "k8s.io/kubernetes/pkg/scheduler/internal/queue" 39 st "k8s.io/kubernetes/pkg/scheduler/testing" 40 tf "k8s.io/kubernetes/pkg/scheduler/testing/framework" 41 ) 42 43 func TestSchedulerWithExtenders(t *testing.T) { 44 tests := []struct { 45 name string 46 registerPlugins []tf.RegisterPluginFunc 47 extenders []tf.FakeExtender 48 nodes []string 49 expectedResult ScheduleResult 50 expectsErr bool 51 }{ 52 { 53 registerPlugins: []tf.RegisterPluginFunc{ 54 tf.RegisterFilterPlugin("TrueFilter", tf.NewTrueFilterPlugin), 55 tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New), 56 tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New), 57 }, 58 extenders: []tf.FakeExtender{ 59 { 60 ExtenderName: "FakeExtender1", 61 Predicates: []tf.FitPredicate{tf.TruePredicateExtender}, 62 }, 63 { 64 ExtenderName: "FakeExtender2", 65 Predicates: []tf.FitPredicate{tf.ErrorPredicateExtender}, 66 }, 67 }, 68 nodes: []string{"node1", "node2"}, 69 expectsErr: true, 70 name: "test 1", 71 }, 72 { 73 registerPlugins: []tf.RegisterPluginFunc{ 74 tf.RegisterFilterPlugin("TrueFilter", tf.NewTrueFilterPlugin), 75 tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New), 76 tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New), 77 }, 78 extenders: []tf.FakeExtender{ 79 { 80 ExtenderName: "FakeExtender1", 81 Predicates: []tf.FitPredicate{tf.TruePredicateExtender}, 82 }, 83 { 84 ExtenderName: "FakeExtender2", 85 Predicates: []tf.FitPredicate{tf.FalsePredicateExtender}, 86 }, 87 }, 88 nodes: []string{"node1", "node2"}, 89 expectsErr: true, 90 name: "test 2", 91 }, 92 { 93 registerPlugins: []tf.RegisterPluginFunc{ 94 tf.RegisterFilterPlugin("TrueFilter", tf.NewTrueFilterPlugin), 95 tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New), 96 tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New), 97 }, 98 extenders: []tf.FakeExtender{ 99 { 100 ExtenderName: "FakeExtender1", 101 Predicates: []tf.FitPredicate{tf.TruePredicateExtender}, 102 }, 103 { 104 ExtenderName: "FakeExtender2", 105 Predicates: []tf.FitPredicate{tf.Node1PredicateExtender}, 106 }, 107 }, 108 nodes: []string{"node1", "node2"}, 109 expectedResult: ScheduleResult{ 110 SuggestedHost: "node1", 111 EvaluatedNodes: 2, 112 FeasibleNodes: 1, 113 }, 114 name: "test 3", 115 }, 116 { 117 registerPlugins: []tf.RegisterPluginFunc{ 118 tf.RegisterFilterPlugin("TrueFilter", tf.NewTrueFilterPlugin), 119 tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New), 120 tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New), 121 }, 122 extenders: []tf.FakeExtender{ 123 { 124 ExtenderName: "FakeExtender1", 125 Predicates: []tf.FitPredicate{tf.Node2PredicateExtender}, 126 }, 127 { 128 ExtenderName: "FakeExtender2", 129 Predicates: []tf.FitPredicate{tf.Node1PredicateExtender}, 130 }, 131 }, 132 nodes: []string{"node1", "node2"}, 133 expectsErr: true, 134 name: "test 4", 135 }, 136 { 137 registerPlugins: []tf.RegisterPluginFunc{ 138 tf.RegisterFilterPlugin("TrueFilter", tf.NewTrueFilterPlugin), 139 tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New), 140 tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New), 141 }, 142 extenders: []tf.FakeExtender{ 143 { 144 ExtenderName: "FakeExtender1", 145 Predicates: []tf.FitPredicate{tf.TruePredicateExtender}, 146 Prioritizers: []tf.PriorityConfig{{Function: tf.ErrorPrioritizerExtender, Weight: 10}}, 147 Weight: 1, 148 }, 149 }, 150 nodes: []string{"node1"}, 151 expectedResult: ScheduleResult{ 152 SuggestedHost: "node1", 153 EvaluatedNodes: 1, 154 FeasibleNodes: 1, 155 }, 156 name: "test 5", 157 }, 158 { 159 registerPlugins: []tf.RegisterPluginFunc{ 160 tf.RegisterFilterPlugin("TrueFilter", tf.NewTrueFilterPlugin), 161 tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New), 162 tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New), 163 }, 164 extenders: []tf.FakeExtender{ 165 { 166 ExtenderName: "FakeExtender1", 167 Predicates: []tf.FitPredicate{tf.TruePredicateExtender}, 168 Prioritizers: []tf.PriorityConfig{{Function: tf.Node1PrioritizerExtender, Weight: 10}}, 169 Weight: 1, 170 }, 171 { 172 ExtenderName: "FakeExtender2", 173 Predicates: []tf.FitPredicate{tf.TruePredicateExtender}, 174 Prioritizers: []tf.PriorityConfig{{Function: tf.Node2PrioritizerExtender, Weight: 10}}, 175 Weight: 5, 176 }, 177 }, 178 nodes: []string{"node1", "node2"}, 179 expectedResult: ScheduleResult{ 180 SuggestedHost: "node2", 181 EvaluatedNodes: 2, 182 FeasibleNodes: 2, 183 }, 184 name: "test 6", 185 }, 186 { 187 registerPlugins: []tf.RegisterPluginFunc{ 188 tf.RegisterFilterPlugin("TrueFilter", tf.NewTrueFilterPlugin), 189 tf.RegisterScorePlugin("Node2Prioritizer", tf.NewNode2PrioritizerPlugin(), 20), 190 tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New), 191 tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New), 192 }, 193 extenders: []tf.FakeExtender{ 194 { 195 ExtenderName: "FakeExtender1", 196 Predicates: []tf.FitPredicate{tf.TruePredicateExtender}, 197 Prioritizers: []tf.PriorityConfig{{Function: tf.Node1PrioritizerExtender, Weight: 10}}, 198 Weight: 1, 199 }, 200 }, 201 nodes: []string{"node1", "node2"}, 202 expectedResult: ScheduleResult{ 203 SuggestedHost: "node2", 204 EvaluatedNodes: 2, 205 FeasibleNodes: 2, 206 }, // node2 has higher score 207 name: "test 7", 208 }, 209 { 210 // Scheduler is expected to not send pod to extender in 211 // Filter/Prioritize phases if the extender is not interested in 212 // the pod. 213 // 214 // If scheduler sends the pod by mistake, the test would fail 215 // because of the errors from errorPredicateExtender and/or 216 // errorPrioritizerExtender. 217 registerPlugins: []tf.RegisterPluginFunc{ 218 tf.RegisterFilterPlugin("TrueFilter", tf.NewTrueFilterPlugin), 219 tf.RegisterScorePlugin("Node2Prioritizer", tf.NewNode2PrioritizerPlugin(), 1), 220 tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New), 221 tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New), 222 }, 223 extenders: []tf.FakeExtender{ 224 { 225 ExtenderName: "FakeExtender1", 226 Predicates: []tf.FitPredicate{tf.ErrorPredicateExtender}, 227 Prioritizers: []tf.PriorityConfig{{Function: tf.ErrorPrioritizerExtender, Weight: 10}}, 228 UnInterested: true, 229 }, 230 }, 231 nodes: []string{"node1", "node2"}, 232 expectsErr: false, 233 expectedResult: ScheduleResult{ 234 SuggestedHost: "node2", 235 EvaluatedNodes: 2, 236 FeasibleNodes: 2, 237 }, // node2 has higher score 238 name: "test 8", 239 }, 240 { 241 // Scheduling is expected to not fail in 242 // Filter/Prioritize phases if the extender is not available and ignorable. 243 // 244 // If scheduler did not ignore the extender, the test would fail 245 // because of the errors from errorPredicateExtender. 246 registerPlugins: []tf.RegisterPluginFunc{ 247 tf.RegisterFilterPlugin("TrueFilter", tf.NewTrueFilterPlugin), 248 tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New), 249 tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New), 250 }, 251 extenders: []tf.FakeExtender{ 252 { 253 ExtenderName: "FakeExtender1", 254 Predicates: []tf.FitPredicate{tf.ErrorPredicateExtender}, 255 Ignorable: true, 256 }, 257 { 258 ExtenderName: "FakeExtender2", 259 Predicates: []tf.FitPredicate{tf.Node1PredicateExtender}, 260 }, 261 }, 262 nodes: []string{"node1", "node2"}, 263 expectsErr: false, 264 expectedResult: ScheduleResult{ 265 SuggestedHost: "node1", 266 EvaluatedNodes: 2, 267 FeasibleNodes: 1, 268 }, 269 name: "test 9", 270 }, 271 } 272 273 for _, test := range tests { 274 t.Run(test.name, func(t *testing.T) { 275 client := clientsetfake.NewSimpleClientset() 276 informerFactory := informers.NewSharedInformerFactory(client, 0) 277 278 var extenders []framework.Extender 279 for ii := range test.extenders { 280 extenders = append(extenders, &test.extenders[ii]) 281 } 282 logger, ctx := ktesting.NewTestContext(t) 283 ctx, cancel := context.WithCancel(ctx) 284 defer cancel() 285 286 cache := internalcache.New(ctx, time.Duration(0)) 287 for _, name := range test.nodes { 288 cache.AddNode(logger, createNode(name)) 289 } 290 fwk, err := tf.NewFramework( 291 ctx, 292 test.registerPlugins, "", 293 runtime.WithClientSet(client), 294 runtime.WithInformerFactory(informerFactory), 295 runtime.WithPodNominator(internalqueue.NewPodNominator(informerFactory.Core().V1().Pods().Lister())), 296 runtime.WithLogger(logger), 297 ) 298 if err != nil { 299 t.Fatal(err) 300 } 301 302 sched := &Scheduler{ 303 Cache: cache, 304 nodeInfoSnapshot: emptySnapshot, 305 percentageOfNodesToScore: schedulerapi.DefaultPercentageOfNodesToScore, 306 Extenders: extenders, 307 logger: logger, 308 } 309 sched.applyDefaultHandlers() 310 311 podIgnored := &v1.Pod{} 312 result, err := sched.SchedulePod(ctx, fwk, framework.NewCycleState(), podIgnored) 313 if test.expectsErr { 314 if err == nil { 315 t.Errorf("Unexpected non-error, result %+v", result) 316 } 317 } else { 318 if err != nil { 319 t.Errorf("Unexpected error: %v", err) 320 return 321 } 322 323 if !reflect.DeepEqual(result, test.expectedResult) { 324 t.Errorf("Expected: %+v, Saw: %+v", test.expectedResult, result) 325 } 326 } 327 }) 328 } 329 } 330 331 func createNode(name string) *v1.Node { 332 return &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: name}} 333 } 334 335 func TestIsInterested(t *testing.T) { 336 mem := &HTTPExtender{ 337 managedResources: sets.New[string](), 338 } 339 mem.managedResources.Insert("memory") 340 341 for _, tc := range []struct { 342 label string 343 extender *HTTPExtender 344 pod *v1.Pod 345 want bool 346 }{ 347 { 348 label: "Empty managed resources", 349 extender: &HTTPExtender{ 350 managedResources: sets.New[string](), 351 }, 352 pod: &v1.Pod{}, 353 want: true, 354 }, 355 { 356 label: "Managed memory, empty resources", 357 extender: mem, 358 pod: st.MakePod().Container("app").Obj(), 359 want: false, 360 }, 361 { 362 label: "Managed memory, container memory with Requests", 363 extender: mem, 364 pod: st.MakePod().Req(map[v1.ResourceName]string{ 365 "memory": "0", 366 }).Obj(), 367 want: true, 368 }, 369 { 370 label: "Managed memory, container memory with Limits", 371 extender: mem, 372 pod: st.MakePod().Lim(map[v1.ResourceName]string{ 373 "memory": "0", 374 }).Obj(), 375 want: true, 376 }, 377 { 378 label: "Managed memory, init container memory", 379 extender: mem, 380 pod: st.MakePod().Container("app").InitReq(map[v1.ResourceName]string{ 381 "memory": "0", 382 }).Obj(), 383 want: true, 384 }, 385 } { 386 t.Run(tc.label, func(t *testing.T) { 387 if got := tc.extender.IsInterested(tc.pod); got != tc.want { 388 t.Fatalf("IsInterested(%v) = %v, wanted %v", tc.pod, got, tc.want) 389 } 390 }) 391 } 392 } 393 394 func TestConvertToMetaVictims(t *testing.T) { 395 tests := []struct { 396 name string 397 nodeNameToVictims map[string]*extenderv1.Victims 398 want map[string]*extenderv1.MetaVictims 399 }{ 400 { 401 name: "test NumPDBViolations is transferred from nodeNameToVictims to nodeNameToMetaVictims", 402 nodeNameToVictims: map[string]*extenderv1.Victims{ 403 "node1": { 404 Pods: []*v1.Pod{ 405 st.MakePod().Name("pod1").UID("uid1").Obj(), 406 st.MakePod().Name("pod3").UID("uid3").Obj(), 407 }, 408 NumPDBViolations: 1, 409 }, 410 "node2": { 411 Pods: []*v1.Pod{ 412 st.MakePod().Name("pod2").UID("uid2").Obj(), 413 st.MakePod().Name("pod4").UID("uid4").Obj(), 414 }, 415 NumPDBViolations: 2, 416 }, 417 }, 418 want: map[string]*extenderv1.MetaVictims{ 419 "node1": { 420 Pods: []*extenderv1.MetaPod{ 421 {UID: "uid1"}, 422 {UID: "uid3"}, 423 }, 424 NumPDBViolations: 1, 425 }, 426 "node2": { 427 Pods: []*extenderv1.MetaPod{ 428 {UID: "uid2"}, 429 {UID: "uid4"}, 430 }, 431 NumPDBViolations: 2, 432 }, 433 }, 434 }, 435 } 436 for _, tt := range tests { 437 t.Run(tt.name, func(t *testing.T) { 438 if got := convertToMetaVictims(tt.nodeNameToVictims); !reflect.DeepEqual(got, tt.want) { 439 t.Errorf("convertToMetaVictims() = %v, want %v", got, tt.want) 440 } 441 }) 442 } 443 } 444 445 func TestConvertToVictims(t *testing.T) { 446 tests := []struct { 447 name string 448 httpExtender *HTTPExtender 449 nodeNameToMetaVictims map[string]*extenderv1.MetaVictims 450 nodeNames []string 451 podsInNodeList []*v1.Pod 452 nodeInfos framework.NodeInfoLister 453 want map[string]*extenderv1.Victims 454 wantErr bool 455 }{ 456 { 457 name: "test NumPDBViolations is transferred from NodeNameToMetaVictims to newNodeNameToVictims", 458 httpExtender: &HTTPExtender{}, 459 nodeNameToMetaVictims: map[string]*extenderv1.MetaVictims{ 460 "node1": { 461 Pods: []*extenderv1.MetaPod{ 462 {UID: "uid1"}, 463 {UID: "uid3"}, 464 }, 465 NumPDBViolations: 1, 466 }, 467 "node2": { 468 Pods: []*extenderv1.MetaPod{ 469 {UID: "uid2"}, 470 {UID: "uid4"}, 471 }, 472 NumPDBViolations: 2, 473 }, 474 }, 475 nodeNames: []string{"node1", "node2"}, 476 podsInNodeList: []*v1.Pod{ 477 st.MakePod().Name("pod1").UID("uid1").Obj(), 478 st.MakePod().Name("pod2").UID("uid2").Obj(), 479 st.MakePod().Name("pod3").UID("uid3").Obj(), 480 st.MakePod().Name("pod4").UID("uid4").Obj(), 481 }, 482 nodeInfos: nil, 483 want: map[string]*extenderv1.Victims{ 484 "node1": { 485 Pods: []*v1.Pod{ 486 st.MakePod().Name("pod1").UID("uid1").Obj(), 487 st.MakePod().Name("pod3").UID("uid3").Obj(), 488 }, 489 NumPDBViolations: 1, 490 }, 491 "node2": { 492 Pods: []*v1.Pod{ 493 st.MakePod().Name("pod2").UID("uid2").Obj(), 494 st.MakePod().Name("pod4").UID("uid4").Obj(), 495 }, 496 NumPDBViolations: 2, 497 }, 498 }, 499 }, 500 } 501 for _, tt := range tests { 502 t.Run(tt.name, func(t *testing.T) { 503 // nodeInfos instantiations 504 nodeInfoList := make([]*framework.NodeInfo, 0, len(tt.nodeNames)) 505 for i, nm := range tt.nodeNames { 506 nodeInfo := framework.NewNodeInfo() 507 node := createNode(nm) 508 nodeInfo.SetNode(node) 509 nodeInfo.AddPod(tt.podsInNodeList[i]) 510 nodeInfo.AddPod(tt.podsInNodeList[i+2]) 511 nodeInfoList = append(nodeInfoList, nodeInfo) 512 } 513 tt.nodeInfos = tf.NodeInfoLister(nodeInfoList) 514 515 got, err := tt.httpExtender.convertToVictims(tt.nodeNameToMetaVictims, tt.nodeInfos) 516 if (err != nil) != tt.wantErr { 517 t.Errorf("convertToVictims() error = %v, wantErr %v", err, tt.wantErr) 518 return 519 } 520 if !reflect.DeepEqual(got, tt.want) { 521 t.Errorf("convertToVictims() got = %v, want %v", got, tt.want) 522 } 523 }) 524 } 525 }