k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/scheduler/apis/config/validation/validation_test.go (about)

     1  /*
     2  Copyright 2018 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 validation
    18  
    19  import (
    20  	"testing"
    21  	"time"
    22  
    23  	"github.com/google/go-cmp/cmp"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/util/validation/field"
    26  	componentbaseconfig "k8s.io/component-base/config"
    27  	"k8s.io/kubernetes/pkg/scheduler/apis/config"
    28  	configv1 "k8s.io/kubernetes/pkg/scheduler/apis/config/v1"
    29  	"k8s.io/utils/ptr"
    30  )
    31  
    32  func TestValidateKubeSchedulerConfigurationV1(t *testing.T) {
    33  	podInitialBackoffSeconds := int64(1)
    34  	podMaxBackoffSeconds := int64(1)
    35  	validConfig := &config.KubeSchedulerConfiguration{
    36  		TypeMeta: metav1.TypeMeta{
    37  			APIVersion: configv1.SchemeGroupVersion.String(),
    38  		},
    39  		Parallelism: 8,
    40  		ClientConnection: componentbaseconfig.ClientConnectionConfiguration{
    41  			AcceptContentTypes: "application/json",
    42  			ContentType:        "application/json",
    43  			QPS:                10,
    44  			Burst:              10,
    45  		},
    46  		LeaderElection: componentbaseconfig.LeaderElectionConfiguration{
    47  			ResourceLock:      "leases",
    48  			LeaderElect:       true,
    49  			LeaseDuration:     metav1.Duration{Duration: 30 * time.Second},
    50  			RenewDeadline:     metav1.Duration{Duration: 15 * time.Second},
    51  			RetryPeriod:       metav1.Duration{Duration: 5 * time.Second},
    52  			ResourceNamespace: "name",
    53  			ResourceName:      "name",
    54  		},
    55  		PodInitialBackoffSeconds: podInitialBackoffSeconds,
    56  		PodMaxBackoffSeconds:     podMaxBackoffSeconds,
    57  		Profiles: []config.KubeSchedulerProfile{{
    58  			SchedulerName:            "me",
    59  			PercentageOfNodesToScore: ptr.To[int32](35),
    60  			Plugins: &config.Plugins{
    61  				QueueSort: config.PluginSet{
    62  					Enabled: []config.Plugin{{Name: "CustomSort"}},
    63  				},
    64  				Score: config.PluginSet{
    65  					Disabled: []config.Plugin{{Name: "*"}},
    66  				},
    67  			},
    68  			PluginConfig: []config.PluginConfig{{
    69  				Name: "DefaultPreemption",
    70  				Args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 10, MinCandidateNodesAbsolute: 100},
    71  			}},
    72  		}, {
    73  			SchedulerName:            "other",
    74  			PercentageOfNodesToScore: ptr.To[int32](35),
    75  			Plugins: &config.Plugins{
    76  				QueueSort: config.PluginSet{
    77  					Enabled: []config.Plugin{{Name: "CustomSort"}},
    78  				},
    79  				Bind: config.PluginSet{
    80  					Enabled: []config.Plugin{{Name: "CustomBind"}},
    81  				},
    82  			},
    83  		}},
    84  		Extenders: []config.Extender{{
    85  			PrioritizeVerb: "prioritize",
    86  			Weight:         1,
    87  		}},
    88  	}
    89  
    90  	invalidParallelismValue := validConfig.DeepCopy()
    91  	invalidParallelismValue.Parallelism = 0
    92  
    93  	resourceNameNotSet := validConfig.DeepCopy()
    94  	resourceNameNotSet.LeaderElection.ResourceName = ""
    95  
    96  	resourceNamespaceNotSet := validConfig.DeepCopy()
    97  	resourceNamespaceNotSet.LeaderElection.ResourceNamespace = ""
    98  
    99  	resourceLockNotLeases := validConfig.DeepCopy()
   100  	resourceLockNotLeases.LeaderElection.ResourceLock = "configmap"
   101  
   102  	enableContentProfilingSetWithoutEnableProfiling := validConfig.DeepCopy()
   103  	enableContentProfilingSetWithoutEnableProfiling.EnableProfiling = false
   104  	enableContentProfilingSetWithoutEnableProfiling.EnableContentionProfiling = true
   105  
   106  	percentageOfNodesToScore101 := validConfig.DeepCopy()
   107  	percentageOfNodesToScore101.PercentageOfNodesToScore = ptr.To[int32](101)
   108  
   109  	percentageOfNodesToScoreNegative := validConfig.DeepCopy()
   110  	percentageOfNodesToScoreNegative.PercentageOfNodesToScore = ptr.To[int32](-1)
   111  
   112  	schedulerNameNotSet := validConfig.DeepCopy()
   113  	schedulerNameNotSet.Profiles[1].SchedulerName = ""
   114  
   115  	repeatedSchedulerName := validConfig.DeepCopy()
   116  	repeatedSchedulerName.Profiles[0].SchedulerName = "other"
   117  
   118  	profilePercentageOfNodesToScore101 := validConfig.DeepCopy()
   119  	profilePercentageOfNodesToScore101.Profiles[1].PercentageOfNodesToScore = ptr.To[int32](101)
   120  
   121  	profilePercentageOfNodesToScoreNegative := validConfig.DeepCopy()
   122  	profilePercentageOfNodesToScoreNegative.Profiles[1].PercentageOfNodesToScore = ptr.To[int32](-1)
   123  
   124  	differentQueueSort := validConfig.DeepCopy()
   125  	differentQueueSort.Profiles[1].Plugins.QueueSort.Enabled[0].Name = "AnotherSort"
   126  
   127  	oneEmptyQueueSort := validConfig.DeepCopy()
   128  	oneEmptyQueueSort.Profiles[0].Plugins = nil
   129  
   130  	extenderNegativeWeight := validConfig.DeepCopy()
   131  	extenderNegativeWeight.Extenders[0].Weight = -1
   132  
   133  	invalidNodePercentage := validConfig.DeepCopy()
   134  	invalidNodePercentage.Profiles[0].PluginConfig = []config.PluginConfig{{
   135  		Name: "DefaultPreemption",
   136  		Args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 200, MinCandidateNodesAbsolute: 100},
   137  	}}
   138  
   139  	invalidPluginArgs := validConfig.DeepCopy()
   140  	invalidPluginArgs.Profiles[0].PluginConfig = []config.PluginConfig{{
   141  		Name: "DefaultPreemption",
   142  		Args: &config.InterPodAffinityArgs{},
   143  	}}
   144  
   145  	duplicatedPluginConfig := validConfig.DeepCopy()
   146  	duplicatedPluginConfig.Profiles[0].PluginConfig = []config.PluginConfig{{
   147  		Name: "config",
   148  	}, {
   149  		Name: "config",
   150  	}}
   151  
   152  	mismatchQueueSort := validConfig.DeepCopy()
   153  	mismatchQueueSort.Profiles = []config.KubeSchedulerProfile{{
   154  		SchedulerName: "me",
   155  		Plugins: &config.Plugins{
   156  			QueueSort: config.PluginSet{
   157  				Enabled: []config.Plugin{{Name: "PrioritySort"}},
   158  			},
   159  		},
   160  		PluginConfig: []config.PluginConfig{{
   161  			Name: "PrioritySort",
   162  		}},
   163  	}, {
   164  		SchedulerName: "other",
   165  		Plugins: &config.Plugins{
   166  			QueueSort: config.PluginSet{
   167  				Enabled: []config.Plugin{{Name: "CustomSort"}},
   168  			},
   169  		},
   170  		PluginConfig: []config.PluginConfig{{
   171  			Name: "CustomSort",
   172  		}},
   173  	}}
   174  
   175  	extenderDuplicateManagedResource := validConfig.DeepCopy()
   176  	extenderDuplicateManagedResource.Extenders[0].ManagedResources = []config.ExtenderManagedResource{
   177  		{Name: "example.com/foo", IgnoredByScheduler: false},
   178  		{Name: "example.com/foo", IgnoredByScheduler: false},
   179  	}
   180  
   181  	extenderDuplicateBind := validConfig.DeepCopy()
   182  	extenderDuplicateBind.Extenders[0].BindVerb = "foo"
   183  	extenderDuplicateBind.Extenders = append(extenderDuplicateBind.Extenders, config.Extender{
   184  		PrioritizeVerb: "prioritize",
   185  		BindVerb:       "bar",
   186  		Weight:         1,
   187  	})
   188  
   189  	validPlugins := validConfig.DeepCopy()
   190  	validPlugins.Profiles[0].Plugins.Score.Enabled = append(validPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "PodTopologySpread", Weight: 2})
   191  
   192  	invalidPlugins := validConfig.DeepCopy()
   193  	invalidPlugins.Profiles[0].Plugins.Score.Enabled = append(invalidPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "AzureDiskLimits"})
   194  	invalidPlugins.Profiles[0].Plugins.Score.Enabled = append(invalidPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "CinderLimits"})
   195  	invalidPlugins.Profiles[0].Plugins.Score.Enabled = append(invalidPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "EBSLimits"})
   196  	invalidPlugins.Profiles[0].Plugins.Score.Enabled = append(invalidPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "GCEPDLimits"})
   197  
   198  	scenarios := map[string]struct {
   199  		config   *config.KubeSchedulerConfiguration
   200  		wantErrs field.ErrorList
   201  	}{
   202  		"good": {
   203  			config: validConfig,
   204  		},
   205  		"bad-parallelism-invalid-value": {
   206  			config: invalidParallelismValue,
   207  			wantErrs: field.ErrorList{
   208  				&field.Error{
   209  					Type:  field.ErrorTypeInvalid,
   210  					Field: "parallelism",
   211  				},
   212  			},
   213  		},
   214  		"bad-resource-name-not-set": {
   215  			config: resourceNameNotSet,
   216  			wantErrs: field.ErrorList{
   217  				&field.Error{
   218  					Type:  field.ErrorTypeInvalid,
   219  					Field: "leaderElection.resourceName",
   220  				},
   221  			},
   222  		},
   223  		"bad-resource-namespace-not-set": {
   224  			config: resourceNamespaceNotSet,
   225  			wantErrs: field.ErrorList{
   226  				&field.Error{
   227  					Type:  field.ErrorTypeInvalid,
   228  					Field: "leaderElection.resourceNamespace",
   229  				},
   230  			},
   231  		},
   232  		"bad-resource-lock-not-leases": {
   233  			config: resourceLockNotLeases,
   234  			wantErrs: field.ErrorList{
   235  				&field.Error{
   236  					Type:  field.ErrorTypeInvalid,
   237  					Field: "leaderElection.resourceLock",
   238  				},
   239  			},
   240  		},
   241  		"bad-percentage-of-nodes-to-score": {
   242  			config: percentageOfNodesToScore101,
   243  			wantErrs: field.ErrorList{
   244  				&field.Error{
   245  					Type:  field.ErrorTypeInvalid,
   246  					Field: "percentageOfNodesToScore",
   247  				},
   248  			},
   249  		},
   250  		"negative-percentage-of-nodes-to-score": {
   251  			config: percentageOfNodesToScoreNegative,
   252  			wantErrs: field.ErrorList{
   253  				&field.Error{
   254  					Type:  field.ErrorTypeInvalid,
   255  					Field: "percentageOfNodesToScore",
   256  				},
   257  			},
   258  		},
   259  		"scheduler-name-not-set": {
   260  			config: schedulerNameNotSet,
   261  			wantErrs: field.ErrorList{
   262  				&field.Error{
   263  					Type:  field.ErrorTypeRequired,
   264  					Field: "profiles[1].schedulerName",
   265  				},
   266  			},
   267  		},
   268  		"repeated-scheduler-name": {
   269  			config: repeatedSchedulerName,
   270  			wantErrs: field.ErrorList{
   271  				&field.Error{
   272  					Type:  field.ErrorTypeDuplicate,
   273  					Field: "profiles[1].schedulerName",
   274  				},
   275  			},
   276  		},
   277  		"greater-than-100-profile-percentage-of-nodes-to-score": {
   278  			config: profilePercentageOfNodesToScore101,
   279  			wantErrs: field.ErrorList{
   280  				&field.Error{
   281  					Type:  field.ErrorTypeInvalid,
   282  					Field: "profiles[1].percentageOfNodesToScore",
   283  				},
   284  			},
   285  		},
   286  		"negative-profile-percentage-of-nodes-to-score": {
   287  			config: profilePercentageOfNodesToScoreNegative,
   288  			wantErrs: field.ErrorList{
   289  				&field.Error{
   290  					Type:  field.ErrorTypeInvalid,
   291  					Field: "profiles[1].percentageOfNodesToScore",
   292  				},
   293  			},
   294  		},
   295  		"different-queue-sort": {
   296  			config: differentQueueSort,
   297  			wantErrs: field.ErrorList{
   298  				&field.Error{
   299  					Type:  field.ErrorTypeInvalid,
   300  					Field: "profiles[1].plugins.queueSort",
   301  				},
   302  			},
   303  		},
   304  		"one-empty-queue-sort": {
   305  			config: oneEmptyQueueSort,
   306  			wantErrs: field.ErrorList{
   307  				&field.Error{
   308  					Type:  field.ErrorTypeInvalid,
   309  					Field: "profiles[1].plugins.queueSort",
   310  				},
   311  			},
   312  		},
   313  		"extender-negative-weight": {
   314  			config: extenderNegativeWeight,
   315  			wantErrs: field.ErrorList{
   316  				&field.Error{
   317  					Type:  field.ErrorTypeInvalid,
   318  					Field: "extenders[0].weight",
   319  				},
   320  			},
   321  		},
   322  		"extender-duplicate-managed-resources": {
   323  			config: extenderDuplicateManagedResource,
   324  			wantErrs: field.ErrorList{
   325  				&field.Error{
   326  					Type:  field.ErrorTypeInvalid,
   327  					Field: "extenders[0].managedResources[1].name",
   328  				},
   329  			},
   330  		},
   331  		"extender-duplicate-bind": {
   332  			config: extenderDuplicateBind,
   333  			wantErrs: field.ErrorList{
   334  				&field.Error{
   335  					Type:  field.ErrorTypeInvalid,
   336  					Field: "extenders",
   337  				},
   338  			},
   339  		},
   340  		"invalid-node-percentage": {
   341  			config: invalidNodePercentage,
   342  			wantErrs: field.ErrorList{
   343  				&field.Error{
   344  					Type:  field.ErrorTypeInvalid,
   345  					Field: "profiles[0].pluginConfig[0].args.minCandidateNodesPercentage",
   346  				},
   347  			},
   348  		},
   349  		"invalid-plugin-args": {
   350  			config: invalidPluginArgs,
   351  			wantErrs: field.ErrorList{
   352  				&field.Error{
   353  					Type:  field.ErrorTypeInvalid,
   354  					Field: "profiles[0].pluginConfig[0].args",
   355  				},
   356  			},
   357  		},
   358  		"duplicated-plugin-config": {
   359  			config: duplicatedPluginConfig,
   360  			wantErrs: field.ErrorList{
   361  				&field.Error{
   362  					Type:  field.ErrorTypeDuplicate,
   363  					Field: "profiles[0].pluginConfig[1]",
   364  				},
   365  			},
   366  		},
   367  		"mismatch-queue-sort": {
   368  			config: mismatchQueueSort,
   369  			wantErrs: field.ErrorList{
   370  				&field.Error{
   371  					Type:  field.ErrorTypeInvalid,
   372  					Field: "profiles[1].plugins.queueSort",
   373  				},
   374  			},
   375  		},
   376  		"invalid-plugins": {
   377  			config: invalidPlugins,
   378  			wantErrs: field.ErrorList{
   379  				&field.Error{
   380  					Type:  field.ErrorTypeInvalid,
   381  					Field: "profiles[0].plugins.score.enabled[0]",
   382  				},
   383  				&field.Error{
   384  					Type:  field.ErrorTypeInvalid,
   385  					Field: "profiles[0].plugins.score.enabled[1]",
   386  				},
   387  				&field.Error{
   388  					Type:  field.ErrorTypeInvalid,
   389  					Field: "profiles[0].plugins.score.enabled[2]",
   390  				},
   391  				&field.Error{
   392  					Type:  field.ErrorTypeInvalid,
   393  					Field: "profiles[0].plugins.score.enabled[3]",
   394  				},
   395  			},
   396  		},
   397  		"valid-plugins": {
   398  			config: validPlugins,
   399  		},
   400  	}
   401  
   402  	for name, scenario := range scenarios {
   403  		t.Run(name, func(t *testing.T) {
   404  			errs := ValidateKubeSchedulerConfiguration(scenario.config)
   405  			diff := cmp.Diff(scenario.wantErrs.ToAggregate(), errs, ignoreBadValueDetail)
   406  			if diff != "" {
   407  				t.Errorf("KubeSchedulerConfiguration returned err (-want,+got):\n%s", diff)
   408  			}
   409  		})
   410  	}
   411  }