mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 03:11:40 +00:00
Fix API doc and tolerance field handling when gate is flipped.
This commit is contained in:
parent
2dd9eda47f
commit
dc1696d807
@ -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.
|
||||
//
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -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.
|
||||
//
|
||||
|
Loading…
Reference in New Issue
Block a user