diff --git a/pkg/apis/autoscaling/types.go b/pkg/apis/autoscaling/types.go index aa7e0ca0159..db9ac44083b 100644 --- a/pkg/apis/autoscaling/types.go +++ b/pkg/apis/autoscaling/types.go @@ -174,6 +174,10 @@ type HPAScalingRules struct { // replicas (e.g. 0.01 for 1%). Must be greater than or equal to zero. If not // set, the default cluster-wide tolerance is applied (by default 10%). // + // For example, if autoscaling is configured with a memory consumption target of 100Mi, + // and scale-down and scale-up tolerances of 5% and 1% respectively, scaling will be + // triggered when the actual consumption falls below 95Mi or exceeds 101Mi. + // // This is an alpha field and requires enabling the HPAConfigurableTolerance // feature gate. // diff --git a/pkg/apis/autoscaling/validation/validation.go b/pkg/apis/autoscaling/validation/validation.go index 8b5ac06c442..aa643694a55 100644 --- a/pkg/apis/autoscaling/validation/validation.go +++ b/pkg/apis/autoscaling/validation/validation.go @@ -23,11 +23,9 @@ import ( pathvalidation "k8s.io/apimachinery/pkg/api/validation/path" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" - utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/apis/autoscaling" corevalidation "k8s.io/kubernetes/pkg/apis/core/v1/validation" apivalidation "k8s.io/kubernetes/pkg/apis/core/validation" - "k8s.io/kubernetes/pkg/features" ) const ( @@ -103,48 +101,16 @@ func ValidateCrossVersionObjectReference(ref autoscaling.CrossVersionObjectRefer // ValidateHorizontalPodAutoscaler validates a HorizontalPodAutoscaler and returns an // ErrorList with any errors. -func ValidateHorizontalPodAutoscaler(autoscaler *autoscaling.HorizontalPodAutoscaler) field.ErrorList { +func ValidateHorizontalPodAutoscaler(autoscaler *autoscaling.HorizontalPodAutoscaler, opts HorizontalPodAutoscalerSpecValidationOptions) field.ErrorList { allErrs := apivalidation.ValidateObjectMeta(&autoscaler.ObjectMeta, true, ValidateHorizontalPodAutoscalerName, field.NewPath("metadata")) - - // MinReplicasLowerBound represents a minimum value for minReplicas - // 0 when HPA scale-to-zero feature is enabled - var minReplicasLowerBound int32 - - if utilfeature.DefaultFeatureGate.Enabled(features.HPAScaleToZero) { - minReplicasLowerBound = 0 - } else { - minReplicasLowerBound = 1 - } - - opts := HorizontalPodAutoscalerSpecValidationOptions{ - AllowTolerance: utilfeature.DefaultMutableFeatureGate.Enabled(features.HPAConfigurableTolerance), - MinReplicasLowerBound: minReplicasLowerBound, - } - allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(autoscaler.Spec, field.NewPath("spec"), opts)...) return allErrs } // ValidateHorizontalPodAutoscalerUpdate validates an update to a HorizontalPodAutoscaler and returns an // ErrorList with any errors. -func ValidateHorizontalPodAutoscalerUpdate(newAutoscaler, oldAutoscaler *autoscaling.HorizontalPodAutoscaler) field.ErrorList { +func ValidateHorizontalPodAutoscalerUpdate(newAutoscaler, oldAutoscaler *autoscaling.HorizontalPodAutoscaler, opts HorizontalPodAutoscalerSpecValidationOptions) field.ErrorList { allErrs := apivalidation.ValidateObjectMetaUpdate(&newAutoscaler.ObjectMeta, &oldAutoscaler.ObjectMeta, field.NewPath("metadata")) - - // minReplicasLowerBound represents a minimum value for minReplicas - // 0 when HPA scale-to-zero feature is enabled or HPA object already has minReplicas=0 - var minReplicasLowerBound int32 - - if utilfeature.DefaultFeatureGate.Enabled(features.HPAScaleToZero) || (oldAutoscaler.Spec.MinReplicas != nil && *oldAutoscaler.Spec.MinReplicas == 0) { - minReplicasLowerBound = 0 - } else { - minReplicasLowerBound = 1 - } - - opts := HorizontalPodAutoscalerSpecValidationOptions{ - AllowTolerance: utilfeature.DefaultMutableFeatureGate.Enabled(features.HPAConfigurableTolerance), - MinReplicasLowerBound: minReplicasLowerBound, - } - allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(newAutoscaler.Spec, field.NewPath("spec"), opts)...) return allErrs } @@ -162,8 +128,6 @@ func ValidateHorizontalPodAutoscalerStatusUpdate(newAutoscaler, oldAutoscaler *a // HorizontalPodAutoscalerSpecValidationOptions contains the different settings for // HorizontalPodAutoscaler spec validation. type HorizontalPodAutoscalerSpecValidationOptions struct { - // Allow setting a tolerance on HPAScalingRules. - AllowTolerance bool // The minimum value for minReplicas. MinReplicasLowerBound int32 } @@ -234,12 +198,8 @@ func validateScalingRules(rules *autoscaling.HPAScalingRules, fldPath *field.Pat allErrs = append(allErrs, policyErrs...) } } - - if rules.Tolerance != nil && !opts.AllowTolerance { - allErrs = append(allErrs, field.Forbidden(fldPath.Child("tolerance"), "tolerance is not supported when the HPAConfigurableTolerance feature gate is not enabled")) - } - if rules.Tolerance != nil && rules.Tolerance.Sign() < 0 { - allErrs = append(allErrs, field.Invalid(fldPath.Child("tolerance"), rules.Tolerance, "must be greater or equal to zero")) + if rules.Tolerance != nil { + allErrs = append(allErrs, apivalidation.ValidateNonnegativeQuantity(*rules.Tolerance, fldPath.Child("tolerance"))...) } } return allErrs diff --git a/pkg/apis/autoscaling/validation/validation_test.go b/pkg/apis/autoscaling/validation/validation_test.go index 003e97c1edc..aeac3c46381 100644 --- a/pkg/apis/autoscaling/validation/validation_test.go +++ b/pkg/apis/autoscaling/validation/validation_test.go @@ -22,15 +22,21 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - utilfeature "k8s.io/apiserver/pkg/util/feature" - featuregatetesting "k8s.io/component-base/featuregate/testing" "k8s.io/kubernetes/pkg/apis/autoscaling" api "k8s.io/kubernetes/pkg/apis/core" - "k8s.io/kubernetes/pkg/features" utilpointer "k8s.io/utils/pointer" "k8s.io/utils/ptr" ) +var ( + hpaSpecValidationOpts = HorizontalPodAutoscalerSpecValidationOptions{ + MinReplicasLowerBound: 1, + } + hpaScaleToZeroSpecValidationOpts = HorizontalPodAutoscalerSpecValidationOptions{ + MinReplicasLowerBound: 0, + } +) + func TestValidateScale(t *testing.T) { successCases := []autoscaling.Scale{{ ObjectMeta: metav1.ObjectMeta{ @@ -163,7 +169,7 @@ func TestValidateBehavior(t *testing.T) { }} for _, behavior := range successCases { hpa := prepareHPAWithBehavior(behavior) - if errs := ValidateHorizontalPodAutoscaler(&hpa); len(errs) != 0 { + if errs := ValidateHorizontalPodAutoscaler(&hpa, hpaSpecValidationOpts); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -357,7 +363,7 @@ func TestValidateBehavior(t *testing.T) { }} for _, c := range errorCases { hpa := prepareHPAWithBehavior(c.behavior) - if errs := ValidateHorizontalPodAutoscaler(&hpa); len(errs) == 0 { + if errs := ValidateHorizontalPodAutoscaler(&hpa, hpaSpecValidationOpts); len(errs) == 0 { t.Errorf("expected failure for %s", c.msg) } else if !strings.Contains(errs[0].Error(), c.msg) { t.Errorf("unexpected error: %v, expected: %s", errs[0], c.msg) @@ -615,7 +621,7 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { }, }} for _, successCase := range successCases { - if errs := ValidateHorizontalPodAutoscaler(&successCase); len(errs) != 0 { + if errs := ValidateHorizontalPodAutoscaler(&successCase, hpaSpecValidationOpts); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -1419,7 +1425,7 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { } for _, c := range errorCases { - errs := ValidateHorizontalPodAutoscaler(&c.horizontalPodAutoscaler) + errs := ValidateHorizontalPodAutoscaler(&c.horizontalPodAutoscaler, hpaSpecValidationOpts) if len(errs) == 0 { t.Errorf("expected failure for %q", c.msg) } else if !strings.Contains(errs[0].Error(), c.msg) { @@ -1490,7 +1496,7 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { MinReplicas: utilpointer.Int32(1), MaxReplicas: 5, Metrics: []autoscaling.MetricSpec{spec}, }, - }) + }, hpaSpecValidationOpts) expectedMsg := "must populate information for the given metric source" @@ -1570,26 +1576,20 @@ func prepareMinReplicasCases(t *testing.T, minReplicas int32) []autoscaling.Hori } func TestValidateHorizontalPodAutoscalerScaleToZeroEnabled(t *testing.T) { - // Enable HPAScaleToZero feature gate. - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAScaleToZero, true) - zeroMinReplicasCases := prepareMinReplicasCases(t, 0) for _, successCase := range zeroMinReplicasCases { - if errs := ValidateHorizontalPodAutoscaler(&successCase); len(errs) != 0 { + if errs := ValidateHorizontalPodAutoscaler(&successCase, hpaScaleToZeroSpecValidationOpts); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } } func TestValidateHorizontalPodAutoscalerScaleToZeroDisabled(t *testing.T) { - // Disable HPAScaleToZero feature gate. - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAScaleToZero, false) - zeroMinReplicasCases := prepareMinReplicasCases(t, 0) errorMsg := "must be greater than or equal to 1" for _, errorCase := range zeroMinReplicasCases { - errs := ValidateHorizontalPodAutoscaler(&errorCase) + errs := ValidateHorizontalPodAutoscaler(&errorCase, hpaSpecValidationOpts) if len(errs) == 0 { t.Errorf("expected failure for %q", errorMsg) } else if !strings.Contains(errs[0].Error(), errorMsg) { @@ -1601,43 +1601,37 @@ func TestValidateHorizontalPodAutoscalerScaleToZeroDisabled(t *testing.T) { for _, successCase := range nonZeroMinReplicasCases { successCase.Spec.MinReplicas = utilpointer.Int32(1) - if errs := ValidateHorizontalPodAutoscaler(&successCase); len(errs) != 0 { + if errs := ValidateHorizontalPodAutoscaler(&successCase, hpaSpecValidationOpts); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } } func TestValidateHorizontalPodAutoscalerUpdateScaleToZeroEnabled(t *testing.T) { - // Enable HPAScaleToZero feature gate. - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAScaleToZero, true) - zeroMinReplicasCases := prepareMinReplicasCases(t, 0) nonZeroMinReplicasCases := prepareMinReplicasCases(t, 1) for i, zeroCase := range zeroMinReplicasCases { nonZeroCase := nonZeroMinReplicasCases[i] - if errs := ValidateHorizontalPodAutoscalerUpdate(&nonZeroCase, &zeroCase); len(errs) != 0 { + if errs := ValidateHorizontalPodAutoscalerUpdate(&nonZeroCase, &zeroCase, hpaScaleToZeroSpecValidationOpts); len(errs) != 0 { t.Errorf("expected success: %v", errs) } - if errs := ValidateHorizontalPodAutoscalerUpdate(&zeroCase, &nonZeroCase); len(errs) != 0 { + if errs := ValidateHorizontalPodAutoscalerUpdate(&zeroCase, &nonZeroCase, hpaScaleToZeroSpecValidationOpts); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } } func TestValidateHorizontalPodAutoscalerScaleToZeroUpdateDisabled(t *testing.T) { - // Disable HPAScaleToZero feature gate. - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAScaleToZero, false) - zeroMinReplicasCases := prepareMinReplicasCases(t, 0) nonZeroMinReplicasCases := prepareMinReplicasCases(t, 1) errorMsg := "must be greater than or equal to 1" for i, zeroCase := range zeroMinReplicasCases { nonZeroCase := nonZeroMinReplicasCases[i] - errs := ValidateHorizontalPodAutoscalerUpdate(&zeroCase, &nonZeroCase) + errs := ValidateHorizontalPodAutoscalerUpdate(&zeroCase, &nonZeroCase, hpaSpecValidationOpts) if len(errs) == 0 { t.Errorf("expected failure for %q", errorMsg) @@ -1645,20 +1639,13 @@ func TestValidateHorizontalPodAutoscalerScaleToZeroUpdateDisabled(t *testing.T) t.Errorf("unexpected error: %q, expected: %q", errs[0], errorMsg) } - if errs := ValidateHorizontalPodAutoscalerUpdate(&zeroCase, &zeroCase); len(errs) != 0 { - t.Errorf("expected success: %v", errs) - } - - if errs := ValidateHorizontalPodAutoscalerUpdate(&nonZeroCase, &zeroCase); len(errs) != 0 { + if errs := ValidateHorizontalPodAutoscalerUpdate(&nonZeroCase, &zeroCase, hpaSpecValidationOpts); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } } func TestValidateHorizontalPodAutoscalerConfigurableToleranceEnabled(t *testing.T) { - // Enable HPAConfigurableTolerance feature gate. - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAConfigurableTolerance, true) - policiesList := []autoscaling.HPAScalingPolicy{{ Type: autoscaling.PodsScalingPolicy, Value: 1, @@ -1695,7 +1682,7 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceEnabled(t *testing. ScaleDown: &c, } hpa := prepareHPAWithBehavior(b) - if errs := ValidateHorizontalPodAutoscaler(&hpa); len(errs) != 0 { + if errs := ValidateHorizontalPodAutoscaler(&hpa, hpaScaleToZeroSpecValidationOpts); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -1713,14 +1700,14 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceEnabled(t *testing. Policies: policiesList, Tolerance: ptr.To(resource.MustParse("-0.001")), }, - msg: "greater or equal to zero", + msg: "greater than or equal to 0", }, { rule: autoscaling.HPAScalingRules{ Policies: policiesList, Tolerance: resource.NewMilliQuantity(-10, resource.DecimalSI), }, - msg: "greater or equal to zero", + msg: "greater than or equal to 0", }, { rule: autoscaling.HPAScalingRules{ @@ -1741,7 +1728,7 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceEnabled(t *testing. ScaleUp: &c.rule, } hpa := prepareHPAWithBehavior(b) - errs := ValidateHorizontalPodAutoscaler(&hpa) + errs := ValidateHorizontalPodAutoscaler(&hpa, hpaScaleToZeroSpecValidationOpts) if len(errs) != 1 { t.Fatalf("expected exactly one error, got: %v", errs) } @@ -1752,9 +1739,6 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceEnabled(t *testing. } func TestValidateHorizontalPodAutoscalerConfigurableToleranceDisabled(t *testing.T) { - // Disable HPAConfigurableTolerance feature gate. - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAConfigurableTolerance, false) - maxPolicy := autoscaling.MaxPolicySelect policiesList := []autoscaling.HPAScalingPolicy{{ Type: autoscaling.PodsScalingPolicy, @@ -1780,7 +1764,7 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceDisabled(t *testing ScaleDown: &c, } hpa := prepareHPAWithBehavior(b) - if errs := ValidateHorizontalPodAutoscaler(&hpa); len(errs) != 0 { + if errs := ValidateHorizontalPodAutoscaler(&hpa, hpaSpecValidationOpts); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } @@ -1789,13 +1773,6 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceDisabled(t *testing rule autoscaling.HPAScalingRules msg string }{ - { - rule: autoscaling.HPAScalingRules{ - Policies: policiesList, - Tolerance: resource.NewMilliQuantity(1, resource.DecimalSI), - }, - msg: "not supported", - }, { rule: autoscaling.HPAScalingRules{}, msg: "at least one Policy", @@ -1812,7 +1789,7 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceDisabled(t *testing ScaleUp: &c.rule, } hpa := prepareHPAWithBehavior(b) - errs := ValidateHorizontalPodAutoscaler(&hpa) + errs := ValidateHorizontalPodAutoscaler(&hpa, hpaSpecValidationOpts) if len(errs) != 1 { t.Fatalf("expected exactly one error, got: %v", errs) } @@ -1823,9 +1800,6 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceDisabled(t *testing } func TestValidateHorizontalPodAutoscalerUpdateConfigurableToleranceEnabled(t *testing.T) { - // Enable HPAConfigurableTolerance feature gate. - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAConfigurableTolerance, true) - policiesList := []autoscaling.HPAScalingPolicy{{ Type: autoscaling.PodsScalingPolicy, Value: 1, @@ -1842,48 +1816,11 @@ func TestValidateHorizontalPodAutoscalerUpdateConfigurableToleranceEnabled(t *te Policies: policiesList, }}) - if errs := ValidateHorizontalPodAutoscalerUpdate(&withToleranceHPA, &withoutToleranceHPA); len(errs) != 0 { + if errs := ValidateHorizontalPodAutoscalerUpdate(&withToleranceHPA, &withoutToleranceHPA, hpaScaleToZeroSpecValidationOpts); len(errs) != 0 { t.Errorf("expected success: %v", errs) } - if errs := ValidateHorizontalPodAutoscalerUpdate(&withoutToleranceHPA, &withToleranceHPA); len(errs) != 0 { - t.Errorf("expected success: %v", errs) - } -} - -func TestValidateHorizontalPodAutoscalerConfigurableToleranceUpdateDisabled(t *testing.T) { - // Disable HPAConfigurableTolerance feature gate. - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAConfigurableTolerance, false) - - policiesList := []autoscaling.HPAScalingPolicy{{ - Type: autoscaling.PodsScalingPolicy, - Value: 1, - PeriodSeconds: 1800, - }} - - withToleranceHPA := prepareHPAWithBehavior(autoscaling.HorizontalPodAutoscalerBehavior{ - ScaleUp: &autoscaling.HPAScalingRules{ - Policies: policiesList, - Tolerance: resource.NewMilliQuantity(10, resource.DecimalSI), - }}) - withoutToleranceHPA := prepareHPAWithBehavior(autoscaling.HorizontalPodAutoscalerBehavior{ - ScaleUp: &autoscaling.HPAScalingRules{ - Policies: policiesList, - }}) - notSupportedErrorMsg := "not supported" - - errs := ValidateHorizontalPodAutoscalerUpdate(&withToleranceHPA, &withoutToleranceHPA) - if len(errs) == 0 { - t.Errorf("expected failure for %q", notSupportedErrorMsg) - } else if !strings.Contains(errs[0].Error(), notSupportedErrorMsg) { - t.Errorf("unexpected error: %q, expected: %q", errs[0], notSupportedErrorMsg) - } - - if errs := ValidateHorizontalPodAutoscalerUpdate(&withoutToleranceHPA, &withoutToleranceHPA); len(errs) != 0 { - t.Errorf("expected success: %v", errs) - } - - if errs := ValidateHorizontalPodAutoscalerUpdate(&withoutToleranceHPA, &withToleranceHPA); len(errs) != 0 { + if errs := ValidateHorizontalPodAutoscalerUpdate(&withoutToleranceHPA, &withToleranceHPA, hpaScaleToZeroSpecValidationOpts); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } diff --git a/pkg/registry/autoscaling/horizontalpodautoscaler/strategy.go b/pkg/registry/autoscaling/horizontalpodautoscaler/strategy.go index 8558440f7aa..ddd374de51e 100644 --- a/pkg/registry/autoscaling/horizontalpodautoscaler/strategy.go +++ b/pkg/registry/autoscaling/horizontalpodautoscaler/strategy.go @@ -22,9 +22,11 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apiserver/pkg/storage/names" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/autoscaling" "k8s.io/kubernetes/pkg/apis/autoscaling/validation" + "k8s.io/kubernetes/pkg/features" "sigs.k8s.io/structured-merge-diff/v4/fieldpath" ) @@ -70,12 +72,15 @@ func (autoscalerStrategy) PrepareForCreate(ctx context.Context, obj runtime.Obje // create cannot set status newHPA.Status = autoscaling.HorizontalPodAutoscalerStatus{} + + dropDisabledFields(newHPA, nil) } // Validate validates a new autoscaler. func (autoscalerStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { autoscaler := obj.(*autoscaling.HorizontalPodAutoscaler) - return validation.ValidateHorizontalPodAutoscaler(autoscaler) + opts := validationOptionsForHorizontalPodAutoscaler(autoscaler, nil) + return validation.ValidateHorizontalPodAutoscaler(autoscaler, opts) } // WarningsOnCreate returns warnings for the creation of the given object. @@ -98,11 +103,16 @@ func (autoscalerStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime oldHPA := old.(*autoscaling.HorizontalPodAutoscaler) // Update is not allowed to set status newHPA.Status = oldHPA.Status + + dropDisabledFields(newHPA, oldHPA) } // ValidateUpdate is the default update validation for an end user. func (autoscalerStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { - return validation.ValidateHorizontalPodAutoscalerUpdate(obj.(*autoscaling.HorizontalPodAutoscaler), old.(*autoscaling.HorizontalPodAutoscaler)) + newHPA := obj.(*autoscaling.HorizontalPodAutoscaler) + oldHPA := old.(*autoscaling.HorizontalPodAutoscaler) + opts := validationOptionsForHorizontalPodAutoscaler(newHPA, oldHPA) + return validation.ValidateHorizontalPodAutoscalerUpdate(newHPA, oldHPA, opts) } // WarningsOnUpdate returns warnings for the given update. @@ -157,3 +167,48 @@ func (autoscalerStatusStrategy) ValidateUpdate(ctx context.Context, obj, old run func (autoscalerStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { return nil } + +func validationOptionsForHorizontalPodAutoscaler(newHPA, oldHPA *autoscaling.HorizontalPodAutoscaler) validation.HorizontalPodAutoscalerSpecValidationOptions { + opts := validation.HorizontalPodAutoscalerSpecValidationOptions{ + MinReplicasLowerBound: 1, + } + + oldHasZeroMinReplicas := oldHPA != nil && (oldHPA.Spec.MinReplicas != nil && *oldHPA.Spec.MinReplicas == 0) + if utilfeature.DefaultFeatureGate.Enabled(features.HPAScaleToZero) || oldHasZeroMinReplicas { + opts.MinReplicasLowerBound = 0 + } + return opts +} + +// dropDisabledFields will drop any disabled fields that have not previously been +// set on the old HPA. oldHPA is ignored if nil. +func dropDisabledFields(newHPA, oldHPA *autoscaling.HorizontalPodAutoscaler) { + if utilfeature.DefaultFeatureGate.Enabled(features.HPAConfigurableTolerance) { + return + } + if toleranceInUse(oldHPA) { + return + } + newBehavior := newHPA.Spec.Behavior + if newBehavior == nil { + return + } + + for _, sr := range []*autoscaling.HPAScalingRules{newBehavior.ScaleDown, newBehavior.ScaleUp} { + if sr != nil { + sr.Tolerance = nil + } + } +} + +func toleranceInUse(hpa *autoscaling.HorizontalPodAutoscaler) bool { + if hpa == nil || hpa.Spec.Behavior == nil { + return false + } + for _, sr := range []*autoscaling.HPAScalingRules{hpa.Spec.Behavior.ScaleDown, hpa.Spec.Behavior.ScaleUp} { + if sr != nil && sr.Tolerance != nil { + return true + } + } + return false +} diff --git a/pkg/registry/autoscaling/horizontalpodautoscaler/strategy_test.go b/pkg/registry/autoscaling/horizontalpodautoscaler/strategy_test.go new file mode 100644 index 00000000000..ba055629100 --- /dev/null +++ b/pkg/registry/autoscaling/horizontalpodautoscaler/strategy_test.go @@ -0,0 +1,161 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package horizontalpodautoscaler + +import ( + "context" + "testing" + + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" + "k8s.io/kubernetes/pkg/apis/autoscaling" + api "k8s.io/kubernetes/pkg/apis/core" + "k8s.io/kubernetes/pkg/features" + "k8s.io/utils/ptr" +) + +type toleranceSet bool +type zeroMinReplicasSet bool + +const ( + withTolerance toleranceSet = true + withoutTolerance = false + zeroMinReplicas zeroMinReplicasSet = true + oneMinReplicas = false +) + +func TestPrepareForCreateConfigurableToleranceEnabled(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAConfigurableTolerance, true) + hpa := prepareHPA(oneMinReplicas, withTolerance) + + Strategy.PrepareForCreate(context.Background(), &hpa) + if hpa.Spec.Behavior.ScaleUp.Tolerance == nil { + t.Error("Expected tolerance field, got none") + } +} + +func TestPrepareForCreateConfigurableToleranceDisabled(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAConfigurableTolerance, false) + hpa := prepareHPA(oneMinReplicas, withTolerance) + + Strategy.PrepareForCreate(context.Background(), &hpa) + if hpa.Spec.Behavior.ScaleUp.Tolerance != nil { + t.Errorf("Expected tolerance field wiped out, got %v", hpa.Spec.Behavior.ScaleUp.Tolerance) + } +} + +func TestPrepareForUpdateConfigurableToleranceEnabled(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAConfigurableTolerance, true) + newHPA := prepareHPA(oneMinReplicas, withTolerance) + oldHPA := prepareHPA(oneMinReplicas, withTolerance) + + Strategy.PrepareForUpdate(context.Background(), &newHPA, &oldHPA) + if newHPA.Spec.Behavior.ScaleUp.Tolerance == nil { + t.Error("Expected tolerance field, got none") + } +} + +func TestPrepareForUpdateConfigurableToleranceDisabled(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAConfigurableTolerance, false) + newHPA := prepareHPA(oneMinReplicas, withTolerance) + oldHPA := prepareHPA(oneMinReplicas, withoutTolerance) + + Strategy.PrepareForUpdate(context.Background(), &newHPA, &oldHPA) + if newHPA.Spec.Behavior.ScaleUp.Tolerance != nil { + t.Errorf("Expected tolerance field wiped out, got %v", newHPA.Spec.Behavior.ScaleUp.Tolerance) + } + + newHPA = prepareHPA(oneMinReplicas, withTolerance) + oldHPA = prepareHPA(oneMinReplicas, withTolerance) + Strategy.PrepareForUpdate(context.Background(), &newHPA, &oldHPA) + if newHPA.Spec.Behavior.ScaleUp.Tolerance == nil { + t.Errorf("Expected tolerance field not wiped out, got nil") + } +} + +func TestValidateOptionsScaleToZeroEnabled(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAScaleToZero, true) + oneReplicasHPA := prepareHPA(oneMinReplicas, withoutTolerance) + + opts := validationOptionsForHorizontalPodAutoscaler(&oneReplicasHPA, &oneReplicasHPA) + if opts.MinReplicasLowerBound != 0 { + t.Errorf("Expected zero minReplicasLowerBound, got %v", opts.MinReplicasLowerBound) + } +} + +func TestValidateOptionsScaleToZeroDisabled(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAScaleToZero, false) + zeroReplicasHPA := prepareHPA(zeroMinReplicas, withoutTolerance) + oneReplicasHPA := prepareHPA(oneMinReplicas, withoutTolerance) + + // MinReplicas should be 0 despite the gate being disabled since the old HPA + // had MinReplicas set to 0 already. + opts := validationOptionsForHorizontalPodAutoscaler(&zeroReplicasHPA, &zeroReplicasHPA) + if opts.MinReplicasLowerBound != 0 { + t.Errorf("Expected zero minReplicasLowerBound, got %v", opts.MinReplicasLowerBound) + } + + opts = validationOptionsForHorizontalPodAutoscaler(&zeroReplicasHPA, &oneReplicasHPA) + if opts.MinReplicasLowerBound == 0 { + t.Errorf("Expected non-zero minReplicasLowerBound, got 0") + } +} + +func prepareHPA(hasZeroMinReplicas zeroMinReplicasSet, hasTolerance toleranceSet) autoscaling.HorizontalPodAutoscaler { + tolerance := ptr.To(resource.MustParse("0.1")) + if !hasTolerance { + tolerance = nil + } + + minReplicas := int32(0) + if !hasZeroMinReplicas { + minReplicas = 1 + } + + return autoscaling.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myautoscaler", + Namespace: metav1.NamespaceDefault, + ResourceVersion: "1", + }, + Spec: autoscaling.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscaling.CrossVersionObjectReference{ + Kind: "ReplicationController", + Name: "myrc", + }, + MinReplicas: &minReplicas, + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{{ + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + Target: autoscaling.MetricTarget{ + Type: autoscaling.UtilizationMetricType, + AverageUtilization: ptr.To(int32(70)), + }, + }, + }}, + Behavior: &autoscaling.HorizontalPodAutoscalerBehavior{ + ScaleUp: &autoscaling.HPAScalingRules{ + Tolerance: tolerance, + }, + }, + }, + } +} diff --git a/staging/src/k8s.io/api/autoscaling/v2/types.go b/staging/src/k8s.io/api/autoscaling/v2/types.go index e6b9a5fc205..9ce69b1edc8 100644 --- a/staging/src/k8s.io/api/autoscaling/v2/types.go +++ b/staging/src/k8s.io/api/autoscaling/v2/types.go @@ -211,6 +211,10 @@ type HPAScalingRules struct { // replicas (e.g. 0.01 for 1%). Must be greater than or equal to zero. If not // set, the default cluster-wide tolerance is applied (by default 10%). // + // For example, if autoscaling is configured with a memory consumption target of 100Mi, + // and scale-down and scale-up tolerances of 5% and 1% respectively, scaling will be + // triggered when the actual consumption falls below 95Mi or exceeds 101Mi. + // // This is an alpha field and requires enabling the HPAConfigurableTolerance // feature gate. //