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  }