HPA: Enable DV support for MaxReplicas (#135412)

Signed-off-by: Omer Aplatony <omerap12@gmail.com>
This commit is contained in:
Omer Aplatony
2026-01-12 21:24:09 +02:00
committed by GitHub
parent 19d344fbee
commit 6cbb58349d
10 changed files with 388 additions and 4 deletions

View File

@@ -26,6 +26,7 @@ import (
fmt "fmt"
autoscalingv1 "k8s.io/api/autoscaling/v1"
equality "k8s.io/apimachinery/pkg/api/equality"
operation "k8s.io/apimachinery/pkg/api/operation"
safe "k8s.io/apimachinery/pkg/api/safe"
validate "k8s.io/apimachinery/pkg/api/validate"
@@ -38,6 +39,14 @@ func init() { localSchemeBuilder.Register(RegisterValidations) }
// RegisterValidations adds validation functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterValidations(scheme *runtime.Scheme) error {
// type HorizontalPodAutoscaler
scheme.AddValidationFunc((*autoscalingv1.HorizontalPodAutoscaler)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_HorizontalPodAutoscaler(ctx, op, nil /* fldPath */, obj.(*autoscalingv1.HorizontalPodAutoscaler), safe.Cast[*autoscalingv1.HorizontalPodAutoscaler](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
// type Scale
scheme.AddValidationFunc((*autoscalingv1.Scale)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
@@ -49,6 +58,77 @@ func RegisterValidations(scheme *runtime.Scheme) error {
return nil
}
// Validate_HorizontalPodAutoscaler validates an instance of HorizontalPodAutoscaler according
// to declarative validation rules in the API schema.
func Validate_HorizontalPodAutoscaler(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *autoscalingv1.HorizontalPodAutoscaler) (errs field.ErrorList) {
// field autoscalingv1.HorizontalPodAutoscaler.TypeMeta has no validation
// field autoscalingv1.HorizontalPodAutoscaler.ObjectMeta has no validation
// field autoscalingv1.HorizontalPodAutoscaler.Spec
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *autoscalingv1.HorizontalPodAutoscalerSpec, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call the type's validation function
errs = append(errs, Validate_HorizontalPodAutoscalerSpec(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("spec"), &obj.Spec, safe.Field(oldObj, func(oldObj *autoscalingv1.HorizontalPodAutoscaler) *autoscalingv1.HorizontalPodAutoscalerSpec {
return &oldObj.Spec
}), oldObj != nil)...)
// field autoscalingv1.HorizontalPodAutoscaler.Status has no validation
return errs
}
// Validate_HorizontalPodAutoscalerSpec validates an instance of HorizontalPodAutoscalerSpec according
// to declarative validation rules in the API schema.
func Validate_HorizontalPodAutoscalerSpec(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *autoscalingv1.HorizontalPodAutoscalerSpec) (errs field.ErrorList) {
// field autoscalingv1.HorizontalPodAutoscalerSpec.ScaleTargetRef has no validation
// field autoscalingv1.HorizontalPodAutoscalerSpec.MinReplicas
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int32, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("minReplicas"), obj.MinReplicas, safe.Field(oldObj, func(oldObj *autoscalingv1.HorizontalPodAutoscalerSpec) *int32 { return oldObj.MinReplicas }), oldObj != nil)...)
// field autoscalingv1.HorizontalPodAutoscalerSpec.MaxReplicas
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int32, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
return
}(fldPath.Child("maxReplicas"), &obj.MaxReplicas, safe.Field(oldObj, func(oldObj *autoscalingv1.HorizontalPodAutoscalerSpec) *int32 { return &oldObj.MaxReplicas }), oldObj != nil)...)
// field autoscalingv1.HorizontalPodAutoscalerSpec.TargetCPUUtilizationPercentage has no validation
return errs
}
// Validate_Scale validates an instance of Scale according
// to declarative validation rules in the API schema.
func Validate_Scale(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *autoscalingv1.Scale) (errs field.ErrorList) {

View File

@@ -18,5 +18,7 @@ limitations under the License.
// +k8s:conversion-gen-external-types=k8s.io/api/autoscaling/v2
// +k8s:defaulter-gen=TypeMeta
// +k8s:defaulter-gen-input=k8s.io/api/autoscaling/v2
// +k8s:validation-gen=TypeMeta
// +k8s:validation-gen-input=k8s.io/api/autoscaling/v2
package v2

View File

@@ -0,0 +1,123 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 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.
*/
// Code generated by validation-gen. DO NOT EDIT.
package v2
import (
context "context"
fmt "fmt"
autoscalingv2 "k8s.io/api/autoscaling/v2"
equality "k8s.io/apimachinery/pkg/api/equality"
operation "k8s.io/apimachinery/pkg/api/operation"
safe "k8s.io/apimachinery/pkg/api/safe"
validate "k8s.io/apimachinery/pkg/api/validate"
runtime "k8s.io/apimachinery/pkg/runtime"
field "k8s.io/apimachinery/pkg/util/validation/field"
)
func init() { localSchemeBuilder.Register(RegisterValidations) }
// RegisterValidations adds validation functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterValidations(scheme *runtime.Scheme) error {
// type HorizontalPodAutoscaler
scheme.AddValidationFunc((*autoscalingv2.HorizontalPodAutoscaler)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_HorizontalPodAutoscaler(ctx, op, nil /* fldPath */, obj.(*autoscalingv2.HorizontalPodAutoscaler), safe.Cast[*autoscalingv2.HorizontalPodAutoscaler](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
return nil
}
// Validate_HorizontalPodAutoscaler validates an instance of HorizontalPodAutoscaler according
// to declarative validation rules in the API schema.
func Validate_HorizontalPodAutoscaler(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *autoscalingv2.HorizontalPodAutoscaler) (errs field.ErrorList) {
// field autoscalingv2.HorizontalPodAutoscaler.TypeMeta has no validation
// field autoscalingv2.HorizontalPodAutoscaler.ObjectMeta has no validation
// field autoscalingv2.HorizontalPodAutoscaler.Spec
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *autoscalingv2.HorizontalPodAutoscalerSpec, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call the type's validation function
errs = append(errs, Validate_HorizontalPodAutoscalerSpec(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("spec"), &obj.Spec, safe.Field(oldObj, func(oldObj *autoscalingv2.HorizontalPodAutoscaler) *autoscalingv2.HorizontalPodAutoscalerSpec {
return &oldObj.Spec
}), oldObj != nil)...)
// field autoscalingv2.HorizontalPodAutoscaler.Status has no validation
return errs
}
// Validate_HorizontalPodAutoscalerSpec validates an instance of HorizontalPodAutoscalerSpec according
// to declarative validation rules in the API schema.
func Validate_HorizontalPodAutoscalerSpec(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *autoscalingv2.HorizontalPodAutoscalerSpec) (errs field.ErrorList) {
// field autoscalingv2.HorizontalPodAutoscalerSpec.ScaleTargetRef has no validation
// field autoscalingv2.HorizontalPodAutoscalerSpec.MinReplicas
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int32, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("minReplicas"), obj.MinReplicas, safe.Field(oldObj, func(oldObj *autoscalingv2.HorizontalPodAutoscalerSpec) *int32 { return oldObj.MinReplicas }), oldObj != nil)...)
// field autoscalingv2.HorizontalPodAutoscalerSpec.MaxReplicas
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int32, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 1)...)
return
}(fldPath.Child("maxReplicas"), &obj.MaxReplicas, safe.Field(oldObj, func(oldObj *autoscalingv2.HorizontalPodAutoscalerSpec) *int32 { return &oldObj.MaxReplicas }), oldObj != nil)...)
// field autoscalingv2.HorizontalPodAutoscalerSpec.Metrics has no validation
// field autoscalingv2.HorizontalPodAutoscalerSpec.Behavior has no validation
return errs
}

View File

@@ -59,8 +59,10 @@ func validateHorizontalPodAutoscalerSpec(autoscaler autoscaling.HorizontalPodAut
allErrs = append(allErrs, field.Invalid(fldPath.Child("minReplicas"), *autoscaler.MinReplicas,
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"))
if autoscaler.MaxReplicas == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("maxReplicas"), "must be set and greater than 0").MarkCoveredByDeclarative())
} else if autoscaler.MaxReplicas < 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("maxReplicas"), autoscaler.MaxReplicas, "must be greater than or equal to 1").WithOrigin("minimum").MarkCoveredByDeclarative())
}
if autoscaler.MinReplicas != nil && autoscaler.MaxReplicas < *autoscaler.MinReplicas {
allErrs = append(allErrs, field.Invalid(fldPath.Child("maxReplicas"), autoscaler.MaxReplicas, "must be greater than or equal to `minReplicas`"))

View File

@@ -0,0 +1,157 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package horizontalpodautoscaler
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
apitesting "k8s.io/kubernetes/pkg/api/testing"
api "k8s.io/kubernetes/pkg/apis/autoscaling"
)
var apiVersions = []string{"v1", "v2"}
func TestDeclarativeValidate(t *testing.T) {
for _, apiVersion := range apiVersions {
testDeclarativeValidate(t, apiVersion)
}
}
func testDeclarativeValidate(t *testing.T, apiVersion string) {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "autoscaling",
APIVersion: apiVersion,
Resource: "horizontalpodautoscalers",
})
testCases := map[string]struct {
input api.HorizontalPodAutoscaler
expectedErrs field.ErrorList
}{
"valid: minReplicas = 5": {
input: makeValidHPA(tweakMinReplicas(5)),
},
"valid: minReplicas not set (nil)": {
input: makeValidHPA(), // Default, no minReplicas set
},
"invalid: maxReplicas = 0 (required)": {
input: makeValidHPA(tweakMaxReplicas(0)),
expectedErrs: field.ErrorList{
field.Required(field.NewPath("spec", "maxReplicas"), ""),
},
},
"invalid: maxReplicas negative": {
input: makeValidHPA(tweakMaxReplicas(-1)),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "maxReplicas"), int32(-1), "must be greater than or equal to 1").WithOrigin("minimum"),
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
apitesting.VerifyValidationEquivalence(t, ctx, &tc.input, Strategy.Validate, tc.expectedErrs)
})
}
}
func TestDeclarativeValidateUpdate(t *testing.T) {
for _, apiVersion := range apiVersions {
t.Run(apiVersion, func(t *testing.T) {
testDeclarativeValidateUpdate(t, apiVersion)
})
}
}
func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
testCases := map[string]struct {
oldObj api.HorizontalPodAutoscaler
updateObj api.HorizontalPodAutoscaler
expectedErrs field.ErrorList
}{
"valid update": {
oldObj: makeValidHPA(),
updateObj: makeValidHPA(tweakMaxReplicas(20)),
},
"valid update: change minReplicas": {
oldObj: makeValidHPA(tweakMinReplicas(1)),
updateObj: makeValidHPA(tweakMinReplicas(5)),
},
"invalid update: maxReplicas = 0 (required)": {
oldObj: makeValidHPA(),
updateObj: makeValidHPA(tweakMaxReplicas(0)),
expectedErrs: field.ErrorList{
field.Required(field.NewPath("spec", "maxReplicas"), ""),
},
},
"invalid update: maxReplicas negative": {
oldObj: makeValidHPA(),
updateObj: makeValidHPA(tweakMaxReplicas(-1)),
expectedErrs: field.ErrorList{
field.Invalid(field.NewPath("spec", "maxReplicas"), int32(-1), "must be greater than or equal to 1").WithOrigin("minimum"),
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "autoscaling",
APIVersion: apiVersion,
Resource: "horizontalpodautoscalers",
Name: "test-hpa",
IsResourceRequest: true,
Verb: "update",
})
apitesting.VerifyUpdateValidationEquivalence(t, ctx, &tc.updateObj, &tc.oldObj, Strategy.ValidateUpdate, tc.expectedErrs)
})
}
}
func makeValidHPA(mutators ...func(*api.HorizontalPodAutoscaler)) api.HorizontalPodAutoscaler {
hpa := api.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: "test-hpa",
Namespace: "default",
ResourceVersion: "1",
},
Spec: api.HorizontalPodAutoscalerSpec{
ScaleTargetRef: api.CrossVersionObjectReference{
Kind: "Deployment",
Name: "test-deployment",
APIVersion: "apps/v1",
},
MaxReplicas: 10,
},
}
for _, mutate := range mutators {
mutate(&hpa)
}
return hpa
}
func tweakMinReplicas(replicas int32) func(*api.HorizontalPodAutoscaler) {
return func(hpa *api.HorizontalPodAutoscaler) {
hpa.Spec.MinReplicas = &replicas
}
}
func tweakMaxReplicas(replicas int32) func(*api.HorizontalPodAutoscaler) {
return func(hpa *api.HorizontalPodAutoscaler) {
hpa.Spec.MaxReplicas = replicas
}
}

View File

@@ -19,8 +19,10 @@ package horizontalpodautoscaler
import (
"context"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/storage/names"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/api/legacyscheme"
@@ -74,7 +76,8 @@ func (autoscalerStrategy) PrepareForCreate(ctx context.Context, obj runtime.Obje
func (autoscalerStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
autoscaler := obj.(*autoscaling.HorizontalPodAutoscaler)
opts := validationOptionsForHorizontalPodAutoscaler(autoscaler, nil)
return validation.ValidateHorizontalPodAutoscaler(autoscaler, opts)
allErrs := validation.ValidateHorizontalPodAutoscaler(autoscaler, opts)
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, autoscaler, nil, allErrs, operation.Create)
}
// WarningsOnCreate returns warnings for the creation of the given object.
@@ -106,7 +109,8 @@ func (autoscalerStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.O
newHPA := obj.(*autoscaling.HorizontalPodAutoscaler)
oldHPA := old.(*autoscaling.HorizontalPodAutoscaler)
opts := validationOptionsForHorizontalPodAutoscaler(newHPA, oldHPA)
return validation.ValidateHorizontalPodAutoscalerUpdate(newHPA, oldHPA, opts)
errs := validation.ValidateHorizontalPodAutoscalerUpdate(newHPA, oldHPA, opts)
return rest.ValidateDeclarativelyWithMigrationChecks(ctx, legacyscheme.Scheme, newHPA, oldHPA, errs, operation.Update)
}
// WarningsOnUpdate returns warnings for the given update.

View File

@@ -202,9 +202,13 @@ message HorizontalPodAutoscalerSpec {
// metric is configured. Scaling is active as long as at least one metric value is
// available.
// +optional
// +k8s:optional
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.
// +required
// +k8s:required
// +k8s:minimum=1
optional int32 maxReplicas = 3;
// targetCPUUtilizationPercentage is the target average CPU utilization (represented as a percentage of requested CPU) over all the pods;

View File

@@ -47,9 +47,13 @@ type HorizontalPodAutoscalerSpec struct {
// metric is configured. Scaling is active as long as at least one metric value is
// available.
// +optional
// +k8s:optional
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.
// +required
// +k8s:required
// +k8s:minimum=1
MaxReplicas int32 `json:"maxReplicas" protobuf:"varint,3,opt,name=maxReplicas"`
// targetCPUUtilizationPercentage is the target average CPU utilization (represented as a percentage of requested CPU) over all the pods;

View File

@@ -248,10 +248,14 @@ message HorizontalPodAutoscalerSpec {
// metric is configured. Scaling is active as long as at least one metric value is
// available.
// +optional
// +k8s:optional
optional int32 minReplicas = 2;
// maxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up.
// It cannot be less that minReplicas.
// +required
// +k8s:required
// +k8s:minimum=1
optional int32 maxReplicas = 3;
// metrics contains the specifications for which to use to calculate the

View File

@@ -59,10 +59,14 @@ type HorizontalPodAutoscalerSpec struct {
// metric is configured. Scaling is active as long as at least one metric value is
// available.
// +optional
// +k8s:optional
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.
// It cannot be less that minReplicas.
// +required
// +k8s:required
// +k8s:minimum=1
MaxReplicas int32 `json:"maxReplicas" protobuf:"varint,3,opt,name=maxReplicas"`
// metrics contains the specifications for which to use to calculate the