From 36fcbaef232f736c62f527e8d9a238f8cb989c49 Mon Sep 17 00:00:00 2001 From: yongruilin Date: Thu, 12 Feb 2026 21:17:24 +0000 Subject: [PATCH] test: add declarative validation tests for HPA MinReplicas --- .../declarative_validation_test.go | 67 +++++++++++++++++-- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/pkg/registry/autoscaling/horizontalpodautoscaler/declarative_validation_test.go b/pkg/registry/autoscaling/horizontalpodautoscaler/declarative_validation_test.go index 36212dc348e..c77363afa83 100644 --- a/pkg/registry/autoscaling/horizontalpodautoscaler/declarative_validation_test.go +++ b/pkg/registry/autoscaling/horizontalpodautoscaler/declarative_validation_test.go @@ -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,17 @@ 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"), + }, + }, } 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 +186,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", + }, + }, + }, +}