k8s.io/kubernetes@v1.29.3/pkg/scheduler/apis/config/validation/validation.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 "fmt" 21 "net" 22 "reflect" 23 "strconv" 24 "strings" 25 26 "github.com/google/go-cmp/cmp" 27 v1 "k8s.io/api/core/v1" 28 "k8s.io/apimachinery/pkg/runtime" 29 utilerrors "k8s.io/apimachinery/pkg/util/errors" 30 "k8s.io/apimachinery/pkg/util/sets" 31 "k8s.io/apimachinery/pkg/util/validation" 32 "k8s.io/apimachinery/pkg/util/validation/field" 33 componentbasevalidation "k8s.io/component-base/config/validation" 34 v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper" 35 "k8s.io/kubernetes/pkg/scheduler/apis/config" 36 ) 37 38 // ValidateKubeSchedulerConfiguration ensures validation of the KubeSchedulerConfiguration struct 39 func ValidateKubeSchedulerConfiguration(cc *config.KubeSchedulerConfiguration) utilerrors.Aggregate { 40 var errs []error 41 errs = append(errs, componentbasevalidation.ValidateClientConnectionConfiguration(&cc.ClientConnection, field.NewPath("clientConnection")).ToAggregate()) 42 errs = append(errs, componentbasevalidation.ValidateLeaderElectionConfiguration(&cc.LeaderElection, field.NewPath("leaderElection")).ToAggregate()) 43 44 // TODO: This can be removed when ResourceLock is not available 45 // Only ResourceLock values with leases are allowed 46 if cc.LeaderElection.LeaderElect && cc.LeaderElection.ResourceLock != "leases" { 47 leaderElectionPath := field.NewPath("leaderElection") 48 errs = append(errs, field.Invalid(leaderElectionPath.Child("resourceLock"), cc.LeaderElection.ResourceLock, `resourceLock value must be "leases"`)) 49 } 50 51 profilesPath := field.NewPath("profiles") 52 if cc.Parallelism <= 0 { 53 errs = append(errs, field.Invalid(field.NewPath("parallelism"), cc.Parallelism, "should be an integer value greater than zero")) 54 } 55 56 if len(cc.Profiles) == 0 { 57 errs = append(errs, field.Required(profilesPath, "")) 58 } else { 59 existingProfiles := make(map[string]int, len(cc.Profiles)) 60 for i := range cc.Profiles { 61 profile := &cc.Profiles[i] 62 path := profilesPath.Index(i) 63 errs = append(errs, validateKubeSchedulerProfile(path, cc.APIVersion, profile)...) 64 if idx, ok := existingProfiles[profile.SchedulerName]; ok { 65 errs = append(errs, field.Duplicate(path.Child("schedulerName"), profilesPath.Index(idx).Child("schedulerName"))) 66 } 67 existingProfiles[profile.SchedulerName] = i 68 } 69 errs = append(errs, validateCommonQueueSort(profilesPath, cc.Profiles)...) 70 } 71 if len(cc.HealthzBindAddress) > 0 { 72 host, port, err := splitHostIntPort(cc.HealthzBindAddress) 73 if err != nil { 74 errs = append(errs, field.Invalid(field.NewPath("healthzBindAddress"), cc.HealthzBindAddress, err.Error())) 75 } else { 76 if errMsgs := validation.IsValidIP(host); errMsgs != nil { 77 errs = append(errs, field.Invalid(field.NewPath("healthzBindAddress"), cc.HealthzBindAddress, strings.Join(errMsgs, ","))) 78 } 79 if port != 0 { 80 errs = append(errs, field.Invalid(field.NewPath("healthzBindAddress"), cc.HealthzBindAddress, "must be empty or with an explicit 0 port")) 81 } 82 } 83 } 84 if len(cc.MetricsBindAddress) > 0 { 85 host, port, err := splitHostIntPort(cc.MetricsBindAddress) 86 if err != nil { 87 errs = append(errs, field.Invalid(field.NewPath("metricsBindAddress"), cc.MetricsBindAddress, err.Error())) 88 } else { 89 if errMsgs := validation.IsValidIP(host); errMsgs != nil { 90 errs = append(errs, field.Invalid(field.NewPath("metricsBindAddress"), cc.MetricsBindAddress, strings.Join(errMsgs, ","))) 91 } 92 if port != 0 { 93 errs = append(errs, field.Invalid(field.NewPath("metricsBindAddress"), cc.MetricsBindAddress, "must be empty or with an explicit 0 port")) 94 } 95 } 96 } 97 98 errs = append(errs, validatePercentageOfNodesToScore(field.NewPath("percentageOfNodesToScore"), cc.PercentageOfNodesToScore)) 99 100 if cc.PodInitialBackoffSeconds <= 0 { 101 errs = append(errs, field.Invalid(field.NewPath("podInitialBackoffSeconds"), 102 cc.PodInitialBackoffSeconds, "must be greater than 0")) 103 } 104 if cc.PodMaxBackoffSeconds < cc.PodInitialBackoffSeconds { 105 errs = append(errs, field.Invalid(field.NewPath("podMaxBackoffSeconds"), 106 cc.PodMaxBackoffSeconds, "must be greater than or equal to PodInitialBackoffSeconds")) 107 } 108 109 errs = append(errs, validateExtenders(field.NewPath("extenders"), cc.Extenders)...) 110 return utilerrors.Flatten(utilerrors.NewAggregate(errs)) 111 } 112 113 func splitHostIntPort(s string) (string, int, error) { 114 host, port, err := net.SplitHostPort(s) 115 if err != nil { 116 return "", 0, err 117 } 118 portInt, err := strconv.Atoi(port) 119 if err != nil { 120 return "", 0, err 121 } 122 return host, portInt, err 123 } 124 125 func validatePercentageOfNodesToScore(path *field.Path, percentageOfNodesToScore *int32) error { 126 if percentageOfNodesToScore != nil { 127 if *percentageOfNodesToScore < 0 || *percentageOfNodesToScore > 100 { 128 return field.Invalid(path, *percentageOfNodesToScore, "not in valid range [0-100]") 129 } 130 } 131 return nil 132 } 133 134 type invalidPlugins struct { 135 schemeGroupVersion string 136 plugins []string 137 } 138 139 // invalidPluginsByVersion maintains a list of removed/deprecated plugins in each version. 140 // Remember to add an entry to that list when creating a new component config 141 // version (even if the list of invalid plugins is empty). 142 var invalidPluginsByVersion = []invalidPlugins{ 143 { 144 schemeGroupVersion: v1.SchemeGroupVersion.String(), 145 plugins: []string{}, 146 }, 147 } 148 149 // isPluginInvalid checks if a given plugin was removed/deprecated in the given component 150 // config version or earlier. 151 func isPluginInvalid(apiVersion string, name string) (bool, string) { 152 for _, dp := range invalidPluginsByVersion { 153 for _, plugin := range dp.plugins { 154 if name == plugin { 155 return true, dp.schemeGroupVersion 156 } 157 } 158 if apiVersion == dp.schemeGroupVersion { 159 break 160 } 161 } 162 return false, "" 163 } 164 165 func validatePluginSetForInvalidPlugins(path *field.Path, apiVersion string, ps config.PluginSet) []error { 166 var errs []error 167 for i, plugin := range ps.Enabled { 168 if invalid, invalidVersion := isPluginInvalid(apiVersion, plugin.Name); invalid { 169 errs = append(errs, field.Invalid(path.Child("enabled").Index(i), plugin.Name, fmt.Sprintf("was invalid in version %q (KubeSchedulerConfiguration is version %q)", invalidVersion, apiVersion))) 170 } 171 } 172 return errs 173 } 174 175 func validateKubeSchedulerProfile(path *field.Path, apiVersion string, profile *config.KubeSchedulerProfile) []error { 176 var errs []error 177 if len(profile.SchedulerName) == 0 { 178 errs = append(errs, field.Required(path.Child("schedulerName"), "")) 179 } 180 errs = append(errs, validatePercentageOfNodesToScore(path.Child("percentageOfNodesToScore"), profile.PercentageOfNodesToScore)) 181 errs = append(errs, validatePluginConfig(path, apiVersion, profile)...) 182 return errs 183 } 184 185 func validatePluginConfig(path *field.Path, apiVersion string, profile *config.KubeSchedulerProfile) []error { 186 var errs []error 187 m := map[string]interface{}{ 188 "DefaultPreemption": ValidateDefaultPreemptionArgs, 189 "InterPodAffinity": ValidateInterPodAffinityArgs, 190 "NodeAffinity": ValidateNodeAffinityArgs, 191 "NodeResourcesBalancedAllocation": ValidateNodeResourcesBalancedAllocationArgs, 192 "NodeResourcesFitArgs": ValidateNodeResourcesFitArgs, 193 "PodTopologySpread": ValidatePodTopologySpreadArgs, 194 "VolumeBinding": ValidateVolumeBindingArgs, 195 } 196 197 if profile.Plugins != nil { 198 stagesToPluginSet := map[string]config.PluginSet{ 199 "preEnqueue": profile.Plugins.PreEnqueue, 200 "queueSort": profile.Plugins.QueueSort, 201 "preFilter": profile.Plugins.PreFilter, 202 "filter": profile.Plugins.Filter, 203 "postFilter": profile.Plugins.PostFilter, 204 "preScore": profile.Plugins.PreScore, 205 "score": profile.Plugins.Score, 206 "reserve": profile.Plugins.Reserve, 207 "permit": profile.Plugins.Permit, 208 "preBind": profile.Plugins.PreBind, 209 "bind": profile.Plugins.Bind, 210 "postBind": profile.Plugins.PostBind, 211 } 212 213 pluginsPath := path.Child("plugins") 214 for s, p := range stagesToPluginSet { 215 errs = append(errs, validatePluginSetForInvalidPlugins( 216 pluginsPath.Child(s), apiVersion, p)...) 217 } 218 } 219 220 seenPluginConfig := sets.New[string]() 221 222 for i := range profile.PluginConfig { 223 pluginConfigPath := path.Child("pluginConfig").Index(i) 224 name := profile.PluginConfig[i].Name 225 args := profile.PluginConfig[i].Args 226 if seenPluginConfig.Has(name) { 227 errs = append(errs, field.Duplicate(pluginConfigPath, name)) 228 } else { 229 seenPluginConfig.Insert(name) 230 } 231 if invalid, invalidVersion := isPluginInvalid(apiVersion, name); invalid { 232 errs = append(errs, field.Invalid(pluginConfigPath, name, fmt.Sprintf("was invalid in version %q (KubeSchedulerConfiguration is version %q)", invalidVersion, apiVersion))) 233 } else if validateFunc, ok := m[name]; ok { 234 // type mismatch, no need to validate the `args`. 235 if reflect.TypeOf(args) != reflect.ValueOf(validateFunc).Type().In(1) { 236 errs = append(errs, field.Invalid(pluginConfigPath.Child("args"), args, "has to match plugin args")) 237 } else { 238 in := []reflect.Value{reflect.ValueOf(pluginConfigPath.Child("args")), reflect.ValueOf(args)} 239 res := reflect.ValueOf(validateFunc).Call(in) 240 // It's possible that validation function return a Aggregate, just append here and it will be flattened at the end of CC validation. 241 if res[0].Interface() != nil { 242 errs = append(errs, res[0].Interface().(error)) 243 } 244 } 245 } 246 } 247 return errs 248 } 249 250 func validateCommonQueueSort(path *field.Path, profiles []config.KubeSchedulerProfile) []error { 251 var errs []error 252 var canon config.PluginSet 253 var queueSortName string 254 var queueSortArgs runtime.Object 255 if profiles[0].Plugins != nil { 256 canon = profiles[0].Plugins.QueueSort 257 if len(profiles[0].Plugins.QueueSort.Enabled) != 0 { 258 queueSortName = profiles[0].Plugins.QueueSort.Enabled[0].Name 259 } 260 length := len(profiles[0].Plugins.QueueSort.Enabled) 261 if length > 1 { 262 errs = append(errs, field.Invalid(path.Index(0).Child("plugins", "queueSort", "Enabled"), length, "only one queue sort plugin can be enabled")) 263 } 264 } 265 for _, cfg := range profiles[0].PluginConfig { 266 if len(queueSortName) > 0 && cfg.Name == queueSortName { 267 queueSortArgs = cfg.Args 268 } 269 } 270 for i := 1; i < len(profiles); i++ { 271 var curr config.PluginSet 272 if profiles[i].Plugins != nil { 273 curr = profiles[i].Plugins.QueueSort 274 } 275 if !cmp.Equal(canon, curr) { 276 errs = append(errs, field.Invalid(path.Index(i).Child("plugins", "queueSort"), curr, "has to match for all profiles")) 277 } 278 for _, cfg := range profiles[i].PluginConfig { 279 if cfg.Name == queueSortName && !cmp.Equal(queueSortArgs, cfg.Args) { 280 errs = append(errs, field.Invalid(path.Index(i).Child("pluginConfig", "args"), cfg.Args, "has to match for all profiles")) 281 } 282 } 283 } 284 return errs 285 } 286 287 // validateExtenders validates the configured extenders for the Scheduler 288 func validateExtenders(fldPath *field.Path, extenders []config.Extender) []error { 289 var errs []error 290 binders := 0 291 extenderManagedResources := sets.New[string]() 292 for i, extender := range extenders { 293 path := fldPath.Index(i) 294 if len(extender.PrioritizeVerb) > 0 && extender.Weight <= 0 { 295 errs = append(errs, field.Invalid(path.Child("weight"), 296 extender.Weight, "must have a positive weight applied to it")) 297 } 298 if extender.BindVerb != "" { 299 binders++ 300 } 301 for j, resource := range extender.ManagedResources { 302 managedResourcesPath := path.Child("managedResources").Index(j) 303 validationErrors := validateExtendedResourceName(managedResourcesPath.Child("name"), v1.ResourceName(resource.Name)) 304 errs = append(errs, validationErrors...) 305 if extenderManagedResources.Has(resource.Name) { 306 errs = append(errs, field.Invalid(managedResourcesPath.Child("name"), 307 resource.Name, "duplicate extender managed resource name")) 308 } 309 extenderManagedResources.Insert(resource.Name) 310 } 311 } 312 if binders > 1 { 313 errs = append(errs, field.Invalid(fldPath, fmt.Sprintf("found %d extenders implementing bind", binders), "only one extender can implement bind")) 314 } 315 return errs 316 } 317 318 // validateExtendedResourceName checks whether the specified name is a valid 319 // extended resource name. 320 func validateExtendedResourceName(path *field.Path, name v1.ResourceName) []error { 321 var validationErrors []error 322 for _, msg := range validation.IsQualifiedName(string(name)) { 323 validationErrors = append(validationErrors, field.Invalid(path, name, msg)) 324 } 325 if len(validationErrors) != 0 { 326 return validationErrors 327 } 328 if !v1helper.IsExtendedResourceName(name) { 329 validationErrors = append(validationErrors, field.Invalid(path, string(name), "is an invalid extended resource name")) 330 } 331 return validationErrors 332 }