k8s.io/kubernetes@v1.29.3/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  	metricsBindAddrInvalid := validConfig.DeepCopy()
   107  	metricsBindAddrInvalid.MetricsBindAddress = "0.0.0.0:9090"
   108  
   109  	healthzBindAddrInvalid := validConfig.DeepCopy()
   110  	healthzBindAddrInvalid.HealthzBindAddress = "0.0.0.0:9090"
   111  
   112  	percentageOfNodesToScore101 := validConfig.DeepCopy()
   113  	percentageOfNodesToScore101.PercentageOfNodesToScore = ptr.To[int32](101)
   114  
   115  	percentageOfNodesToScoreNegative := validConfig.DeepCopy()
   116  	percentageOfNodesToScoreNegative.PercentageOfNodesToScore = ptr.To[int32](-1)
   117  
   118  	schedulerNameNotSet := validConfig.DeepCopy()
   119  	schedulerNameNotSet.Profiles[1].SchedulerName = ""
   120  
   121  	repeatedSchedulerName := validConfig.DeepCopy()
   122  	repeatedSchedulerName.Profiles[0].SchedulerName = "other"
   123  
   124  	profilePercentageOfNodesToScore101 := validConfig.DeepCopy()
   125  	profilePercentageOfNodesToScore101.Profiles[1].PercentageOfNodesToScore = ptr.To[int32](101)
   126  
   127  	profilePercentageOfNodesToScoreNegative := validConfig.DeepCopy()
   128  	profilePercentageOfNodesToScoreNegative.Profiles[1].PercentageOfNodesToScore = ptr.To[int32](-1)
   129  
   130  	differentQueueSort := validConfig.DeepCopy()
   131  	differentQueueSort.Profiles[1].Plugins.QueueSort.Enabled[0].Name = "AnotherSort"
   132  
   133  	oneEmptyQueueSort := validConfig.DeepCopy()
   134  	oneEmptyQueueSort.Profiles[0].Plugins = nil
   135  
   136  	extenderNegativeWeight := validConfig.DeepCopy()
   137  	extenderNegativeWeight.Extenders[0].Weight = -1
   138  
   139  	invalidNodePercentage := validConfig.DeepCopy()
   140  	invalidNodePercentage.Profiles[0].PluginConfig = []config.PluginConfig{{
   141  		Name: "DefaultPreemption",
   142  		Args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 200, MinCandidateNodesAbsolute: 100},
   143  	}}
   144  
   145  	invalidPluginArgs := validConfig.DeepCopy()
   146  	invalidPluginArgs.Profiles[0].PluginConfig = []config.PluginConfig{{
   147  		Name: "DefaultPreemption",
   148  		Args: &config.InterPodAffinityArgs{},
   149  	}}
   150  
   151  	duplicatedPluginConfig := validConfig.DeepCopy()
   152  	duplicatedPluginConfig.Profiles[0].PluginConfig = []config.PluginConfig{{
   153  		Name: "config",
   154  	}, {
   155  		Name: "config",
   156  	}}
   157  
   158  	mismatchQueueSort := validConfig.DeepCopy()
   159  	mismatchQueueSort.Profiles = []config.KubeSchedulerProfile{{
   160  		SchedulerName: "me",
   161  		Plugins: &config.Plugins{
   162  			QueueSort: config.PluginSet{
   163  				Enabled: []config.Plugin{{Name: "PrioritySort"}},
   164  			},
   165  		},
   166  		PluginConfig: []config.PluginConfig{{
   167  			Name: "PrioritySort",
   168  		}},
   169  	}, {
   170  		SchedulerName: "other",
   171  		Plugins: &config.Plugins{
   172  			QueueSort: config.PluginSet{
   173  				Enabled: []config.Plugin{{Name: "CustomSort"}},
   174  			},
   175  		},
   176  		PluginConfig: []config.PluginConfig{{
   177  			Name: "CustomSort",
   178  		}},
   179  	}}
   180  
   181  	extenderDuplicateManagedResource := validConfig.DeepCopy()
   182  	extenderDuplicateManagedResource.Extenders[0].ManagedResources = []config.ExtenderManagedResource{
   183  		{Name: "example.com/foo", IgnoredByScheduler: false},
   184  		{Name: "example.com/foo", IgnoredByScheduler: false},
   185  	}
   186  
   187  	extenderDuplicateBind := validConfig.DeepCopy()
   188  	extenderDuplicateBind.Extenders[0].BindVerb = "foo"
   189  	extenderDuplicateBind.Extenders = append(extenderDuplicateBind.Extenders, config.Extender{
   190  		PrioritizeVerb: "prioritize",
   191  		BindVerb:       "bar",
   192  		Weight:         1,
   193  	})
   194  
   195  	validPlugins := validConfig.DeepCopy()
   196  	validPlugins.Profiles[0].Plugins.Score.Enabled = append(validPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "PodTopologySpread", Weight: 2})
   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  		"non-empty-metrics-bind-addr": {
   242  			config: metricsBindAddrInvalid,
   243  			wantErrs: field.ErrorList{
   244  				&field.Error{
   245  					Type:  field.ErrorTypeInvalid,
   246  					Field: "metricsBindAddress",
   247  				},
   248  			},
   249  		},
   250  		"non-empty-healthz-bind-addr": {
   251  			config: healthzBindAddrInvalid,
   252  			wantErrs: field.ErrorList{
   253  				&field.Error{
   254  					Type:  field.ErrorTypeInvalid,
   255  					Field: "healthzBindAddress",
   256  				},
   257  			},
   258  		},
   259  		"bad-percentage-of-nodes-to-score": {
   260  			config: percentageOfNodesToScore101,
   261  			wantErrs: field.ErrorList{
   262  				&field.Error{
   263  					Type:  field.ErrorTypeInvalid,
   264  					Field: "percentageOfNodesToScore",
   265  				},
   266  			},
   267  		},
   268  		"negative-percentage-of-nodes-to-score": {
   269  			config: percentageOfNodesToScoreNegative,
   270  			wantErrs: field.ErrorList{
   271  				&field.Error{
   272  					Type:  field.ErrorTypeInvalid,
   273  					Field: "percentageOfNodesToScore",
   274  				},
   275  			},
   276  		},
   277  		"scheduler-name-not-set": {
   278  			config: schedulerNameNotSet,
   279  			wantErrs: field.ErrorList{
   280  				&field.Error{
   281  					Type:  field.ErrorTypeRequired,
   282  					Field: "profiles[1].schedulerName",
   283  				},
   284  			},
   285  		},
   286  		"repeated-scheduler-name": {
   287  			config: repeatedSchedulerName,
   288  			wantErrs: field.ErrorList{
   289  				&field.Error{
   290  					Type:  field.ErrorTypeDuplicate,
   291  					Field: "profiles[1].schedulerName",
   292  				},
   293  			},
   294  		},
   295  		"greater-than-100-profile-percentage-of-nodes-to-score": {
   296  			config: profilePercentageOfNodesToScore101,
   297  			wantErrs: field.ErrorList{
   298  				&field.Error{
   299  					Type:  field.ErrorTypeInvalid,
   300  					Field: "profiles[1].percentageOfNodesToScore",
   301  				},
   302  			},
   303  		},
   304  		"negative-profile-percentage-of-nodes-to-score": {
   305  			config: profilePercentageOfNodesToScoreNegative,
   306  			wantErrs: field.ErrorList{
   307  				&field.Error{
   308  					Type:  field.ErrorTypeInvalid,
   309  					Field: "profiles[1].percentageOfNodesToScore",
   310  				},
   311  			},
   312  		},
   313  		"different-queue-sort": {
   314  			config: differentQueueSort,
   315  			wantErrs: field.ErrorList{
   316  				&field.Error{
   317  					Type:  field.ErrorTypeInvalid,
   318  					Field: "profiles[1].plugins.queueSort",
   319  				},
   320  			},
   321  		},
   322  		"one-empty-queue-sort": {
   323  			config: oneEmptyQueueSort,
   324  			wantErrs: field.ErrorList{
   325  				&field.Error{
   326  					Type:  field.ErrorTypeInvalid,
   327  					Field: "profiles[1].plugins.queueSort",
   328  				},
   329  			},
   330  		},
   331  		"extender-negative-weight": {
   332  			config: extenderNegativeWeight,
   333  			wantErrs: field.ErrorList{
   334  				&field.Error{
   335  					Type:  field.ErrorTypeInvalid,
   336  					Field: "extenders[0].weight",
   337  				},
   338  			},
   339  		},
   340  		"extender-duplicate-managed-resources": {
   341  			config: extenderDuplicateManagedResource,
   342  			wantErrs: field.ErrorList{
   343  				&field.Error{
   344  					Type:  field.ErrorTypeInvalid,
   345  					Field: "extenders[0].managedResources[1].name",
   346  				},
   347  			},
   348  		},
   349  		"extender-duplicate-bind": {
   350  			config: extenderDuplicateBind,
   351  			wantErrs: field.ErrorList{
   352  				&field.Error{
   353  					Type:  field.ErrorTypeInvalid,
   354  					Field: "extenders",
   355  				},
   356  			},
   357  		},
   358  		"invalid-node-percentage": {
   359  			config: invalidNodePercentage,
   360  			wantErrs: field.ErrorList{
   361  				&field.Error{
   362  					Type:  field.ErrorTypeInvalid,
   363  					Field: "profiles[0].pluginConfig[0].args.minCandidateNodesPercentage",
   364  				},
   365  			},
   366  		},
   367  		"invalid-plugin-args": {
   368  			config: invalidPluginArgs,
   369  			wantErrs: field.ErrorList{
   370  				&field.Error{
   371  					Type:  field.ErrorTypeInvalid,
   372  					Field: "profiles[0].pluginConfig[0].args",
   373  				},
   374  			},
   375  		},
   376  		"duplicated-plugin-config": {
   377  			config: duplicatedPluginConfig,
   378  			wantErrs: field.ErrorList{
   379  				&field.Error{
   380  					Type:  field.ErrorTypeDuplicate,
   381  					Field: "profiles[0].pluginConfig[1]",
   382  				},
   383  			},
   384  		},
   385  		"mismatch-queue-sort": {
   386  			config: mismatchQueueSort,
   387  			wantErrs: field.ErrorList{
   388  				&field.Error{
   389  					Type:  field.ErrorTypeInvalid,
   390  					Field: "profiles[1].plugins.queueSort",
   391  				},
   392  			},
   393  		},
   394  		"valid-plugins": {
   395  			config: validPlugins,
   396  		},
   397  	}
   398  
   399  	for name, scenario := range scenarios {
   400  		t.Run(name, func(t *testing.T) {
   401  			errs := ValidateKubeSchedulerConfiguration(scenario.config)
   402  			diff := cmp.Diff(scenario.wantErrs.ToAggregate(), errs, ignoreBadValueDetail)
   403  			if diff != "" {
   404  				t.Errorf("KubeSchedulerConfiguration returned err (-want,+got):\n%s", diff)
   405  			}
   406  		})
   407  	}
   408  }