k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/apis/autoscaling/validation/validation.go (about) 1 /* 2 Copyright 2016 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 22 apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" 23 pathvalidation "k8s.io/apimachinery/pkg/api/validation/path" 24 "k8s.io/apimachinery/pkg/util/sets" 25 "k8s.io/apimachinery/pkg/util/validation/field" 26 utilfeature "k8s.io/apiserver/pkg/util/feature" 27 "k8s.io/kubernetes/pkg/apis/autoscaling" 28 corevalidation "k8s.io/kubernetes/pkg/apis/core/v1/validation" 29 apivalidation "k8s.io/kubernetes/pkg/apis/core/validation" 30 "k8s.io/kubernetes/pkg/features" 31 ) 32 33 const ( 34 // MaxPeriodSeconds is the largest allowed scaling policy period (in seconds) 35 MaxPeriodSeconds int32 = 1800 36 // MaxStabilizationWindowSeconds is the largest allowed stabilization window (in seconds) 37 MaxStabilizationWindowSeconds int32 = 3600 38 ) 39 40 // ValidateScale validates a Scale and returns an ErrorList with any errors. 41 func ValidateScale(scale *autoscaling.Scale) field.ErrorList { 42 allErrs := field.ErrorList{} 43 allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&scale.ObjectMeta, true, apimachineryvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))...) 44 45 if scale.Spec.Replicas < 0 { 46 allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "replicas"), scale.Spec.Replicas, "must be greater than or equal to 0")) 47 } 48 49 return allErrs 50 } 51 52 // ValidateHorizontalPodAutoscalerName can be used to check whether the given autoscaler name is valid. 53 // Prefix indicates this name will be used as part of generation, in which case trailing dashes are allowed. 54 var ValidateHorizontalPodAutoscalerName = apivalidation.ValidateReplicationControllerName 55 56 func validateHorizontalPodAutoscalerSpec(autoscaler autoscaling.HorizontalPodAutoscalerSpec, fldPath *field.Path, minReplicasLowerBound int32) field.ErrorList { 57 allErrs := field.ErrorList{} 58 59 if autoscaler.MinReplicas != nil && *autoscaler.MinReplicas < minReplicasLowerBound { 60 allErrs = append(allErrs, field.Invalid(fldPath.Child("minReplicas"), *autoscaler.MinReplicas, 61 fmt.Sprintf("must be greater than or equal to %d", minReplicasLowerBound))) 62 } 63 if autoscaler.MaxReplicas < 1 { 64 allErrs = append(allErrs, field.Invalid(fldPath.Child("maxReplicas"), autoscaler.MaxReplicas, "must be greater than 0")) 65 } 66 if autoscaler.MinReplicas != nil && autoscaler.MaxReplicas < *autoscaler.MinReplicas { 67 allErrs = append(allErrs, field.Invalid(fldPath.Child("maxReplicas"), autoscaler.MaxReplicas, "must be greater than or equal to `minReplicas`")) 68 } 69 if refErrs := ValidateCrossVersionObjectReference(autoscaler.ScaleTargetRef, fldPath.Child("scaleTargetRef")); len(refErrs) > 0 { 70 allErrs = append(allErrs, refErrs...) 71 } 72 if refErrs := validateMetrics(autoscaler.Metrics, fldPath.Child("metrics"), autoscaler.MinReplicas); len(refErrs) > 0 { 73 allErrs = append(allErrs, refErrs...) 74 } 75 if refErrs := validateBehavior(autoscaler.Behavior, fldPath.Child("behavior")); len(refErrs) > 0 { 76 allErrs = append(allErrs, refErrs...) 77 } 78 return allErrs 79 } 80 81 // ValidateCrossVersionObjectReference validates a CrossVersionObjectReference and returns an 82 // ErrorList with any errors. 83 func ValidateCrossVersionObjectReference(ref autoscaling.CrossVersionObjectReference, fldPath *field.Path) field.ErrorList { 84 allErrs := field.ErrorList{} 85 if len(ref.Kind) == 0 { 86 allErrs = append(allErrs, field.Required(fldPath.Child("kind"), "")) 87 } else { 88 for _, msg := range pathvalidation.IsValidPathSegmentName(ref.Kind) { 89 allErrs = append(allErrs, field.Invalid(fldPath.Child("kind"), ref.Kind, msg)) 90 } 91 } 92 93 if len(ref.Name) == 0 { 94 allErrs = append(allErrs, field.Required(fldPath.Child("name"), "")) 95 } else { 96 for _, msg := range pathvalidation.IsValidPathSegmentName(ref.Name) { 97 allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), ref.Name, msg)) 98 } 99 } 100 101 return allErrs 102 } 103 104 // ValidateHorizontalPodAutoscaler validates a HorizontalPodAutoscaler and returns an 105 // ErrorList with any errors. 106 func ValidateHorizontalPodAutoscaler(autoscaler *autoscaling.HorizontalPodAutoscaler) field.ErrorList { 107 allErrs := apivalidation.ValidateObjectMeta(&autoscaler.ObjectMeta, true, ValidateHorizontalPodAutoscalerName, field.NewPath("metadata")) 108 109 // MinReplicasLowerBound represents a minimum value for minReplicas 110 // 0 when HPA scale-to-zero feature is enabled 111 var minReplicasLowerBound int32 112 113 if utilfeature.DefaultFeatureGate.Enabled(features.HPAScaleToZero) { 114 minReplicasLowerBound = 0 115 } else { 116 minReplicasLowerBound = 1 117 } 118 allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(autoscaler.Spec, field.NewPath("spec"), minReplicasLowerBound)...) 119 return allErrs 120 } 121 122 // ValidateHorizontalPodAutoscalerUpdate validates an update to a HorizontalPodAutoscaler and returns an 123 // ErrorList with any errors. 124 func ValidateHorizontalPodAutoscalerUpdate(newAutoscaler, oldAutoscaler *autoscaling.HorizontalPodAutoscaler) field.ErrorList { 125 allErrs := apivalidation.ValidateObjectMetaUpdate(&newAutoscaler.ObjectMeta, &oldAutoscaler.ObjectMeta, field.NewPath("metadata")) 126 127 // minReplicasLowerBound represents a minimum value for minReplicas 128 // 0 when HPA scale-to-zero feature is enabled or HPA object already has minReplicas=0 129 var minReplicasLowerBound int32 130 131 if utilfeature.DefaultFeatureGate.Enabled(features.HPAScaleToZero) || (oldAutoscaler.Spec.MinReplicas != nil && *oldAutoscaler.Spec.MinReplicas == 0) { 132 minReplicasLowerBound = 0 133 } else { 134 minReplicasLowerBound = 1 135 } 136 137 allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(newAutoscaler.Spec, field.NewPath("spec"), minReplicasLowerBound)...) 138 return allErrs 139 } 140 141 // ValidateHorizontalPodAutoscalerStatusUpdate validates an update to status on a HorizontalPodAutoscaler and 142 // returns an ErrorList with any errors. 143 func ValidateHorizontalPodAutoscalerStatusUpdate(newAutoscaler, oldAutoscaler *autoscaling.HorizontalPodAutoscaler) field.ErrorList { 144 allErrs := apivalidation.ValidateObjectMetaUpdate(&newAutoscaler.ObjectMeta, &oldAutoscaler.ObjectMeta, field.NewPath("metadata")) 145 status := newAutoscaler.Status 146 allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.CurrentReplicas), field.NewPath("status", "currentReplicas"))...) 147 allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.DesiredReplicas), field.NewPath("status", "desiredReplicas"))...) 148 return allErrs 149 } 150 151 func validateMetrics(metrics []autoscaling.MetricSpec, fldPath *field.Path, minReplicas *int32) field.ErrorList { 152 allErrs := field.ErrorList{} 153 hasObjectMetrics := false 154 hasExternalMetrics := false 155 156 for i, metricSpec := range metrics { 157 idxPath := fldPath.Index(i) 158 if targetErrs := validateMetricSpec(metricSpec, idxPath); len(targetErrs) > 0 { 159 allErrs = append(allErrs, targetErrs...) 160 } 161 if metricSpec.Type == autoscaling.ObjectMetricSourceType { 162 hasObjectMetrics = true 163 } 164 if metricSpec.Type == autoscaling.ExternalMetricSourceType { 165 hasExternalMetrics = true 166 } 167 } 168 169 if minReplicas != nil && *minReplicas == 0 { 170 if !hasObjectMetrics && !hasExternalMetrics { 171 allErrs = append(allErrs, field.Forbidden(fldPath, "must specify at least one Object or External metric to support scaling to zero replicas")) 172 } 173 } 174 175 return allErrs 176 } 177 178 func validateBehavior(behavior *autoscaling.HorizontalPodAutoscalerBehavior, fldPath *field.Path) field.ErrorList { 179 allErrs := field.ErrorList{} 180 if behavior != nil { 181 if scaleUpErrs := validateScalingRules(behavior.ScaleUp, fldPath.Child("scaleUp")); len(scaleUpErrs) > 0 { 182 allErrs = append(allErrs, scaleUpErrs...) 183 } 184 if scaleDownErrs := validateScalingRules(behavior.ScaleDown, fldPath.Child("scaleDown")); len(scaleDownErrs) > 0 { 185 allErrs = append(allErrs, scaleDownErrs...) 186 } 187 } 188 return allErrs 189 } 190 191 var validSelectPolicyTypes = sets.NewString(string(autoscaling.MaxPolicySelect), string(autoscaling.MinPolicySelect), string(autoscaling.DisabledPolicySelect)) 192 var validSelectPolicyTypesList = validSelectPolicyTypes.List() 193 194 func validateScalingRules(rules *autoscaling.HPAScalingRules, fldPath *field.Path) field.ErrorList { 195 allErrs := field.ErrorList{} 196 if rules != nil { 197 if rules.StabilizationWindowSeconds != nil && *rules.StabilizationWindowSeconds < 0 { 198 allErrs = append(allErrs, field.Invalid(fldPath.Child("stabilizationWindowSeconds"), rules.StabilizationWindowSeconds, "must be greater than or equal to zero")) 199 } 200 if rules.StabilizationWindowSeconds != nil && *rules.StabilizationWindowSeconds > MaxStabilizationWindowSeconds { 201 allErrs = append(allErrs, field.Invalid(fldPath.Child("stabilizationWindowSeconds"), rules.StabilizationWindowSeconds, 202 fmt.Sprintf("must be less than or equal to %v", MaxStabilizationWindowSeconds))) 203 } 204 if rules.SelectPolicy != nil && !validSelectPolicyTypes.Has(string(*rules.SelectPolicy)) { 205 allErrs = append(allErrs, field.NotSupported(fldPath.Child("selectPolicy"), rules.SelectPolicy, validSelectPolicyTypesList)) 206 } 207 policiesPath := fldPath.Child("policies") 208 if len(rules.Policies) == 0 { 209 allErrs = append(allErrs, field.Required(policiesPath, "must specify at least one Policy")) 210 } 211 for i, policy := range rules.Policies { 212 idxPath := policiesPath.Index(i) 213 if policyErrs := validateScalingPolicy(policy, idxPath); len(policyErrs) > 0 { 214 allErrs = append(allErrs, policyErrs...) 215 } 216 } 217 } 218 return allErrs 219 } 220 221 var validPolicyTypes = sets.NewString(string(autoscaling.PodsScalingPolicy), string(autoscaling.PercentScalingPolicy)) 222 var validPolicyTypesList = validPolicyTypes.List() 223 224 func validateScalingPolicy(policy autoscaling.HPAScalingPolicy, fldPath *field.Path) field.ErrorList { 225 allErrs := field.ErrorList{} 226 if policy.Type != autoscaling.PodsScalingPolicy && policy.Type != autoscaling.PercentScalingPolicy { 227 allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), policy.Type, validPolicyTypesList)) 228 } 229 if policy.Value <= 0 { 230 allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), policy.Value, "must be greater than zero")) 231 } 232 if policy.PeriodSeconds <= 0 { 233 allErrs = append(allErrs, field.Invalid(fldPath.Child("periodSeconds"), policy.PeriodSeconds, "must be greater than zero")) 234 } 235 if policy.PeriodSeconds > MaxPeriodSeconds { 236 allErrs = append(allErrs, field.Invalid(fldPath.Child("periodSeconds"), policy.PeriodSeconds, 237 fmt.Sprintf("must be less than or equal to %v", MaxPeriodSeconds))) 238 } 239 return allErrs 240 } 241 242 var validMetricSourceTypes = sets.NewString( 243 string(autoscaling.ObjectMetricSourceType), string(autoscaling.PodsMetricSourceType), 244 string(autoscaling.ResourceMetricSourceType), string(autoscaling.ExternalMetricSourceType), 245 string(autoscaling.ContainerResourceMetricSourceType)) 246 var validMetricSourceTypesList = validMetricSourceTypes.List() 247 248 func validateMetricSpec(spec autoscaling.MetricSpec, fldPath *field.Path) field.ErrorList { 249 allErrs := field.ErrorList{} 250 251 if len(string(spec.Type)) == 0 { 252 allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must specify a metric source type")) 253 } 254 255 if !validMetricSourceTypes.Has(string(spec.Type)) { 256 allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), spec.Type, validMetricSourceTypesList)) 257 } 258 259 typesPresent := sets.NewString() 260 if spec.Object != nil { 261 typesPresent.Insert("object") 262 if typesPresent.Len() == 1 { 263 allErrs = append(allErrs, validateObjectSource(spec.Object, fldPath.Child("object"))...) 264 } 265 } 266 267 if spec.External != nil { 268 typesPresent.Insert("external") 269 if typesPresent.Len() == 1 { 270 allErrs = append(allErrs, validateExternalSource(spec.External, fldPath.Child("external"))...) 271 } 272 } 273 274 if spec.Pods != nil { 275 typesPresent.Insert("pods") 276 if typesPresent.Len() == 1 { 277 allErrs = append(allErrs, validatePodsSource(spec.Pods, fldPath.Child("pods"))...) 278 } 279 } 280 281 if spec.Resource != nil { 282 typesPresent.Insert("resource") 283 if typesPresent.Len() == 1 { 284 allErrs = append(allErrs, validateResourceSource(spec.Resource, fldPath.Child("resource"))...) 285 } 286 } 287 288 if spec.ContainerResource != nil { 289 typesPresent.Insert("containerResource") 290 if typesPresent.Len() == 1 { 291 allErrs = append(allErrs, validateContainerResourceSource(spec.ContainerResource, fldPath.Child("containerResource"))...) 292 } 293 } 294 295 var expectedField string 296 switch spec.Type { 297 298 case autoscaling.ObjectMetricSourceType: 299 if spec.Object == nil { 300 allErrs = append(allErrs, field.Required(fldPath.Child("object"), "must populate information for the given metric source")) 301 } 302 expectedField = "object" 303 case autoscaling.PodsMetricSourceType: 304 if spec.Pods == nil { 305 allErrs = append(allErrs, field.Required(fldPath.Child("pods"), "must populate information for the given metric source")) 306 } 307 expectedField = "pods" 308 case autoscaling.ResourceMetricSourceType: 309 if spec.Resource == nil { 310 allErrs = append(allErrs, field.Required(fldPath.Child("resource"), "must populate information for the given metric source")) 311 } 312 expectedField = "resource" 313 case autoscaling.ExternalMetricSourceType: 314 if spec.External == nil { 315 allErrs = append(allErrs, field.Required(fldPath.Child("external"), "must populate information for the given metric source")) 316 } 317 expectedField = "external" 318 case autoscaling.ContainerResourceMetricSourceType: 319 if spec.ContainerResource == nil { 320 allErrs = append(allErrs, field.Required(fldPath.Child("containerResource"), "must populate information for the given metric source")) 321 } 322 expectedField = "containerResource" 323 default: 324 allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), spec.Type, validMetricSourceTypesList)) 325 } 326 327 if typesPresent.Len() != 1 { 328 typesPresent.Delete(expectedField) 329 for typ := range typesPresent { 330 allErrs = append(allErrs, field.Forbidden(fldPath.Child(typ), "must populate the given metric source only")) 331 } 332 } 333 334 return allErrs 335 } 336 337 func validateObjectSource(src *autoscaling.ObjectMetricSource, fldPath *field.Path) field.ErrorList { 338 allErrs := field.ErrorList{} 339 340 allErrs = append(allErrs, ValidateCrossVersionObjectReference(src.DescribedObject, fldPath.Child("describedObject"))...) 341 allErrs = append(allErrs, validateMetricIdentifier(src.Metric, fldPath.Child("metric"))...) 342 allErrs = append(allErrs, validateMetricTarget(src.Target, fldPath.Child("target"))...) 343 344 if src.Target.Value == nil && src.Target.AverageValue == nil { 345 allErrs = append(allErrs, field.Required(fldPath.Child("target").Child("averageValue"), "must set either a target value or averageValue")) 346 } 347 348 return allErrs 349 } 350 351 func validateExternalSource(src *autoscaling.ExternalMetricSource, fldPath *field.Path) field.ErrorList { 352 allErrs := field.ErrorList{} 353 354 allErrs = append(allErrs, validateMetricIdentifier(src.Metric, fldPath.Child("metric"))...) 355 allErrs = append(allErrs, validateMetricTarget(src.Target, fldPath.Child("target"))...) 356 357 if src.Target.Value == nil && src.Target.AverageValue == nil { 358 allErrs = append(allErrs, field.Required(fldPath.Child("target").Child("averageValue"), "must set either a target value for metric or a per-pod target")) 359 } 360 361 if src.Target.Value != nil && src.Target.AverageValue != nil { 362 allErrs = append(allErrs, field.Forbidden(fldPath.Child("target").Child("value"), "may not set both a target value for metric and a per-pod target")) 363 } 364 365 return allErrs 366 } 367 368 func validatePodsSource(src *autoscaling.PodsMetricSource, fldPath *field.Path) field.ErrorList { 369 allErrs := field.ErrorList{} 370 371 allErrs = append(allErrs, validateMetricIdentifier(src.Metric, fldPath.Child("metric"))...) 372 allErrs = append(allErrs, validateMetricTarget(src.Target, fldPath.Child("target"))...) 373 374 if src.Target.AverageValue == nil { 375 allErrs = append(allErrs, field.Required(fldPath.Child("target").Child("averageValue"), "must specify a positive target averageValue")) 376 } 377 378 return allErrs 379 } 380 381 func validateContainerResourceSource(src *autoscaling.ContainerResourceMetricSource, fldPath *field.Path) field.ErrorList { 382 allErrs := field.ErrorList{} 383 384 if len(src.Name) == 0 { 385 allErrs = append(allErrs, field.Required(fldPath.Child("name"), "must specify a resource name")) 386 } else { 387 allErrs = append(allErrs, corevalidation.ValidateContainerResourceName(src.Name, fldPath.Child("name"))...) 388 } 389 390 if len(src.Container) == 0 { 391 allErrs = append(allErrs, field.Required(fldPath.Child("container"), "must specify a container")) 392 } else { 393 allErrs = append(allErrs, apivalidation.ValidateDNS1123Label(src.Container, fldPath.Child("container"))...) 394 } 395 396 allErrs = append(allErrs, validateMetricTarget(src.Target, fldPath.Child("target"))...) 397 398 if src.Target.AverageUtilization == nil && src.Target.AverageValue == nil { 399 allErrs = append(allErrs, field.Required(fldPath.Child("target").Child("averageUtilization"), "must set either a target raw value or a target utilization")) 400 } 401 402 if src.Target.AverageUtilization != nil && src.Target.AverageValue != nil { 403 allErrs = append(allErrs, field.Forbidden(fldPath.Child("target").Child("averageValue"), "may not set both a target raw value and a target utilization")) 404 } 405 406 return allErrs 407 } 408 409 func validateResourceSource(src *autoscaling.ResourceMetricSource, fldPath *field.Path) field.ErrorList { 410 allErrs := field.ErrorList{} 411 412 if len(src.Name) == 0 { 413 allErrs = append(allErrs, field.Required(fldPath.Child("name"), "must specify a resource name")) 414 } 415 416 allErrs = append(allErrs, validateMetricTarget(src.Target, fldPath.Child("target"))...) 417 418 if src.Target.AverageUtilization == nil && src.Target.AverageValue == nil { 419 allErrs = append(allErrs, field.Required(fldPath.Child("target").Child("averageUtilization"), "must set either a target raw value or a target utilization")) 420 } 421 422 if src.Target.AverageUtilization != nil && src.Target.AverageValue != nil { 423 allErrs = append(allErrs, field.Forbidden(fldPath.Child("target").Child("averageValue"), "may not set both a target raw value and a target utilization")) 424 } 425 426 return allErrs 427 } 428 429 func validateMetricTarget(mt autoscaling.MetricTarget, fldPath *field.Path) field.ErrorList { 430 allErrs := field.ErrorList{} 431 432 if len(mt.Type) == 0 { 433 allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must specify a metric target type")) 434 } 435 436 if mt.Type != autoscaling.UtilizationMetricType && 437 mt.Type != autoscaling.ValueMetricType && 438 mt.Type != autoscaling.AverageValueMetricType { 439 allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), mt.Type, "must be either Utilization, Value, or AverageValue")) 440 } 441 442 if mt.Value != nil && mt.Value.Sign() != 1 { 443 allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), mt.Value, "must be positive")) 444 } 445 446 if mt.AverageValue != nil && mt.AverageValue.Sign() != 1 { 447 allErrs = append(allErrs, field.Invalid(fldPath.Child("averageValue"), mt.AverageValue, "must be positive")) 448 } 449 450 if mt.AverageUtilization != nil && *mt.AverageUtilization < 1 { 451 allErrs = append(allErrs, field.Invalid(fldPath.Child("averageUtilization"), mt.AverageUtilization, "must be greater than 0")) 452 } 453 454 return allErrs 455 } 456 457 func validateMetricIdentifier(id autoscaling.MetricIdentifier, fldPath *field.Path) field.ErrorList { 458 allErrs := field.ErrorList{} 459 460 if len(id.Name) == 0 { 461 allErrs = append(allErrs, field.Required(fldPath.Child("name"), "must specify a metric name")) 462 } else { 463 for _, msg := range pathvalidation.IsValidPathSegmentName(id.Name) { 464 allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), id.Name, msg)) 465 } 466 } 467 return allErrs 468 }