k8s.io/kubernetes@v1.29.3/pkg/scheduler/scheduler_test.go (about) 1 /* 2 Copyright 2014 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 "fmt" 22 "sort" 23 "strings" 24 "testing" 25 "time" 26 27 "github.com/google/go-cmp/cmp" 28 v1 "k8s.io/api/core/v1" 29 apierrors "k8s.io/apimachinery/pkg/api/errors" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/runtime" 32 "k8s.io/apimachinery/pkg/util/sets" 33 utilfeature "k8s.io/apiserver/pkg/util/feature" 34 "k8s.io/client-go/informers" 35 "k8s.io/client-go/kubernetes" 36 "k8s.io/client-go/kubernetes/fake" 37 "k8s.io/client-go/kubernetes/scheme" 38 "k8s.io/client-go/tools/cache" 39 "k8s.io/client-go/tools/events" 40 featuregatetesting "k8s.io/component-base/featuregate/testing" 41 "k8s.io/klog/v2" 42 "k8s.io/klog/v2/ktesting" 43 "k8s.io/kubernetes/pkg/features" 44 schedulerapi "k8s.io/kubernetes/pkg/scheduler/apis/config" 45 "k8s.io/kubernetes/pkg/scheduler/apis/config/testing/defaults" 46 "k8s.io/kubernetes/pkg/scheduler/framework" 47 "k8s.io/kubernetes/pkg/scheduler/framework/plugins" 48 "k8s.io/kubernetes/pkg/scheduler/framework/plugins/defaultbinder" 49 "k8s.io/kubernetes/pkg/scheduler/framework/plugins/queuesort" 50 frameworkruntime "k8s.io/kubernetes/pkg/scheduler/framework/runtime" 51 internalcache "k8s.io/kubernetes/pkg/scheduler/internal/cache" 52 internalqueue "k8s.io/kubernetes/pkg/scheduler/internal/queue" 53 "k8s.io/kubernetes/pkg/scheduler/profile" 54 st "k8s.io/kubernetes/pkg/scheduler/testing" 55 tf "k8s.io/kubernetes/pkg/scheduler/testing/framework" 56 testingclock "k8s.io/utils/clock/testing" 57 "k8s.io/utils/ptr" 58 ) 59 60 func TestSchedulerCreation(t *testing.T) { 61 invalidRegistry := map[string]frameworkruntime.PluginFactory{ 62 defaultbinder.Name: defaultbinder.New, 63 } 64 validRegistry := map[string]frameworkruntime.PluginFactory{ 65 "Foo": defaultbinder.New, 66 } 67 cases := []struct { 68 name string 69 opts []Option 70 wantErr string 71 wantProfiles []string 72 wantExtenders []string 73 }{ 74 { 75 name: "valid out-of-tree registry", 76 opts: []Option{ 77 WithFrameworkOutOfTreeRegistry(validRegistry), 78 WithProfiles( 79 schedulerapi.KubeSchedulerProfile{ 80 SchedulerName: "default-scheduler", 81 Plugins: &schedulerapi.Plugins{ 82 QueueSort: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "PrioritySort"}}}, 83 Bind: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "DefaultBinder"}}}, 84 }, 85 }, 86 )}, 87 wantProfiles: []string{"default-scheduler"}, 88 }, 89 { 90 name: "repeated plugin name in out-of-tree plugin", 91 opts: []Option{ 92 WithFrameworkOutOfTreeRegistry(invalidRegistry), 93 WithProfiles( 94 schedulerapi.KubeSchedulerProfile{ 95 SchedulerName: "default-scheduler", 96 Plugins: &schedulerapi.Plugins{ 97 QueueSort: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "PrioritySort"}}}, 98 Bind: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "DefaultBinder"}}}, 99 }, 100 }, 101 )}, 102 wantProfiles: []string{"default-scheduler"}, 103 wantErr: "a plugin named DefaultBinder already exists", 104 }, 105 { 106 name: "multiple profiles", 107 opts: []Option{ 108 WithProfiles( 109 schedulerapi.KubeSchedulerProfile{ 110 SchedulerName: "foo", 111 Plugins: &schedulerapi.Plugins{ 112 QueueSort: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "PrioritySort"}}}, 113 Bind: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "DefaultBinder"}}}, 114 }, 115 }, 116 schedulerapi.KubeSchedulerProfile{ 117 SchedulerName: "bar", 118 Plugins: &schedulerapi.Plugins{ 119 QueueSort: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "PrioritySort"}}}, 120 Bind: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "DefaultBinder"}}}, 121 }, 122 }, 123 )}, 124 wantProfiles: []string{"bar", "foo"}, 125 }, 126 { 127 name: "Repeated profiles", 128 opts: []Option{ 129 WithProfiles( 130 schedulerapi.KubeSchedulerProfile{ 131 SchedulerName: "foo", 132 Plugins: &schedulerapi.Plugins{ 133 QueueSort: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "PrioritySort"}}}, 134 Bind: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "DefaultBinder"}}}, 135 }, 136 }, 137 schedulerapi.KubeSchedulerProfile{ 138 SchedulerName: "bar", 139 Plugins: &schedulerapi.Plugins{ 140 QueueSort: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "PrioritySort"}}}, 141 Bind: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "DefaultBinder"}}}, 142 }, 143 }, 144 schedulerapi.KubeSchedulerProfile{ 145 SchedulerName: "foo", 146 Plugins: &schedulerapi.Plugins{ 147 QueueSort: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "PrioritySort"}}}, 148 Bind: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "DefaultBinder"}}}, 149 }, 150 }, 151 )}, 152 wantErr: "duplicate profile with scheduler name \"foo\"", 153 }, 154 { 155 name: "With extenders", 156 opts: []Option{ 157 WithProfiles( 158 schedulerapi.KubeSchedulerProfile{ 159 SchedulerName: "default-scheduler", 160 Plugins: &schedulerapi.Plugins{ 161 QueueSort: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "PrioritySort"}}}, 162 Bind: schedulerapi.PluginSet{Enabled: []schedulerapi.Plugin{{Name: "DefaultBinder"}}}, 163 }, 164 }, 165 ), 166 WithExtenders( 167 schedulerapi.Extender{ 168 URLPrefix: "http://extender.kube-system/", 169 }, 170 ), 171 }, 172 wantProfiles: []string{"default-scheduler"}, 173 wantExtenders: []string{"http://extender.kube-system/"}, 174 }, 175 } 176 177 for _, tc := range cases { 178 t.Run(tc.name, func(t *testing.T) { 179 client := fake.NewSimpleClientset() 180 informerFactory := informers.NewSharedInformerFactory(client, 0) 181 182 eventBroadcaster := events.NewBroadcaster(&events.EventSinkImpl{Interface: client.EventsV1()}) 183 184 _, ctx := ktesting.NewTestContext(t) 185 ctx, cancel := context.WithCancel(ctx) 186 defer cancel() 187 s, err := New( 188 ctx, 189 client, 190 informerFactory, 191 nil, 192 profile.NewRecorderFactory(eventBroadcaster), 193 tc.opts..., 194 ) 195 196 // Errors 197 if len(tc.wantErr) != 0 { 198 if err == nil || !strings.Contains(err.Error(), tc.wantErr) { 199 t.Errorf("got error %q, want %q", err, tc.wantErr) 200 } 201 return 202 } 203 if err != nil { 204 t.Fatalf("Failed to create scheduler: %v", err) 205 } 206 207 // Profiles 208 profiles := make([]string, 0, len(s.Profiles)) 209 for name := range s.Profiles { 210 profiles = append(profiles, name) 211 } 212 sort.Strings(profiles) 213 if diff := cmp.Diff(tc.wantProfiles, profiles); diff != "" { 214 t.Errorf("unexpected profiles (-want, +got):\n%s", diff) 215 } 216 217 // Extenders 218 if len(tc.wantExtenders) != 0 { 219 // Scheduler.Extenders 220 extenders := make([]string, 0, len(s.Extenders)) 221 for _, e := range s.Extenders { 222 extenders = append(extenders, e.Name()) 223 } 224 if diff := cmp.Diff(tc.wantExtenders, extenders); diff != "" { 225 t.Errorf("unexpected extenders (-want, +got):\n%s", diff) 226 } 227 228 // framework.Handle.Extenders() 229 for _, p := range s.Profiles { 230 extenders := make([]string, 0, len(p.Extenders())) 231 for _, e := range p.Extenders() { 232 extenders = append(extenders, e.Name()) 233 } 234 if diff := cmp.Diff(tc.wantExtenders, extenders); diff != "" { 235 t.Errorf("unexpected extenders (-want, +got):\n%s", diff) 236 } 237 } 238 } 239 }) 240 } 241 } 242 243 func TestFailureHandler(t *testing.T) { 244 testPod := st.MakePod().Name("test-pod").Namespace(v1.NamespaceDefault).Obj() 245 testPodUpdated := testPod.DeepCopy() 246 testPodUpdated.Labels = map[string]string{"foo": ""} 247 248 tests := []struct { 249 name string 250 podUpdatedDuringScheduling bool // pod is updated during a scheduling cycle 251 podDeletedDuringScheduling bool // pod is deleted during a scheduling cycle 252 expect *v1.Pod 253 }{ 254 { 255 name: "pod is updated during a scheduling cycle", 256 podUpdatedDuringScheduling: true, 257 expect: testPodUpdated, 258 }, 259 { 260 name: "pod is not updated during a scheduling cycle", 261 expect: testPod, 262 }, 263 { 264 name: "pod is deleted during a scheduling cycle", 265 podDeletedDuringScheduling: true, 266 expect: nil, 267 }, 268 } 269 270 for _, tt := range tests { 271 t.Run(tt.name, func(t *testing.T) { 272 logger, ctx := ktesting.NewTestContext(t) 273 ctx, cancel := context.WithCancel(ctx) 274 defer cancel() 275 276 client := fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testPod}}) 277 informerFactory := informers.NewSharedInformerFactory(client, 0) 278 podInformer := informerFactory.Core().V1().Pods() 279 // Need to add/update/delete testPod to the store. 280 podInformer.Informer().GetStore().Add(testPod) 281 282 queue := internalqueue.NewPriorityQueue(nil, informerFactory, internalqueue.WithClock(testingclock.NewFakeClock(time.Now()))) 283 schedulerCache := internalcache.New(ctx, 30*time.Second) 284 285 if err := queue.Add(logger, testPod); err != nil { 286 t.Fatalf("Add failed: %v", err) 287 } 288 289 if _, err := queue.Pop(logger); err != nil { 290 t.Fatalf("Pop failed: %v", err) 291 } 292 293 if tt.podUpdatedDuringScheduling { 294 podInformer.Informer().GetStore().Update(testPodUpdated) 295 queue.Update(logger, testPod, testPodUpdated) 296 } 297 if tt.podDeletedDuringScheduling { 298 podInformer.Informer().GetStore().Delete(testPod) 299 queue.Delete(testPod) 300 } 301 302 s, fwk, err := initScheduler(ctx, schedulerCache, queue, client, informerFactory) 303 if err != nil { 304 t.Fatal(err) 305 } 306 307 testPodInfo := &framework.QueuedPodInfo{PodInfo: mustNewPodInfo(t, testPod)} 308 s.FailureHandler(ctx, fwk, testPodInfo, framework.NewStatus(framework.Unschedulable), nil, time.Now()) 309 310 var got *v1.Pod 311 if tt.podUpdatedDuringScheduling { 312 head, e := queue.Pop(logger) 313 if e != nil { 314 t.Fatalf("Cannot pop pod from the activeQ: %v", e) 315 } 316 got = head.Pod 317 } else { 318 got = getPodFromPriorityQueue(queue, testPod) 319 } 320 321 if diff := cmp.Diff(tt.expect, got); diff != "" { 322 t.Errorf("Unexpected pod (-want, +got): %s", diff) 323 } 324 }) 325 } 326 } 327 328 func TestFailureHandler_NodeNotFound(t *testing.T) { 329 nodeFoo := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} 330 nodeBar := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "bar"}} 331 testPod := st.MakePod().Name("test-pod").Namespace(v1.NamespaceDefault).Obj() 332 tests := []struct { 333 name string 334 nodes []v1.Node 335 nodeNameToDelete string 336 injectErr error 337 expectNodeNames sets.Set[string] 338 }{ 339 { 340 name: "node is deleted during a scheduling cycle", 341 nodes: []v1.Node{*nodeFoo, *nodeBar}, 342 nodeNameToDelete: "foo", 343 injectErr: apierrors.NewNotFound(v1.Resource("node"), nodeFoo.Name), 344 expectNodeNames: sets.New("bar"), 345 }, 346 { 347 name: "node is not deleted but NodeNotFound is received incorrectly", 348 nodes: []v1.Node{*nodeFoo, *nodeBar}, 349 injectErr: apierrors.NewNotFound(v1.Resource("node"), nodeFoo.Name), 350 expectNodeNames: sets.New("foo", "bar"), 351 }, 352 } 353 354 for _, tt := range tests { 355 t.Run(tt.name, func(t *testing.T) { 356 logger, ctx := ktesting.NewTestContext(t) 357 ctx, cancel := context.WithCancel(ctx) 358 defer cancel() 359 360 client := fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testPod}}, &v1.NodeList{Items: tt.nodes}) 361 informerFactory := informers.NewSharedInformerFactory(client, 0) 362 podInformer := informerFactory.Core().V1().Pods() 363 // Need to add testPod to the store. 364 podInformer.Informer().GetStore().Add(testPod) 365 366 queue := internalqueue.NewPriorityQueue(nil, informerFactory, internalqueue.WithClock(testingclock.NewFakeClock(time.Now()))) 367 schedulerCache := internalcache.New(ctx, 30*time.Second) 368 369 for i := range tt.nodes { 370 node := tt.nodes[i] 371 // Add node to schedulerCache no matter it's deleted in API server or not. 372 schedulerCache.AddNode(logger, &node) 373 if node.Name == tt.nodeNameToDelete { 374 client.CoreV1().Nodes().Delete(ctx, node.Name, metav1.DeleteOptions{}) 375 } 376 } 377 378 s, fwk, err := initScheduler(ctx, schedulerCache, queue, client, informerFactory) 379 if err != nil { 380 t.Fatal(err) 381 } 382 383 testPodInfo := &framework.QueuedPodInfo{PodInfo: mustNewPodInfo(t, testPod)} 384 s.FailureHandler(ctx, fwk, testPodInfo, framework.NewStatus(framework.Unschedulable).WithError(tt.injectErr), nil, time.Now()) 385 386 gotNodes := schedulerCache.Dump().Nodes 387 gotNodeNames := sets.New[string]() 388 for _, nodeInfo := range gotNodes { 389 gotNodeNames.Insert(nodeInfo.Node().Name) 390 } 391 if diff := cmp.Diff(tt.expectNodeNames, gotNodeNames); diff != "" { 392 t.Errorf("Unexpected nodes (-want, +got): %s", diff) 393 } 394 }) 395 } 396 } 397 398 func TestFailureHandler_PodAlreadyBound(t *testing.T) { 399 logger, ctx := ktesting.NewTestContext(t) 400 ctx, cancel := context.WithCancel(ctx) 401 defer cancel() 402 403 nodeFoo := v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} 404 testPod := st.MakePod().Name("test-pod").Namespace(v1.NamespaceDefault).Node("foo").Obj() 405 406 client := fake.NewSimpleClientset(&v1.PodList{Items: []v1.Pod{*testPod}}, &v1.NodeList{Items: []v1.Node{nodeFoo}}) 407 informerFactory := informers.NewSharedInformerFactory(client, 0) 408 podInformer := informerFactory.Core().V1().Pods() 409 // Need to add testPod to the store. 410 podInformer.Informer().GetStore().Add(testPod) 411 412 queue := internalqueue.NewPriorityQueue(nil, informerFactory, internalqueue.WithClock(testingclock.NewFakeClock(time.Now()))) 413 schedulerCache := internalcache.New(ctx, 30*time.Second) 414 415 // Add node to schedulerCache no matter it's deleted in API server or not. 416 schedulerCache.AddNode(logger, &nodeFoo) 417 418 s, fwk, err := initScheduler(ctx, schedulerCache, queue, client, informerFactory) 419 if err != nil { 420 t.Fatal(err) 421 } 422 423 testPodInfo := &framework.QueuedPodInfo{PodInfo: mustNewPodInfo(t, testPod)} 424 s.FailureHandler(ctx, fwk, testPodInfo, framework.NewStatus(framework.Unschedulable).WithError(fmt.Errorf("binding rejected: timeout")), nil, time.Now()) 425 426 pod := getPodFromPriorityQueue(queue, testPod) 427 if pod != nil { 428 t.Fatalf("Unexpected pod: %v should not be in PriorityQueue when the NodeName of pod is not empty", pod.Name) 429 } 430 } 431 432 // TestWithPercentageOfNodesToScore tests scheduler's PercentageOfNodesToScore is set correctly. 433 func TestWithPercentageOfNodesToScore(t *testing.T) { 434 tests := []struct { 435 name string 436 percentageOfNodesToScoreConfig *int32 437 wantedPercentageOfNodesToScore int32 438 }{ 439 { 440 name: "percentageOfNodesScore is nil", 441 percentageOfNodesToScoreConfig: nil, 442 wantedPercentageOfNodesToScore: schedulerapi.DefaultPercentageOfNodesToScore, 443 }, 444 { 445 name: "percentageOfNodesScore is not nil", 446 percentageOfNodesToScoreConfig: ptr.To[int32](10), 447 wantedPercentageOfNodesToScore: 10, 448 }, 449 } 450 451 for _, tt := range tests { 452 t.Run(tt.name, func(t *testing.T) { 453 client := fake.NewSimpleClientset() 454 informerFactory := informers.NewSharedInformerFactory(client, 0) 455 eventBroadcaster := events.NewBroadcaster(&events.EventSinkImpl{Interface: client.EventsV1()}) 456 _, ctx := ktesting.NewTestContext(t) 457 ctx, cancel := context.WithCancel(ctx) 458 defer cancel() 459 sched, err := New( 460 ctx, 461 client, 462 informerFactory, 463 nil, 464 profile.NewRecorderFactory(eventBroadcaster), 465 WithPercentageOfNodesToScore(tt.percentageOfNodesToScoreConfig), 466 ) 467 if err != nil { 468 t.Fatalf("Failed to create scheduler: %v", err) 469 } 470 if sched.percentageOfNodesToScore != tt.wantedPercentageOfNodesToScore { 471 t.Errorf("scheduler.percercentageOfNodesToScore = %v, want %v", sched.percentageOfNodesToScore, tt.wantedPercentageOfNodesToScore) 472 } 473 }) 474 } 475 } 476 477 // getPodFromPriorityQueue is the function used in the TestDefaultErrorFunc test to get 478 // the specific pod from the given priority queue. It returns the found pod in the priority queue. 479 func getPodFromPriorityQueue(queue *internalqueue.PriorityQueue, pod *v1.Pod) *v1.Pod { 480 podList, _ := queue.PendingPods() 481 if len(podList) == 0 { 482 return nil 483 } 484 485 queryPodKey, err := cache.MetaNamespaceKeyFunc(pod) 486 if err != nil { 487 return nil 488 } 489 490 for _, foundPod := range podList { 491 foundPodKey, err := cache.MetaNamespaceKeyFunc(foundPod) 492 if err != nil { 493 return nil 494 } 495 496 if foundPodKey == queryPodKey { 497 return foundPod 498 } 499 } 500 501 return nil 502 } 503 504 func initScheduler(ctx context.Context, cache internalcache.Cache, queue internalqueue.SchedulingQueue, 505 client kubernetes.Interface, informerFactory informers.SharedInformerFactory) (*Scheduler, framework.Framework, error) { 506 logger := klog.FromContext(ctx) 507 registerPluginFuncs := []tf.RegisterPluginFunc{ 508 tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New), 509 tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New), 510 } 511 eventBroadcaster := events.NewBroadcaster(&events.EventSinkImpl{Interface: client.EventsV1()}) 512 fwk, err := tf.NewFramework(ctx, 513 registerPluginFuncs, 514 testSchedulerName, 515 frameworkruntime.WithClientSet(client), 516 frameworkruntime.WithInformerFactory(informerFactory), 517 frameworkruntime.WithEventRecorder(eventBroadcaster.NewRecorder(scheme.Scheme, testSchedulerName)), 518 ) 519 if err != nil { 520 return nil, nil, err 521 } 522 523 s := &Scheduler{ 524 Cache: cache, 525 client: client, 526 StopEverything: ctx.Done(), 527 SchedulingQueue: queue, 528 Profiles: profile.Map{testSchedulerName: fwk}, 529 logger: logger, 530 } 531 s.applyDefaultHandlers() 532 533 return s, fwk, nil 534 } 535 536 func TestInitPluginsWithIndexers(t *testing.T) { 537 tests := []struct { 538 name string 539 // the plugin registration ordering must not matter, being map traversal random 540 entrypoints map[string]frameworkruntime.PluginFactory 541 wantErr string 542 }{ 543 { 544 name: "register indexer, no conflicts", 545 entrypoints: map[string]frameworkruntime.PluginFactory{ 546 "AddIndexer": func(ctx context.Context, obj runtime.Object, handle framework.Handle) (framework.Plugin, error) { 547 podInformer := handle.SharedInformerFactory().Core().V1().Pods() 548 err := podInformer.Informer().AddIndexers(cache.Indexers{ 549 "nodeName": indexByPodSpecNodeName, 550 }) 551 return &TestPlugin{name: "AddIndexer"}, err 552 }, 553 }, 554 }, 555 { 556 name: "register the same indexer name multiple times, conflict", 557 // order of registration doesn't matter 558 entrypoints: map[string]frameworkruntime.PluginFactory{ 559 "AddIndexer1": func(ctx context.Context, obj runtime.Object, handle framework.Handle) (framework.Plugin, error) { 560 podInformer := handle.SharedInformerFactory().Core().V1().Pods() 561 err := podInformer.Informer().AddIndexers(cache.Indexers{ 562 "nodeName": indexByPodSpecNodeName, 563 }) 564 return &TestPlugin{name: "AddIndexer1"}, err 565 }, 566 "AddIndexer2": func(ctx context.Context, obj runtime.Object, handle framework.Handle) (framework.Plugin, error) { 567 podInformer := handle.SharedInformerFactory().Core().V1().Pods() 568 err := podInformer.Informer().AddIndexers(cache.Indexers{ 569 "nodeName": indexByPodAnnotationNodeName, 570 }) 571 return &TestPlugin{name: "AddIndexer1"}, err 572 }, 573 }, 574 wantErr: "indexer conflict", 575 }, 576 { 577 name: "register the same indexer body with different names, no conflicts", 578 // order of registration doesn't matter 579 entrypoints: map[string]frameworkruntime.PluginFactory{ 580 "AddIndexer1": func(ctx context.Context, obj runtime.Object, handle framework.Handle) (framework.Plugin, error) { 581 podInformer := handle.SharedInformerFactory().Core().V1().Pods() 582 err := podInformer.Informer().AddIndexers(cache.Indexers{ 583 "nodeName1": indexByPodSpecNodeName, 584 }) 585 return &TestPlugin{name: "AddIndexer1"}, err 586 }, 587 "AddIndexer2": func(ctx context.Context, obj runtime.Object, handle framework.Handle) (framework.Plugin, error) { 588 podInformer := handle.SharedInformerFactory().Core().V1().Pods() 589 err := podInformer.Informer().AddIndexers(cache.Indexers{ 590 "nodeName2": indexByPodAnnotationNodeName, 591 }) 592 return &TestPlugin{name: "AddIndexer2"}, err 593 }, 594 }, 595 }, 596 } 597 598 for _, tt := range tests { 599 t.Run(tt.name, func(t *testing.T) { 600 fakeInformerFactory := NewInformerFactory(&fake.Clientset{}, 0*time.Second) 601 602 var registerPluginFuncs []tf.RegisterPluginFunc 603 for name, entrypoint := range tt.entrypoints { 604 registerPluginFuncs = append(registerPluginFuncs, 605 // anything supported by TestPlugin is fine 606 tf.RegisterFilterPlugin(name, entrypoint), 607 ) 608 } 609 // we always need this 610 registerPluginFuncs = append(registerPluginFuncs, 611 tf.RegisterQueueSortPlugin(queuesort.Name, queuesort.New), 612 tf.RegisterBindPlugin(defaultbinder.Name, defaultbinder.New), 613 ) 614 _, ctx := ktesting.NewTestContext(t) 615 ctx, cancel := context.WithCancel(ctx) 616 defer cancel() 617 _, err := tf.NewFramework(ctx, registerPluginFuncs, "test", frameworkruntime.WithInformerFactory(fakeInformerFactory)) 618 619 if len(tt.wantErr) > 0 { 620 if err == nil || !strings.Contains(err.Error(), tt.wantErr) { 621 t.Errorf("got error %q, want %q", err, tt.wantErr) 622 } 623 return 624 } 625 if err != nil { 626 t.Fatalf("Failed to create scheduler: %v", err) 627 } 628 }) 629 } 630 } 631 632 func indexByPodSpecNodeName(obj interface{}) ([]string, error) { 633 pod, ok := obj.(*v1.Pod) 634 if !ok { 635 return []string{}, nil 636 } 637 if len(pod.Spec.NodeName) == 0 { 638 return []string{}, nil 639 } 640 return []string{pod.Spec.NodeName}, nil 641 } 642 643 func indexByPodAnnotationNodeName(obj interface{}) ([]string, error) { 644 pod, ok := obj.(*v1.Pod) 645 if !ok { 646 return []string{}, nil 647 } 648 if len(pod.Annotations) == 0 { 649 return []string{}, nil 650 } 651 nodeName, ok := pod.Annotations["node-name"] 652 if !ok { 653 return []string{}, nil 654 } 655 return []string{nodeName}, nil 656 } 657 658 const ( 659 filterWithoutEnqueueExtensions = "filterWithoutEnqueueExtensions" 660 fakeNode = "fakeNode" 661 fakePod = "fakePod" 662 emptyEventsToRegister = "emptyEventsToRegister" 663 queueSort = "no-op-queue-sort-plugin" 664 fakeBind = "bind-plugin" 665 emptyEventExtensions = "emptyEventExtensions" 666 ) 667 668 func Test_buildQueueingHintMap(t *testing.T) { 669 tests := []struct { 670 name string 671 plugins []framework.Plugin 672 want map[framework.ClusterEvent][]*internalqueue.QueueingHintFunction 673 featuregateDisabled bool 674 }{ 675 { 676 name: "filter without EnqueueExtensions plugin", 677 plugins: []framework.Plugin{&filterWithoutEnqueueExtensionsPlugin{}}, 678 want: map[framework.ClusterEvent][]*internalqueue.QueueingHintFunction{ 679 {Resource: framework.Pod, ActionType: framework.All}: { 680 {PluginName: filterWithoutEnqueueExtensions, QueueingHintFn: defaultQueueingHintFn}, 681 }, 682 {Resource: framework.Node, ActionType: framework.All}: { 683 {PluginName: filterWithoutEnqueueExtensions, QueueingHintFn: defaultQueueingHintFn}, 684 }, 685 {Resource: framework.CSINode, ActionType: framework.All}: { 686 {PluginName: filterWithoutEnqueueExtensions, QueueingHintFn: defaultQueueingHintFn}, 687 }, 688 {Resource: framework.CSIDriver, ActionType: framework.All}: { 689 {PluginName: filterWithoutEnqueueExtensions, QueueingHintFn: defaultQueueingHintFn}, 690 }, 691 {Resource: framework.CSIStorageCapacity, ActionType: framework.All}: { 692 {PluginName: filterWithoutEnqueueExtensions, QueueingHintFn: defaultQueueingHintFn}, 693 }, 694 {Resource: framework.PersistentVolume, ActionType: framework.All}: { 695 {PluginName: filterWithoutEnqueueExtensions, QueueingHintFn: defaultQueueingHintFn}, 696 }, 697 {Resource: framework.StorageClass, ActionType: framework.All}: { 698 {PluginName: filterWithoutEnqueueExtensions, QueueingHintFn: defaultQueueingHintFn}, 699 }, 700 {Resource: framework.PersistentVolumeClaim, ActionType: framework.All}: { 701 {PluginName: filterWithoutEnqueueExtensions, QueueingHintFn: defaultQueueingHintFn}, 702 }, 703 {Resource: framework.PodSchedulingContext, ActionType: framework.All}: { 704 {PluginName: filterWithoutEnqueueExtensions, QueueingHintFn: defaultQueueingHintFn}, 705 }, 706 }, 707 }, 708 { 709 name: "node and pod plugin", 710 plugins: []framework.Plugin{&fakeNodePlugin{}, &fakePodPlugin{}}, 711 want: map[framework.ClusterEvent][]*internalqueue.QueueingHintFunction{ 712 {Resource: framework.Pod, ActionType: framework.Add}: { 713 {PluginName: fakePod, QueueingHintFn: fakePodPluginQueueingFn}, 714 }, 715 {Resource: framework.Node, ActionType: framework.Add}: { 716 {PluginName: fakeNode, QueueingHintFn: fakeNodePluginQueueingFn}, 717 }, 718 }, 719 }, 720 { 721 name: "node and pod plugin (featuregate is disabled)", 722 plugins: []framework.Plugin{&fakeNodePlugin{}, &fakePodPlugin{}}, 723 featuregateDisabled: true, 724 want: map[framework.ClusterEvent][]*internalqueue.QueueingHintFunction{ 725 {Resource: framework.Pod, ActionType: framework.Add}: { 726 {PluginName: fakePod, QueueingHintFn: defaultQueueingHintFn}, // default queueing hint due to disabled feature gate. 727 }, 728 {Resource: framework.Node, ActionType: framework.Add}: { 729 {PluginName: fakeNode, QueueingHintFn: defaultQueueingHintFn}, // default queueing hint due to disabled feature gate. 730 }, 731 }, 732 }, 733 { 734 name: "register plugin with empty event", 735 plugins: []framework.Plugin{&emptyEventPlugin{}}, 736 want: map[framework.ClusterEvent][]*internalqueue.QueueingHintFunction{}, 737 }, 738 { 739 name: "register plugins including emptyEventPlugin", 740 plugins: []framework.Plugin{&emptyEventPlugin{}, &fakeNodePlugin{}}, 741 want: map[framework.ClusterEvent][]*internalqueue.QueueingHintFunction{ 742 {Resource: framework.Pod, ActionType: framework.Add}: { 743 {PluginName: fakePod, QueueingHintFn: fakePodPluginQueueingFn}, 744 }, 745 {Resource: framework.Node, ActionType: framework.Add}: { 746 {PluginName: fakeNode, QueueingHintFn: fakeNodePluginQueueingFn}, 747 }, 748 }, 749 }, 750 } 751 752 for _, tt := range tests { 753 t.Run(tt.name, func(t *testing.T) { 754 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SchedulerQueueingHints, !tt.featuregateDisabled)() 755 logger, ctx := ktesting.NewTestContext(t) 756 ctx, cancel := context.WithCancel(ctx) 757 defer cancel() 758 registry := frameworkruntime.Registry{} 759 cfgPls := &schedulerapi.Plugins{} 760 plugins := append(tt.plugins, &fakebindPlugin{}, &fakeQueueSortPlugin{}) 761 for _, pl := range plugins { 762 tmpPl := pl 763 if err := registry.Register(pl.Name(), func(_ context.Context, _ runtime.Object, _ framework.Handle) (framework.Plugin, error) { 764 return tmpPl, nil 765 }); err != nil { 766 t.Fatalf("fail to register filter plugin (%s)", pl.Name()) 767 } 768 cfgPls.MultiPoint.Enabled = append(cfgPls.MultiPoint.Enabled, schedulerapi.Plugin{Name: pl.Name()}) 769 } 770 771 profile := schedulerapi.KubeSchedulerProfile{Plugins: cfgPls} 772 fwk, err := newFramework(ctx, registry, profile) 773 if err != nil { 774 t.Fatal(err) 775 } 776 777 exts := fwk.EnqueueExtensions() 778 // need to sort to make the test result stable. 779 sort.Slice(exts, func(i, j int) bool { 780 return exts[i].Name() < exts[j].Name() 781 }) 782 783 got := buildQueueingHintMap(exts) 784 785 for e, fns := range got { 786 wantfns, ok := tt.want[e] 787 if !ok { 788 t.Errorf("got unexpected event %v", e) 789 continue 790 } 791 if len(fns) != len(wantfns) { 792 t.Errorf("got %v queueing hint functions, want %v", len(fns), len(wantfns)) 793 continue 794 } 795 for i, fn := range fns { 796 if fn.PluginName != wantfns[i].PluginName { 797 t.Errorf("got plugin name %v, want %v", fn.PluginName, wantfns[i].PluginName) 798 continue 799 } 800 got, gotErr := fn.QueueingHintFn(logger, nil, nil, nil) 801 want, wantErr := wantfns[i].QueueingHintFn(logger, nil, nil, nil) 802 if got != want || gotErr != wantErr { 803 t.Errorf("got queueing hint function (%v) returning (%v, %v), expect it to return (%v, %v)", fn.PluginName, got, gotErr, want, wantErr) 804 continue 805 } 806 } 807 } 808 }) 809 } 810 } 811 812 // Test_UnionedGVKs tests UnionedGVKs worked with buildQueueingHintMap. 813 func Test_UnionedGVKs(t *testing.T) { 814 tests := []struct { 815 name string 816 plugins schedulerapi.PluginSet 817 want map[framework.GVK]framework.ActionType 818 }{ 819 { 820 name: "filter without EnqueueExtensions plugin", 821 plugins: schedulerapi.PluginSet{ 822 Enabled: []schedulerapi.Plugin{ 823 {Name: filterWithoutEnqueueExtensions}, 824 {Name: queueSort}, 825 {Name: fakeBind}, 826 }, 827 Disabled: []schedulerapi.Plugin{{Name: "*"}}, // disable default plugins 828 }, 829 want: map[framework.GVK]framework.ActionType{ 830 framework.Pod: framework.All, 831 framework.Node: framework.All, 832 framework.CSINode: framework.All, 833 framework.CSIDriver: framework.All, 834 framework.CSIStorageCapacity: framework.All, 835 framework.PersistentVolume: framework.All, 836 framework.PersistentVolumeClaim: framework.All, 837 framework.StorageClass: framework.All, 838 framework.PodSchedulingContext: framework.All, 839 }, 840 }, 841 { 842 name: "node plugin", 843 plugins: schedulerapi.PluginSet{ 844 Enabled: []schedulerapi.Plugin{ 845 {Name: fakeNode}, 846 {Name: queueSort}, 847 {Name: fakeBind}, 848 }, 849 Disabled: []schedulerapi.Plugin{{Name: "*"}}, // disable default plugins 850 }, 851 want: map[framework.GVK]framework.ActionType{ 852 framework.Node: framework.Add, 853 }, 854 }, 855 { 856 name: "pod plugin", 857 plugins: schedulerapi.PluginSet{ 858 Enabled: []schedulerapi.Plugin{ 859 {Name: fakePod}, 860 {Name: queueSort}, 861 {Name: fakeBind}, 862 }, 863 Disabled: []schedulerapi.Plugin{{Name: "*"}}, // disable default plugins 864 }, 865 want: map[framework.GVK]framework.ActionType{ 866 framework.Pod: framework.Add, 867 }, 868 }, 869 { 870 name: "node and pod plugin", 871 plugins: schedulerapi.PluginSet{ 872 Enabled: []schedulerapi.Plugin{ 873 {Name: fakePod}, 874 {Name: fakeNode}, 875 {Name: queueSort}, 876 {Name: fakeBind}, 877 }, 878 Disabled: []schedulerapi.Plugin{{Name: "*"}}, // disable default plugins 879 }, 880 want: map[framework.GVK]framework.ActionType{ 881 framework.Pod: framework.Add, 882 framework.Node: framework.Add, 883 }, 884 }, 885 { 886 name: "empty EventsToRegister plugin", 887 plugins: schedulerapi.PluginSet{ 888 Enabled: []schedulerapi.Plugin{ 889 {Name: emptyEventsToRegister}, 890 {Name: queueSort}, 891 {Name: fakeBind}, 892 }, 893 Disabled: []schedulerapi.Plugin{{Name: "*"}}, // disable default plugins 894 }, 895 want: map[framework.GVK]framework.ActionType{}, 896 }, 897 { 898 name: "plugins with default profile", 899 plugins: schedulerapi.PluginSet{Enabled: defaults.PluginsV1.MultiPoint.Enabled}, 900 want: map[framework.GVK]framework.ActionType{ 901 framework.Pod: framework.All, 902 framework.Node: framework.All, 903 framework.CSINode: framework.All - framework.Delete, 904 framework.CSIDriver: framework.All - framework.Delete, 905 framework.CSIStorageCapacity: framework.All - framework.Delete, 906 framework.PersistentVolume: framework.All - framework.Delete, 907 framework.PersistentVolumeClaim: framework.All - framework.Delete, 908 framework.StorageClass: framework.All - framework.Delete, 909 }, 910 }, 911 } 912 for _, tt := range tests { 913 t.Run(tt.name, func(t *testing.T) { 914 _, ctx := ktesting.NewTestContext(t) 915 ctx, cancel := context.WithCancel(ctx) 916 defer cancel() 917 registry := plugins.NewInTreeRegistry() 918 919 cfgPls := &schedulerapi.Plugins{MultiPoint: tt.plugins} 920 plugins := []framework.Plugin{&fakeNodePlugin{}, &fakePodPlugin{}, &filterWithoutEnqueueExtensionsPlugin{}, &emptyEventsToRegisterPlugin{}, &fakeQueueSortPlugin{}, &fakebindPlugin{}} 921 for _, pl := range plugins { 922 tmpPl := pl 923 if err := registry.Register(pl.Name(), func(_ context.Context, _ runtime.Object, _ framework.Handle) (framework.Plugin, error) { 924 return tmpPl, nil 925 }); err != nil { 926 t.Fatalf("fail to register filter plugin (%s)", pl.Name()) 927 } 928 } 929 930 profile := schedulerapi.KubeSchedulerProfile{Plugins: cfgPls, PluginConfig: defaults.PluginConfigsV1} 931 fwk, err := newFramework(ctx, registry, profile) 932 if err != nil { 933 t.Fatal(err) 934 } 935 936 queueingHintsPerProfile := internalqueue.QueueingHintMapPerProfile{ 937 "default": buildQueueingHintMap(fwk.EnqueueExtensions()), 938 } 939 got := unionedGVKs(queueingHintsPerProfile) 940 941 if diff := cmp.Diff(tt.want, got); diff != "" { 942 t.Errorf("Unexpected eventToPlugin map (-want,+got):%s", diff) 943 } 944 }) 945 } 946 } 947 948 func newFramework(ctx context.Context, r frameworkruntime.Registry, profile schedulerapi.KubeSchedulerProfile) (framework.Framework, error) { 949 return frameworkruntime.NewFramework(ctx, r, &profile, 950 frameworkruntime.WithSnapshotSharedLister(internalcache.NewSnapshot(nil, nil)), 951 frameworkruntime.WithInformerFactory(informers.NewSharedInformerFactory(fake.NewSimpleClientset(), 0)), 952 ) 953 } 954 955 var _ framework.QueueSortPlugin = &fakeQueueSortPlugin{} 956 957 // fakeQueueSortPlugin is a no-op implementation for QueueSort extension point. 958 type fakeQueueSortPlugin struct{} 959 960 func (pl *fakeQueueSortPlugin) Name() string { 961 return queueSort 962 } 963 964 func (pl *fakeQueueSortPlugin) Less(_, _ *framework.QueuedPodInfo) bool { 965 return false 966 } 967 968 var _ framework.BindPlugin = &fakebindPlugin{} 969 970 // fakebindPlugin is a no-op implementation for Bind extension point. 971 type fakebindPlugin struct{} 972 973 func (t *fakebindPlugin) Name() string { 974 return fakeBind 975 } 976 977 func (t *fakebindPlugin) Bind(ctx context.Context, state *framework.CycleState, p *v1.Pod, nodeName string) *framework.Status { 978 return nil 979 } 980 981 // filterWithoutEnqueueExtensionsPlugin implements Filter, but doesn't implement EnqueueExtensions. 982 type filterWithoutEnqueueExtensionsPlugin struct{} 983 984 func (*filterWithoutEnqueueExtensionsPlugin) Name() string { return filterWithoutEnqueueExtensions } 985 986 func (*filterWithoutEnqueueExtensionsPlugin) Filter(_ context.Context, _ *framework.CycleState, _ *v1.Pod, _ *framework.NodeInfo) *framework.Status { 987 return nil 988 } 989 990 var hintFromFakeNode = framework.QueueingHint(100) 991 992 type fakeNodePlugin struct{} 993 994 var fakeNodePluginQueueingFn = func(_ klog.Logger, _ *v1.Pod, _, _ interface{}) (framework.QueueingHint, error) { 995 return hintFromFakeNode, nil 996 } 997 998 func (*fakeNodePlugin) Name() string { return fakeNode } 999 1000 func (*fakeNodePlugin) Filter(_ context.Context, _ *framework.CycleState, _ *v1.Pod, _ *framework.NodeInfo) *framework.Status { 1001 return nil 1002 } 1003 1004 func (pl *fakeNodePlugin) EventsToRegister() []framework.ClusterEventWithHint { 1005 return []framework.ClusterEventWithHint{ 1006 {Event: framework.ClusterEvent{Resource: framework.Node, ActionType: framework.Add}, QueueingHintFn: fakeNodePluginQueueingFn}, 1007 } 1008 } 1009 1010 var hintFromFakePod = framework.QueueingHint(101) 1011 1012 type fakePodPlugin struct{} 1013 1014 var fakePodPluginQueueingFn = func(_ klog.Logger, _ *v1.Pod, _, _ interface{}) (framework.QueueingHint, error) { 1015 return hintFromFakePod, nil 1016 } 1017 1018 func (*fakePodPlugin) Name() string { return fakePod } 1019 1020 func (*fakePodPlugin) Filter(_ context.Context, _ *framework.CycleState, _ *v1.Pod, _ *framework.NodeInfo) *framework.Status { 1021 return nil 1022 } 1023 1024 func (pl *fakePodPlugin) EventsToRegister() []framework.ClusterEventWithHint { 1025 return []framework.ClusterEventWithHint{ 1026 {Event: framework.ClusterEvent{Resource: framework.Pod, ActionType: framework.Add}, QueueingHintFn: fakePodPluginQueueingFn}, 1027 } 1028 } 1029 1030 type emptyEventPlugin struct{} 1031 1032 func (*emptyEventPlugin) Name() string { return emptyEventExtensions } 1033 1034 func (*emptyEventPlugin) Filter(_ context.Context, _ *framework.CycleState, _ *v1.Pod, _ *framework.NodeInfo) *framework.Status { 1035 return nil 1036 } 1037 1038 func (pl *emptyEventPlugin) EventsToRegister() []framework.ClusterEventWithHint { 1039 return nil 1040 } 1041 1042 // emptyEventsToRegisterPlugin implement interface framework.EnqueueExtensions, but returns nil from EventsToRegister. 1043 // This can simulate a plugin registered at scheduler setup, but does nothing 1044 // due to some disabled feature gate. 1045 type emptyEventsToRegisterPlugin struct{} 1046 1047 func (*emptyEventsToRegisterPlugin) Name() string { return emptyEventsToRegister } 1048 1049 func (*emptyEventsToRegisterPlugin) Filter(_ context.Context, _ *framework.CycleState, _ *v1.Pod, _ *framework.NodeInfo) *framework.Status { 1050 return nil 1051 } 1052 1053 func (*emptyEventsToRegisterPlugin) EventsToRegister() []framework.ClusterEventWithHint { return nil }