/* Copyright 2018 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package validation import ( "testing" "time" "github.com/google/go-cmp/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" componentbaseconfig "k8s.io/component-base/config" "k8s.io/kubernetes/pkg/scheduler/apis/config" configv1 "k8s.io/kubernetes/pkg/scheduler/apis/config/v1" "k8s.io/kubernetes/pkg/scheduler/apis/config/v1beta2" "k8s.io/kubernetes/pkg/scheduler/apis/config/v1beta3" "k8s.io/utils/pointer" ) func TestValidateKubeSchedulerConfigurationV1beta2(t *testing.T) { podInitialBackoffSeconds := int64(1) podMaxBackoffSeconds := int64(1) validConfig := &config.KubeSchedulerConfiguration{ TypeMeta: metav1.TypeMeta{ APIVersion: v1beta2.SchemeGroupVersion.String(), }, Parallelism: 8, ClientConnection: componentbaseconfig.ClientConnectionConfiguration{ AcceptContentTypes: "application/json", ContentType: "application/json", QPS: 10, Burst: 10, }, LeaderElection: componentbaseconfig.LeaderElectionConfiguration{ ResourceLock: "leases", LeaderElect: true, LeaseDuration: metav1.Duration{Duration: 30 * time.Second}, RenewDeadline: metav1.Duration{Duration: 15 * time.Second}, RetryPeriod: metav1.Duration{Duration: 5 * time.Second}, ResourceNamespace: "name", ResourceName: "name", }, PodInitialBackoffSeconds: podInitialBackoffSeconds, PodMaxBackoffSeconds: podMaxBackoffSeconds, PercentageOfNodesToScore: pointer.Int32(35), Profiles: []config.KubeSchedulerProfile{ { SchedulerName: "me", Plugins: &config.Plugins{ QueueSort: config.PluginSet{ Enabled: []config.Plugin{{Name: "CustomSort"}}, }, Score: config.PluginSet{ Disabled: []config.Plugin{{Name: "*"}}, }, }, PluginConfig: []config.PluginConfig{ { Name: "DefaultPreemption", Args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 10, MinCandidateNodesAbsolute: 100}, }, }, }, { SchedulerName: "other", Plugins: &config.Plugins{ QueueSort: config.PluginSet{ Enabled: []config.Plugin{{Name: "CustomSort"}}, }, Bind: config.PluginSet{ Enabled: []config.Plugin{{Name: "CustomBind"}}, }, }, }, }, Extenders: []config.Extender{ { PrioritizeVerb: "prioritize", Weight: 1, }, }, } invalidParallelismValue := validConfig.DeepCopy() invalidParallelismValue.Parallelism = 0 resourceNameNotSet := validConfig.DeepCopy() resourceNameNotSet.LeaderElection.ResourceName = "" resourceNamespaceNotSet := validConfig.DeepCopy() resourceNamespaceNotSet.LeaderElection.ResourceNamespace = "" resourceLockNotLeases := validConfig.DeepCopy() resourceLockNotLeases.LeaderElection.ResourceLock = "configmap" enableContentProfilingSetWithoutEnableProfiling := validConfig.DeepCopy() enableContentProfilingSetWithoutEnableProfiling.EnableProfiling = false enableContentProfilingSetWithoutEnableProfiling.EnableContentionProfiling = true metricsBindAddrInvalid := validConfig.DeepCopy() metricsBindAddrInvalid.MetricsBindAddress = "0.0.0.0:9090" healthzBindAddrInvalid := validConfig.DeepCopy() healthzBindAddrInvalid.HealthzBindAddress = "0.0.0.0:9090" percentageOfNodesToScore101 := validConfig.DeepCopy() percentageOfNodesToScore101.PercentageOfNodesToScore = pointer.Int32(101) percentageOfNodesToScoreNegative := validConfig.DeepCopy() percentageOfNodesToScoreNegative.PercentageOfNodesToScore = pointer.Int32(-1) schedulerNameNotSet := validConfig.DeepCopy() schedulerNameNotSet.Profiles[1].SchedulerName = "" repeatedSchedulerName := validConfig.DeepCopy() repeatedSchedulerName.Profiles[0].SchedulerName = "other" profilePercentageOfNodesToScore101 := validConfig.DeepCopy() profilePercentageOfNodesToScore101.Profiles[1].PercentageOfNodesToScore = pointer.Int32(101) profilePercentageOfNodesToScoreNegative := validConfig.DeepCopy() profilePercentageOfNodesToScoreNegative.Profiles[1].PercentageOfNodesToScore = pointer.Int32(-1) differentQueueSort := validConfig.DeepCopy() differentQueueSort.Profiles[1].Plugins.QueueSort.Enabled[0].Name = "AnotherSort" oneEmptyQueueSort := validConfig.DeepCopy() oneEmptyQueueSort.Profiles[0].Plugins = nil extenderNegativeWeight := validConfig.DeepCopy() extenderNegativeWeight.Extenders[0].Weight = -1 invalidNodePercentage := validConfig.DeepCopy() invalidNodePercentage.Profiles[0].PluginConfig = []config.PluginConfig{ { Name: "DefaultPreemption", Args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 200, MinCandidateNodesAbsolute: 100}, }, } invalidPluginArgs := validConfig.DeepCopy() invalidPluginArgs.Profiles[0].PluginConfig = []config.PluginConfig{ { Name: "DefaultPreemption", Args: &config.InterPodAffinityArgs{}, }, } duplicatedPluginConfig := validConfig.DeepCopy() duplicatedPluginConfig.Profiles[0].PluginConfig = []config.PluginConfig{ { Name: "config", }, { Name: "config", }, } mismatchQueueSort := validConfig.DeepCopy() mismatchQueueSort.Profiles = []config.KubeSchedulerProfile{ { SchedulerName: "me", Plugins: &config.Plugins{ QueueSort: config.PluginSet{ Enabled: []config.Plugin{{Name: "PrioritySort"}}, }, }, PluginConfig: []config.PluginConfig{ { Name: "PrioritySort", }, }, }, { SchedulerName: "other", Plugins: &config.Plugins{ QueueSort: config.PluginSet{ Enabled: []config.Plugin{{Name: "CustomSort"}}, }, }, PluginConfig: []config.PluginConfig{ { Name: "CustomSort", }, }, }, } extenderDuplicateManagedResource := validConfig.DeepCopy() extenderDuplicateManagedResource.Extenders[0].ManagedResources = []config.ExtenderManagedResource{ {Name: "example.com/foo", IgnoredByScheduler: false}, {Name: "example.com/foo", IgnoredByScheduler: false}, } extenderDuplicateBind := validConfig.DeepCopy() extenderDuplicateBind.Extenders[0].BindVerb = "foo" extenderDuplicateBind.Extenders = append(extenderDuplicateBind.Extenders, config.Extender{ PrioritizeVerb: "prioritize", BindVerb: "bar", Weight: 1, }) validPlugins := validConfig.DeepCopy() validPlugins.Profiles[0].Plugins.Score.Enabled = append(validPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "PodTopologySpread", Weight: 2}) scenarios := map[string]struct { config *config.KubeSchedulerConfiguration wantErrs field.ErrorList }{ "good": { config: validConfig, }, "bad-parallelism-invalid-value": { config: invalidParallelismValue, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "parallelism", }, }, }, "bad-resource-name-not-set": { config: resourceNameNotSet, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "leaderElection.resourceName", }, }, }, "bad-resource-namespace-not-set": { config: resourceNamespaceNotSet, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "leaderElection.resourceNamespace", }, }, }, "bad-resource-lock-not-leases": { config: resourceLockNotLeases, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "leaderElection.resourceLock", }, }, }, "non-empty-metrics-bind-addr": { config: metricsBindAddrInvalid, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "metricsBindAddress", }, }, }, "non-empty-healthz-bind-addr": { config: healthzBindAddrInvalid, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "healthzBindAddress", }, }, }, "greater-than-100-percentage-of-nodes-to-score": { config: percentageOfNodesToScore101, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "percentageOfNodesToScore", }, }, }, "negative-percentage-of-nodes-to-score": { config: percentageOfNodesToScoreNegative, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "percentageOfNodesToScore", }, }, }, "scheduler-name-not-set": { config: schedulerNameNotSet, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeRequired, Field: "profiles[1].schedulerName", }, }, }, "repeated-scheduler-name": { config: repeatedSchedulerName, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeDuplicate, Field: "profiles[1].schedulerName", }, }, }, "greater-than-100-profile-percentage-of-nodes-to-score": { config: profilePercentageOfNodesToScore101, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].percentageOfNodesToScore", }, }, }, "negative-100-profile-percentage-of-nodes-to-score": { config: profilePercentageOfNodesToScoreNegative, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].percentageOfNodesToScore", }, }, }, "different-queue-sort": { config: differentQueueSort, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].plugins.queueSort", }, }, }, "one-empty-queue-sort": { config: oneEmptyQueueSort, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].plugins.queueSort", }, }, }, "extender-negative-weight": { config: extenderNegativeWeight, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "extenders[0].weight", }, }, }, "extender-duplicate-managed-resources": { config: extenderDuplicateManagedResource, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "extenders[0].managedResources[1].name", }, }, }, "extender-duplicate-bind": { config: extenderDuplicateBind, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "extenders", }, }, }, "invalid-node-percentage": { config: invalidNodePercentage, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[0].pluginConfig[0].args.minCandidateNodesPercentage", }, }, }, "invalid-plugin-args": { config: invalidPluginArgs, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[0].pluginConfig[0].args", }, }, }, "duplicated-plugin-config": { config: duplicatedPluginConfig, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeDuplicate, Field: `profiles[0].pluginConfig[1]`, }, }, }, "mismatch-queue-sort": { config: mismatchQueueSort, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].plugins.queueSort", }, }, }, "valid-plugins": { config: validPlugins, }, } for name, scenario := range scenarios { t.Run(name, func(t *testing.T) { errs := ValidateKubeSchedulerConfiguration(scenario.config) diff := cmp.Diff(scenario.wantErrs.ToAggregate(), errs, ignoreBadValueDetail) if diff != "" { t.Errorf("KubeSchedulerConfiguration returned err (-want,+got):\n%s", diff) } }) } } func TestValidateKubeSchedulerConfigurationV1beta3(t *testing.T) { podInitialBackoffSeconds := int64(1) podMaxBackoffSeconds := int64(1) validConfig := &config.KubeSchedulerConfiguration{ TypeMeta: metav1.TypeMeta{ APIVersion: v1beta3.SchemeGroupVersion.String(), }, Parallelism: 8, ClientConnection: componentbaseconfig.ClientConnectionConfiguration{ AcceptContentTypes: "application/json", ContentType: "application/json", QPS: 10, Burst: 10, }, LeaderElection: componentbaseconfig.LeaderElectionConfiguration{ ResourceLock: "leases", LeaderElect: true, LeaseDuration: metav1.Duration{Duration: 30 * time.Second}, RenewDeadline: metav1.Duration{Duration: 15 * time.Second}, RetryPeriod: metav1.Duration{Duration: 5 * time.Second}, ResourceNamespace: "name", ResourceName: "name", }, PodInitialBackoffSeconds: podInitialBackoffSeconds, PodMaxBackoffSeconds: podMaxBackoffSeconds, PercentageOfNodesToScore: pointer.Int32(35), Profiles: []config.KubeSchedulerProfile{ { SchedulerName: "me", Plugins: &config.Plugins{ QueueSort: config.PluginSet{ Enabled: []config.Plugin{{Name: "CustomSort"}}, }, Score: config.PluginSet{ Disabled: []config.Plugin{{Name: "*"}}, }, }, PluginConfig: []config.PluginConfig{ { Name: "DefaultPreemption", Args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 10, MinCandidateNodesAbsolute: 100}, }, }, }, { SchedulerName: "other", Plugins: &config.Plugins{ QueueSort: config.PluginSet{ Enabled: []config.Plugin{{Name: "CustomSort"}}, }, Bind: config.PluginSet{ Enabled: []config.Plugin{{Name: "CustomBind"}}, }, }, }, }, Extenders: []config.Extender{ { PrioritizeVerb: "prioritize", Weight: 1, }, }, } invalidParallelismValue := validConfig.DeepCopy() invalidParallelismValue.Parallelism = 0 resourceNameNotSet := validConfig.DeepCopy() resourceNameNotSet.LeaderElection.ResourceName = "" resourceLockNotLeases := validConfig.DeepCopy() resourceLockNotLeases.LeaderElection.ResourceLock = "configmap" resourceNamespaceNotSet := validConfig.DeepCopy() resourceNamespaceNotSet.LeaderElection.ResourceNamespace = "" enableContentProfilingSetWithoutEnableProfiling := validConfig.DeepCopy() enableContentProfilingSetWithoutEnableProfiling.EnableProfiling = false enableContentProfilingSetWithoutEnableProfiling.EnableContentionProfiling = true metricsBindAddrInvalid := validConfig.DeepCopy() metricsBindAddrInvalid.MetricsBindAddress = "0.0.0.0:9090" healthzBindAddrInvalid := validConfig.DeepCopy() healthzBindAddrInvalid.HealthzBindAddress = "0.0.0.0:9090" percentageOfNodesToScore101 := validConfig.DeepCopy() percentageOfNodesToScore101.PercentageOfNodesToScore = pointer.Int32(101) percentageOfNodesToScoreNegative := validConfig.DeepCopy() percentageOfNodesToScoreNegative.Profiles[1].PercentageOfNodesToScore = pointer.Int32(-1) schedulerNameNotSet := validConfig.DeepCopy() schedulerNameNotSet.Profiles[1].SchedulerName = "" repeatedSchedulerName := validConfig.DeepCopy() repeatedSchedulerName.Profiles[0].SchedulerName = "other" profilePercentageOfNodesToScore101 := validConfig.DeepCopy() profilePercentageOfNodesToScore101.Profiles[1].PercentageOfNodesToScore = pointer.Int32(101) profilePercentageOfNodesToScoreNegative := validConfig.DeepCopy() profilePercentageOfNodesToScoreNegative.Profiles[1].PercentageOfNodesToScore = pointer.Int32(-1) differentQueueSort := validConfig.DeepCopy() differentQueueSort.Profiles[1].Plugins.QueueSort.Enabled[0].Name = "AnotherSort" oneEmptyQueueSort := validConfig.DeepCopy() oneEmptyQueueSort.Profiles[0].Plugins = nil extenderNegativeWeight := validConfig.DeepCopy() extenderNegativeWeight.Extenders[0].Weight = -1 invalidNodePercentage := validConfig.DeepCopy() invalidNodePercentage.Profiles[0].PluginConfig = []config.PluginConfig{ { Name: "DefaultPreemption", Args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 200, MinCandidateNodesAbsolute: 100}, }, } invalidPluginArgs := validConfig.DeepCopy() invalidPluginArgs.Profiles[0].PluginConfig = []config.PluginConfig{ { Name: "DefaultPreemption", Args: &config.InterPodAffinityArgs{}, }, } duplicatedPlugins := validConfig.DeepCopy() duplicatedPlugins.Profiles[0].Plugins.PreEnqueue.Enabled = []config.Plugin{ {Name: "CustomPreEnqueue"}, {Name: "CustomPreEnqueue"}, } duplicatedPluginConfig := validConfig.DeepCopy() duplicatedPluginConfig.Profiles[0].PluginConfig = []config.PluginConfig{ { Name: "config", }, { Name: "config", }, } mismatchQueueSort := validConfig.DeepCopy() mismatchQueueSort.Profiles = []config.KubeSchedulerProfile{ { SchedulerName: "me", Plugins: &config.Plugins{ QueueSort: config.PluginSet{ Enabled: []config.Plugin{{Name: "PrioritySort"}}, }, }, PluginConfig: []config.PluginConfig{ { Name: "PrioritySort", }, }, }, { SchedulerName: "other", Plugins: &config.Plugins{ QueueSort: config.PluginSet{ Enabled: []config.Plugin{{Name: "CustomSort"}}, }, }, PluginConfig: []config.PluginConfig{ { Name: "CustomSort", }, }, }, } extenderDuplicateManagedResource := validConfig.DeepCopy() extenderDuplicateManagedResource.Extenders[0].ManagedResources = []config.ExtenderManagedResource{ {Name: "example.com/foo", IgnoredByScheduler: false}, {Name: "example.com/foo", IgnoredByScheduler: false}, } extenderDuplicateBind := validConfig.DeepCopy() extenderDuplicateBind.Extenders[0].BindVerb = "foo" extenderDuplicateBind.Extenders = append(extenderDuplicateBind.Extenders, config.Extender{ PrioritizeVerb: "prioritize", BindVerb: "bar", Weight: 1, }) validPlugins := validConfig.DeepCopy() validPlugins.Profiles[0].Plugins.Score.Enabled = append(validPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "PodTopologySpread", Weight: 2}) scenarios := map[string]struct { config *config.KubeSchedulerConfiguration wantErrs field.ErrorList }{ "good": { config: validConfig, }, "bad-parallelism-invalid-value": { config: invalidParallelismValue, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "parallelism", }, }, }, "bad-resource-name-not-set": { config: resourceNameNotSet, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "leaderElection.resourceName", }, }, }, "bad-resource-lock-not-leases": { config: resourceLockNotLeases, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "leaderElection.resourceLock", }, }, }, "bad-resource-namespace-not-set": { config: resourceNamespaceNotSet, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "leaderElection.resourceNamespace", }, }, }, "non-empty-metrics-bind-addr": { config: metricsBindAddrInvalid, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "metricsBindAddress", }, }, }, "non-empty-healthz-bind-addr": { config: healthzBindAddrInvalid, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "healthzBindAddress", }, }, }, "bad-percentage-of-nodes-to-score": { config: percentageOfNodesToScore101, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "percentageOfNodesToScore", }, }, }, "negative-percentage-of-nodes-to-score": { config: percentageOfNodesToScoreNegative, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].percentageOfNodesToScore", }, }, }, "scheduler-name-not-set": { config: schedulerNameNotSet, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeRequired, Field: "profiles[1].schedulerName", }, }, }, "repeated-scheduler-name": { config: repeatedSchedulerName, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeDuplicate, Field: "profiles[1].schedulerName", }, }, }, "greater-than-100-profile-percentage-of-nodes-to-score": { config: profilePercentageOfNodesToScore101, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].percentageOfNodesToScore", }, }, }, "negative-100-profile-percentage-of-nodes-to-score": { config: profilePercentageOfNodesToScoreNegative, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].percentageOfNodesToScore", }, }, }, "different-queue-sort": { config: differentQueueSort, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].plugins.queueSort", }, }, }, "one-empty-queue-sort": { config: oneEmptyQueueSort, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].plugins.queueSort", }, }, }, "extender-negative-weight": { config: extenderNegativeWeight, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "extenders[0].weight", }, }, }, "extender-duplicate-managed-resources": { config: extenderDuplicateManagedResource, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "extenders[0].managedResources[1].name", }, }, }, "extender-duplicate-bind": { config: extenderDuplicateBind, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "extenders", }, }, }, "invalid-node-percentage": { config: invalidNodePercentage, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[0].pluginConfig[0].args.minCandidateNodesPercentage", }, }, }, "invalid-plugin-args": { config: invalidPluginArgs, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[0].pluginConfig[0].args", }, }, }, "duplicated-plugin-config": { config: duplicatedPluginConfig, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeDuplicate, Field: "profiles[0].pluginConfig[1]", }, }, }, "mismatch-queue-sort": { config: mismatchQueueSort, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].plugins.queueSort", }, }, }, "valid-plugins": { config: validPlugins, }, } for name, scenario := range scenarios { t.Run(name, func(t *testing.T) { errs := ValidateKubeSchedulerConfiguration(scenario.config) diff := cmp.Diff(scenario.wantErrs.ToAggregate(), errs, ignoreBadValueDetail) if diff != "" { t.Errorf("KubeSchedulerConfiguration returned err (-want,+got):\n%s", diff) } }) } } func TestValidateKubeSchedulerConfigurationV1(t *testing.T) { podInitialBackoffSeconds := int64(1) podMaxBackoffSeconds := int64(1) validConfig := &config.KubeSchedulerConfiguration{ TypeMeta: metav1.TypeMeta{ APIVersion: configv1.SchemeGroupVersion.String(), }, Parallelism: 8, ClientConnection: componentbaseconfig.ClientConnectionConfiguration{ AcceptContentTypes: "application/json", ContentType: "application/json", QPS: 10, Burst: 10, }, LeaderElection: componentbaseconfig.LeaderElectionConfiguration{ ResourceLock: "leases", LeaderElect: true, LeaseDuration: metav1.Duration{Duration: 30 * time.Second}, RenewDeadline: metav1.Duration{Duration: 15 * time.Second}, RetryPeriod: metav1.Duration{Duration: 5 * time.Second}, ResourceNamespace: "name", ResourceName: "name", }, PodInitialBackoffSeconds: podInitialBackoffSeconds, PodMaxBackoffSeconds: podMaxBackoffSeconds, Profiles: []config.KubeSchedulerProfile{ { SchedulerName: "me", PercentageOfNodesToScore: pointer.Int32(35), Plugins: &config.Plugins{ QueueSort: config.PluginSet{ Enabled: []config.Plugin{{Name: "CustomSort"}}, }, Score: config.PluginSet{ Disabled: []config.Plugin{{Name: "*"}}, }, }, PluginConfig: []config.PluginConfig{ { Name: "DefaultPreemption", Args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 10, MinCandidateNodesAbsolute: 100}, }, }, }, { SchedulerName: "other", PercentageOfNodesToScore: pointer.Int32(35), Plugins: &config.Plugins{ QueueSort: config.PluginSet{ Enabled: []config.Plugin{{Name: "CustomSort"}}, }, Bind: config.PluginSet{ Enabled: []config.Plugin{{Name: "CustomBind"}}, }, }, }, }, Extenders: []config.Extender{ { PrioritizeVerb: "prioritize", Weight: 1, }, }, } invalidParallelismValue := validConfig.DeepCopy() invalidParallelismValue.Parallelism = 0 resourceNameNotSet := validConfig.DeepCopy() resourceNameNotSet.LeaderElection.ResourceName = "" resourceNamespaceNotSet := validConfig.DeepCopy() resourceNamespaceNotSet.LeaderElection.ResourceNamespace = "" resourceLockNotLeases := validConfig.DeepCopy() resourceLockNotLeases.LeaderElection.ResourceLock = "configmap" enableContentProfilingSetWithoutEnableProfiling := validConfig.DeepCopy() enableContentProfilingSetWithoutEnableProfiling.EnableProfiling = false enableContentProfilingSetWithoutEnableProfiling.EnableContentionProfiling = true metricsBindAddrInvalid := validConfig.DeepCopy() metricsBindAddrInvalid.MetricsBindAddress = "0.0.0.0:9090" healthzBindAddrInvalid := validConfig.DeepCopy() healthzBindAddrInvalid.HealthzBindAddress = "0.0.0.0:9090" percentageOfNodesToScore101 := validConfig.DeepCopy() percentageOfNodesToScore101.PercentageOfNodesToScore = pointer.Int32(101) percentageOfNodesToScoreNegative := validConfig.DeepCopy() percentageOfNodesToScoreNegative.PercentageOfNodesToScore = pointer.Int32(-1) schedulerNameNotSet := validConfig.DeepCopy() schedulerNameNotSet.Profiles[1].SchedulerName = "" repeatedSchedulerName := validConfig.DeepCopy() repeatedSchedulerName.Profiles[0].SchedulerName = "other" profilePercentageOfNodesToScore101 := validConfig.DeepCopy() profilePercentageOfNodesToScore101.Profiles[1].PercentageOfNodesToScore = pointer.Int32(101) profilePercentageOfNodesToScoreNegative := validConfig.DeepCopy() profilePercentageOfNodesToScoreNegative.Profiles[1].PercentageOfNodesToScore = pointer.Int32(-1) differentQueueSort := validConfig.DeepCopy() differentQueueSort.Profiles[1].Plugins.QueueSort.Enabled[0].Name = "AnotherSort" oneEmptyQueueSort := validConfig.DeepCopy() oneEmptyQueueSort.Profiles[0].Plugins = nil extenderNegativeWeight := validConfig.DeepCopy() extenderNegativeWeight.Extenders[0].Weight = -1 invalidNodePercentage := validConfig.DeepCopy() invalidNodePercentage.Profiles[0].PluginConfig = []config.PluginConfig{ { Name: "DefaultPreemption", Args: &config.DefaultPreemptionArgs{MinCandidateNodesPercentage: 200, MinCandidateNodesAbsolute: 100}, }, } invalidPluginArgs := validConfig.DeepCopy() invalidPluginArgs.Profiles[0].PluginConfig = []config.PluginConfig{ { Name: "DefaultPreemption", Args: &config.InterPodAffinityArgs{}, }, } duplicatedPluginConfig := validConfig.DeepCopy() duplicatedPluginConfig.Profiles[0].PluginConfig = []config.PluginConfig{ { Name: "config", }, { Name: "config", }, } mismatchQueueSort := validConfig.DeepCopy() mismatchQueueSort.Profiles = []config.KubeSchedulerProfile{ { SchedulerName: "me", Plugins: &config.Plugins{ QueueSort: config.PluginSet{ Enabled: []config.Plugin{{Name: "PrioritySort"}}, }, }, PluginConfig: []config.PluginConfig{ { Name: "PrioritySort", }, }, }, { SchedulerName: "other", Plugins: &config.Plugins{ QueueSort: config.PluginSet{ Enabled: []config.Plugin{{Name: "CustomSort"}}, }, }, PluginConfig: []config.PluginConfig{ { Name: "CustomSort", }, }, }, } extenderDuplicateManagedResource := validConfig.DeepCopy() extenderDuplicateManagedResource.Extenders[0].ManagedResources = []config.ExtenderManagedResource{ {Name: "example.com/foo", IgnoredByScheduler: false}, {Name: "example.com/foo", IgnoredByScheduler: false}, } extenderDuplicateBind := validConfig.DeepCopy() extenderDuplicateBind.Extenders[0].BindVerb = "foo" extenderDuplicateBind.Extenders = append(extenderDuplicateBind.Extenders, config.Extender{ PrioritizeVerb: "prioritize", BindVerb: "bar", Weight: 1, }) validPlugins := validConfig.DeepCopy() validPlugins.Profiles[0].Plugins.Score.Enabled = append(validPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "PodTopologySpread", Weight: 2}) invalidPlugins := validConfig.DeepCopy() invalidPlugins.Profiles[0].Plugins.Score.Enabled = append(invalidPlugins.Profiles[0].Plugins.Score.Enabled, config.Plugin{Name: "SelectorSpread"}) scenarios := map[string]struct { config *config.KubeSchedulerConfiguration wantErrs field.ErrorList }{ "good": { config: validConfig, }, "bad-parallelism-invalid-value": { config: invalidParallelismValue, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "parallelism", }, }, }, "bad-resource-name-not-set": { config: resourceNameNotSet, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "leaderElection.resourceName", }, }, }, "bad-resource-namespace-not-set": { config: resourceNamespaceNotSet, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "leaderElection.resourceNamespace", }, }, }, "bad-resource-lock-not-leases": { config: resourceLockNotLeases, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "leaderElection.resourceLock", }, }, }, "non-empty-metrics-bind-addr": { config: metricsBindAddrInvalid, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "metricsBindAddress", }, }, }, "non-empty-healthz-bind-addr": { config: healthzBindAddrInvalid, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "healthzBindAddress", }, }, }, "bad-percentage-of-nodes-to-score": { config: percentageOfNodesToScore101, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "percentageOfNodesToScore", }, }, }, "negative-percentage-of-nodes-to-score": { config: percentageOfNodesToScoreNegative, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "percentageOfNodesToScore", }, }, }, "scheduler-name-not-set": { config: schedulerNameNotSet, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeRequired, Field: "profiles[1].schedulerName", }, }, }, "repeated-scheduler-name": { config: repeatedSchedulerName, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeDuplicate, Field: "profiles[1].schedulerName", }, }, }, "greater-than-100-profile-percentage-of-nodes-to-score": { config: profilePercentageOfNodesToScore101, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].percentageOfNodesToScore", }, }, }, "negative-profile-percentage-of-nodes-to-score": { config: profilePercentageOfNodesToScoreNegative, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].percentageOfNodesToScore", }, }, }, "different-queue-sort": { config: differentQueueSort, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].plugins.queueSort", }, }, }, "one-empty-queue-sort": { config: oneEmptyQueueSort, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].plugins.queueSort", }, }, }, "extender-negative-weight": { config: extenderNegativeWeight, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "extenders[0].weight", }, }, }, "extender-duplicate-managed-resources": { config: extenderDuplicateManagedResource, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "extenders[0].managedResources[1].name", }, }, }, "extender-duplicate-bind": { config: extenderDuplicateBind, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "extenders", }, }, }, "invalid-node-percentage": { config: invalidNodePercentage, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[0].pluginConfig[0].args.minCandidateNodesPercentage", }, }, }, "invalid-plugin-args": { config: invalidPluginArgs, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[0].pluginConfig[0].args", }, }, }, "duplicated-plugin-config": { config: duplicatedPluginConfig, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeDuplicate, Field: "profiles[0].pluginConfig[1]", }, }, }, "mismatch-queue-sort": { config: mismatchQueueSort, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[1].plugins.queueSort", }, }, }, "invalid-plugins": { config: invalidPlugins, wantErrs: field.ErrorList{ &field.Error{ Type: field.ErrorTypeInvalid, Field: "profiles[0].plugins.score.enabled[0]", }, }, }, "valid-plugins": { config: validPlugins, }, } for name, scenario := range scenarios { t.Run(name, func(t *testing.T) { errs := ValidateKubeSchedulerConfiguration(scenario.config) diff := cmp.Diff(scenario.wantErrs.ToAggregate(), errs, ignoreBadValueDetail) if diff != "" { t.Errorf("KubeSchedulerConfiguration returned err (-want,+got):\n%s", diff) } }) } }