k8s.io/kubernetes@v1.29.3/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 if utilfeature.DefaultFeatureGate.Enabled(features.HPAContainerMetrics) { 321 allErrs = append(allErrs, field.Required(fldPath.Child("containerResource"), "must populate information for the given metric source")) 322 } else { 323 allErrs = append(allErrs, field.Required(fldPath.Child("containerResource"), "must populate information for the given metric source (only allowed when HPAContainerMetrics feature is enabled)")) 324 } 325 } 326 expectedField = "containerResource" 327 default: 328 allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), spec.Type, validMetricSourceTypesList)) 329 } 330 331 if typesPresent.Len() != 1 { 332 typesPresent.Delete(expectedField) 333 for typ := range typesPresent { 334 allErrs = append(allErrs, field.Forbidden(fldPath.Child(typ), "must populate the given metric source only")) 335 } 336 } 337 338 return allErrs 339 } 340 341 func validateObjectSource(src *autoscaling.ObjectMetricSource, fldPath *field.Path) field.ErrorList { 342 allErrs := field.ErrorList{} 343 344 allErrs = append(allErrs, ValidateCrossVersionObjectReference(src.DescribedObject, fldPath.Child("describedObject"))...) 345 allErrs = append(allErrs, validateMetricIdentifier(src.Metric, fldPath.Child("metric"))...) 346 allErrs = append(allErrs, validateMetricTarget(src.Target, fldPath.Child("target"))...) 347 348 if src.Target.Value == nil && src.Target.AverageValue == nil { 349 allErrs = append(allErrs, field.Required(fldPath.Child("target").Child("averageValue"), "must set either a target value or averageValue")) 350 } 351 352 return allErrs 353 } 354 355 func validateExternalSource(src *autoscaling.ExternalMetricSource, fldPath *field.Path) field.ErrorList { 356 allErrs := field.ErrorList{} 357 358 allErrs = append(allErrs, validateMetricIdentifier(src.Metric, fldPath.Child("metric"))...) 359 allErrs = append(allErrs, validateMetricTarget(src.Target, fldPath.Child("target"))...) 360 361 if src.Target.Value == nil && src.Target.AverageValue == nil { 362 allErrs = append(allErrs, field.Required(fldPath.Child("target").Child("averageValue"), "must set either a target value for metric or a per-pod target")) 363 } 364 365 if src.Target.Value != nil && src.Target.AverageValue != nil { 366 allErrs = append(allErrs, field.Forbidden(fldPath.Child("target").Child("value"), "may not set both a target value for metric and a per-pod target")) 367 } 368 369 return allErrs 370 } 371 372 func validatePodsSource(src *autoscaling.PodsMetricSource, fldPath *field.Path) field.ErrorList { 373 allErrs := field.ErrorList{} 374 375 allErrs = append(allErrs, validateMetricIdentifier(src.Metric, fldPath.Child("metric"))...) 376 allErrs = append(allErrs, validateMetricTarget(src.Target, fldPath.Child("target"))...) 377 378 if src.Target.AverageValue == nil { 379 allErrs = append(allErrs, field.Required(fldPath.Child("target").Child("averageValue"), "must specify a positive target averageValue")) 380 } 381 382 return allErrs 383 } 384 385 func validateContainerResourceSource(src *autoscaling.ContainerResourceMetricSource, fldPath *field.Path) field.ErrorList { 386 allErrs := field.ErrorList{} 387 388 if len(src.Name) == 0 { 389 allErrs = append(allErrs, field.Required(fldPath.Child("name"), "must specify a resource name")) 390 } else { 391 allErrs = append(allErrs, corevalidation.ValidateContainerResourceName(src.Name, fldPath.Child("name"))...) 392 } 393 394 if len(src.Container) == 0 { 395 allErrs = append(allErrs, field.Required(fldPath.Child("container"), "must specify a container")) 396 } else { 397 allErrs = append(allErrs, apivalidation.ValidateDNS1123Label(src.Container, fldPath.Child("container"))...) 398 } 399 400 allErrs = append(allErrs, validateMetricTarget(src.Target, fldPath.Child("target"))...) 401 402 if src.Target.AverageUtilization == nil && src.Target.AverageValue == nil { 403 allErrs = append(allErrs, field.Required(fldPath.Child("target").Child("averageUtilization"), "must set either a target raw value or a target utilization")) 404 } 405 406 if src.Target.AverageUtilization != nil && src.Target.AverageValue != nil { 407 allErrs = append(allErrs, field.Forbidden(fldPath.Child("target").Child("averageValue"), "may not set both a target raw value and a target utilization")) 408 } 409 410 return allErrs 411 } 412 413 func validateResourceSource(src *autoscaling.ResourceMetricSource, fldPath *field.Path) field.ErrorList { 414 allErrs := field.ErrorList{} 415 416 if len(src.Name) == 0 { 417 allErrs = append(allErrs, field.Required(fldPath.Child("name"), "must specify a resource name")) 418 } 419 420 allErrs = append(allErrs, validateMetricTarget(src.Target, fldPath.Child("target"))...) 421 422 if src.Target.AverageUtilization == nil && src.Target.AverageValue == nil { 423 allErrs = append(allErrs, field.Required(fldPath.Child("target").Child("averageUtilization"), "must set either a target raw value or a target utilization")) 424 } 425 426 if src.Target.AverageUtilization != nil && src.Target.AverageValue != nil { 427 allErrs = append(allErrs, field.Forbidden(fldPath.Child("target").Child("averageValue"), "may not set both a target raw value and a target utilization")) 428 } 429 430 return allErrs 431 } 432 433 func validateMetricTarget(mt autoscaling.MetricTarget, fldPath *field.Path) field.ErrorList { 434 allErrs := field.ErrorList{} 435 436 if len(mt.Type) == 0 { 437 allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must specify a metric target type")) 438 } 439 440 if mt.Type != autoscaling.UtilizationMetricType && 441 mt.Type != autoscaling.ValueMetricType && 442 mt.Type != autoscaling.AverageValueMetricType { 443 allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), mt.Type, "must be either Utilization, Value, or AverageValue")) 444 } 445 446 if mt.Value != nil && mt.Value.Sign() != 1 { 447 allErrs = append(allErrs, field.Invalid(fldPath.Child("value"), mt.Value, "must be positive")) 448 } 449 450 if mt.AverageValue != nil && mt.AverageValue.Sign() != 1 { 451 allErrs = append(allErrs, field.Invalid(fldPath.Child("averageValue"), mt.AverageValue, "must be positive")) 452 } 453 454 if mt.AverageUtilization != nil && *mt.AverageUtilization < 1 { 455 allErrs = append(allErrs, field.Invalid(fldPath.Child("averageUtilization"), mt.AverageUtilization, "must be greater than 0")) 456 } 457 458 return allErrs 459 } 460 461 func validateMetricIdentifier(id autoscaling.MetricIdentifier, fldPath *field.Path) field.ErrorList { 462 allErrs := field.ErrorList{} 463 464 if len(id.Name) == 0 { 465 allErrs = append(allErrs, field.Required(fldPath.Child("name"), "must specify a metric name")) 466 } else { 467 for _, msg := range pathvalidation.IsValidPathSegmentName(id.Name) { 468 allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), id.Name, msg)) 469 } 470 } 471 return allErrs 472 }