Fix API doc and tolerance field handling when gate is flipped.

This commit is contained in:
Jean-Marc François 2025-03-21 17:14:12 -04:00
parent 2dd9eda47f
commit dc1696d807
6 changed files with 259 additions and 138 deletions

View File

@ -174,6 +174,10 @@ type HPAScalingRules struct {
// replicas (e.g. 0.01 for 1%). Must be greater than or equal to zero. If not // 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%). // 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 // This is an alpha field and requires enabling the HPAConfigurableTolerance
// feature gate. // feature gate.
// //

View File

@ -23,11 +23,9 @@ import (
pathvalidation "k8s.io/apimachinery/pkg/api/validation/path" pathvalidation "k8s.io/apimachinery/pkg/api/validation/path"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/apis/autoscaling" "k8s.io/kubernetes/pkg/apis/autoscaling"
corevalidation "k8s.io/kubernetes/pkg/apis/core/v1/validation" corevalidation "k8s.io/kubernetes/pkg/apis/core/v1/validation"
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation" apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/kubernetes/pkg/features"
) )
const ( const (
@ -103,48 +101,16 @@ func ValidateCrossVersionObjectReference(ref autoscaling.CrossVersionObjectRefer
// ValidateHorizontalPodAutoscaler validates a HorizontalPodAutoscaler and returns an // ValidateHorizontalPodAutoscaler validates a HorizontalPodAutoscaler and returns an
// ErrorList with any errors. // 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")) 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)...) allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(autoscaler.Spec, field.NewPath("spec"), opts)...)
return allErrs return allErrs
} }
// ValidateHorizontalPodAutoscalerUpdate validates an update to a HorizontalPodAutoscaler and returns an // ValidateHorizontalPodAutoscalerUpdate validates an update to a HorizontalPodAutoscaler and returns an
// ErrorList with any errors. // 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")) 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)...) allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(newAutoscaler.Spec, field.NewPath("spec"), opts)...)
return allErrs return allErrs
} }
@ -162,8 +128,6 @@ func ValidateHorizontalPodAutoscalerStatusUpdate(newAutoscaler, oldAutoscaler *a
// HorizontalPodAutoscalerSpecValidationOptions contains the different settings for // HorizontalPodAutoscalerSpecValidationOptions contains the different settings for
// HorizontalPodAutoscaler spec validation. // HorizontalPodAutoscaler spec validation.
type HorizontalPodAutoscalerSpecValidationOptions struct { type HorizontalPodAutoscalerSpecValidationOptions struct {
// Allow setting a tolerance on HPAScalingRules.
AllowTolerance bool
// The minimum value for minReplicas. // The minimum value for minReplicas.
MinReplicasLowerBound int32 MinReplicasLowerBound int32
} }
@ -234,12 +198,8 @@ func validateScalingRules(rules *autoscaling.HPAScalingRules, fldPath *field.Pat
allErrs = append(allErrs, policyErrs...) allErrs = append(allErrs, policyErrs...)
} }
} }
if rules.Tolerance != nil {
if rules.Tolerance != nil && !opts.AllowTolerance { allErrs = append(allErrs, apivalidation.ValidateNonnegativeQuantity(*rules.Tolerance, fldPath.Child("tolerance"))...)
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"))
} }
} }
return allErrs return allErrs

View File

@ -22,15 +22,21 @@ import (
"k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 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" "k8s.io/kubernetes/pkg/apis/autoscaling"
api "k8s.io/kubernetes/pkg/apis/core" api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/features"
utilpointer "k8s.io/utils/pointer" utilpointer "k8s.io/utils/pointer"
"k8s.io/utils/ptr" "k8s.io/utils/ptr"
) )
var (
hpaSpecValidationOpts = HorizontalPodAutoscalerSpecValidationOptions{
MinReplicasLowerBound: 1,
}
hpaScaleToZeroSpecValidationOpts = HorizontalPodAutoscalerSpecValidationOptions{
MinReplicasLowerBound: 0,
}
)
func TestValidateScale(t *testing.T) { func TestValidateScale(t *testing.T) {
successCases := []autoscaling.Scale{{ successCases := []autoscaling.Scale{{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -163,7 +169,7 @@ func TestValidateBehavior(t *testing.T) {
}} }}
for _, behavior := range successCases { for _, behavior := range successCases {
hpa := prepareHPAWithBehavior(behavior) hpa := prepareHPAWithBehavior(behavior)
if errs := ValidateHorizontalPodAutoscaler(&hpa); len(errs) != 0 { if errs := ValidateHorizontalPodAutoscaler(&hpa, hpaSpecValidationOpts); len(errs) != 0 {
t.Errorf("expected success: %v", errs) t.Errorf("expected success: %v", errs)
} }
} }
@ -357,7 +363,7 @@ func TestValidateBehavior(t *testing.T) {
}} }}
for _, c := range errorCases { for _, c := range errorCases {
hpa := prepareHPAWithBehavior(c.behavior) 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) t.Errorf("expected failure for %s", c.msg)
} else if !strings.Contains(errs[0].Error(), c.msg) { } else if !strings.Contains(errs[0].Error(), c.msg) {
t.Errorf("unexpected error: %v, expected: %s", errs[0], 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 { 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) t.Errorf("expected success: %v", errs)
} }
} }
@ -1419,7 +1425,7 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) {
} }
for _, c := range errorCases { for _, c := range errorCases {
errs := ValidateHorizontalPodAutoscaler(&c.horizontalPodAutoscaler) errs := ValidateHorizontalPodAutoscaler(&c.horizontalPodAutoscaler, hpaSpecValidationOpts)
if len(errs) == 0 { if len(errs) == 0 {
t.Errorf("expected failure for %q", c.msg) t.Errorf("expected failure for %q", c.msg)
} else if !strings.Contains(errs[0].Error(), c.msg) { } else if !strings.Contains(errs[0].Error(), c.msg) {
@ -1490,7 +1496,7 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) {
MinReplicas: utilpointer.Int32(1), MinReplicas: utilpointer.Int32(1),
MaxReplicas: 5, Metrics: []autoscaling.MetricSpec{spec}, MaxReplicas: 5, Metrics: []autoscaling.MetricSpec{spec},
}, },
}) }, hpaSpecValidationOpts)
expectedMsg := "must populate information for the given metric source" 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) { func TestValidateHorizontalPodAutoscalerScaleToZeroEnabled(t *testing.T) {
// Enable HPAScaleToZero feature gate.
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAScaleToZero, true)
zeroMinReplicasCases := prepareMinReplicasCases(t, 0) zeroMinReplicasCases := prepareMinReplicasCases(t, 0)
for _, successCase := range zeroMinReplicasCases { 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) t.Errorf("expected success: %v", errs)
} }
} }
} }
func TestValidateHorizontalPodAutoscalerScaleToZeroDisabled(t *testing.T) { func TestValidateHorizontalPodAutoscalerScaleToZeroDisabled(t *testing.T) {
// Disable HPAScaleToZero feature gate.
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAScaleToZero, false)
zeroMinReplicasCases := prepareMinReplicasCases(t, 0) zeroMinReplicasCases := prepareMinReplicasCases(t, 0)
errorMsg := "must be greater than or equal to 1" errorMsg := "must be greater than or equal to 1"
for _, errorCase := range zeroMinReplicasCases { for _, errorCase := range zeroMinReplicasCases {
errs := ValidateHorizontalPodAutoscaler(&errorCase) errs := ValidateHorizontalPodAutoscaler(&errorCase, hpaSpecValidationOpts)
if len(errs) == 0 { if len(errs) == 0 {
t.Errorf("expected failure for %q", errorMsg) t.Errorf("expected failure for %q", errorMsg)
} else if !strings.Contains(errs[0].Error(), errorMsg) { } else if !strings.Contains(errs[0].Error(), errorMsg) {
@ -1601,43 +1601,37 @@ func TestValidateHorizontalPodAutoscalerScaleToZeroDisabled(t *testing.T) {
for _, successCase := range nonZeroMinReplicasCases { for _, successCase := range nonZeroMinReplicasCases {
successCase.Spec.MinReplicas = utilpointer.Int32(1) 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) t.Errorf("expected success: %v", errs)
} }
} }
} }
func TestValidateHorizontalPodAutoscalerUpdateScaleToZeroEnabled(t *testing.T) { func TestValidateHorizontalPodAutoscalerUpdateScaleToZeroEnabled(t *testing.T) {
// Enable HPAScaleToZero feature gate.
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAScaleToZero, true)
zeroMinReplicasCases := prepareMinReplicasCases(t, 0) zeroMinReplicasCases := prepareMinReplicasCases(t, 0)
nonZeroMinReplicasCases := prepareMinReplicasCases(t, 1) nonZeroMinReplicasCases := prepareMinReplicasCases(t, 1)
for i, zeroCase := range zeroMinReplicasCases { for i, zeroCase := range zeroMinReplicasCases {
nonZeroCase := nonZeroMinReplicasCases[i] 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) 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) t.Errorf("expected success: %v", errs)
} }
} }
} }
func TestValidateHorizontalPodAutoscalerScaleToZeroUpdateDisabled(t *testing.T) { func TestValidateHorizontalPodAutoscalerScaleToZeroUpdateDisabled(t *testing.T) {
// Disable HPAScaleToZero feature gate.
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAScaleToZero, false)
zeroMinReplicasCases := prepareMinReplicasCases(t, 0) zeroMinReplicasCases := prepareMinReplicasCases(t, 0)
nonZeroMinReplicasCases := prepareMinReplicasCases(t, 1) nonZeroMinReplicasCases := prepareMinReplicasCases(t, 1)
errorMsg := "must be greater than or equal to 1" errorMsg := "must be greater than or equal to 1"
for i, zeroCase := range zeroMinReplicasCases { for i, zeroCase := range zeroMinReplicasCases {
nonZeroCase := nonZeroMinReplicasCases[i] nonZeroCase := nonZeroMinReplicasCases[i]
errs := ValidateHorizontalPodAutoscalerUpdate(&zeroCase, &nonZeroCase) errs := ValidateHorizontalPodAutoscalerUpdate(&zeroCase, &nonZeroCase, hpaSpecValidationOpts)
if len(errs) == 0 { if len(errs) == 0 {
t.Errorf("expected failure for %q", errorMsg) 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) t.Errorf("unexpected error: %q, expected: %q", errs[0], errorMsg)
} }
if errs := ValidateHorizontalPodAutoscalerUpdate(&zeroCase, &zeroCase); len(errs) != 0 { if errs := ValidateHorizontalPodAutoscalerUpdate(&nonZeroCase, &zeroCase, hpaSpecValidationOpts); len(errs) != 0 {
t.Errorf("expected success: %v", errs)
}
if errs := ValidateHorizontalPodAutoscalerUpdate(&nonZeroCase, &zeroCase); len(errs) != 0 {
t.Errorf("expected success: %v", errs) t.Errorf("expected success: %v", errs)
} }
} }
} }
func TestValidateHorizontalPodAutoscalerConfigurableToleranceEnabled(t *testing.T) { func TestValidateHorizontalPodAutoscalerConfigurableToleranceEnabled(t *testing.T) {
// Enable HPAConfigurableTolerance feature gate.
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAConfigurableTolerance, true)
policiesList := []autoscaling.HPAScalingPolicy{{ policiesList := []autoscaling.HPAScalingPolicy{{
Type: autoscaling.PodsScalingPolicy, Type: autoscaling.PodsScalingPolicy,
Value: 1, Value: 1,
@ -1695,7 +1682,7 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceEnabled(t *testing.
ScaleDown: &c, ScaleDown: &c,
} }
hpa := prepareHPAWithBehavior(b) hpa := prepareHPAWithBehavior(b)
if errs := ValidateHorizontalPodAutoscaler(&hpa); len(errs) != 0 { if errs := ValidateHorizontalPodAutoscaler(&hpa, hpaScaleToZeroSpecValidationOpts); len(errs) != 0 {
t.Errorf("expected success: %v", errs) t.Errorf("expected success: %v", errs)
} }
} }
@ -1713,14 +1700,14 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceEnabled(t *testing.
Policies: policiesList, Policies: policiesList,
Tolerance: ptr.To(resource.MustParse("-0.001")), Tolerance: ptr.To(resource.MustParse("-0.001")),
}, },
msg: "greater or equal to zero", msg: "greater than or equal to 0",
}, },
{ {
rule: autoscaling.HPAScalingRules{ rule: autoscaling.HPAScalingRules{
Policies: policiesList, Policies: policiesList,
Tolerance: resource.NewMilliQuantity(-10, resource.DecimalSI), Tolerance: resource.NewMilliQuantity(-10, resource.DecimalSI),
}, },
msg: "greater or equal to zero", msg: "greater than or equal to 0",
}, },
{ {
rule: autoscaling.HPAScalingRules{ rule: autoscaling.HPAScalingRules{
@ -1741,7 +1728,7 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceEnabled(t *testing.
ScaleUp: &c.rule, ScaleUp: &c.rule,
} }
hpa := prepareHPAWithBehavior(b) hpa := prepareHPAWithBehavior(b)
errs := ValidateHorizontalPodAutoscaler(&hpa) errs := ValidateHorizontalPodAutoscaler(&hpa, hpaScaleToZeroSpecValidationOpts)
if len(errs) != 1 { if len(errs) != 1 {
t.Fatalf("expected exactly one error, got: %v", errs) t.Fatalf("expected exactly one error, got: %v", errs)
} }
@ -1752,9 +1739,6 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceEnabled(t *testing.
} }
func TestValidateHorizontalPodAutoscalerConfigurableToleranceDisabled(t *testing.T) { func TestValidateHorizontalPodAutoscalerConfigurableToleranceDisabled(t *testing.T) {
// Disable HPAConfigurableTolerance feature gate.
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAConfigurableTolerance, false)
maxPolicy := autoscaling.MaxPolicySelect maxPolicy := autoscaling.MaxPolicySelect
policiesList := []autoscaling.HPAScalingPolicy{{ policiesList := []autoscaling.HPAScalingPolicy{{
Type: autoscaling.PodsScalingPolicy, Type: autoscaling.PodsScalingPolicy,
@ -1780,7 +1764,7 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceDisabled(t *testing
ScaleDown: &c, ScaleDown: &c,
} }
hpa := prepareHPAWithBehavior(b) hpa := prepareHPAWithBehavior(b)
if errs := ValidateHorizontalPodAutoscaler(&hpa); len(errs) != 0 { if errs := ValidateHorizontalPodAutoscaler(&hpa, hpaSpecValidationOpts); len(errs) != 0 {
t.Errorf("expected success: %v", errs) t.Errorf("expected success: %v", errs)
} }
} }
@ -1789,13 +1773,6 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceDisabled(t *testing
rule autoscaling.HPAScalingRules rule autoscaling.HPAScalingRules
msg string msg string
}{ }{
{
rule: autoscaling.HPAScalingRules{
Policies: policiesList,
Tolerance: resource.NewMilliQuantity(1, resource.DecimalSI),
},
msg: "not supported",
},
{ {
rule: autoscaling.HPAScalingRules{}, rule: autoscaling.HPAScalingRules{},
msg: "at least one Policy", msg: "at least one Policy",
@ -1812,7 +1789,7 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceDisabled(t *testing
ScaleUp: &c.rule, ScaleUp: &c.rule,
} }
hpa := prepareHPAWithBehavior(b) hpa := prepareHPAWithBehavior(b)
errs := ValidateHorizontalPodAutoscaler(&hpa) errs := ValidateHorizontalPodAutoscaler(&hpa, hpaSpecValidationOpts)
if len(errs) != 1 { if len(errs) != 1 {
t.Fatalf("expected exactly one error, got: %v", errs) t.Fatalf("expected exactly one error, got: %v", errs)
} }
@ -1823,9 +1800,6 @@ func TestValidateHorizontalPodAutoscalerConfigurableToleranceDisabled(t *testing
} }
func TestValidateHorizontalPodAutoscalerUpdateConfigurableToleranceEnabled(t *testing.T) { func TestValidateHorizontalPodAutoscalerUpdateConfigurableToleranceEnabled(t *testing.T) {
// Enable HPAConfigurableTolerance feature gate.
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAConfigurableTolerance, true)
policiesList := []autoscaling.HPAScalingPolicy{{ policiesList := []autoscaling.HPAScalingPolicy{{
Type: autoscaling.PodsScalingPolicy, Type: autoscaling.PodsScalingPolicy,
Value: 1, Value: 1,
@ -1842,48 +1816,11 @@ func TestValidateHorizontalPodAutoscalerUpdateConfigurableToleranceEnabled(t *te
Policies: policiesList, 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) 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)
}
}
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 {
t.Errorf("expected success: %v", errs) t.Errorf("expected success: %v", errs)
} }
} }

View File

@ -22,9 +22,11 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/storage/names" "k8s.io/apiserver/pkg/storage/names"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/autoscaling" "k8s.io/kubernetes/pkg/apis/autoscaling"
"k8s.io/kubernetes/pkg/apis/autoscaling/validation" "k8s.io/kubernetes/pkg/apis/autoscaling/validation"
"k8s.io/kubernetes/pkg/features"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath" "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 // create cannot set status
newHPA.Status = autoscaling.HorizontalPodAutoscalerStatus{} newHPA.Status = autoscaling.HorizontalPodAutoscalerStatus{}
dropDisabledFields(newHPA, nil)
} }
// Validate validates a new autoscaler. // Validate validates a new autoscaler.
func (autoscalerStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { func (autoscalerStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
autoscaler := obj.(*autoscaling.HorizontalPodAutoscaler) 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. // 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) oldHPA := old.(*autoscaling.HorizontalPodAutoscaler)
// Update is not allowed to set status // Update is not allowed to set status
newHPA.Status = oldHPA.Status newHPA.Status = oldHPA.Status
dropDisabledFields(newHPA, oldHPA)
} }
// ValidateUpdate is the default update validation for an end user. // ValidateUpdate is the default update validation for an end user.
func (autoscalerStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { 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. // 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 { func (autoscalerStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
return nil 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
}

View File

@ -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,
},
},
},
}
}

View File

@ -211,6 +211,10 @@ type HPAScalingRules struct {
// replicas (e.g. 0.01 for 1%). Must be greater than or equal to zero. If not // 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%). // 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 // This is an alpha field and requires enabling the HPAConfigurableTolerance
// feature gate. // feature gate.
// //