mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
Add the HorizontalPodAutoscaler tolerance field.
Includes v2beta2 HPA round-trip conversion, defaulting, and validation.
This commit is contained in:
parent
463b15b9b2
commit
a41284d9fa
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
124
pkg/apis/autoscaling/v2beta2/conversion_test.go
Normal file
124
pkg/apis/autoscaling/v2beta2/conversion_test.go
Normal 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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
@ -369,6 +370,7 @@ func prepareHPAWithBehavior(b autoscaling.HorizontalPodAutoscalerBehavior) autos
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user