k8s.io/kubernetes@v1.29.3/pkg/scheduler/apis/config/validation/validation_pluginargs.go (about) 1 /* 2 Copyright 2020 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 "strings" 22 23 v1 "k8s.io/api/core/v1" 24 metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" 25 "k8s.io/apimachinery/pkg/util/errors" 26 "k8s.io/apimachinery/pkg/util/sets" 27 "k8s.io/apimachinery/pkg/util/validation/field" 28 utilfeature "k8s.io/apiserver/pkg/util/feature" 29 "k8s.io/component-helpers/scheduling/corev1/nodeaffinity" 30 "k8s.io/kubernetes/pkg/features" 31 "k8s.io/kubernetes/pkg/scheduler/apis/config" 32 ) 33 34 // supportedScoringStrategyTypes has to be a set of strings for use with field.Unsupported 35 var supportedScoringStrategyTypes = sets.New( 36 string(config.LeastAllocated), 37 string(config.MostAllocated), 38 string(config.RequestedToCapacityRatio), 39 ) 40 41 // ValidateDefaultPreemptionArgs validates that DefaultPreemptionArgs are correct. 42 func ValidateDefaultPreemptionArgs(path *field.Path, args *config.DefaultPreemptionArgs) error { 43 var allErrs field.ErrorList 44 percentagePath := path.Child("minCandidateNodesPercentage") 45 absolutePath := path.Child("minCandidateNodesAbsolute") 46 if err := validateMinCandidateNodesPercentage(args.MinCandidateNodesPercentage, percentagePath); err != nil { 47 allErrs = append(allErrs, err) 48 } 49 if err := validateMinCandidateNodesAbsolute(args.MinCandidateNodesAbsolute, absolutePath); err != nil { 50 allErrs = append(allErrs, err) 51 } 52 if args.MinCandidateNodesPercentage == 0 && args.MinCandidateNodesAbsolute == 0 { 53 allErrs = append(allErrs, 54 field.Invalid(percentagePath, args.MinCandidateNodesPercentage, "cannot be zero at the same time as minCandidateNodesAbsolute"), 55 field.Invalid(absolutePath, args.MinCandidateNodesAbsolute, "cannot be zero at the same time as minCandidateNodesPercentage")) 56 } 57 return allErrs.ToAggregate() 58 } 59 60 // validateMinCandidateNodesPercentage validates that 61 // minCandidateNodesPercentage is within the allowed range. 62 func validateMinCandidateNodesPercentage(minCandidateNodesPercentage int32, p *field.Path) *field.Error { 63 if minCandidateNodesPercentage < 0 || minCandidateNodesPercentage > 100 { 64 return field.Invalid(p, minCandidateNodesPercentage, "not in valid range [0, 100]") 65 } 66 return nil 67 } 68 69 // validateMinCandidateNodesAbsolute validates that minCandidateNodesAbsolute 70 // is within the allowed range. 71 func validateMinCandidateNodesAbsolute(minCandidateNodesAbsolute int32, p *field.Path) *field.Error { 72 if minCandidateNodesAbsolute < 0 { 73 return field.Invalid(p, minCandidateNodesAbsolute, "not in valid range [0, inf)") 74 } 75 return nil 76 } 77 78 // ValidateInterPodAffinityArgs validates that InterPodAffinityArgs are correct. 79 func ValidateInterPodAffinityArgs(path *field.Path, args *config.InterPodAffinityArgs) error { 80 return validateHardPodAffinityWeight(path.Child("hardPodAffinityWeight"), args.HardPodAffinityWeight) 81 } 82 83 // validateHardPodAffinityWeight validates that weight is within allowed range. 84 func validateHardPodAffinityWeight(path *field.Path, w int32) error { 85 const ( 86 minHardPodAffinityWeight = 0 87 maxHardPodAffinityWeight = 100 88 ) 89 90 if w < minHardPodAffinityWeight || w > maxHardPodAffinityWeight { 91 msg := fmt.Sprintf("not in valid range [%d, %d]", minHardPodAffinityWeight, maxHardPodAffinityWeight) 92 return field.Invalid(path, w, msg) 93 } 94 return nil 95 } 96 97 // ValidatePodTopologySpreadArgs validates that PodTopologySpreadArgs are correct. 98 // It replicates the validation from pkg/apis/core/validation.validateTopologySpreadConstraints 99 // with an additional check for .labelSelector to be nil. 100 func ValidatePodTopologySpreadArgs(path *field.Path, args *config.PodTopologySpreadArgs) error { 101 var allErrs field.ErrorList 102 if err := validateDefaultingType(path.Child("defaultingType"), args.DefaultingType, args.DefaultConstraints); err != nil { 103 allErrs = append(allErrs, err) 104 } 105 106 defaultConstraintsPath := path.Child("defaultConstraints") 107 for i, c := range args.DefaultConstraints { 108 p := defaultConstraintsPath.Index(i) 109 if c.MaxSkew <= 0 { 110 f := p.Child("maxSkew") 111 allErrs = append(allErrs, field.Invalid(f, c.MaxSkew, "not in valid range (0, inf)")) 112 } 113 allErrs = append(allErrs, validateTopologyKey(p.Child("topologyKey"), c.TopologyKey)...) 114 if err := validateWhenUnsatisfiable(p.Child("whenUnsatisfiable"), c.WhenUnsatisfiable); err != nil { 115 allErrs = append(allErrs, err) 116 } 117 if c.LabelSelector != nil { 118 f := field.Forbidden(p.Child("labelSelector"), "constraint must not define a selector, as they deduced for each pod") 119 allErrs = append(allErrs, f) 120 } 121 if err := validateConstraintNotRepeat(defaultConstraintsPath, args.DefaultConstraints, i); err != nil { 122 allErrs = append(allErrs, err) 123 } 124 } 125 if len(allErrs) == 0 { 126 return nil 127 } 128 return allErrs.ToAggregate() 129 } 130 131 func validateDefaultingType(p *field.Path, v config.PodTopologySpreadConstraintsDefaulting, constraints []v1.TopologySpreadConstraint) *field.Error { 132 if v != config.SystemDefaulting && v != config.ListDefaulting { 133 return field.NotSupported(p, v, []string{string(config.SystemDefaulting), string(config.ListDefaulting)}) 134 } 135 if v == config.SystemDefaulting && len(constraints) > 0 { 136 return field.Invalid(p, v, "when .defaultConstraints are not empty") 137 } 138 return nil 139 } 140 141 func validateTopologyKey(p *field.Path, v string) field.ErrorList { 142 var allErrs field.ErrorList 143 if len(v) == 0 { 144 allErrs = append(allErrs, field.Required(p, "can not be empty")) 145 } else { 146 allErrs = append(allErrs, metav1validation.ValidateLabelName(v, p)...) 147 } 148 return allErrs 149 } 150 151 func validateWhenUnsatisfiable(p *field.Path, v v1.UnsatisfiableConstraintAction) *field.Error { 152 supportedScheduleActions := sets.New(string(v1.DoNotSchedule), string(v1.ScheduleAnyway)) 153 154 if len(v) == 0 { 155 return field.Required(p, "can not be empty") 156 } 157 if !supportedScheduleActions.Has(string(v)) { 158 return field.NotSupported(p, v, sets.List(supportedScheduleActions)) 159 } 160 return nil 161 } 162 163 func validateConstraintNotRepeat(path *field.Path, constraints []v1.TopologySpreadConstraint, idx int) *field.Error { 164 c := &constraints[idx] 165 for i := range constraints[:idx] { 166 other := &constraints[i] 167 if c.TopologyKey == other.TopologyKey && c.WhenUnsatisfiable == other.WhenUnsatisfiable { 168 return field.Duplicate(path.Index(idx), fmt.Sprintf("{%v, %v}", c.TopologyKey, c.WhenUnsatisfiable)) 169 } 170 } 171 return nil 172 } 173 174 func validateFunctionShape(shape []config.UtilizationShapePoint, path *field.Path) field.ErrorList { 175 const ( 176 minUtilization = 0 177 maxUtilization = 100 178 minScore = 0 179 maxScore = int32(config.MaxCustomPriorityScore) 180 ) 181 182 var allErrs field.ErrorList 183 184 if len(shape) == 0 { 185 allErrs = append(allErrs, field.Required(path, "at least one point must be specified")) 186 return allErrs 187 } 188 189 for i := 1; i < len(shape); i++ { 190 if shape[i-1].Utilization >= shape[i].Utilization { 191 allErrs = append(allErrs, field.Invalid(path.Index(i).Child("utilization"), shape[i].Utilization, "utilization values must be sorted in increasing order")) 192 break 193 } 194 } 195 196 for i, point := range shape { 197 if point.Utilization < minUtilization || point.Utilization > maxUtilization { 198 msg := fmt.Sprintf("not in valid range [%d, %d]", minUtilization, maxUtilization) 199 allErrs = append(allErrs, field.Invalid(path.Index(i).Child("utilization"), point.Utilization, msg)) 200 } 201 202 if point.Score < minScore || point.Score > maxScore { 203 msg := fmt.Sprintf("not in valid range [%d, %d]", minScore, maxScore) 204 allErrs = append(allErrs, field.Invalid(path.Index(i).Child("score"), point.Score, msg)) 205 } 206 } 207 208 return allErrs 209 } 210 211 func validateResources(resources []config.ResourceSpec, p *field.Path) field.ErrorList { 212 var allErrs field.ErrorList 213 for i, resource := range resources { 214 if resource.Weight <= 0 || resource.Weight > 100 { 215 msg := fmt.Sprintf("resource weight of %v not in valid range (0, 100]", resource.Name) 216 allErrs = append(allErrs, field.Invalid(p.Index(i).Child("weight"), resource.Weight, msg)) 217 } 218 } 219 return allErrs 220 } 221 222 // ValidateNodeResourcesBalancedAllocationArgs validates that NodeResourcesBalancedAllocationArgs are set correctly. 223 func ValidateNodeResourcesBalancedAllocationArgs(path *field.Path, args *config.NodeResourcesBalancedAllocationArgs) error { 224 var allErrs field.ErrorList 225 seenResources := sets.New[string]() 226 for i, resource := range args.Resources { 227 if seenResources.Has(resource.Name) { 228 allErrs = append(allErrs, field.Duplicate(path.Child("resources").Index(i).Child("name"), resource.Name)) 229 } else { 230 seenResources.Insert(resource.Name) 231 } 232 if resource.Weight != 1 { 233 allErrs = append(allErrs, field.Invalid(path.Child("resources").Index(i).Child("weight"), resource.Weight, "must be 1")) 234 } 235 } 236 return allErrs.ToAggregate() 237 } 238 239 // ValidateNodeAffinityArgs validates that NodeAffinityArgs are correct. 240 func ValidateNodeAffinityArgs(path *field.Path, args *config.NodeAffinityArgs) error { 241 if args.AddedAffinity == nil { 242 return nil 243 } 244 affinity := args.AddedAffinity 245 var errs []error 246 if ns := affinity.RequiredDuringSchedulingIgnoredDuringExecution; ns != nil { 247 _, err := nodeaffinity.NewNodeSelector(ns, field.WithPath(path.Child("addedAffinity", "requiredDuringSchedulingIgnoredDuringExecution"))) 248 if err != nil { 249 errs = append(errs, err) 250 } 251 } 252 // TODO: Add validation for requiredDuringSchedulingRequiredDuringExecution when it gets added to the API. 253 if terms := affinity.PreferredDuringSchedulingIgnoredDuringExecution; len(terms) != 0 { 254 _, err := nodeaffinity.NewPreferredSchedulingTerms(terms, field.WithPath(path.Child("addedAffinity", "preferredDuringSchedulingIgnoredDuringExecution"))) 255 if err != nil { 256 errs = append(errs, err) 257 } 258 } 259 return errors.Flatten(errors.NewAggregate(errs)) 260 } 261 262 // VolumeBindingArgsValidationOptions contains the different settings for validation. 263 type VolumeBindingArgsValidationOptions struct { 264 AllowVolumeCapacityPriority bool 265 } 266 267 // ValidateVolumeBindingArgs validates that VolumeBindingArgs are set correctly. 268 func ValidateVolumeBindingArgs(path *field.Path, args *config.VolumeBindingArgs) error { 269 return ValidateVolumeBindingArgsWithOptions(path, args, VolumeBindingArgsValidationOptions{ 270 AllowVolumeCapacityPriority: utilfeature.DefaultFeatureGate.Enabled(features.VolumeCapacityPriority), 271 }) 272 } 273 274 // ValidateVolumeBindingArgsWithOptions validates that VolumeBindingArgs and VolumeBindingArgsValidationOptions with scheduler features. 275 func ValidateVolumeBindingArgsWithOptions(path *field.Path, args *config.VolumeBindingArgs, opts VolumeBindingArgsValidationOptions) error { 276 var allErrs field.ErrorList 277 278 if args.BindTimeoutSeconds < 0 { 279 allErrs = append(allErrs, field.Invalid(path.Child("bindTimeoutSeconds"), args.BindTimeoutSeconds, "invalid BindTimeoutSeconds, should not be a negative value")) 280 } 281 282 if opts.AllowVolumeCapacityPriority { 283 allErrs = append(allErrs, validateFunctionShape(args.Shape, path.Child("shape"))...) 284 } else if args.Shape != nil { 285 // When the feature is off, return an error if the config is not nil. 286 // This prevents unexpected configuration from taking effect when the 287 // feature turns on in the future. 288 allErrs = append(allErrs, field.Invalid(path.Child("shape"), args.Shape, "unexpected field `shape`, remove it or turn on the feature gate VolumeCapacityPriority")) 289 } 290 return allErrs.ToAggregate() 291 } 292 293 func ValidateNodeResourcesFitArgs(path *field.Path, args *config.NodeResourcesFitArgs) error { 294 var allErrs field.ErrorList 295 resPath := path.Child("ignoredResources") 296 for i, res := range args.IgnoredResources { 297 path := resPath.Index(i) 298 if errs := metav1validation.ValidateLabelName(res, path); len(errs) != 0 { 299 allErrs = append(allErrs, errs...) 300 } 301 } 302 303 groupPath := path.Child("ignoredResourceGroups") 304 for i, group := range args.IgnoredResourceGroups { 305 path := groupPath.Index(i) 306 if strings.Contains(group, "/") { 307 allErrs = append(allErrs, field.Invalid(path, group, "resource group name can't contain '/'")) 308 } 309 if errs := metav1validation.ValidateLabelName(group, path); len(errs) != 0 { 310 allErrs = append(allErrs, errs...) 311 } 312 } 313 314 strategyPath := path.Child("scoringStrategy") 315 if args.ScoringStrategy != nil { 316 if !supportedScoringStrategyTypes.Has(string(args.ScoringStrategy.Type)) { 317 allErrs = append(allErrs, field.NotSupported(strategyPath.Child("type"), args.ScoringStrategy.Type, sets.List(supportedScoringStrategyTypes))) 318 } 319 allErrs = append(allErrs, validateResources(args.ScoringStrategy.Resources, strategyPath.Child("resources"))...) 320 if args.ScoringStrategy.RequestedToCapacityRatio != nil { 321 allErrs = append(allErrs, validateFunctionShape(args.ScoringStrategy.RequestedToCapacityRatio.Shape, strategyPath.Child("shape"))...) 322 } 323 } 324 325 if len(allErrs) == 0 { 326 return nil 327 } 328 return allErrs.ToAggregate() 329 }