diff --git a/cmd/kube-scheduler/app/options/BUILD b/cmd/kube-scheduler/app/options/BUILD index d4dbea230c..ebe68f0834 100644 --- a/cmd/kube-scheduler/app/options/BUILD +++ b/cmd/kube-scheduler/app/options/BUILD @@ -17,6 +17,7 @@ go_library( "//pkg/scheduler/apis/config:go_default_library", "//pkg/scheduler/apis/config/scheme:go_default_library", "//pkg/scheduler/apis/config/v1alpha1:go_default_library", + "//pkg/scheduler/apis/config/validation:go_default_library", "//pkg/scheduler/factory:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/config:go_default_library", @@ -24,6 +25,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/uuid:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", "//staging/src/k8s.io/apiserver/pkg/server/options:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", "//staging/src/k8s.io/client-go/informers:go_default_library", @@ -58,6 +60,7 @@ filegroup( go_test( name = "go_default_test", srcs = [ + "deprecated_test.go", "insecure_serving_test.go", "options_test.go", ], diff --git a/cmd/kube-scheduler/app/options/deprecated.go b/cmd/kube-scheduler/app/options/deprecated.go index 99fbeff6ab..756bc3368b 100644 --- a/cmd/kube-scheduler/app/options/deprecated.go +++ b/cmd/kube-scheduler/app/options/deprecated.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/spf13/pflag" + "k8s.io/apimachinery/pkg/util/validation/field" kubeschedulerconfig "k8s.io/kubernetes/pkg/scheduler/apis/config" "k8s.io/kubernetes/pkg/scheduler/factory" ) @@ -43,7 +44,6 @@ func (o *DeprecatedOptions) AddFlags(fs *pflag.FlagSet, cfg *kubeschedulerconfig } // TODO: unify deprecation mechanism, string prefix or MarkDeprecated (the latter hides the flag. We also don't want that). - fs.StringVar(&o.AlgorithmProvider, "algorithm-provider", o.AlgorithmProvider, "DEPRECATED: the scheduling algorithm provider to use, one of: "+factory.ListAlgorithmProviders()) fs.StringVar(&o.PolicyConfigFile, "policy-config-file", o.PolicyConfigFile, "DEPRECATED: file with scheduler policy configuration. This file is used if policy ConfigMap is not provided or --use-legacy-policy-config=true") usage := fmt.Sprintf("DEPRECATED: name of the ConfigMap object that contains scheduler's policy configuration. It must exist in the system namespace before scheduler initialization if --use-legacy-policy-config=false. The config must be provided as the value of an element in 'Data' map with the key='%v'", kubeschedulerconfig.SchedulerPolicyConfigMapKey) @@ -63,7 +63,7 @@ func (o *DeprecatedOptions) AddFlags(fs *pflag.FlagSet, cfg *kubeschedulerconfig fs.Int32Var(&cfg.HardPodAffinitySymmetricWeight, "hard-pod-affinity-symmetric-weight", cfg.HardPodAffinitySymmetricWeight, "RequiredDuringScheduling affinity is not symmetric, but there is an implicit PreferredDuringScheduling affinity rule corresponding "+ - "to every RequiredDuringScheduling affinity rule. --hard-pod-affinity-symmetric-weight represents the weight of implicit PreferredDuringScheduling affinity rule.") + "to every RequiredDuringScheduling affinity rule. --hard-pod-affinity-symmetric-weight represents the weight of implicit PreferredDuringScheduling affinity rule. Must be in the range 0-100.") fs.MarkDeprecated("hard-pod-affinity-symmetric-weight", "This option was moved to the policy configuration file") fs.StringVar(&cfg.FailureDomains, "failure-domains", cfg.FailureDomains, "Indicate the \"all topologies\" set for an empty topologyKey when it's used for PreferredDuringScheduling pod anti-affinity.") fs.MarkDeprecated("failure-domains", "Doesn't have any effect. Will be removed in future version.") @@ -71,10 +71,12 @@ func (o *DeprecatedOptions) AddFlags(fs *pflag.FlagSet, cfg *kubeschedulerconfig // Validate validates the deprecated scheduler options. func (o *DeprecatedOptions) Validate() []error { - if o == nil { - return nil + var errs []error + + if o.UseLegacyPolicyConfig && len(o.PolicyConfigFile) == 0 { + errs = append(errs, field.Required(field.NewPath("policyConfigFile"), "required when --use-legacy-policy-config is true")) } - return nil + return errs } // ApplyTo sets cfg.AlgorithmSource from flags passed on the command line in the following precedence order: diff --git a/cmd/kube-scheduler/app/options/deprecated_test.go b/cmd/kube-scheduler/app/options/deprecated_test.go new file mode 100644 index 0000000000..15f781edfe --- /dev/null +++ b/cmd/kube-scheduler/app/options/deprecated_test.go @@ -0,0 +1,55 @@ +/* +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 options + +import ( + "testing" +) + +func TestValidateDeprecatedKubeSchedulerConfiguration(t *testing.T) { + scenarios := map[string]struct { + expectedToFail bool + config *DeprecatedOptions + }{ + "good": { + expectedToFail: false, + config: &DeprecatedOptions{ + PolicyConfigFile: "/some/file", + UseLegacyPolicyConfig: true, + AlgorithmProvider: "", + }, + }, + "bad-policy-config-file-null": { + expectedToFail: true, + config: &DeprecatedOptions{ + PolicyConfigFile: "", + UseLegacyPolicyConfig: true, + AlgorithmProvider: "", + }, + }, + } + + for name, scenario := range scenarios { + errs := scenario.config.Validate() + if len(errs) == 0 && scenario.expectedToFail { + t.Errorf("Unexpected success for scenario: %s", name) + } + if len(errs) > 0 && !scenario.expectedToFail { + t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) + } + } +} diff --git a/cmd/kube-scheduler/app/options/options.go b/cmd/kube-scheduler/app/options/options.go index 15cbe66cee..20768465ec 100644 --- a/cmd/kube-scheduler/app/options/options.go +++ b/cmd/kube-scheduler/app/options/options.go @@ -47,6 +47,7 @@ import ( "k8s.io/kubernetes/pkg/client/leaderelectionconfig" kubeschedulerconfig "k8s.io/kubernetes/pkg/scheduler/apis/config" kubeschedulerscheme "k8s.io/kubernetes/pkg/scheduler/apis/config/scheme" + "k8s.io/kubernetes/pkg/scheduler/apis/config/validation" "k8s.io/kubernetes/pkg/scheduler/factory" ) @@ -185,6 +186,9 @@ func (o *Options) ApplyTo(c *schedulerappconfig.Config) error { func (o *Options) Validate() []error { var errs []error + if err := validation.ValidateKubeSchedulerConfiguration(&o.ComponentConfig).ToAggregate(); err != nil { + errs = append(errs, err.Errors()...) + } errs = append(errs, o.SecureServing.Validate()...) errs = append(errs, o.CombinedInsecureServing.Validate()...) errs = append(errs, o.Authentication.Validate()...) diff --git a/pkg/scheduler/apis/config/BUILD b/pkg/scheduler/apis/config/BUILD index cfeee9de9d..f5b97f3946 100644 --- a/pkg/scheduler/apis/config/BUILD +++ b/pkg/scheduler/apis/config/BUILD @@ -32,6 +32,7 @@ filegroup( ":package-srcs", "//pkg/scheduler/apis/config/scheme:all-srcs", "//pkg/scheduler/apis/config/v1alpha1:all-srcs", + "//pkg/scheduler/apis/config/validation:all-srcs", ], tags = ["automanaged"], visibility = ["//visibility:public"], diff --git a/pkg/scheduler/apis/config/validation/BUILD b/pkg/scheduler/apis/config/validation/BUILD new file mode 100644 index 0000000000..8af91c1804 --- /dev/null +++ b/pkg/scheduler/apis/config/validation/BUILD @@ -0,0 +1,41 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["validation.go"], + importpath = "k8s.io/kubernetes/pkg/scheduler/apis/config/validation", + visibility = ["//visibility:public"], + deps = [ + "//pkg/scheduler/apis/config:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/config/validation:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/apis/config/validation:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["validation_test.go"], + embed = [":go_default_library"], + deps = [ + "//pkg/scheduler/apis/config:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/config:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/apis/config:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/scheduler/apis/config/validation/validation.go b/pkg/scheduler/apis/config/validation/validation.go new file mode 100644 index 0000000000..679a448148 --- /dev/null +++ b/pkg/scheduler/apis/config/validation/validation.go @@ -0,0 +1,61 @@ +/* +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 ( + apimachinery "k8s.io/apimachinery/pkg/apis/config/validation" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + apiserver "k8s.io/apiserver/pkg/apis/config/validation" + "k8s.io/kubernetes/pkg/scheduler/apis/config" +) + +// ValidateKubeSchedulerConfiguration ensures validation of the KubeSchedulerConfiguration struct +func ValidateKubeSchedulerConfiguration(cc *config.KubeSchedulerConfiguration) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, apimachinery.ValidateClientConnectionConfiguration(&cc.ClientConnection, field.NewPath("clientConnection"))...) + allErrs = append(allErrs, ValidateKubeSchedulerLeaderElectionConfiguration(&cc.LeaderElection, field.NewPath("leaderElection"))...) + if len(cc.SchedulerName) == 0 { + allErrs = append(allErrs, field.Required(field.NewPath("schedulerName"), "")) + } + for _, msg := range validation.IsValidSocketAddr(cc.HealthzBindAddress) { + allErrs = append(allErrs, field.Invalid(field.NewPath("healthzBindAddress"), cc.HealthzBindAddress, msg)) + } + for _, msg := range validation.IsValidSocketAddr(cc.MetricsBindAddress) { + allErrs = append(allErrs, field.Invalid(field.NewPath("metricsBindAddress"), cc.MetricsBindAddress, msg)) + } + if cc.HardPodAffinitySymmetricWeight < 0 || cc.HardPodAffinitySymmetricWeight > 100 { + allErrs = append(allErrs, field.Invalid(field.NewPath("hardPodAffinitySymmetricWeight"), cc.HardPodAffinitySymmetricWeight, "not in valid range 0-100")) + } + return allErrs +} + +// ValidateKubeSchedulerLeaderElectionConfiguration ensures validation of the KubeSchedulerLeaderElectionConfiguration struct +func ValidateKubeSchedulerLeaderElectionConfiguration(cc *config.KubeSchedulerLeaderElectionConfiguration, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if !cc.LeaderElectionConfiguration.LeaderElect { + return allErrs + } + allErrs = append(allErrs, apiserver.ValidateLeaderElectionConfiguration(&cc.LeaderElectionConfiguration, field.NewPath("leaderElectionConfiguration"))...) + if len(cc.LockObjectNamespace) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("lockObjectNamespace"), "")) + } + if len(cc.LockObjectName) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("lockObjectName"), "")) + } + return allErrs +} diff --git a/pkg/scheduler/apis/config/validation/validation_test.go b/pkg/scheduler/apis/config/validation/validation_test.go new file mode 100644 index 0000000000..4800741c32 --- /dev/null +++ b/pkg/scheduler/apis/config/validation/validation_test.go @@ -0,0 +1,140 @@ +/* +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 ( + apimachinery "k8s.io/apimachinery/pkg/apis/config" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apiserver "k8s.io/apiserver/pkg/apis/config" + "k8s.io/kubernetes/pkg/scheduler/apis/config" + "testing" + "time" +) + +func TestValidateKubeSchedulerConfiguration(t *testing.T) { + validConfig := &config.KubeSchedulerConfiguration{ + SchedulerName: "me", + HealthzBindAddress: "0.0.0.0:10254", + MetricsBindAddress: "0.0.0.0:10254", + HardPodAffinitySymmetricWeight: 80, + ClientConnection: apimachinery.ClientConnectionConfiguration{ + AcceptContentTypes: "application/json", + ContentType: "application/json", + QPS: 10, + Burst: 10, + }, + AlgorithmSource: config.SchedulerAlgorithmSource{ + Policy: &config.SchedulerPolicySource{ + ConfigMap: &config.SchedulerPolicyConfigMapSource{ + Namespace: "name", + Name: "name", + }, + }, + }, + LeaderElection: config.KubeSchedulerLeaderElectionConfiguration{ + LockObjectNamespace: "name", + LockObjectName: "name", + LeaderElectionConfiguration: apiserver.LeaderElectionConfiguration{ + ResourceLock: "configmap", + LeaderElect: true, + LeaseDuration: metav1.Duration{Duration: 30 * time.Second}, + RenewDeadline: metav1.Duration{Duration: 15 * time.Second}, + RetryPeriod: metav1.Duration{Duration: 5 * time.Second}, + }, + }, + } + + HardPodAffinitySymmetricWeightGt100 := validConfig.DeepCopy() + HardPodAffinitySymmetricWeightGt100.HardPodAffinitySymmetricWeight = 120 + + HardPodAffinitySymmetricWeightLt0 := validConfig.DeepCopy() + HardPodAffinitySymmetricWeightLt0.HardPodAffinitySymmetricWeight = -1 + + lockObjectNameNotSet := validConfig.DeepCopy() + lockObjectNameNotSet.LeaderElection.LockObjectName = "" + + lockObjectNamespaceNotSet := validConfig.DeepCopy() + lockObjectNamespaceNotSet.LeaderElection.LockObjectNamespace = "" + + metricsBindAddrHostInvalid := validConfig.DeepCopy() + metricsBindAddrHostInvalid.MetricsBindAddress = "0.0.0.0.0:9090" + + metricsBindAddrPortInvalid := validConfig.DeepCopy() + metricsBindAddrPortInvalid.MetricsBindAddress = "0.0.0.0:909090" + + healthzBindAddrHostInvalid := validConfig.DeepCopy() + healthzBindAddrHostInvalid.HealthzBindAddress = "0.0.0.0.0:9090" + + healthzBindAddrPortInvalid := validConfig.DeepCopy() + healthzBindAddrPortInvalid.HealthzBindAddress = "0.0.0.0:909090" + + enableContentProfilingSetWithoutEnableProfiling := validConfig.DeepCopy() + enableContentProfilingSetWithoutEnableProfiling.EnableProfiling = false + enableContentProfilingSetWithoutEnableProfiling.EnableContentionProfiling = true + + scenarios := map[string]struct { + expectedToFail bool + config *config.KubeSchedulerConfiguration + }{ + "good": { + expectedToFail: false, + config: validConfig, + }, + "bad-lock-object-names-not-set": { + expectedToFail: true, + config: lockObjectNameNotSet, + }, + "bad-lock-object-namespace-not-set": { + expectedToFail: true, + config: lockObjectNamespaceNotSet, + }, + "bad-healthz-port-invalid": { + expectedToFail: true, + config: healthzBindAddrPortInvalid, + }, + "bad-healthz-host-invalid": { + expectedToFail: true, + config: healthzBindAddrHostInvalid, + }, + "bad-metrics-port-invalid": { + expectedToFail: true, + config: metricsBindAddrPortInvalid, + }, + "bad-metrics-host-invalid": { + expectedToFail: true, + config: metricsBindAddrHostInvalid, + }, + "bad-hard-pod-affinity-symmetric-weight-lt-0": { + expectedToFail: true, + config: HardPodAffinitySymmetricWeightGt100, + }, + "bad-hard-pod-affinity-symmetric-weight-gt-100": { + expectedToFail: true, + config: HardPodAffinitySymmetricWeightLt0, + }, + } + + for name, scenario := range scenarios { + errs := ValidateKubeSchedulerConfiguration(scenario.config) + if len(errs) == 0 && scenario.expectedToFail { + t.Errorf("Unexpected success for scenario: %s", name) + } + if len(errs) > 0 && !scenario.expectedToFail { + t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) + } + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/config/BUILD b/staging/src/k8s.io/apimachinery/pkg/apis/config/BUILD index 9a8c653360..45016d21ac 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/config/BUILD +++ b/staging/src/k8s.io/apimachinery/pkg/apis/config/BUILD @@ -24,6 +24,7 @@ filegroup( srcs = [ ":package-srcs", "//staging/src/k8s.io/apimachinery/pkg/apis/config/v1alpha1:all-srcs", + "//staging/src/k8s.io/apimachinery/pkg/apis/config/validation:all-srcs", ], tags = ["automanaged"], visibility = ["//visibility:public"], diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/config/validation/BUILD b/staging/src/k8s.io/apimachinery/pkg/apis/config/validation/BUILD new file mode 100644 index 0000000000..f84ca74498 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/apis/config/validation/BUILD @@ -0,0 +1,37 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["validation.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/apimachinery/pkg/apis/config/validation", + importpath = "k8s.io/apimachinery/pkg/apis/config/validation", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/apis/config:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["validation_test.go"], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/apis/config:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/config/validation/validation.go b/staging/src/k8s.io/apimachinery/pkg/apis/config/validation/validation.go new file mode 100644 index 0000000000..dba3767744 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/apis/config/validation/validation.go @@ -0,0 +1,31 @@ +/* +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 ( + "k8s.io/apimachinery/pkg/apis/config" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// ValidateClientConnectionConfiguration ensures validation of the ClientConnectionConfiguration struct +func ValidateClientConnectionConfiguration(cc *config.ClientConnectionConfiguration, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if cc.Burst < 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("burst"), cc.Burst, "must be non-negative")) + } + return allErrs +} diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/config/validation/validation_test.go b/staging/src/k8s.io/apimachinery/pkg/apis/config/validation/validation_test.go new file mode 100644 index 0000000000..dfa0376740 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/apis/config/validation/validation_test.go @@ -0,0 +1,66 @@ +/* +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 ( + "k8s.io/apimachinery/pkg/apis/config" + "k8s.io/apimachinery/pkg/util/validation/field" + "testing" +) + +func TestValidateClientConnectionConfiguration(t *testing.T) { + validConfig := &config.ClientConnectionConfiguration{ + AcceptContentTypes: "application/json", + ContentType: "application/json", + QPS: 10, + Burst: 10, + } + + qpsLessThanZero := validConfig.DeepCopy() + qpsLessThanZero.QPS = -1 + + burstLessThanZero := validConfig.DeepCopy() + burstLessThanZero.Burst = -1 + + scenarios := map[string]struct { + expectedToFail bool + config *config.ClientConnectionConfiguration + }{ + "good": { + expectedToFail: false, + config: validConfig, + }, + "good-qps-less-than-zero": { + expectedToFail: false, + config: qpsLessThanZero, + }, + "bad-burst-less-then-zero": { + expectedToFail: true, + config: burstLessThanZero, + }, + } + + for name, scenario := range scenarios { + errs := ValidateClientConnectionConfiguration(scenario.config, field.NewPath("clientConnectionConfiguration")) + if len(errs) == 0 && scenario.expectedToFail { + t.Errorf("Unexpected success for scenario: %s", name) + } + if len(errs) > 0 && !scenario.expectedToFail { + t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) + } + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go b/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go index 7da6a17d99..e0d1715420 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/validation/validation.go @@ -21,6 +21,7 @@ import ( "math" "net" "regexp" + "strconv" "strings" "k8s.io/apimachinery/pkg/util/validation/field" @@ -389,3 +390,18 @@ func hasChDirPrefix(value string) []string { } return errs } + +// IsSocketAddr checks that a string conforms is a valid socket address +// as defined in RFC 789. (e.g 0.0.0.0:10254 or [::]:10254)) +func IsValidSocketAddr(value string) []string { + var errs []string + ip, port, err := net.SplitHostPort(value) + if err != nil { + return append(errs, "must be a valid socket address format, (e.g. 0.0.0.0:10254 or [::]:10254)") + return errs + } + portInt, _ := strconv.Atoi(port) + errs = append(errs, IsValidPortNum(portInt)...) + errs = append(errs, IsValidIP(ip)...) + return errs +} diff --git a/staging/src/k8s.io/apimachinery/pkg/util/validation/validation_test.go b/staging/src/k8s.io/apimachinery/pkg/util/validation/validation_test.go index 4c628bbc44..b3892e1c9c 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/validation/validation_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/validation/validation_test.go @@ -511,3 +511,31 @@ func TestIsFullyQualifiedName(t *testing.T) { } } } + +func TestIsValidSocketAddr(t *testing.T) { + goodValues := []string{ + "0.0.0.0:10254", + "127.0.0.1:8888", + "[2001:db8:1f70::999:de8:7648:6e8]:10254", + "[::]:10254", + } + for _, val := range goodValues { + if errs := IsValidSocketAddr(val); len(errs) != 0 { + t.Errorf("expected no errors for %q: %v", val, errs) + } + } + + badValues := []string{ + "0.0.0.0.0:2020", + "0.0.0.0", + "6.6.6.6:909090", + "2001:db8:1f70::999:de8:7648:6e8:87567:102545", + "", + "*", + } + for _, val := range badValues { + if errs := IsValidSocketAddr(val); len(errs) == 0 { + t.Errorf("expected errors for %q", val) + } + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/apis/config/BUILD b/staging/src/k8s.io/apiserver/pkg/apis/config/BUILD index 4c6d33d75b..eadb944d51 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/config/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/apis/config/BUILD @@ -25,6 +25,7 @@ filegroup( srcs = [ ":package-srcs", "//staging/src/k8s.io/apiserver/pkg/apis/config/v1alpha1:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/apis/config/validation:all-srcs", ], tags = ["automanaged"], visibility = ["//visibility:public"], diff --git a/staging/src/k8s.io/apiserver/pkg/apis/config/validation/BUILD b/staging/src/k8s.io/apiserver/pkg/apis/config/validation/BUILD new file mode 100644 index 0000000000..e66a553c3c --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/apis/config/validation/BUILD @@ -0,0 +1,38 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["validation.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/apis/config/validation", + importpath = "k8s.io/apiserver/pkg/apis/config/validation", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/apis/config:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["validation_test.go"], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/apis/config:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/apiserver/pkg/apis/config/validation/validation.go b/staging/src/k8s.io/apiserver/pkg/apis/config/validation/validation.go new file mode 100644 index 0000000000..00cadf1013 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/apis/config/validation/validation.go @@ -0,0 +1,46 @@ +/* +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 ( + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/apis/config" +) + +// ValidateLeaderElectionConfiguration ensures validation of the LeaderElectionConfiguration struct +func ValidateLeaderElectionConfiguration(cc *config.LeaderElectionConfiguration, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if !cc.LeaderElect { + return allErrs + } + if cc.LeaseDuration.Duration <= 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("leaseDuration"), cc.LeaseDuration, "must be greater than zero")) + } + if cc.RenewDeadline.Duration <= 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("renewDeadline"), cc.LeaseDuration, "must be greater than zero")) + } + if cc.RetryPeriod.Duration <= 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("retryPeriod"), cc.RetryPeriod, "must be greater than zero")) + } + if cc.LeaseDuration.Duration < cc.RenewDeadline.Duration { + allErrs = append(allErrs, field.Invalid(fldPath.Child("leaseDuration"), cc.RenewDeadline, "LeaseDuration must be greater than RenewDeadline")) + } + if len(cc.ResourceLock) == 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceLock"), cc.RenewDeadline, "resourceLock is required")) + } + return allErrs +} diff --git a/staging/src/k8s.io/apiserver/pkg/apis/config/validation/validation_test.go b/staging/src/k8s.io/apiserver/pkg/apis/config/validation/validation_test.go new file mode 100644 index 0000000000..b55c9fb1ba --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/apis/config/validation/validation_test.go @@ -0,0 +1,112 @@ +/* +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 ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/apis/config" + "testing" + "time" +) + +func TestValidateLeaderElectionConfiguration(t *testing.T) { + validConfig := &config.LeaderElectionConfiguration{ + ResourceLock: "configmap", + LeaderElect: true, + LeaseDuration: metav1.Duration{Duration: 30 * time.Second}, + RenewDeadline: metav1.Duration{Duration: 15 * time.Second}, + RetryPeriod: metav1.Duration{Duration: 5 * time.Second}, + } + + renewDeadlineExceedsLeaseDuration := validConfig.DeepCopy() + renewDeadlineExceedsLeaseDuration.RenewDeadline = metav1.Duration{Duration: 45 * time.Second} + + renewDeadlineZero := validConfig.DeepCopy() + renewDeadlineZero.RenewDeadline = metav1.Duration{Duration: 0 * time.Second} + + leaseDurationZero := validConfig.DeepCopy() + leaseDurationZero.LeaseDuration = metav1.Duration{Duration: 0 * time.Second} + + negativeValForRetryPeriod := validConfig.DeepCopy() + negativeValForRetryPeriod.RetryPeriod = metav1.Duration{Duration: -45 * time.Second} + + negativeValForLeaseDuration := validConfig.DeepCopy() + negativeValForLeaseDuration.LeaseDuration = metav1.Duration{Duration: -45 * time.Second} + + negativeValForRenewDeadline := validConfig.DeepCopy() + negativeValForRenewDeadline.RenewDeadline = metav1.Duration{Duration: -45 * time.Second} + + LeaderElectButLeaderElectNotEnabled := validConfig.DeepCopy() + LeaderElectButLeaderElectNotEnabled.LeaderElect = false + LeaderElectButLeaderElectNotEnabled.LeaseDuration = metav1.Duration{Duration: -45 * time.Second} + + resourceLockNotDefined := validConfig.DeepCopy() + resourceLockNotDefined.ResourceLock = "" + + scenarios := map[string]struct { + expectedToFail bool + config *config.LeaderElectionConfiguration + }{ + "good": { + expectedToFail: false, + config: validConfig, + }, + "good-dont-check-leader-config-if-not-enabled": { + expectedToFail: false, + config: LeaderElectButLeaderElectNotEnabled, + }, + "bad-renew-deadline-exceeds-lease-duration": { + expectedToFail: true, + config: renewDeadlineExceedsLeaseDuration, + }, + "bad-negative-value-for-retry-period": { + expectedToFail: true, + config: negativeValForRetryPeriod, + }, + "bad-negative-value-for-lease-duration": { + expectedToFail: true, + config: negativeValForLeaseDuration, + }, + "bad-negative-value-for-renew-deadline": { + expectedToFail: true, + config: negativeValForRenewDeadline, + }, + "bad-renew-deadline-zero": { + expectedToFail: true, + config: renewDeadlineZero, + }, + "bad-lease-duration-zero": { + expectedToFail: true, + config: leaseDurationZero, + }, + "bad-resource-lock-not-defined": { + expectedToFail: true, + config: resourceLockNotDefined, + }, + } + + for name, scenario := range scenarios { + errs := ValidateLeaderElectionConfiguration(scenario.config, field.NewPath("leaderElectionConfiguration")) + if len(errs) == 0 && scenario.expectedToFail { + t.Errorf("Unexpected success for scenario: %s", name) + } + if len(errs) > 0 && !scenario.expectedToFail { + t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) + } + } +}