1
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
35 MaxPeriodSeconds int32 = 1800
36
37 MaxStabilizationWindowSeconds int32 = 3600
38 )
39
40
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
53
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
82
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
105
106 func ValidateHorizontalPodAutoscaler(autoscaler *autoscaling.HorizontalPodAutoscaler) field.ErrorList {
107 allErrs := apivalidation.ValidateObjectMeta(&autoscaler.ObjectMeta, true, ValidateHorizontalPodAutoscalerName, field.NewPath("metadata"))
108
109
110
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
123
124 func ValidateHorizontalPodAutoscalerUpdate(newAutoscaler, oldAutoscaler *autoscaling.HorizontalPodAutoscaler) field.ErrorList {
125 allErrs := apivalidation.ValidateObjectMetaUpdate(&newAutoscaler.ObjectMeta, &oldAutoscaler.ObjectMeta, field.NewPath("metadata"))
126
127
128
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
142
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 }
469
View as plain text