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
// when converting the `Behavior` field from autoscaling/v2beta2
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
// 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.
//
// It should always be called when converting internal -> external versions, prior
// 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) {
_, hasMetricsSpecs := in[MetricSpecsAnnotation]
_, hasBehaviorSpecs := in[BehaviorSpecsAnnotation]
_, hasToleranceScaleDown := in[ToleranceScaleDownAnnotation]
_, hasToleranceScaleUp := in[ToleranceScaleUpAnnotation]
_, hasMetricsStatuses := in[MetricStatusesAnnotation]
_, hasConditions := in[HorizontalPodAutoscalerConditionsAnnotation]
if hasMetricsSpecs || hasBehaviorSpecs || hasMetricsStatuses || hasConditions {
if hasMetricsSpecs || hasBehaviorSpecs || hasToleranceScaleDown || hasToleranceScaleUp || hasMetricsStatuses || hasConditions {
out = DeepCopyStringMap(in)
delete(out, MetricSpecsAnnotation)
delete(out, BehaviorSpecsAnnotation)
delete(out, ToleranceScaleDownAnnotation)
delete(out, ToleranceScaleUpAnnotation)
delete(out, MetricStatusesAnnotation)
delete(out, HorizontalPodAutoscalerConditionsAnnotation)
return out, true

View File

@ -138,12 +138,18 @@ const (
DisabledPolicySelect ScalingPolicySelect = "Disabled"
)
// HPAScalingRules configures the scaling behavior for one direction.
// These Rules are applied after calculating DesiredReplicas from metrics for the HPA.
// HPAScalingRules configures the scaling behavior for one direction via
// 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 prevent flapping by specifying the stabilization window, so that the
// number of replicas is not set instantly, instead, the safest value from the stabilization
// 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 {
// StabilizationWindowSeconds is the number of seconds for which past recommendations should be
// considered while scaling up or scaling down.
@ -157,10 +163,23 @@ type HPAScalingRules struct {
// If not set, the default value MaxPolicySelect is used.
// +optional
SelectPolicy *ScalingPolicySelect
// policies is a list of potential scaling polices which can used during scaling.
// At least one policy must be specified, otherwise the HPAScalingRules will be discarded as invalid
// policies is a list of potential scaling polices which can be used during scaling.
// 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
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.

View File

@ -91,9 +91,12 @@ func SetDefaults_HorizontalPodAutoscaler(obj *autoscalingv2.HorizontalPodAutosca
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) {
// 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 {
obj.Spec.Behavior.ScaleUp = GenerateHPAScaleUpRules(obj.Spec.Behavior.ScaleUp)
obj.Spec.Behavior.ScaleDown = GenerateHPAScaleDownRules(obj.Spec.Behavior.ScaleDown)
@ -129,5 +132,8 @@ func copyHPAScalingRules(from, to *autoscalingv2.HPAScalingRules) *autoscalingv2
if from.Policies != nil {
to.Policies = from.Policies
}
if from.Tolerance != nil {
to.Tolerance = from.Tolerance
}
return to
}

View File

@ -20,8 +20,12 @@ import (
"reflect"
"testing"
"k8s.io/apimachinery/pkg/api/resource"
"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/features"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
@ -132,14 +136,17 @@ func TestGenerateScaleUpRules(t *testing.T) {
rateUpPercentPeriodSeconds int32
stabilizationSeconds *int32
selectPolicy *autoscalingv2.ScalingPolicySelect
tolerance *resource.Quantity
expectedPolicies []autoscalingv2.HPAScalingPolicy
expectedStabilization *int32
expectedSelectPolicy string
expectedTolerance *resource.Quantity
annotation string
}
maxPolicy := autoscalingv2.MaxChangePolicySelect
minPolicy := autoscalingv2.MinChangePolicySelect
sampleTolerance := resource.MustParse("0.5")
tests := []TestCase{
{
annotation: "Default values",
@ -208,12 +215,25 @@ func TestGenerateScaleUpRules(t *testing.T) {
expectedStabilization: utilpointer.Int32(25),
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 {
t.Run(tc.annotation, func(t *testing.T) {
scaleUpRules := &autoscalingv2.HPAScalingRules{
StabilizationWindowSeconds: tc.stabilizationSeconds,
SelectPolicy: tc.selectPolicy,
Tolerance: tc.tolerance,
}
if tc.rateUpPods != 0 || tc.rateUpPodsPeriodSeconds != 0 {
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, 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) {
tests := []struct {
hpa autoscalingv2.HorizontalPodAutoscaler

View File

@ -17,8 +17,11 @@ limitations under the License.
package v2beta2
import (
"fmt"
autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/conversion"
"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 {
return err
}
// v2beta2 round-trips to internal without any serialized annotations, make sure any from other versions don't get serialized
out.Annotations, _ = autoscaling.DropRoundTripHorizontalPodAutoscalerAnnotations(out.Annotations)
// Ensure old round-trips annotations are discarded
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
}
@ -36,7 +60,44 @@ func Convert_v2beta2_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutosca
if err := autoConvert_v2beta2_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler(in, out, s); err != nil {
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)
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 {
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 {
return Convert_v2beta2_HorizontalPodAutoscalerBehavior_To_autoscaling_HorizontalPodAutoscalerBehavior(a.(*autoscalingv2beta2.HorizontalPodAutoscalerBehavior), b.(*autoscaling.HorizontalPodAutoscalerBehavior), scope)
}); err != nil {
@ -271,11 +261,21 @@ func RegisterConversions(s *runtime.Scheme) error {
}); err != nil {
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 {
return Convert_autoscaling_HorizontalPodAutoscaler_To_v2beta2_HorizontalPodAutoscaler(a.(*autoscaling.HorizontalPodAutoscaler), b.(*autoscalingv2beta2.HorizontalPodAutoscaler), scope)
}); err != nil {
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 {
return Convert_v2beta2_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler(a.(*autoscalingv2beta2.HorizontalPodAutoscaler), b.(*autoscaling.HorizontalPodAutoscaler), scope)
}); err != nil {
@ -455,23 +455,14 @@ func autoConvert_v2beta2_HPAScalingRules_To_autoscaling_HPAScalingRules(in *auto
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 {
out.StabilizationWindowSeconds = (*int32)(unsafe.Pointer(in.StabilizationWindowSeconds))
out.SelectPolicy = (*autoscalingv2beta2.ScalingPolicySelect)(unsafe.Pointer(in.SelectPolicy))
out.Policies = *(*[]autoscalingv2beta2.HPAScalingPolicy)(unsafe.Pointer(&in.Policies))
// WARNING: in.Tolerance requires manual conversion: does not exist in peer-type
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 {
out.ObjectMeta = in.ObjectMeta
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 {
out.ScaleUp = (*autoscaling.HPAScalingRules)(unsafe.Pointer(in.ScaleUp))
out.ScaleDown = (*autoscaling.HPAScalingRules)(unsafe.Pointer(in.ScaleDown))
if in.ScaleUp != nil {
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
}
@ -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 {
out.ScaleUp = (*autoscalingv2beta2.HPAScalingRules)(unsafe.Pointer(in.ScaleUp))
out.ScaleDown = (*autoscalingv2beta2.HPAScalingRules)(unsafe.Pointer(in.ScaleDown))
if in.ScaleUp != nil {
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
}
@ -603,7 +626,15 @@ func autoConvert_v2beta2_HorizontalPodAutoscalerSpec_To_autoscaling_HorizontalPo
} else {
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
}
@ -629,7 +660,15 @@ func autoConvert_autoscaling_HorizontalPodAutoscalerSpec_To_v2beta2_HorizontalPo
} else {
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
}

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.
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{}
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,
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 {
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 {
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...)
}
return allErrs
@ -115,7 +115,13 @@ func ValidateHorizontalPodAutoscaler(autoscaler *autoscaling.HorizontalPodAutosc
} else {
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
}
@ -134,7 +140,12 @@ func ValidateHorizontalPodAutoscalerUpdate(newAutoscaler, oldAutoscaler *autosca
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
}
@ -148,6 +159,15 @@ func ValidateHorizontalPodAutoscalerStatusUpdate(newAutoscaler, oldAutoscaler *a
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 {
allErrs := field.ErrorList{}
hasObjectMetrics := false
@ -175,13 +195,13 @@ func validateMetrics(metrics []autoscaling.MetricSpec, fldPath *field.Path, minR
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{}
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...)
}
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...)
}
}
@ -191,7 +211,7 @@ func validateBehavior(behavior *autoscaling.HorizontalPodAutoscalerBehavior, fld
var validSelectPolicyTypes = sets.NewString(string(autoscaling.MaxPolicySelect), string(autoscaling.MinPolicySelect), string(autoscaling.DisabledPolicySelect))
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{}
if rules != nil {
if rules.StabilizationWindowSeconds != nil && *rules.StabilizationWindowSeconds < 0 {
@ -214,6 +234,13 @@ 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"))
}
}
return allErrs
}

View File

@ -28,6 +28,7 @@ import (
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/features"
utilpointer "k8s.io/utils/pointer"
"k8s.io/utils/ptr"
)
func TestValidateScale(t *testing.T) {
@ -367,8 +368,9 @@ func TestValidateBehavior(t *testing.T) {
func prepareHPAWithBehavior(b autoscaling.HorizontalPodAutoscalerBehavior) autoscaling.HorizontalPodAutoscaler {
return autoscaling.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: "myautoscaler",
Namespace: metav1.NamespaceDefault,
Name: "myautoscaler",
Namespace: metav1.NamespaceDefault,
ResourceVersion: "1",
},
Spec: autoscaling.HorizontalPodAutoscalerSpec{
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"
)
// HPAScalingRules configures the scaling behavior for one direction.
// These Rules are applied after calculating DesiredReplicas from metrics for the HPA.
// HPAScalingRules configures the scaling behavior for one direction via
// 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 prevent flapping by specifying the stabilization window, so that the
// number of replicas is not set instantly, instead, the safest value from the stabilization
// 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 {
// stabilizationWindowSeconds is the number of seconds for which past recommendations should be
// 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"`
// 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
// +optional
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.