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 }