Merge pull request #136998 from yongruilin/master-vg_hpa-ifenable

Migrate HPA MinReplicas to Declarative Validation with Feature Gate Support
This commit is contained in:
Kubernetes Prow Robot
2026-02-14 06:59:58 +05:30
committed by GitHub
9 changed files with 99 additions and 8 deletions

View File

@@ -102,6 +102,12 @@ func Validate_HorizontalPodAutoscalerSpec(ctx context.Context, op operation.Oper
if earlyReturn {
return // do not proceed
}
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "HPAScaleToZero", false, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int32) field.ErrorList {
return validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)
})...)
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "HPAScaleToZero", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int32) field.ErrorList {
return validate.Minimum(ctx, op, fldPath, obj, oldObj, 0)
})...)
return
}(fldPath.Child("minReplicas"), obj.MinReplicas, safe.Field(oldObj, func(oldObj *autoscalingv1.HorizontalPodAutoscalerSpec) *int32 { return oldObj.MinReplicas }), oldObj != nil)...)

View File

@@ -94,6 +94,12 @@ func Validate_HorizontalPodAutoscalerSpec(ctx context.Context, op operation.Oper
if earlyReturn {
return // do not proceed
}
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "HPAScaleToZero", false, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int32) field.ErrorList {
return validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)
})...)
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "HPAScaleToZero", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int32) field.ErrorList {
return validate.Minimum(ctx, op, fldPath, obj, oldObj, 0)
})...)
return
}(fldPath.Child("minReplicas"), obj.MinReplicas, safe.Field(oldObj, func(oldObj *autoscalingv2.HorizontalPodAutoscalerSpec) *int32 { return oldObj.MinReplicas }), oldObj != nil)...)

View File

@@ -57,7 +57,7 @@ func validateHorizontalPodAutoscalerSpec(autoscaler autoscaling.HorizontalPodAut
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", opts.MinReplicasLowerBound)))
fmt.Sprintf("must be greater than or equal to %d", opts.MinReplicasLowerBound)).WithOrigin("minimum").MarkCoveredByDeclarative())
}
if autoscaler.MaxReplicas == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("maxReplicas"), "must be set and greater than 0").MarkCoveredByDeclarative())

View File

@@ -19,11 +19,16 @@ package horizontalpodautoscaler
import (
"testing"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
apitesting "k8s.io/kubernetes/pkg/api/testing"
api "k8s.io/kubernetes/pkg/apis/autoscaling"
"k8s.io/kubernetes/pkg/features"
"k8s.io/utils/ptr"
)
var apiVersions = []string{"v1", "v2"}
@@ -41,12 +46,17 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) {
Resource: "horizontalpodautoscalers",
})
testCases := map[string]struct {
input api.HorizontalPodAutoscaler
expectedErrs field.ErrorList
input api.HorizontalPodAutoscaler
expectedErrs field.ErrorList
enableScaleToZero bool
}{
"valid: minReplicas = 5": {
input: makeValidHPA(tweakMinReplicas(5)),
},
"valid: minReplicas = 0 (gate enabled)": {
input: makeValidHPA(tweakMinReplicas(0), tweakMetrics(validScaleToZeroMetrics...)),
enableScaleToZero: true,
},
"valid: minReplicas not set (nil)": {
input: makeValidHPA(), // Default, no minReplicas set
},
@@ -62,9 +72,16 @@ func testDeclarativeValidate(t *testing.T, apiVersion string) {
field.Invalid(field.NewPath("spec", "maxReplicas"), int32(-1), "must be greater than or equal to 1").WithOrigin("minimum"),
},
},
"invalid: minReplicas = 0 (gate disabled)": {
input: makeValidHPA(tweakMinReplicas(0), tweakMetrics(validScaleToZeroMetrics...)),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "minReplicas"), int32(0), "must be greater than or equal to 1").WithOrigin("minimum"),
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAScaleToZero, tc.enableScaleToZero)
apitesting.VerifyValidationEquivalence(t, ctx, &tc.input, Strategy.Validate, tc.expectedErrs)
})
}
@@ -80,9 +97,10 @@ func TestDeclarativeValidateUpdate(t *testing.T) {
func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
testCases := map[string]struct {
oldObj api.HorizontalPodAutoscaler
updateObj api.HorizontalPodAutoscaler
expectedErrs field.ErrorList
oldObj api.HorizontalPodAutoscaler
updateObj api.HorizontalPodAutoscaler
expectedErrs field.ErrorList
enableScaleToZero bool
}{
"valid update": {
oldObj: makeValidHPA(),
@@ -92,6 +110,11 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
oldObj: makeValidHPA(tweakMinReplicas(1)),
updateObj: makeValidHPA(tweakMinReplicas(5)),
},
"valid update: minReplicas 1 -> 0 (gate enabled)": {
oldObj: makeValidHPA(tweakMinReplicas(1), tweakMetrics(validScaleToZeroMetrics...)),
updateObj: makeValidHPA(tweakMinReplicas(0), tweakMetrics(validScaleToZeroMetrics...)),
enableScaleToZero: true,
},
"invalid update: maxReplicas = 0 (required)": {
oldObj: makeValidHPA(),
updateObj: makeValidHPA(tweakMaxReplicas(0)),
@@ -106,9 +129,22 @@ func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
field.Invalid(field.NewPath("spec", "maxReplicas"), int32(-1), "must be greater than or equal to 1").WithOrigin("minimum"),
},
},
"invalid update: minReplicas 1 -> 0 (gate disabled)": {
oldObj: makeValidHPA(tweakMinReplicas(1), tweakMetrics(validScaleToZeroMetrics...)),
updateObj: makeValidHPA(tweakMinReplicas(0), tweakMetrics(validScaleToZeroMetrics...)),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "minReplicas"), int32(0), "must be greater than or equal to 1").WithOrigin("minimum"),
},
},
"valid update: ratcheting minReplicas=0 when gate disabled": {
oldObj: makeValidHPA(tweakMinReplicas(0), tweakMetrics(validScaleToZeroMetrics...)),
updateObj: makeValidHPA(tweakMinReplicas(0), tweakMetrics(validScaleToZeroMetrics...), tweakMaxReplicas(20)),
enableScaleToZero: false,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAScaleToZero, tc.enableScaleToZero)
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "autoscaling",
APIVersion: apiVersion,
@@ -155,3 +191,29 @@ func tweakMaxReplicas(replicas int32) func(*api.HorizontalPodAutoscaler) {
hpa.Spec.MaxReplicas = replicas
}
}
func tweakMetrics(metrics ...api.MetricSpec) func(*api.HorizontalPodAutoscaler) {
return func(hpa *api.HorizontalPodAutoscaler) {
hpa.Spec.Metrics = metrics
}
}
var validScaleToZeroMetrics = []api.MetricSpec{
{
Type: api.ObjectMetricSourceType,
Object: &api.ObjectMetricSource{
Metric: api.MetricIdentifier{
Name: "requests-per-second",
},
Target: api.MetricTarget{
Type: api.ValueMetricType,
Value: ptr.To(resource.MustParse("10k")),
},
DescribedObject: api.CrossVersionObjectReference{
Kind: "Ingress",
Name: "main-route",
APIVersion: "networking.k8s.io/v1",
},
},
},
}

View File

@@ -77,7 +77,11 @@ func (autoscalerStrategy) Validate(ctx context.Context, obj runtime.Object) fiel
autoscaler := obj.(*autoscaling.HorizontalPodAutoscaler)
opts := validationOptionsForHorizontalPodAutoscaler(autoscaler, nil)
allErrs := validation.ValidateHorizontalPodAutoscaler(autoscaler, opts)
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, autoscaler, nil, allErrs, operation.Create)
var options []string
if utilfeature.DefaultFeatureGate.Enabled(features.HPAScaleToZero) {
options = append(options, "HPAScaleToZero")
}
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, autoscaler, nil, allErrs, operation.Create, rest.WithOptions(options))
}
// WarningsOnCreate returns warnings for the creation of the given object.
@@ -110,7 +114,12 @@ func (autoscalerStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.O
oldHPA := old.(*autoscaling.HorizontalPodAutoscaler)
opts := validationOptionsForHorizontalPodAutoscaler(newHPA, oldHPA)
errs := validation.ValidateHorizontalPodAutoscalerUpdate(newHPA, oldHPA, opts)
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newHPA, oldHPA, errs, operation.Update)
var options []string
oldHasZeroMinReplicas := oldHPA.Spec.MinReplicas != nil && *oldHPA.Spec.MinReplicas == 0
if utilfeature.DefaultFeatureGate.Enabled(features.HPAScaleToZero) || oldHasZeroMinReplicas {
options = append(options, "HPAScaleToZero")
}
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newHPA, oldHPA, errs, operation.Update, rest.WithOptions(options))
}
// WarningsOnUpdate returns warnings for the given update.

View File

@@ -203,6 +203,8 @@ message HorizontalPodAutoscalerSpec {
// available.
// +optional
// +k8s:optional
// +k8s:ifEnabled(HPAScaleToZero)=+k8s:minimum=0
// +k8s:ifDisabled(HPAScaleToZero)=+k8s:minimum=1
optional int32 minReplicas = 2;
// maxReplicas is the upper limit for the number of pods that can be set by the autoscaler; cannot be smaller than MinReplicas.

View File

@@ -48,6 +48,8 @@ type HorizontalPodAutoscalerSpec struct {
// available.
// +optional
// +k8s:optional
// +k8s:ifEnabled(HPAScaleToZero)=+k8s:minimum=0
// +k8s:ifDisabled(HPAScaleToZero)=+k8s:minimum=1
MinReplicas *int32 `json:"minReplicas,omitempty" protobuf:"varint,2,opt,name=minReplicas"`
// maxReplicas is the upper limit for the number of pods that can be set by the autoscaler; cannot be smaller than MinReplicas.

View File

@@ -249,6 +249,8 @@ message HorizontalPodAutoscalerSpec {
// available.
// +optional
// +k8s:optional
// +k8s:ifEnabled(HPAScaleToZero)=+k8s:minimum=0
// +k8s:ifDisabled(HPAScaleToZero)=+k8s:minimum=1
optional int32 minReplicas = 2;
// maxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up.

View File

@@ -60,6 +60,8 @@ type HorizontalPodAutoscalerSpec struct {
// available.
// +optional
// +k8s:optional
// +k8s:ifEnabled(HPAScaleToZero)=+k8s:minimum=0
// +k8s:ifDisabled(HPAScaleToZero)=+k8s:minimum=1
MinReplicas *int32 `json:"minReplicas,omitempty" protobuf:"varint,2,opt,name=minReplicas"`
// maxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up.