Add the HorizontalPodAutoscaler tolerance field.

Includes v2beta2 HPA round-trip conversion, defaulting, and validation.
This commit is contained in:
Jean-Marc François 2025-03-21 16:47:44 -04:00
parent 463b15b9b2
commit a41284d9fa
11 changed files with 745 additions and 52 deletions

View File

@ -36,3 +36,11 @@ const DefaultCPUUtilization = 80
// BehaviorSpecsAnnotation is the annotation which holds the HPA constraints specs // BehaviorSpecsAnnotation is the annotation which holds the HPA constraints specs
// when converting the `Behavior` field from autoscaling/v2beta2 // when converting the `Behavior` field from autoscaling/v2beta2
const BehaviorSpecsAnnotation = "autoscaling.alpha.kubernetes.io/behavior" const BehaviorSpecsAnnotation = "autoscaling.alpha.kubernetes.io/behavior"
// ToleranceScaleDownAnnotation is the annotation which holds the HPA tolerance specs
// when converting the `ScaleDown.Tolerance` field from autoscaling/v2
const ToleranceScaleDownAnnotation = "autoscaling.alpha.kubernetes.io/scale-down-tolerance"
// ToleranceScaleUpAnnotation is the annotation which holds the HPA tolerance specs
// when converting the `ScaleUp.Tolerance` field from autoscaling/v2
const ToleranceScaleUpAnnotation = "autoscaling.alpha.kubernetes.io/scale-up-tolerance"

View File

@ -16,8 +16,10 @@ limitations under the License.
package autoscaling package autoscaling
// DropRoundTripHorizontalPodAutoscalerAnnotations removes any annotations used to serialize round-tripped fields from later API versions, // DropRoundTripHorizontalPodAutoscalerAnnotations removes any annotations used to
// serialize round-tripped fields from HorizontalPodAutoscaler later API versions,
// and returns false if no changes were made and the original input object was returned. // and returns false if no changes were made and the original input object was returned.
//
// It should always be called when converting internal -> external versions, prior // It should always be called when converting internal -> external versions, prior
// to setting any of the custom annotations: // to setting any of the custom annotations:
// //
@ -34,12 +36,16 @@ package autoscaling
func DropRoundTripHorizontalPodAutoscalerAnnotations(in map[string]string) (out map[string]string, copied bool) { func DropRoundTripHorizontalPodAutoscalerAnnotations(in map[string]string) (out map[string]string, copied bool) {
_, hasMetricsSpecs := in[MetricSpecsAnnotation] _, hasMetricsSpecs := in[MetricSpecsAnnotation]
_, hasBehaviorSpecs := in[BehaviorSpecsAnnotation] _, hasBehaviorSpecs := in[BehaviorSpecsAnnotation]
_, hasToleranceScaleDown := in[ToleranceScaleDownAnnotation]
_, hasToleranceScaleUp := in[ToleranceScaleUpAnnotation]
_, hasMetricsStatuses := in[MetricStatusesAnnotation] _, hasMetricsStatuses := in[MetricStatusesAnnotation]
_, hasConditions := in[HorizontalPodAutoscalerConditionsAnnotation] _, hasConditions := in[HorizontalPodAutoscalerConditionsAnnotation]
if hasMetricsSpecs || hasBehaviorSpecs || hasMetricsStatuses || hasConditions { if hasMetricsSpecs || hasBehaviorSpecs || hasToleranceScaleDown || hasToleranceScaleUp || hasMetricsStatuses || hasConditions {
out = DeepCopyStringMap(in) out = DeepCopyStringMap(in)
delete(out, MetricSpecsAnnotation) delete(out, MetricSpecsAnnotation)
delete(out, BehaviorSpecsAnnotation) delete(out, BehaviorSpecsAnnotation)
delete(out, ToleranceScaleDownAnnotation)
delete(out, ToleranceScaleUpAnnotation)
delete(out, MetricStatusesAnnotation) delete(out, MetricStatusesAnnotation)
delete(out, HorizontalPodAutoscalerConditionsAnnotation) delete(out, HorizontalPodAutoscalerConditionsAnnotation)
return out, true return out, true

View File

@ -138,12 +138,18 @@ const (
DisabledPolicySelect ScalingPolicySelect = "Disabled" DisabledPolicySelect ScalingPolicySelect = "Disabled"
) )
// HPAScalingRules configures the scaling behavior for one direction. // HPAScalingRules configures the scaling behavior for one direction via
// These Rules are applied after calculating DesiredReplicas from metrics for the HPA. // scaling Policy Rules and a configurable metric tolerance.
//
// Scaling Policy Rules are applied after calculating DesiredReplicas from metrics for the HPA.
// They can limit the scaling velocity by specifying scaling policies. // They can limit the scaling velocity by specifying scaling policies.
// They can prevent flapping by specifying the stabilization window, so that the // They can prevent flapping by specifying the stabilization window, so that the
// number of replicas is not set instantly, instead, the safest value from the stabilization // number of replicas is not set instantly, instead, the safest value from the stabilization
// window is chosen. // window is chosen.
//
// The tolerance is applied to the metric values and prevents scaling too
// eagerly for small metric variations. (Note that setting a tolerance requires
// enabling the alpha HPAConfigurableTolerance feature gate.)
type HPAScalingRules struct { type HPAScalingRules struct {
// StabilizationWindowSeconds is the number of seconds for which past recommendations should be // StabilizationWindowSeconds is the number of seconds for which past recommendations should be
// considered while scaling up or scaling down. // considered while scaling up or scaling down.
@ -157,10 +163,23 @@ type HPAScalingRules struct {
// If not set, the default value MaxPolicySelect is used. // If not set, the default value MaxPolicySelect is used.
// +optional // +optional
SelectPolicy *ScalingPolicySelect SelectPolicy *ScalingPolicySelect
// policies is a list of potential scaling polices which can used during scaling. // policies is a list of potential scaling polices which can be used during scaling.
// At least one policy must be specified, otherwise the HPAScalingRules will be discarded as invalid // If not set, use the default values:
// - For scale up: allow doubling the number of pods, or an absolute change of 4 pods in a 15s window.
// - For scale down: allow all pods to be removed in a 15s window.
// +optional // +optional
Policies []HPAScalingPolicy Policies []HPAScalingPolicy
// tolerance is the tolerance on the ratio between the current and desired
// metric value under which no updates are made to the desired number of
// 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%).
//
// This is an alpha field and requires enabling the HPAConfigurableTolerance
// feature gate.
//
// +featureGate=HPAConfigurableTolerance
// +optional
Tolerance *resource.Quantity
} }
// HPAScalingPolicyType is the type of the policy which could be used while making scaling decisions. // HPAScalingPolicyType is the type of the policy which could be used while making scaling decisions.

View File

@ -91,9 +91,12 @@ func SetDefaults_HorizontalPodAutoscaler(obj *autoscalingv2.HorizontalPodAutosca
SetDefaults_HorizontalPodAutoscalerBehavior(obj) SetDefaults_HorizontalPodAutoscalerBehavior(obj)
} }
// SetDefaults_HorizontalPodAutoscalerBehavior fills the behavior if it is not null // SetDefaults_HorizontalPodAutoscalerBehavior fills the behavior if it contains
// at least one scaling rule policy (for scale-up or scale-down)
func SetDefaults_HorizontalPodAutoscalerBehavior(obj *autoscalingv2.HorizontalPodAutoscaler) { func SetDefaults_HorizontalPodAutoscalerBehavior(obj *autoscalingv2.HorizontalPodAutoscaler) {
// if behavior is specified, we should fill all the 'nil' values with the default ones // If behavior contains a scaling rule policy (either for scale-up, scale-down, or both), we
// should fill all the unset scaling policy fields (i.e. StabilizationWindowSeconds,
// SelectPolicy, Policies) with default values
if obj.Spec.Behavior != nil { if obj.Spec.Behavior != nil {
obj.Spec.Behavior.ScaleUp = GenerateHPAScaleUpRules(obj.Spec.Behavior.ScaleUp) obj.Spec.Behavior.ScaleUp = GenerateHPAScaleUpRules(obj.Spec.Behavior.ScaleUp)
obj.Spec.Behavior.ScaleDown = GenerateHPAScaleDownRules(obj.Spec.Behavior.ScaleDown) obj.Spec.Behavior.ScaleDown = GenerateHPAScaleDownRules(obj.Spec.Behavior.ScaleDown)
@ -129,5 +132,8 @@ func copyHPAScalingRules(from, to *autoscalingv2.HPAScalingRules) *autoscalingv2
if from.Policies != nil { if from.Policies != nil {
to.Policies = from.Policies to.Policies = from.Policies
} }
if from.Tolerance != nil {
to.Tolerance = from.Tolerance
}
return to return to
} }

View File

@ -20,8 +20,12 @@ import (
"reflect" "reflect"
"testing" "testing"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/features"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -132,14 +136,17 @@ func TestGenerateScaleUpRules(t *testing.T) {
rateUpPercentPeriodSeconds int32 rateUpPercentPeriodSeconds int32
stabilizationSeconds *int32 stabilizationSeconds *int32
selectPolicy *autoscalingv2.ScalingPolicySelect selectPolicy *autoscalingv2.ScalingPolicySelect
tolerance *resource.Quantity
expectedPolicies []autoscalingv2.HPAScalingPolicy expectedPolicies []autoscalingv2.HPAScalingPolicy
expectedStabilization *int32 expectedStabilization *int32
expectedSelectPolicy string expectedSelectPolicy string
expectedTolerance *resource.Quantity
annotation string annotation string
} }
maxPolicy := autoscalingv2.MaxChangePolicySelect maxPolicy := autoscalingv2.MaxChangePolicySelect
minPolicy := autoscalingv2.MinChangePolicySelect minPolicy := autoscalingv2.MinChangePolicySelect
sampleTolerance := resource.MustParse("0.5")
tests := []TestCase{ tests := []TestCase{
{ {
annotation: "Default values", annotation: "Default values",
@ -208,12 +215,25 @@ func TestGenerateScaleUpRules(t *testing.T) {
expectedStabilization: utilpointer.Int32(25), expectedStabilization: utilpointer.Int32(25),
expectedSelectPolicy: string(autoscalingv2.MaxChangePolicySelect), expectedSelectPolicy: string(autoscalingv2.MaxChangePolicySelect),
}, },
{
annotation: "Percent policy and tolerance is specified",
rateUpPercent: 7,
rateUpPercentPeriodSeconds: 10,
tolerance: &sampleTolerance,
expectedPolicies: []autoscalingv2.HPAScalingPolicy{
{Type: autoscalingv2.PercentScalingPolicy, Value: 7, PeriodSeconds: 10},
},
expectedStabilization: utilpointer.Int32(0),
expectedSelectPolicy: string(autoscalingv2.MaxChangePolicySelect),
expectedTolerance: &sampleTolerance,
},
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.annotation, func(t *testing.T) { t.Run(tc.annotation, func(t *testing.T) {
scaleUpRules := &autoscalingv2.HPAScalingRules{ scaleUpRules := &autoscalingv2.HPAScalingRules{
StabilizationWindowSeconds: tc.stabilizationSeconds, StabilizationWindowSeconds: tc.stabilizationSeconds,
SelectPolicy: tc.selectPolicy, SelectPolicy: tc.selectPolicy,
Tolerance: tc.tolerance,
} }
if tc.rateUpPods != 0 || tc.rateUpPodsPeriodSeconds != 0 { if tc.rateUpPods != 0 || tc.rateUpPodsPeriodSeconds != 0 {
scaleUpRules.Policies = append(scaleUpRules.Policies, autoscalingv2.HPAScalingPolicy{ scaleUpRules.Policies = append(scaleUpRules.Policies, autoscalingv2.HPAScalingPolicy{
@ -234,10 +254,138 @@ func TestGenerateScaleUpRules(t *testing.T) {
} }
assert.Equal(t, autoscalingv2.ScalingPolicySelect(tc.expectedSelectPolicy), *up.SelectPolicy) assert.Equal(t, autoscalingv2.ScalingPolicySelect(tc.expectedSelectPolicy), *up.SelectPolicy)
assert.Equal(t, tc.expectedTolerance, up.Tolerance)
}) })
} }
} }
func TestSetBehaviorDefaults(t *testing.T) {
sampleTolerance := resource.MustParse("0.5")
maxPolicy := autoscalingv2.MaxChangePolicySelect
policies := []autoscalingv2.HPAScalingPolicy{
{Type: autoscalingv2.PercentScalingPolicy, Value: 7, PeriodSeconds: 10},
}
type TestCase struct {
behavior *autoscalingv2.HorizontalPodAutoscalerBehavior
expectedBehavior *autoscalingv2.HorizontalPodAutoscalerBehavior
annotation string
}
tests := []TestCase{
{
annotation: "Nil behavior",
behavior: nil,
expectedBehavior: nil,
},
{
annotation: "Behavior with stabilizationWindowSeconds and tolerance",
behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
ScaleUp: &autoscalingv2.HPAScalingRules{
StabilizationWindowSeconds: utilpointer.Int32(100),
Tolerance: &sampleTolerance,
},
},
expectedBehavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
ScaleDown: &autoscalingv2.HPAScalingRules{
SelectPolicy: &maxPolicy,
Policies: []autoscalingv2.HPAScalingPolicy{
{Type: autoscalingv2.PercentScalingPolicy, Value: 100, PeriodSeconds: 15},
},
},
ScaleUp: &autoscalingv2.HPAScalingRules{
StabilizationWindowSeconds: utilpointer.Int32(100),
SelectPolicy: &maxPolicy,
Policies: []autoscalingv2.HPAScalingPolicy{
{Type: autoscalingv2.PodsScalingPolicy, Value: 4, PeriodSeconds: 15},
{Type: autoscalingv2.PercentScalingPolicy, Value: 100, PeriodSeconds: 15},
},
Tolerance: &sampleTolerance,
},
},
},
{
annotation: "Behavior with policy, without tolerance",
behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
ScaleDown: &autoscalingv2.HPAScalingRules{
Policies: policies,
},
},
expectedBehavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
ScaleDown: &autoscalingv2.HPAScalingRules{
SelectPolicy: &maxPolicy,
Policies: policies,
},
ScaleUp: &autoscalingv2.HPAScalingRules{
StabilizationWindowSeconds: utilpointer.Int32(0),
SelectPolicy: &maxPolicy,
Policies: []autoscalingv2.HPAScalingPolicy{
{Type: autoscalingv2.PodsScalingPolicy, Value: 4, PeriodSeconds: 15},
{Type: autoscalingv2.PercentScalingPolicy, Value: 100, PeriodSeconds: 15},
},
},
},
},
}
for _, tc := range tests {
t.Run(tc.annotation, func(t *testing.T) {
hpa := autoscalingv2.HorizontalPodAutoscaler{
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
Behavior: tc.behavior,
},
}
expectedHPA := autoscalingv2.HorizontalPodAutoscaler{
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
Behavior: tc.expectedBehavior,
},
}
SetDefaults_HorizontalPodAutoscalerBehavior(&hpa)
assert.Equal(t, expectedHPA, hpa)
})
}
}
func TestSetBehaviorDefaultsConfigurableToleranceEnabled(t *testing.T) {
// Enable HPAConfigurableTolerance feature gate.
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAConfigurableTolerance, true)
// Verify that the tolerance field is left unset.
maxPolicy := autoscalingv2.MaxChangePolicySelect
hpa := autoscalingv2.HorizontalPodAutoscaler{
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
ScaleUp: &autoscalingv2.HPAScalingRules{
StabilizationWindowSeconds: utilpointer.Int32(100),
},
},
},
}
expectedHPA := autoscalingv2.HorizontalPodAutoscaler{
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
ScaleDown: &autoscalingv2.HPAScalingRules{
SelectPolicy: &maxPolicy,
Policies: []autoscalingv2.HPAScalingPolicy{
{Type: autoscalingv2.PercentScalingPolicy, Value: 100, PeriodSeconds: 15},
},
},
ScaleUp: &autoscalingv2.HPAScalingRules{
StabilizationWindowSeconds: utilpointer.Int32(100),
SelectPolicy: &maxPolicy,
Policies: []autoscalingv2.HPAScalingPolicy{
{Type: autoscalingv2.PodsScalingPolicy, Value: 4, PeriodSeconds: 15},
{Type: autoscalingv2.PercentScalingPolicy, Value: 100, PeriodSeconds: 15},
},
},
},
},
}
SetDefaults_HorizontalPodAutoscalerBehavior(&hpa)
assert.Equal(t, expectedHPA, hpa)
}
func TestHorizontalPodAutoscalerAnnotations(t *testing.T) { func TestHorizontalPodAutoscalerAnnotations(t *testing.T) {
tests := []struct { tests := []struct {
hpa autoscalingv2.HorizontalPodAutoscaler hpa autoscalingv2.HorizontalPodAutoscaler

View File

@ -17,8 +17,11 @@ limitations under the License.
package v2beta2 package v2beta2
import ( import (
"fmt"
autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2" autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/conversion" "k8s.io/apimachinery/pkg/conversion"
"k8s.io/kubernetes/pkg/apis/autoscaling" "k8s.io/kubernetes/pkg/apis/autoscaling"
) )
@ -27,8 +30,29 @@ func Convert_autoscaling_HorizontalPodAutoscaler_To_v2beta2_HorizontalPodAutosca
if err := autoConvert_autoscaling_HorizontalPodAutoscaler_To_v2beta2_HorizontalPodAutoscaler(in, out, s); err != nil { if err := autoConvert_autoscaling_HorizontalPodAutoscaler_To_v2beta2_HorizontalPodAutoscaler(in, out, s); err != nil {
return err return err
} }
// v2beta2 round-trips to internal without any serialized annotations, make sure any from other versions don't get serialized // Ensure old round-trips annotations are discarded
out.Annotations, _ = autoscaling.DropRoundTripHorizontalPodAutoscalerAnnotations(out.Annotations) annotations, copiedAnnotations := autoscaling.DropRoundTripHorizontalPodAutoscalerAnnotations(out.Annotations)
out.Annotations = annotations
behavior := in.Spec.Behavior
if behavior == nil {
return nil
}
// Save the tolerance fields in annotations for round-trip
if behavior.ScaleDown != nil && behavior.ScaleDown.Tolerance != nil {
if !copiedAnnotations {
copiedAnnotations = true
out.Annotations = autoscaling.DeepCopyStringMap(out.Annotations)
}
out.Annotations[autoscaling.ToleranceScaleDownAnnotation] = behavior.ScaleDown.Tolerance.String()
}
if behavior.ScaleUp != nil && behavior.ScaleUp.Tolerance != nil {
if !copiedAnnotations {
copiedAnnotations = true
out.Annotations = autoscaling.DeepCopyStringMap(out.Annotations)
}
out.Annotations[autoscaling.ToleranceScaleUpAnnotation] = behavior.ScaleUp.Tolerance.String()
}
return nil return nil
} }
@ -36,7 +60,44 @@ func Convert_v2beta2_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutosca
if err := autoConvert_v2beta2_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler(in, out, s); err != nil { if err := autoConvert_v2beta2_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler(in, out, s); err != nil {
return err return err
} }
// v2beta2 round-trips to internal without any serialized annotations, make sure any from other versions don't get serialized // Restore the tolerance fields from annotations for round-trip
if tolerance, ok := out.Annotations[autoscaling.ToleranceScaleDownAnnotation]; ok {
if out.Spec.Behavior == nil {
out.Spec.Behavior = &autoscaling.HorizontalPodAutoscalerBehavior{}
}
if out.Spec.Behavior.ScaleDown == nil {
out.Spec.Behavior.ScaleDown = &autoscaling.HPAScalingRules{}
}
q, err := resource.ParseQuantity(tolerance)
if err != nil {
return fmt.Errorf("failed to parse annotation %q: %w", autoscaling.ToleranceScaleDownAnnotation, err)
}
out.Spec.Behavior.ScaleDown.Tolerance = &q
}
if tolerance, ok := out.Annotations[autoscaling.ToleranceScaleUpAnnotation]; ok {
if out.Spec.Behavior == nil {
out.Spec.Behavior = &autoscaling.HorizontalPodAutoscalerBehavior{}
}
if out.Spec.Behavior.ScaleUp == nil {
out.Spec.Behavior.ScaleUp = &autoscaling.HPAScalingRules{}
}
q, err := resource.ParseQuantity(tolerance)
if err != nil {
return fmt.Errorf("failed to parse annotation %q: %w", autoscaling.ToleranceScaleUpAnnotation, err)
}
out.Spec.Behavior.ScaleUp.Tolerance = &q
}
// Do not save round-trip annotations in internal resource
out.Annotations, _ = autoscaling.DropRoundTripHorizontalPodAutoscalerAnnotations(out.Annotations) out.Annotations, _ = autoscaling.DropRoundTripHorizontalPodAutoscalerAnnotations(out.Annotations)
return nil return nil
} }
func Convert_v2beta2_HPAScalingRules_To_autoscaling_HPAScalingRules(in *autoscalingv2beta2.HPAScalingRules, out *autoscaling.HPAScalingRules, s conversion.Scope) error {
// Tolerance field is handled in the HorizontalPodAutoscaler conversion function.
return autoConvert_v2beta2_HPAScalingRules_To_autoscaling_HPAScalingRules(in, out, s)
}
func Convert_autoscaling_HPAScalingRules_To_v2beta2_HPAScalingRules(in *autoscaling.HPAScalingRules, out *autoscalingv2beta2.HPAScalingRules, s conversion.Scope) error {
// Tolerance field is handled in the HorizontalPodAutoscaler conversion function.
return autoConvert_autoscaling_HPAScalingRules_To_v2beta2_HPAScalingRules(in, out, s)
}

View File

@ -0,0 +1,124 @@
/*
Copyright 2025 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 v2beta2
import (
"testing"
"github.com/google/go-cmp/cmp"
autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/resource"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/apis/autoscaling"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/utils/ptr"
)
func TestConvertRoundTrip(t *testing.T) {
tolerance1 := resource.MustParse("0.1")
tolerance2 := resource.MustParse("0.2")
tests := []struct {
name string
internalHPA *autoscaling.HorizontalPodAutoscaler
}{
{
"Complete HPA with scale-up tolerance",
&autoscaling.HorizontalPodAutoscaler{
ObjectMeta: v1.ObjectMeta{
Name: "hpa",
Namespace: "hpa-ns",
Annotations: map[string]string{"key": "value"},
},
Spec: autoscaling.HorizontalPodAutoscalerSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
Metrics: []autoscaling.MetricSpec{
{
Type: autoscaling.ResourceMetricSourceType,
Resource: &autoscaling.ResourceMetricSource{
Name: api.ResourceCPU,
Target: autoscaling.MetricTarget{
Type: autoscaling.AverageValueMetricType,
AverageValue: resource.NewMilliQuantity(300, resource.DecimalSI),
},
},
},
},
Behavior: &autoscaling.HorizontalPodAutoscalerBehavior{
ScaleUp: &autoscaling.HPAScalingRules{
Policies: []autoscaling.HPAScalingPolicy{
{
Type: autoscaling.PodsScalingPolicy,
Value: 1,
PeriodSeconds: 2,
},
},
Tolerance: &tolerance1,
},
},
},
},
},
{
"Scale-down and scale-up tolerances",
&autoscaling.HorizontalPodAutoscaler{
Spec: autoscaling.HorizontalPodAutoscalerSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
Behavior: &autoscaling.HorizontalPodAutoscalerBehavior{
ScaleUp: &autoscaling.HPAScalingRules{
Tolerance: &tolerance1,
},
ScaleDown: &autoscaling.HPAScalingRules{
Tolerance: &tolerance2,
},
},
},
},
},
{
"Scale-down tolerance only",
&autoscaling.HorizontalPodAutoscaler{
Spec: autoscaling.HorizontalPodAutoscalerSpec{
MinReplicas: ptr.To(int32(1)),
MaxReplicas: 3,
Behavior: &autoscaling.HorizontalPodAutoscalerBehavior{
ScaleDown: &autoscaling.HPAScalingRules{
Tolerance: &tolerance2,
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v2beta2HPA := &autoscalingv2beta2.HorizontalPodAutoscaler{}
if err := Convert_autoscaling_HorizontalPodAutoscaler_To_v2beta2_HorizontalPodAutoscaler(tt.internalHPA, v2beta2HPA, nil); err != nil {
t.Errorf("Convert_autoscaling_HorizontalPodAutoscaler_To_v2beta2_HorizontalPodAutoscaler() error = %v", err)
}
roundtripHPA := &autoscaling.HorizontalPodAutoscaler{}
if err := Convert_v2beta2_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler(v2beta2HPA, roundtripHPA, nil); err != nil {
t.Errorf("Convert_v2beta2_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler() error = %v", err)
}
if !apiequality.Semantic.DeepEqual(tt.internalHPA, roundtripHPA) {
t.Errorf("HPA is not equivalent after round-trip: mismatch (-want +got):\n%s", cmp.Diff(tt.internalHPA, roundtripHPA))
}
})
}
}

View File

@ -101,16 +101,6 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil { }); err != nil {
return err return err
} }
if err := s.AddGeneratedConversionFunc((*autoscalingv2beta2.HPAScalingRules)(nil), (*autoscaling.HPAScalingRules)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v2beta2_HPAScalingRules_To_autoscaling_HPAScalingRules(a.(*autoscalingv2beta2.HPAScalingRules), b.(*autoscaling.HPAScalingRules), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*autoscaling.HPAScalingRules)(nil), (*autoscalingv2beta2.HPAScalingRules)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_autoscaling_HPAScalingRules_To_v2beta2_HPAScalingRules(a.(*autoscaling.HPAScalingRules), b.(*autoscalingv2beta2.HPAScalingRules), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*autoscalingv2beta2.HorizontalPodAutoscalerBehavior)(nil), (*autoscaling.HorizontalPodAutoscalerBehavior)(nil), func(a, b interface{}, scope conversion.Scope) error { if err := s.AddGeneratedConversionFunc((*autoscalingv2beta2.HorizontalPodAutoscalerBehavior)(nil), (*autoscaling.HorizontalPodAutoscalerBehavior)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v2beta2_HorizontalPodAutoscalerBehavior_To_autoscaling_HorizontalPodAutoscalerBehavior(a.(*autoscalingv2beta2.HorizontalPodAutoscalerBehavior), b.(*autoscaling.HorizontalPodAutoscalerBehavior), scope) return Convert_v2beta2_HorizontalPodAutoscalerBehavior_To_autoscaling_HorizontalPodAutoscalerBehavior(a.(*autoscalingv2beta2.HorizontalPodAutoscalerBehavior), b.(*autoscaling.HorizontalPodAutoscalerBehavior), scope)
}); err != nil { }); err != nil {
@ -271,11 +261,21 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil { }); err != nil {
return err return err
} }
if err := s.AddConversionFunc((*autoscaling.HPAScalingRules)(nil), (*autoscalingv2beta2.HPAScalingRules)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_autoscaling_HPAScalingRules_To_v2beta2_HPAScalingRules(a.(*autoscaling.HPAScalingRules), b.(*autoscalingv2beta2.HPAScalingRules), scope)
}); err != nil {
return err
}
if err := s.AddConversionFunc((*autoscaling.HorizontalPodAutoscaler)(nil), (*autoscalingv2beta2.HorizontalPodAutoscaler)(nil), func(a, b interface{}, scope conversion.Scope) error { if err := s.AddConversionFunc((*autoscaling.HorizontalPodAutoscaler)(nil), (*autoscalingv2beta2.HorizontalPodAutoscaler)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_autoscaling_HorizontalPodAutoscaler_To_v2beta2_HorizontalPodAutoscaler(a.(*autoscaling.HorizontalPodAutoscaler), b.(*autoscalingv2beta2.HorizontalPodAutoscaler), scope) return Convert_autoscaling_HorizontalPodAutoscaler_To_v2beta2_HorizontalPodAutoscaler(a.(*autoscaling.HorizontalPodAutoscaler), b.(*autoscalingv2beta2.HorizontalPodAutoscaler), scope)
}); err != nil { }); err != nil {
return err return err
} }
if err := s.AddConversionFunc((*autoscalingv2beta2.HPAScalingRules)(nil), (*autoscaling.HPAScalingRules)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v2beta2_HPAScalingRules_To_autoscaling_HPAScalingRules(a.(*autoscalingv2beta2.HPAScalingRules), b.(*autoscaling.HPAScalingRules), scope)
}); err != nil {
return err
}
if err := s.AddConversionFunc((*autoscalingv2beta2.HorizontalPodAutoscaler)(nil), (*autoscaling.HorizontalPodAutoscaler)(nil), func(a, b interface{}, scope conversion.Scope) error { if err := s.AddConversionFunc((*autoscalingv2beta2.HorizontalPodAutoscaler)(nil), (*autoscaling.HorizontalPodAutoscaler)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v2beta2_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler(a.(*autoscalingv2beta2.HorizontalPodAutoscaler), b.(*autoscaling.HorizontalPodAutoscaler), scope) return Convert_v2beta2_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler(a.(*autoscalingv2beta2.HorizontalPodAutoscaler), b.(*autoscaling.HorizontalPodAutoscaler), scope)
}); err != nil { }); err != nil {
@ -455,23 +455,14 @@ func autoConvert_v2beta2_HPAScalingRules_To_autoscaling_HPAScalingRules(in *auto
return nil return nil
} }
// Convert_v2beta2_HPAScalingRules_To_autoscaling_HPAScalingRules is an autogenerated conversion function.
func Convert_v2beta2_HPAScalingRules_To_autoscaling_HPAScalingRules(in *autoscalingv2beta2.HPAScalingRules, out *autoscaling.HPAScalingRules, s conversion.Scope) error {
return autoConvert_v2beta2_HPAScalingRules_To_autoscaling_HPAScalingRules(in, out, s)
}
func autoConvert_autoscaling_HPAScalingRules_To_v2beta2_HPAScalingRules(in *autoscaling.HPAScalingRules, out *autoscalingv2beta2.HPAScalingRules, s conversion.Scope) error { func autoConvert_autoscaling_HPAScalingRules_To_v2beta2_HPAScalingRules(in *autoscaling.HPAScalingRules, out *autoscalingv2beta2.HPAScalingRules, s conversion.Scope) error {
out.StabilizationWindowSeconds = (*int32)(unsafe.Pointer(in.StabilizationWindowSeconds)) out.StabilizationWindowSeconds = (*int32)(unsafe.Pointer(in.StabilizationWindowSeconds))
out.SelectPolicy = (*autoscalingv2beta2.ScalingPolicySelect)(unsafe.Pointer(in.SelectPolicy)) out.SelectPolicy = (*autoscalingv2beta2.ScalingPolicySelect)(unsafe.Pointer(in.SelectPolicy))
out.Policies = *(*[]autoscalingv2beta2.HPAScalingPolicy)(unsafe.Pointer(&in.Policies)) out.Policies = *(*[]autoscalingv2beta2.HPAScalingPolicy)(unsafe.Pointer(&in.Policies))
// WARNING: in.Tolerance requires manual conversion: does not exist in peer-type
return nil return nil
} }
// Convert_autoscaling_HPAScalingRules_To_v2beta2_HPAScalingRules is an autogenerated conversion function.
func Convert_autoscaling_HPAScalingRules_To_v2beta2_HPAScalingRules(in *autoscaling.HPAScalingRules, out *autoscalingv2beta2.HPAScalingRules, s conversion.Scope) error {
return autoConvert_autoscaling_HPAScalingRules_To_v2beta2_HPAScalingRules(in, out, s)
}
func autoConvert_v2beta2_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler(in *autoscalingv2beta2.HorizontalPodAutoscaler, out *autoscaling.HorizontalPodAutoscaler, s conversion.Scope) error { func autoConvert_v2beta2_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler(in *autoscalingv2beta2.HorizontalPodAutoscaler, out *autoscaling.HorizontalPodAutoscaler, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta out.ObjectMeta = in.ObjectMeta
if err := Convert_v2beta2_HorizontalPodAutoscalerSpec_To_autoscaling_HorizontalPodAutoscalerSpec(&in.Spec, &out.Spec, s); err != nil { if err := Convert_v2beta2_HorizontalPodAutoscalerSpec_To_autoscaling_HorizontalPodAutoscalerSpec(&in.Spec, &out.Spec, s); err != nil {
@ -495,8 +486,24 @@ func autoConvert_autoscaling_HorizontalPodAutoscaler_To_v2beta2_HorizontalPodAut
} }
func autoConvert_v2beta2_HorizontalPodAutoscalerBehavior_To_autoscaling_HorizontalPodAutoscalerBehavior(in *autoscalingv2beta2.HorizontalPodAutoscalerBehavior, out *autoscaling.HorizontalPodAutoscalerBehavior, s conversion.Scope) error { func autoConvert_v2beta2_HorizontalPodAutoscalerBehavior_To_autoscaling_HorizontalPodAutoscalerBehavior(in *autoscalingv2beta2.HorizontalPodAutoscalerBehavior, out *autoscaling.HorizontalPodAutoscalerBehavior, s conversion.Scope) error {
out.ScaleUp = (*autoscaling.HPAScalingRules)(unsafe.Pointer(in.ScaleUp)) if in.ScaleUp != nil {
out.ScaleDown = (*autoscaling.HPAScalingRules)(unsafe.Pointer(in.ScaleDown)) in, out := &in.ScaleUp, &out.ScaleUp
*out = new(autoscaling.HPAScalingRules)
if err := Convert_v2beta2_HPAScalingRules_To_autoscaling_HPAScalingRules(*in, *out, s); err != nil {
return err
}
} else {
out.ScaleUp = nil
}
if in.ScaleDown != nil {
in, out := &in.ScaleDown, &out.ScaleDown
*out = new(autoscaling.HPAScalingRules)
if err := Convert_v2beta2_HPAScalingRules_To_autoscaling_HPAScalingRules(*in, *out, s); err != nil {
return err
}
} else {
out.ScaleDown = nil
}
return nil return nil
} }
@ -506,8 +513,24 @@ func Convert_v2beta2_HorizontalPodAutoscalerBehavior_To_autoscaling_HorizontalPo
} }
func autoConvert_autoscaling_HorizontalPodAutoscalerBehavior_To_v2beta2_HorizontalPodAutoscalerBehavior(in *autoscaling.HorizontalPodAutoscalerBehavior, out *autoscalingv2beta2.HorizontalPodAutoscalerBehavior, s conversion.Scope) error { func autoConvert_autoscaling_HorizontalPodAutoscalerBehavior_To_v2beta2_HorizontalPodAutoscalerBehavior(in *autoscaling.HorizontalPodAutoscalerBehavior, out *autoscalingv2beta2.HorizontalPodAutoscalerBehavior, s conversion.Scope) error {
out.ScaleUp = (*autoscalingv2beta2.HPAScalingRules)(unsafe.Pointer(in.ScaleUp)) if in.ScaleUp != nil {
out.ScaleDown = (*autoscalingv2beta2.HPAScalingRules)(unsafe.Pointer(in.ScaleDown)) in, out := &in.ScaleUp, &out.ScaleUp
*out = new(autoscalingv2beta2.HPAScalingRules)
if err := Convert_autoscaling_HPAScalingRules_To_v2beta2_HPAScalingRules(*in, *out, s); err != nil {
return err
}
} else {
out.ScaleUp = nil
}
if in.ScaleDown != nil {
in, out := &in.ScaleDown, &out.ScaleDown
*out = new(autoscalingv2beta2.HPAScalingRules)
if err := Convert_autoscaling_HPAScalingRules_To_v2beta2_HPAScalingRules(*in, *out, s); err != nil {
return err
}
} else {
out.ScaleDown = nil
}
return nil return nil
} }
@ -603,7 +626,15 @@ func autoConvert_v2beta2_HorizontalPodAutoscalerSpec_To_autoscaling_HorizontalPo
} else { } else {
out.Metrics = nil out.Metrics = nil
} }
out.Behavior = (*autoscaling.HorizontalPodAutoscalerBehavior)(unsafe.Pointer(in.Behavior)) if in.Behavior != nil {
in, out := &in.Behavior, &out.Behavior
*out = new(autoscaling.HorizontalPodAutoscalerBehavior)
if err := Convert_v2beta2_HorizontalPodAutoscalerBehavior_To_autoscaling_HorizontalPodAutoscalerBehavior(*in, *out, s); err != nil {
return err
}
} else {
out.Behavior = nil
}
return nil return nil
} }
@ -629,7 +660,15 @@ func autoConvert_autoscaling_HorizontalPodAutoscalerSpec_To_v2beta2_HorizontalPo
} else { } else {
out.Metrics = nil out.Metrics = nil
} }
out.Behavior = (*autoscalingv2beta2.HorizontalPodAutoscalerBehavior)(unsafe.Pointer(in.Behavior)) if in.Behavior != nil {
in, out := &in.Behavior, &out.Behavior
*out = new(autoscalingv2beta2.HorizontalPodAutoscalerBehavior)
if err := Convert_autoscaling_HorizontalPodAutoscalerBehavior_To_v2beta2_HorizontalPodAutoscalerBehavior(*in, *out, s); err != nil {
return err
}
} else {
out.Behavior = nil
}
return nil return nil
} }

View File

@ -53,12 +53,12 @@ func ValidateScale(scale *autoscaling.Scale) field.ErrorList {
// Prefix indicates this name will be used as part of generation, in which case trailing dashes are allowed. // Prefix indicates this name will be used as part of generation, in which case trailing dashes are allowed.
var ValidateHorizontalPodAutoscalerName = apivalidation.ValidateReplicationControllerName var ValidateHorizontalPodAutoscalerName = apivalidation.ValidateReplicationControllerName
func validateHorizontalPodAutoscalerSpec(autoscaler autoscaling.HorizontalPodAutoscalerSpec, fldPath *field.Path, minReplicasLowerBound int32) field.ErrorList { func validateHorizontalPodAutoscalerSpec(autoscaler autoscaling.HorizontalPodAutoscalerSpec, fldPath *field.Path, opts HorizontalPodAutoscalerSpecValidationOptions) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
if autoscaler.MinReplicas != nil && *autoscaler.MinReplicas < minReplicasLowerBound { if autoscaler.MinReplicas != nil && *autoscaler.MinReplicas < opts.MinReplicasLowerBound {
allErrs = append(allErrs, field.Invalid(fldPath.Child("minReplicas"), *autoscaler.MinReplicas, allErrs = append(allErrs, field.Invalid(fldPath.Child("minReplicas"), *autoscaler.MinReplicas,
fmt.Sprintf("must be greater than or equal to %d", minReplicasLowerBound))) fmt.Sprintf("must be greater than or equal to %d", opts.MinReplicasLowerBound)))
} }
if autoscaler.MaxReplicas < 1 { if autoscaler.MaxReplicas < 1 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("maxReplicas"), autoscaler.MaxReplicas, "must be greater than 0")) allErrs = append(allErrs, field.Invalid(fldPath.Child("maxReplicas"), autoscaler.MaxReplicas, "must be greater than 0"))
@ -72,7 +72,7 @@ func validateHorizontalPodAutoscalerSpec(autoscaler autoscaling.HorizontalPodAut
if refErrs := validateMetrics(autoscaler.Metrics, fldPath.Child("metrics"), autoscaler.MinReplicas); len(refErrs) > 0 { if refErrs := validateMetrics(autoscaler.Metrics, fldPath.Child("metrics"), autoscaler.MinReplicas); len(refErrs) > 0 {
allErrs = append(allErrs, refErrs...) allErrs = append(allErrs, refErrs...)
} }
if refErrs := validateBehavior(autoscaler.Behavior, fldPath.Child("behavior")); len(refErrs) > 0 { if refErrs := validateBehavior(autoscaler.Behavior, fldPath.Child("behavior"), opts); len(refErrs) > 0 {
allErrs = append(allErrs, refErrs...) allErrs = append(allErrs, refErrs...)
} }
return allErrs return allErrs
@ -115,7 +115,13 @@ func ValidateHorizontalPodAutoscaler(autoscaler *autoscaling.HorizontalPodAutosc
} else { } else {
minReplicasLowerBound = 1 minReplicasLowerBound = 1
} }
allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(autoscaler.Spec, field.NewPath("spec"), minReplicasLowerBound)...)
opts := HorizontalPodAutoscalerSpecValidationOptions{
AllowTolerance: utilfeature.DefaultMutableFeatureGate.Enabled(features.HPAConfigurableTolerance),
MinReplicasLowerBound: minReplicasLowerBound,
}
allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(autoscaler.Spec, field.NewPath("spec"), opts)...)
return allErrs return allErrs
} }
@ -134,7 +140,12 @@ func ValidateHorizontalPodAutoscalerUpdate(newAutoscaler, oldAutoscaler *autosca
minReplicasLowerBound = 1 minReplicasLowerBound = 1
} }
allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(newAutoscaler.Spec, field.NewPath("spec"), minReplicasLowerBound)...) opts := HorizontalPodAutoscalerSpecValidationOptions{
AllowTolerance: utilfeature.DefaultMutableFeatureGate.Enabled(features.HPAConfigurableTolerance),
MinReplicasLowerBound: minReplicasLowerBound,
}
allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(newAutoscaler.Spec, field.NewPath("spec"), opts)...)
return allErrs return allErrs
} }
@ -148,6 +159,15 @@ func ValidateHorizontalPodAutoscalerStatusUpdate(newAutoscaler, oldAutoscaler *a
return allErrs return allErrs
} }
// 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
}
func validateMetrics(metrics []autoscaling.MetricSpec, fldPath *field.Path, minReplicas *int32) field.ErrorList { func validateMetrics(metrics []autoscaling.MetricSpec, fldPath *field.Path, minReplicas *int32) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
hasObjectMetrics := false hasObjectMetrics := false
@ -175,13 +195,13 @@ func validateMetrics(metrics []autoscaling.MetricSpec, fldPath *field.Path, minR
return allErrs return allErrs
} }
func validateBehavior(behavior *autoscaling.HorizontalPodAutoscalerBehavior, fldPath *field.Path) field.ErrorList { func validateBehavior(behavior *autoscaling.HorizontalPodAutoscalerBehavior, fldPath *field.Path, opts HorizontalPodAutoscalerSpecValidationOptions) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
if behavior != nil { if behavior != nil {
if scaleUpErrs := validateScalingRules(behavior.ScaleUp, fldPath.Child("scaleUp")); len(scaleUpErrs) > 0 { if scaleUpErrs := validateScalingRules(behavior.ScaleUp, fldPath.Child("scaleUp"), opts); len(scaleUpErrs) > 0 {
allErrs = append(allErrs, scaleUpErrs...) allErrs = append(allErrs, scaleUpErrs...)
} }
if scaleDownErrs := validateScalingRules(behavior.ScaleDown, fldPath.Child("scaleDown")); len(scaleDownErrs) > 0 { if scaleDownErrs := validateScalingRules(behavior.ScaleDown, fldPath.Child("scaleDown"), opts); len(scaleDownErrs) > 0 {
allErrs = append(allErrs, scaleDownErrs...) allErrs = append(allErrs, scaleDownErrs...)
} }
} }
@ -191,7 +211,7 @@ func validateBehavior(behavior *autoscaling.HorizontalPodAutoscalerBehavior, fld
var validSelectPolicyTypes = sets.NewString(string(autoscaling.MaxPolicySelect), string(autoscaling.MinPolicySelect), string(autoscaling.DisabledPolicySelect)) var validSelectPolicyTypes = sets.NewString(string(autoscaling.MaxPolicySelect), string(autoscaling.MinPolicySelect), string(autoscaling.DisabledPolicySelect))
var validSelectPolicyTypesList = validSelectPolicyTypes.List() var validSelectPolicyTypesList = validSelectPolicyTypes.List()
func validateScalingRules(rules *autoscaling.HPAScalingRules, fldPath *field.Path) field.ErrorList { func validateScalingRules(rules *autoscaling.HPAScalingRules, fldPath *field.Path, opts HorizontalPodAutoscalerSpecValidationOptions) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
if rules != nil { if rules != nil {
if rules.StabilizationWindowSeconds != nil && *rules.StabilizationWindowSeconds < 0 { if rules.StabilizationWindowSeconds != nil && *rules.StabilizationWindowSeconds < 0 {
@ -214,6 +234,13 @@ func validateScalingRules(rules *autoscaling.HPAScalingRules, fldPath *field.Pat
allErrs = append(allErrs, policyErrs...) 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"))
}
} }
return allErrs return allErrs
} }

View File

@ -28,6 +28,7 @@ import (
api "k8s.io/kubernetes/pkg/apis/core" api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/features"
utilpointer "k8s.io/utils/pointer" utilpointer "k8s.io/utils/pointer"
"k8s.io/utils/ptr"
) )
func TestValidateScale(t *testing.T) { func TestValidateScale(t *testing.T) {
@ -367,8 +368,9 @@ func TestValidateBehavior(t *testing.T) {
func prepareHPAWithBehavior(b autoscaling.HorizontalPodAutoscalerBehavior) autoscaling.HorizontalPodAutoscaler { func prepareHPAWithBehavior(b autoscaling.HorizontalPodAutoscalerBehavior) autoscaling.HorizontalPodAutoscaler {
return autoscaling.HorizontalPodAutoscaler{ return autoscaling.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "myautoscaler", Name: "myautoscaler",
Namespace: metav1.NamespaceDefault, Namespace: metav1.NamespaceDefault,
ResourceVersion: "1",
}, },
Spec: autoscaling.HorizontalPodAutoscalerSpec{ Spec: autoscaling.HorizontalPodAutoscalerSpec{
ScaleTargetRef: autoscaling.CrossVersionObjectReference{ ScaleTargetRef: autoscaling.CrossVersionObjectReference{
@ -1652,3 +1654,236 @@ func TestValidateHorizontalPodAutoscalerScaleToZeroUpdateDisabled(t *testing.T)
} }
} }
} }
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,
PeriodSeconds: 1800,
}}
successCases := []autoscaling.HPAScalingRules{
{
Policies: policiesList,
Tolerance: ptr.To(resource.MustParse("0.1")),
},
{
Policies: policiesList,
Tolerance: ptr.To(resource.MustParse("10")),
},
{
Policies: policiesList,
Tolerance: ptr.To(resource.MustParse("0")),
},
{
Policies: policiesList,
Tolerance: resource.NewMilliQuantity(100, resource.DecimalSI),
},
{
Policies: policiesList,
Tolerance: resource.NewScaledQuantity(1, resource.Milli),
},
{
Policies: policiesList,
},
}
for _, c := range successCases {
b := autoscaling.HorizontalPodAutoscalerBehavior{
ScaleDown: &c,
}
hpa := prepareHPAWithBehavior(b)
if errs := ValidateHorizontalPodAutoscaler(&hpa); len(errs) != 0 {
t.Errorf("expected success: %v", errs)
}
}
failureCases := []struct {
rule autoscaling.HPAScalingRules
msg string
}{
{
rule: autoscaling.HPAScalingRules{},
msg: "at least one Policy",
},
{
rule: autoscaling.HPAScalingRules{
Policies: policiesList,
Tolerance: ptr.To(resource.MustParse("-0.001")),
},
msg: "greater or equal to zero",
},
{
rule: autoscaling.HPAScalingRules{
Policies: policiesList,
Tolerance: resource.NewMilliQuantity(-10, resource.DecimalSI),
},
msg: "greater or equal to zero",
},
{
rule: autoscaling.HPAScalingRules{
StabilizationWindowSeconds: utilpointer.Int32(60),
},
msg: "at least one Policy",
},
{
rule: autoscaling.HPAScalingRules{
Tolerance: resource.NewMilliQuantity(1, resource.DecimalSI),
StabilizationWindowSeconds: utilpointer.Int32(60),
},
msg: "at least one Policy",
},
}
for _, c := range failureCases {
b := autoscaling.HorizontalPodAutoscalerBehavior{
ScaleUp: &c.rule,
}
hpa := prepareHPAWithBehavior(b)
errs := ValidateHorizontalPodAutoscaler(&hpa)
if len(errs) != 1 {
t.Fatalf("expected exactly one error, got: %v", errs)
}
if !strings.Contains(errs[0].Error(), c.msg) {
t.Errorf("unexpected error: %q, expected: %q", errs[0], c.msg)
}
}
}
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,
Value: 1,
PeriodSeconds: 1800,
}}
successCases := []autoscaling.HPAScalingRules{
{
Policies: policiesList,
},
{
SelectPolicy: &maxPolicy,
Policies: policiesList,
},
{
StabilizationWindowSeconds: utilpointer.Int32(60),
Policies: policiesList,
},
}
for _, c := range successCases {
b := autoscaling.HorizontalPodAutoscalerBehavior{
ScaleDown: &c,
}
hpa := prepareHPAWithBehavior(b)
if errs := ValidateHorizontalPodAutoscaler(&hpa); len(errs) != 0 {
t.Errorf("expected success: %v", errs)
}
}
failureCases := []struct {
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",
},
{
rule: autoscaling.HPAScalingRules{
StabilizationWindowSeconds: utilpointer.Int32(60),
},
msg: "at least one Policy",
},
}
for _, c := range failureCases {
b := autoscaling.HorizontalPodAutoscalerBehavior{
ScaleUp: &c.rule,
}
hpa := prepareHPAWithBehavior(b)
errs := ValidateHorizontalPodAutoscaler(&hpa)
if len(errs) != 1 {
t.Fatalf("expected exactly one error, got: %v", errs)
}
if !strings.Contains(errs[0].Error(), c.msg) {
t.Errorf("unexpected error: %q, expected: %q", errs[0], c.msg)
}
}
}
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,
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,
}})
if errs := ValidateHorizontalPodAutoscalerUpdate(&withToleranceHPA, &withoutToleranceHPA); 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 {
t.Errorf("expected success: %v", errs)
}
}

View File

@ -171,12 +171,18 @@ const (
DisabledPolicySelect ScalingPolicySelect = "Disabled" DisabledPolicySelect ScalingPolicySelect = "Disabled"
) )
// HPAScalingRules configures the scaling behavior for one direction. // HPAScalingRules configures the scaling behavior for one direction via
// These Rules are applied after calculating DesiredReplicas from metrics for the HPA. // scaling Policy Rules and a configurable metric tolerance.
//
// Scaling Policy Rules are applied after calculating DesiredReplicas from metrics for the HPA.
// They can limit the scaling velocity by specifying scaling policies. // They can limit the scaling velocity by specifying scaling policies.
// They can prevent flapping by specifying the stabilization window, so that the // They can prevent flapping by specifying the stabilization window, so that the
// number of replicas is not set instantly, instead, the safest value from the stabilization // number of replicas is not set instantly, instead, the safest value from the stabilization
// window is chosen. // window is chosen.
//
// The tolerance is applied to the metric values and prevents scaling too
// eagerly for small metric variations. (Note that setting a tolerance requires
// enabling the alpha HPAConfigurableTolerance feature gate.)
type HPAScalingRules struct { type HPAScalingRules struct {
// stabilizationWindowSeconds is the number of seconds for which past recommendations should be // stabilizationWindowSeconds is the number of seconds for which past recommendations should be
// considered while scaling up or scaling down. // considered while scaling up or scaling down.
@ -193,10 +199,24 @@ type HPAScalingRules struct {
SelectPolicy *ScalingPolicySelect `json:"selectPolicy,omitempty" protobuf:"bytes,1,opt,name=selectPolicy"` SelectPolicy *ScalingPolicySelect `json:"selectPolicy,omitempty" protobuf:"bytes,1,opt,name=selectPolicy"`
// policies is a list of potential scaling polices which can be used during scaling. // policies is a list of potential scaling polices which can be used during scaling.
// At least one policy must be specified, otherwise the HPAScalingRules will be discarded as invalid // If not set, use the default values:
// - For scale up: allow doubling the number of pods, or an absolute change of 4 pods in a 15s window.
// - For scale down: allow all pods to be removed in a 15s window.
// +listType=atomic // +listType=atomic
// +optional // +optional
Policies []HPAScalingPolicy `json:"policies,omitempty" listType:"atomic" protobuf:"bytes,2,rep,name=policies"` Policies []HPAScalingPolicy `json:"policies,omitempty" listType:"atomic" protobuf:"bytes,2,rep,name=policies"`
// tolerance is the tolerance on the ratio between the current and desired
// metric value under which no updates are made to the desired number of
// 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%).
//
// This is an alpha field and requires enabling the HPAConfigurableTolerance
// feature gate.
//
// +featureGate=HPAConfigurableTolerance
// +optional
Tolerance *resource.Quantity `json:"tolerance,omitempty" protobuf:"bytes,4,opt,name=tolerance"`
} }
// HPAScalingPolicyType is the type of the policy which could be used while making scaling decisions. // HPAScalingPolicyType is the type of the policy which could be used while making scaling decisions.