From 51e419db67f238d87db6ba8c53994489b26591f7 Mon Sep 17 00:00:00 2001 From: Omer Aplatony Date: Fri, 31 Oct 2025 11:27:25 +0000 Subject: [PATCH 1/3] feat: add evictAfterOOMThreshold per vpa Signed-off-by: Omer Aplatony --- .../crds/vpa-v1-crd-gen.yaml | 20 ++ .../deploy/vpa-v1-crd-gen.yaml | 20 ++ vertical-pod-autoscaler/docs/flags.md | 3 +- vertical-pod-autoscaler/e2e/utils/common.go | 5 +- .../e2e/v1/admission_controller.go | 235 ++++++++++++++++++ .../e2e/v1/autoscaling_utils.go | 2 + vertical-pod-autoscaler/e2e/v1/common.go | 17 ++ vertical-pod-autoscaler/e2e/v1/updater.go | 65 +++++ .../resource/vpa/handler.go | 28 ++- .../resource/vpa/handler_test.go | 29 +++ .../pkg/apis/autoscaling.k8s.io/v1/types.go | 12 + .../v1/zz_generated.deepcopy.go | 6 + .../priority/update_priority_calculator.go | 28 ++- .../pkg/utils/test/test_vpa.go | 10 + 14 files changed, 465 insertions(+), 15 deletions(-) diff --git a/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/crds/vpa-v1-crd-gen.yaml b/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/crds/vpa-v1-crd-gen.yaml index a3f8d0e7c319..93db869ed452 100644 --- a/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/crds/vpa-v1-crd-gen.yaml +++ b/vertical-pod-autoscaler/charts/vertical-pod-autoscaler/crds/vpa-v1-crd-gen.yaml @@ -254,6 +254,14 @@ spec: - jsonPath: .metadata.creationTimestamp name: Age type: date + - jsonPath: .spec.updatePolicy.minReplicas + name: MinReplicas + priority: 1 + type: integer + - jsonPath: .spec.updatePolicy.evictAfterOOMThreshold + name: OOMThreshold + priority: 1 + type: string name: v1 schema: openAPIV3Schema: @@ -425,6 +433,14 @@ spec: If not specified, all fields in the `PodUpdatePolicy` are set to their default values. properties: + evictAfterOOMThreshold: + description: |- + EvictAfterOOMThreshold specifies the time to wait after an OOM event before + considering the pod for eviction. Pods that have OOMed in less than this threshold + since start will be evicted. + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string evictionRequirements: description: |- EvictionRequirements is a list of EvictionRequirements that need to @@ -478,6 +494,10 @@ spec: - Auto type: string type: object + x-kubernetes-validations: + - message: evictAfterOOMThreshold must be greater than 0 + rule: '!has(self.evictAfterOOMThreshold) || duration(self.evictAfterOOMThreshold) + >= duration(''0s'')' required: - targetRef type: object diff --git a/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml b/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml index a3f8d0e7c319..93db869ed452 100644 --- a/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml +++ b/vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml @@ -254,6 +254,14 @@ spec: - jsonPath: .metadata.creationTimestamp name: Age type: date + - jsonPath: .spec.updatePolicy.minReplicas + name: MinReplicas + priority: 1 + type: integer + - jsonPath: .spec.updatePolicy.evictAfterOOMThreshold + name: OOMThreshold + priority: 1 + type: string name: v1 schema: openAPIV3Schema: @@ -425,6 +433,14 @@ spec: If not specified, all fields in the `PodUpdatePolicy` are set to their default values. properties: + evictAfterOOMThreshold: + description: |- + EvictAfterOOMThreshold specifies the time to wait after an OOM event before + considering the pod for eviction. Pods that have OOMed in less than this threshold + since start will be evicted. + format: duration + pattern: ^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string evictionRequirements: description: |- EvictionRequirements is a list of EvictionRequirements that need to @@ -478,6 +494,10 @@ spec: - Auto type: string type: object + x-kubernetes-validations: + - message: evictAfterOOMThreshold must be greater than 0 + rule: '!has(self.evictAfterOOMThreshold) || duration(self.evictAfterOOMThreshold) + >= duration(''0s'')' required: - targetRef type: object diff --git a/vertical-pod-autoscaler/docs/flags.md b/vertical-pod-autoscaler/docs/flags.md index 66dc939535ce..abbbd8d4e616 100644 --- a/vertical-pod-autoscaler/docs/flags.md +++ b/vertical-pod-autoscaler/docs/flags.md @@ -140,7 +140,7 @@ This document is auto-generated from the flag definitions in the VPA updater cod | `add-dir-header` | | | If true, adds the file directory to the header of the log messages | | `address` | string | ":8943" | The address to expose Prometheus metrics. | | `alsologtostderr` | | | log to standard error as well as files (no effect when -logtostderr=true) | -| `evict-after-oom-threshold` | | 10m0s | duration Evict pod that has OOMed in less than evict-after-oom-threshold since start. | +| `evict-after-oom-threshold` | | 10m0s | duration The default duration to evict pod that has OOMed in less than evict-after-oom-threshold since start. | | `eviction-rate-burst` | int | 1 | Burst of pods that can be evicted. | | `eviction-rate-limit` | float | | Number of pods that can be evicted per seconds. A rate limit set to 0 or -1 will disable
the rate limiter. (default -1) | | `eviction-tolerance` | float | 0.5 | Fraction of replica count that can be evicted for update, if more than one pod can be evicted. | @@ -174,4 +174,3 @@ This document is auto-generated from the flag definitions in the VPA updater cod | `v,` | | : 4 | , --v Level set the log level verbosity (default 4) | | `vmodule` | moduleSpec | | comma-separated list of pattern=N settings for file-filtered logging | | `vpa-object-namespace` | string | | Specifies the namespace to search for VPA objects. Leave empty to include all namespaces. If provided, the garbage collector will only clean this namespace. | - diff --git a/vertical-pod-autoscaler/e2e/utils/common.go b/vertical-pod-autoscaler/e2e/utils/common.go index b4fc8e12c3f5..9aca878eeb2b 100644 --- a/vertical-pod-autoscaler/e2e/utils/common.go +++ b/vertical-pod-autoscaler/e2e/utils/common.go @@ -70,6 +70,9 @@ var RecommenderLabels = map[string]string{"app": "vpa-recommender"} // HamsterLabels are labels of hamster app var HamsterLabels = map[string]string{"app": "hamster"} +// OOMLabels are labels for OOM test pods +var OOMLabels = map[string]string{"app": "oom-test"} + // SIGDescribe adds sig-autoscaling tag to test description. // Takes args that are passed to ginkgo.Describe. func SIGDescribe(scenario, name string, args ...interface{}) bool { @@ -226,7 +229,7 @@ func NewNHamstersDeployment(f *framework.Framework, n int) *appsv1.Deployment { DefaultHamsterReplicas, /*replicas*/ HamsterLabels, /*podLabels*/ GetHamsterContainerNameByIndex(0), /*imageName*/ - "registry.k8s.io/ubuntu-slim:0.14", /*image*/ + "ubuntu:25.10", /*image*/ appsv1.RollingUpdateDeploymentStrategyType, /*strategyType*/ ) d.ObjectMeta.Namespace = f.Namespace.Name diff --git a/vertical-pod-autoscaler/e2e/v1/admission_controller.go b/vertical-pod-autoscaler/e2e/v1/admission_controller.go index d6d8e03d4121..6dbf651ff260 100644 --- a/vertical-pod-autoscaler/e2e/v1/admission_controller.go +++ b/vertical-pod-autoscaler/e2e/v1/admission_controller.go @@ -1111,3 +1111,238 @@ func waitForVpaWebhookRegistration(f *framework.Framework) { return false }, 3*time.Minute, 5*time.Second).Should(gomega.BeTrue(), "Webhook was not registered in the cluster") } + +var _ = AdmissionControllerE2eDescribe("Admission-controller", ginkgo.Label("FG:PerVPAConfig"), func() { + f := framework.NewDefaultFramework("vertical-pod-autoscaling") + f.NamespacePodSecurityEnforceLevel = podsecurity.LevelBaseline + + ginkgo.BeforeEach(func() { + waitForVpaWebhookRegistration(f) + }) + + ginkgo.It("accepts valid and rejects invalid VPA object", func() { + ginkgo.By("Setting up valid VPA object") + validVPA := []byte(`{ + "kind": "VerticalPodAutoscaler", + "apiVersion": "autoscaling.k8s.io/v1", + "metadata": {"name": "hamster-vpa-valid"}, + "spec": { + "targetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name":"hamster" + }, + "resourcePolicy": { + "containerPolicies": [{"containerName": "*", "minAllowed":{"cpu":"50m"}}] + } + } + }`) + err := InstallRawVPA(f, validVPA) + gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Valid VPA object rejected") + + ginkgo.By("Setting up invalid VPA objects") + testCases := []struct { + name string + vpaJSON string + expectedErr string + }{ + { + name: "Invalid oomBumpUpRatio (negative value)", + vpaJSON: `{ + "apiVersion": "autoscaling.k8s.io/v1", + "kind": "VerticalPodAutoscaler", + "metadata": {"name": "oom-test-vpa"}, + "spec": { + "targetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "oom-test" + }, + "updatePolicy": { + "updateMode": "Auto" + }, + "resourcePolicy": { + "containerPolicies": [{ + "containerName": "*", + "oomBumpUpRatio": -1, + "oomMinBumpUp": 104857600 + }] + } + } + }`, + expectedErr: "admission webhook \"vpa.k8s.io\" denied the request: oomBumpUpRatio must be greater than or equal to 1.0, got -1", + }, + { + name: "Invalid oomBumpUpRatio (string value)", + vpaJSON: `{ + "apiVersion": "autoscaling.k8s.io/v1", + "kind": "VerticalPodAutoscaler", + "metadata": {"name": "oom-test-vpa"}, + "spec": { + "targetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "oom-test" + }, + "updatePolicy": { + "updateMode": "Auto" + }, + "resourcePolicy": { + "containerPolicies": [{ + "containerName": "*", + "oomBumpUpRatio": "not-a-number", + "oomMinBumpUp": 104857600 + }] + } + } + }`, + expectedErr: "admission webhook \"vpa\\.k8s\\.io\" denied the request: quantities must match the regular expression", + }, + { + name: "Invalid oomBumpUpRatio (less than 1)", + vpaJSON: `{ + "apiVersion": "autoscaling.k8s.io/v1", + "kind": "VerticalPodAutoscaler", + "metadata": {"name": "oom-test-vpa"}, + "spec": { + "targetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "oom-test" + }, + "updatePolicy": { + "updateMode": "Auto" + }, + "resourcePolicy": { + "containerPolicies": [{ + "containerName": "*", + "oomBumpUpRatio": 0.5, + "oomMinBumpUp": 104857600 + }] + } + } + }`, + expectedErr: "admission webhook \"vpa.k8s.io\" denied the request: oomBumpUpRatio must be greater than or equal to 1.0, got 0.5", + }, + { + name: "Invalid oomMinBumpUp (negative value)", + vpaJSON: `{ + "apiVersion": "autoscaling.k8s.io/v1", + "kind": "VerticalPodAutoscaler", + "metadata": {"name": "oom-test-vpa"}, + "spec": { + "targetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "oom-test" + }, + "updatePolicy": { + "updateMode": "Auto" + }, + "resourcePolicy": { + "containerPolicies": [{ + "containerName": "*", + "oomBumpUpRatio": 2, + "oomMinBumpUp": -1 + }] + } + } + }`, + expectedErr: "admission webhook \"vpa\\.k8s\\.io\" denied the request: oomMinBumpUp must be greater than or equal to 0, got -1 bytes", + }, + { + name: "Invalid evictAfterOOMThreshold (negative duration)", + vpaJSON: `{ + "apiVersion": "autoscaling.k8s.io/v1", + "kind": "VerticalPodAutoscaler", + "metadata": {"name": "evict-threshold-vpa"}, + "spec": { + "targetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "hamster" + }, + "updatePolicy": { + "updateMode": "Auto", + "evictAfterOOMThreshold": "-5m" + } + } + }`, + expectedErr: "spec\\.updatePolicy\\.evictAfterOOMThreshold: Invalid value:.*evictAfterOOMThreshold must be greater than 0", + }, + { + name: "Invalid evictAfterOOMThreshold (invalid format)", + vpaJSON: `{ + "apiVersion": "autoscaling.k8s.io/v1", + "kind": "VerticalPodAutoscaler", + "metadata": {"name": "evict-threshold-vpa"}, + "spec": { + "targetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "hamster" + }, + "updatePolicy": { + "updateMode": "Auto", + "evictAfterOOMThreshold": "not-a-duration" + } + } + }`, + expectedErr: "admission webhook.*denied the request:.*invalid duration", + }, + { + name: "Invalid evictAfterOOMThreshold (invalid unit)", + vpaJSON: `{ + "apiVersion": "autoscaling.k8s.io/v1", + "kind": "VerticalPodAutoscaler", + "metadata": {"name": "evict-threshold-vpa"}, + "spec": { + "targetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "hamster" + }, + "updatePolicy": { + "updateMode": "Auto", + "evictAfterOOMThreshold": "5x" + } + } + }`, + expectedErr: "admission webhook.*denied the request:.*unknown unit.*in duration", + }, + { + name: "Invalid minAllowed (invalid requests field)", + vpaJSON: `{ + "apiVersion": "autoscaling.k8s.io/v1", + "kind": "VerticalPodAutoscaler", + "metadata": {"name": "hamster-vpa-invalid"}, + "spec": { + "targetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "hamster" + }, + "resourcePolicy": { + "containerPolicies": [{ + "containerName": "*", + "minAllowed": { + "requests": { + "cpu": "50m" + } + } + }] + } + } + }`, + expectedErr: "admission webhook .*vpa.* denied the request:", + }, + } + for _, tc := range testCases { + err := InstallRawVPA(f, []byte(tc.vpaJSON)) + gomega.Expect(err).To(gomega.HaveOccurred(), + fmt.Sprintf("Test case '%s': Invalid VPA object accepted", tc.name)) + gomega.Expect(err.Error()).To(gomega.MatchRegexp(tc.expectedErr), + fmt.Sprintf("Test case '%s': Expected error pattern not matched. Got error: %v", tc.name, err)) + } + }) +}) diff --git a/vertical-pod-autoscaler/e2e/v1/autoscaling_utils.go b/vertical-pod-autoscaler/e2e/v1/autoscaling_utils.go index f6cd913df0f9..80520c64f9e1 100644 --- a/vertical-pod-autoscaler/e2e/v1/autoscaling_utils.go +++ b/vertical-pod-autoscaler/e2e/v1/autoscaling_utils.go @@ -31,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/autoscaler/vertical-pod-autoscaler/e2e/utils" clientset "k8s.io/client-go/kubernetes" "k8s.io/kubernetes/test/e2e/framework" e2edebug "k8s.io/kubernetes/test/e2e/framework/debug" @@ -444,6 +445,7 @@ func runOomingReplicationController(c clientset.Interface, ns, name string, repl Namespace: ns, Timeout: timeoutRC, Replicas: replicas, + Labels: utils.OOMLabels, Annotations: make(map[string]string), MemRequest: 1024 * 1024 * 1024, MemLimit: 1024 * 1024 * 1024, diff --git a/vertical-pod-autoscaler/e2e/v1/common.go b/vertical-pod-autoscaler/e2e/v1/common.go index 17517ff15918..f1ae79f901f2 100644 --- a/vertical-pod-autoscaler/e2e/v1/common.go +++ b/vertical-pod-autoscaler/e2e/v1/common.go @@ -147,6 +147,13 @@ func GetHamsterPods(f *framework.Framework) (*apiv1.PodList, error) { return f.ClientSet.CoreV1().Pods(f.Namespace.Name).List(context.TODO(), options) } +// GetOOMPods returns running OOM test pods (matched by utils.OOMLabels) +func GetOOMPods(f *framework.Framework) (*apiv1.PodList, error) { + label := labels.SelectorFromSet(labels.Set(utils.OOMLabels)) + options := metav1.ListOptions{LabelSelector: label.String(), FieldSelector: getPodSelectorExcludingDonePodsOrDie()} + return f.ClientSet.CoreV1().Pods(f.Namespace.Name).List(context.TODO(), options) +} + // NewTestCronJob returns a CronJob for test purposes. func NewTestCronJob(name, schedule string, replicas int32) *batchv1.CronJob { backoffLimit := utils.DefaultHamsterBackoffLimit @@ -345,6 +352,16 @@ func CheckNoPodsEvicted(f *framework.Framework, initialPodSet PodSet) { gomega.Expect(restarted).To(gomega.Equal(0), "there should be no pod evictions") } +// CheckNoPodsEvictedOOM waits for long enough period for VPA to start evicting +// TODO(omerap12): merge this CheckNoPodsEvicted +func CheckNoPodsEvictedOOM(f *framework.Framework, initialPodSet PodSet) { + time.Sleep(VpaEvictionTimeout) + currentPodList, err := GetOOMPods(f) + gomega.Expect(err).NotTo(gomega.HaveOccurred(), "unexpected error when listing hamster pods to check number of pod evictions") + restarted := GetEvictedPodsCount(MakePodSet(currentPodList), initialPodSet) + gomega.Expect(restarted).To(gomega.Equal(0), "there should be no pod evictions") +} + // WaitForUncappedCPURecommendationAbove pools VPA object until uncapped recommendation is above specified value. // Returns polled VPA object. On timeout returns error. func WaitForUncappedCPURecommendationAbove(c vpa_clientset.Interface, vpa *vpa_types.VerticalPodAutoscaler, minMilliCPU int64) (*vpa_types.VerticalPodAutoscaler, error) { diff --git a/vertical-pod-autoscaler/e2e/v1/updater.go b/vertical-pod-autoscaler/e2e/v1/updater.go index 504d1934268f..d13d9520cccc 100644 --- a/vertical-pod-autoscaler/e2e/v1/updater.go +++ b/vertical-pod-autoscaler/e2e/v1/updater.go @@ -207,6 +207,71 @@ var _ = UpdaterE2eDescribe("Updater", func() { }) }) +var _ = UpdaterE2eDescribe("Updater with PerVPAConfig", ginkgo.Label("FG:PerVPAConfig"), func() { + const replicas = 3 + const statusUpdateInterval = 10 * time.Second + f := framework.NewDefaultFramework("vertical-pod-autoscaling") + f.NamespacePodSecurityEnforceLevel = podsecurity.LevelBaseline + + ginkgo.It("does not evict pods with OOM when threshold is very small", func() { + ginkgo.By("Setting up the Admission Controller status") + stopCh := make(chan struct{}) + statusUpdater := status.NewUpdater( + f.ClientSet, + status.AdmissionControllerStatusName, + status.AdmissionControllerStatusNamespace, + statusUpdateInterval, + "e2e test", + ) + defer func() { + ginkgo.By("Deleting the Admission Controller status") + close(stopCh) + err := f.ClientSet.CoordinationV1().Leases(status.AdmissionControllerStatusNamespace). + Delete(context.TODO(), status.AdmissionControllerStatusName, metav1.DeleteOptions{}) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + }() + statusUpdater.Run(stopCh) + + ginkgo.By("Setting up a deployment that will OOM") + runOomingReplicationController( + f.ClientSet, + f.Namespace.Name, + "hamster", + replicas, + ) + + ginkgo.By("Waiting for pods to be created and OOM") + time.Sleep(10 * time.Second) + + podList, err := GetOOMPods(f) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(len(podList.Items)).To(gomega.BeNumerically(">", 0)) + + // verySmallThreshold := 1 * time.Nanosecond + disabledThreshold := 0 * time.Second // Disable quick OOM eviction + ginkgo.By("Setting up a VPA CRD with very short evictAfterOOMThreshold (1ns)") + targetRef := &autoscaling.CrossVersionObjectReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "hamster", + } + containerName := utils.GetHamsterContainerNameByIndex(0) + vpaCRD := test.VerticalPodAutoscaler(). + WithName("hamster-vpa"). + WithNamespace(f.Namespace.Name). + WithTargetRef(targetRef). + WithUpdateMode(vpa_types.UpdateModeRecreate). + WithEvictAfterOOMThreshold(&metav1.Duration{Duration: disabledThreshold}). + WithContainer(containerName). + Get() + + utils.InstallVPA(f, vpaCRD) + + ginkgo.By("Waiting to verify pods are NOT evicted (OOM time is 0)") + CheckNoPodsEvictedOOM(f, MakePodSet(podList)) + }) +}) + func setupPodsForUpscalingEviction(f *framework.Framework) *apiv1.PodList { return setupPodsForEviction(f, "100m", "100Mi", nil) } diff --git a/vertical-pod-autoscaler/pkg/admission-controller/resource/vpa/handler.go b/vertical-pod-autoscaler/pkg/admission-controller/resource/vpa/handler.go index f03f9e1759a7..e4b77dee279e 100644 --- a/vertical-pod-autoscaler/pkg/admission-controller/resource/vpa/handler.go +++ b/vertical-pod-autoscaler/pkg/admission-controller/resource/vpa/handler.go @@ -115,6 +115,11 @@ func parseVPA(raw []byte) (*vpa_types.VerticalPodAutoscaler, error) { // ValidateVPA checks the correctness of VPA Spec and returns an error if there is a problem. func ValidateVPA(vpa *vpa_types.VerticalPodAutoscaler, isCreate bool) error { + // check that perVPA is on if being used + if err := validatePerVPAFeatureFlag(vpa); err != nil { + return err + } + if vpa.Spec.UpdatePolicy != nil { mode := vpa.Spec.UpdatePolicy.UpdateMode if mode == nil { @@ -130,6 +135,7 @@ func ValidateVPA(vpa *vpa_types.VerticalPodAutoscaler, isCreate bool) error { if minReplicas := vpa.Spec.UpdatePolicy.MinReplicas; minReplicas != nil && *minReplicas <= 0 { return fmt.Errorf("minReplicas has to be positive, got %v", *minReplicas) } + } if vpa.Spec.ResourcePolicy != nil { @@ -138,11 +144,6 @@ func ValidateVPA(vpa *vpa_types.VerticalPodAutoscaler, isCreate bool) error { return fmt.Errorf("containerPolicies.ContainerName is required") } - // check that perVPA is on if being used - if err := validatePerVPAFeatureFlag(&policy); err != nil { - return err - } - // Validate OOMBumpUpRatio if policy.OOMBumpUpRatio != nil { ratio := float64(policy.OOMBumpUpRatio.MilliValue()) / 1000.0 @@ -224,11 +225,20 @@ func validateMemoryResolution(val apires.Quantity) error { return nil } -func validatePerVPAFeatureFlag(policy *vpa_types.ContainerResourcePolicy) error { +func validatePerVPAFeatureFlag(vpa *vpa_types.VerticalPodAutoscaler) error { featureFlagOn := features.Enabled(features.PerVPAConfig) - perVPA := policy.OOMBumpUpRatio != nil || policy.OOMMinBumpUp != nil - if !featureFlagOn && perVPA { - return fmt.Errorf("OOMBumpUpRatio and OOMMinBumpUp are not supported when feature flag %s is disabled", features.PerVPAConfig) + if !featureFlagOn && vpa.Spec.UpdatePolicy.EvictAfterOOMThreshold != nil { + return fmt.Errorf("EvictAfterOOMThreshold is not supported when feature flag %s is disabled", features.PerVPAConfig) + } + + if vpa.Spec.ResourcePolicy != nil { + for _, policy := range vpa.Spec.ResourcePolicy.ContainerPolicies { + perVPA := policy.OOMBumpUpRatio != nil || policy.OOMMinBumpUp != nil + if !featureFlagOn && perVPA { + return fmt.Errorf("OOMBumpUpRatio and OOMMinBumpUp are not supported when feature flag %s is disabled", features.PerVPAConfig) + } + } + } return nil } diff --git a/vertical-pod-autoscaler/pkg/admission-controller/resource/vpa/handler_test.go b/vertical-pod-autoscaler/pkg/admission-controller/resource/vpa/handler_test.go index 78044ff61b9f..7bc884919149 100644 --- a/vertical-pod-autoscaler/pkg/admission-controller/resource/vpa/handler_test.go +++ b/vertical-pod-autoscaler/pkg/admission-controller/resource/vpa/handler_test.go @@ -19,10 +19,12 @@ package vpa import ( "fmt" "testing" + "time" "github.com/stretchr/testify/assert" apiv1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" featuregatetesting "k8s.io/component-base/featuregate/testing" vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1" @@ -346,6 +348,33 @@ func TestValidateVPA(t *testing.T) { }, PerVPAConfigDisabled: false, }, + { + name: "per-vpa config active and used evictOOMThreshold", + vpa: vpa_types.VerticalPodAutoscaler{ + Spec: vpa_types.VerticalPodAutoscalerSpec{ + UpdatePolicy: &vpa_types.PodUpdatePolicy{ + UpdateMode: &validUpdateMode, + EvictAfterOOMThreshold: &metav1.Duration{Duration: 10 * time.Minute}, + }, + ResourcePolicy: &vpa_types.PodResourcePolicy{ + ContainerPolicies: []vpa_types.ContainerResourcePolicy{ + { + ContainerName: "loot box", + Mode: &validScalingMode, + MinAllowed: apiv1.ResourceList{ + cpu: resource.MustParse("10"), + }, + MaxAllowed: apiv1.ResourceList{ + cpu: resource.MustParse("100"), + }, + OOMBumpUpRatio: resource.NewQuantity(2, resource.DecimalSI), + }, + }, + }, + }, + }, + PerVPAConfigDisabled: false, + }, { name: "per-vpa config disabled and used", vpa: vpa_types.VerticalPodAutoscaler{ diff --git a/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/types.go b/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/types.go index dc365b8c686f..04359ff221e3 100644 --- a/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/types.go +++ b/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/types.go @@ -46,6 +46,8 @@ type VerticalPodAutoscalerList struct { // +kubebuilder:printcolumn:name="Mem",type="string",JSONPath=".status.recommendation.containerRecommendations[0].target.memory" // +kubebuilder:printcolumn:name="Provided",type="string",JSONPath=".status.conditions[?(@.type=='RecommendationProvided')].status" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="MinReplicas",type="integer",JSONPath=".spec.updatePolicy.minReplicas",priority=1 +// +kubebuilder:printcolumn:name="OOMThreshold",type="string",JSONPath=".spec.updatePolicy.evictAfterOOMThreshold",priority=1 // +kubebuilder:metadata:annotations="api-approved.kubernetes.io=https://github.com/kubernetes/kubernetes/pull/63797" // VerticalPodAutoscaler is the configuration for a vertical pod @@ -132,6 +134,7 @@ type EvictionRequirement struct { } // PodUpdatePolicy describes the rules on how changes are applied to the pods. +// +kubebuilder:validation:XValidation:rule="!has(self.evictAfterOOMThreshold) || duration(self.evictAfterOOMThreshold) >= duration('0s')",message="evictAfterOOMThreshold must be greater than 0" type PodUpdatePolicy struct { // Controls when autoscaler applies changes to the pod resources. // The default is 'Auto'. @@ -149,6 +152,15 @@ type PodUpdatePolicy struct { // EvictionRequirement is specified, all of them need to be fulfilled to allow eviction. // +optional EvictionRequirements []*EvictionRequirement `json:"evictionRequirements,omitempty" protobuf:"bytes,3,opt,name=evictionRequirements"` + + // EvictAfterOOMThreshold specifies the time to wait after an OOM event before + // considering the pod for eviction. Pods that have OOMed in less than this threshold + // since start will be evicted. + // +optional + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=duration + // +kubebuilder:validation:Pattern=`^([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$` + EvictAfterOOMThreshold *metav1.Duration `json:"evictAfterOOMThreshold,omitempty" protobuf:"bytes,4,opt,name=evictAfterOOMThreshold"` } // UpdateMode controls when autoscaler applies changes to the pod resources. diff --git a/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/zz_generated.deepcopy.go b/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/zz_generated.deepcopy.go index 86ff44f69ea1..4f1aaa3fb80f 100644 --- a/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/zz_generated.deepcopy.go +++ b/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/zz_generated.deepcopy.go @@ -24,6 +24,7 @@ package v1 import ( autoscalingv1 "k8s.io/api/autoscaling/v1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -178,6 +179,11 @@ func (in *PodUpdatePolicy) DeepCopyInto(out *PodUpdatePolicy) { } } } + if in.EvictAfterOOMThreshold != nil { + in, out := &in.EvictAfterOOMThreshold, &out.EvictAfterOOMThreshold + *out = new(metav1.Duration) + **out = **in + } return } diff --git a/vertical-pod-autoscaler/pkg/updater/priority/update_priority_calculator.go b/vertical-pod-autoscaler/pkg/updater/priority/update_priority_calculator.go index 55f2f92931e9..e3e29ec78655 100644 --- a/vertical-pod-autoscaler/pkg/updater/priority/update_priority_calculator.go +++ b/vertical-pod-autoscaler/pkg/updater/priority/update_priority_calculator.go @@ -29,17 +29,23 @@ import ( "k8s.io/klog/v2" vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/features" "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations" vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa" ) +const ( + // DefaultEvictAfterOOMThreshold is the default time threshold for evicting pods after OOM + DefaultEvictAfterOOMThreshold = 10 * time.Minute +) + var ( defaultUpdateThreshold = flag.Float64("pod-update-threshold", 0.1, "Ignore updates that have priority lower than the value of this flag") podLifetimeUpdateThreshold = flag.Duration("in-recommendation-bounds-eviction-lifetime-threshold", time.Hour*12, "Pods that live for at least that long can be evicted even if their request is within the [MinRecommended...MaxRecommended] range") - evictAfterOOMThreshold = flag.Duration("evict-after-oom-threshold", 10*time.Minute, - `Evict pod that has OOMed in less than evict-after-oom-threshold since start.`) + evictAfterOOMThreshold = flag.Duration("evict-after-oom-threshold", DefaultEvictAfterOOMThreshold, + `The default duration to evict pod that has OOMed in less than evict-after-oom-threshold since start.`) ) // UpdatePriorityCalculator is responsible for prioritizing updates on pods. @@ -108,10 +114,11 @@ func (calc *UpdatePriorityCalculator) AddPod(pod *apiv1.Pod, now time.Time) { klog.V(4).InfoS("Container with ContainerScalingModeOff. Skipping container quick OOM calculations", "containerName", cs.Name) continue } + evictOOMThreshold := calc.getEvictOOMThreshold() terminationState := &cs.LastTerminationState if terminationState.Terminated != nil && terminationState.Terminated.Reason == "OOMKilled" && - terminationState.Terminated.FinishedAt.Sub(terminationState.Terminated.StartedAt.Time) < *evictAfterOOMThreshold { + terminationState.Terminated.FinishedAt.Sub(terminationState.Terminated.StartedAt.Time) < evictOOMThreshold { quickOOM = true klog.V(2).InfoS("Quick OOM detected in pod", "pod", klog.KObj(pod), "containerName", cs.Name) } @@ -198,6 +205,21 @@ func (calc *UpdatePriorityCalculator) GetProcessedRecommendationTargets(r *vpa_t return sb.String() } +func (calc *UpdatePriorityCalculator) getEvictOOMThreshold() time.Duration { + evictOOMThreshold := *evictAfterOOMThreshold + + if calc.vpa.Spec.UpdatePolicy == nil || calc.vpa.Spec.UpdatePolicy.EvictAfterOOMThreshold == nil { + return evictOOMThreshold + } + + if !features.Enabled(features.PerVPAConfig) { + klog.V(4).InfoS("feature flag is off, falling back to default EvictAfterOOMThreshold", "PerVPAConfig", features.PerVPAConfig) + return evictOOMThreshold + } + + return calc.vpa.Spec.UpdatePolicy.EvictAfterOOMThreshold.Duration +} + func parseVpaObservedContainers(pod *apiv1.Pod) (bool, sets.Set[string]) { observedContainers, hasObservedContainers := pod.GetAnnotations()[annotations.VpaObservedContainersLabel] vpaContainerSet := sets.New[string]() diff --git a/vertical-pod-autoscaler/pkg/utils/test/test_vpa.go b/vertical-pod-autoscaler/pkg/utils/test/test_vpa.go index 5a32bc0a2069..0fc355915aed 100644 --- a/vertical-pod-autoscaler/pkg/utils/test/test_vpa.go +++ b/vertical-pod-autoscaler/pkg/utils/test/test_vpa.go @@ -33,6 +33,7 @@ type VerticalPodAutoscalerBuilder interface { WithContainer(containerName string) VerticalPodAutoscalerBuilder WithNamespace(namespace string) VerticalPodAutoscalerBuilder WithUpdateMode(updateMode vpa_types.UpdateMode) VerticalPodAutoscalerBuilder + WithEvictAfterOOMThreshold(*meta.Duration) VerticalPodAutoscalerBuilder WithCreationTimestamp(timestamp time.Time) VerticalPodAutoscalerBuilder WithMinAllowed(containerName, cpu, memory string) VerticalPodAutoscalerBuilder WithMaxAllowed(containerName, cpu, memory string) VerticalPodAutoscalerBuilder @@ -121,6 +122,15 @@ func (b *verticalPodAutoscalerBuilder) WithUpdateMode(updateMode vpa_types.Updat return &c } +func (b *verticalPodAutoscalerBuilder) WithEvictAfterOOMThreshold(threshold *meta.Duration) VerticalPodAutoscalerBuilder { + c := *b + if c.updatePolicy == nil { + c.updatePolicy = &vpa_types.PodUpdatePolicy{} + } + c.updatePolicy.EvictAfterOOMThreshold = threshold + return &c +} + func (b *verticalPodAutoscalerBuilder) WithCreationTimestamp(timestamp time.Time) VerticalPodAutoscalerBuilder { c := *b c.creationTimestamp = timestamp From 0db4e6a45e6fdaff413ce51e8e09925e70fe8f0a Mon Sep 17 00:00:00 2001 From: Omer Aplatony Date: Sat, 1 Nov 2025 05:29:17 +0000 Subject: [PATCH 2/3] remove ginkgo.Label from e2e tests Signed-off-by: Omer Aplatony --- vertical-pod-autoscaler/e2e/v1/admission_controller.go | 4 ++-- vertical-pod-autoscaler/e2e/v1/updater.go | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/vertical-pod-autoscaler/e2e/v1/admission_controller.go b/vertical-pod-autoscaler/e2e/v1/admission_controller.go index 6dbf651ff260..45feefe0b9b6 100644 --- a/vertical-pod-autoscaler/e2e/v1/admission_controller.go +++ b/vertical-pod-autoscaler/e2e/v1/admission_controller.go @@ -1112,7 +1112,7 @@ func waitForVpaWebhookRegistration(f *framework.Framework) { }, 3*time.Minute, 5*time.Second).Should(gomega.BeTrue(), "Webhook was not registered in the cluster") } -var _ = AdmissionControllerE2eDescribe("Admission-controller", ginkgo.Label("FG:PerVPAConfig"), func() { +var _ = AdmissionControllerE2eDescribe("Admission-controller", func() { f := framework.NewDefaultFramework("vertical-pod-autoscaling") f.NamespacePodSecurityEnforceLevel = podsecurity.LevelBaseline @@ -1120,7 +1120,7 @@ var _ = AdmissionControllerE2eDescribe("Admission-controller", ginkgo.Label("FG: waitForVpaWebhookRegistration(f) }) - ginkgo.It("accepts valid and rejects invalid VPA object", func() { + f.It("accepts valid and rejects invalid VPA object", framework.WithFeatureGate(features.PerVPAConfig), func() { ginkgo.By("Setting up valid VPA object") validVPA := []byte(`{ "kind": "VerticalPodAutoscaler", diff --git a/vertical-pod-autoscaler/e2e/v1/updater.go b/vertical-pod-autoscaler/e2e/v1/updater.go index d13d9520cccc..14a1c4d142c8 100644 --- a/vertical-pod-autoscaler/e2e/v1/updater.go +++ b/vertical-pod-autoscaler/e2e/v1/updater.go @@ -26,6 +26,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/autoscaler/vertical-pod-autoscaler/e2e/utils" vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/features" "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/status" "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/test" "k8s.io/kubernetes/test/e2e/framework" @@ -207,13 +208,13 @@ var _ = UpdaterE2eDescribe("Updater", func() { }) }) -var _ = UpdaterE2eDescribe("Updater with PerVPAConfig", ginkgo.Label("FG:PerVPAConfig"), func() { +var _ = UpdaterE2eDescribe("Updater with PerVPAConfig", func() { const replicas = 3 const statusUpdateInterval = 10 * time.Second f := framework.NewDefaultFramework("vertical-pod-autoscaling") f.NamespacePodSecurityEnforceLevel = podsecurity.LevelBaseline - ginkgo.It("does not evict pods with OOM when threshold is very small", func() { + f.It("does not evict pods with OOM when threshold is very small", framework.WithFeatureGate(features.PerVPAConfig), func() { ginkgo.By("Setting up the Admission Controller status") stopCh := make(chan struct{}) statusUpdater := status.NewUpdater( From 8dcbdcef46532d9fbdcefc99b2fb74688198b6f7 Mon Sep 17 00:00:00 2001 From: Omer Aplatony Date: Sun, 9 Nov 2025 17:23:12 +0000 Subject: [PATCH 3/3] Removed duplicated tests + comment Signed-off-by: Omer Aplatony --- .../e2e/v1/admission_controller.go | 257 +----------------- vertical-pod-autoscaler/e2e/v1/updater.go | 1 - 2 files changed, 11 insertions(+), 247 deletions(-) diff --git a/vertical-pod-autoscaler/e2e/v1/admission_controller.go b/vertical-pod-autoscaler/e2e/v1/admission_controller.go index 45feefe0b9b6..4558b321d4f8 100644 --- a/vertical-pod-autoscaler/e2e/v1/admission_controller.go +++ b/vertical-pod-autoscaler/e2e/v1/admission_controller.go @@ -956,7 +956,7 @@ var _ = AdmissionControllerE2eDescribe("Admission-controller", func() { expectedErr: "admission webhook \"vpa.k8s.io\" denied the request: oomBumpUpRatio must be greater than or equal to 1.0, got -1", }, { - name: "Invalid oomBumpUpRatio (string value)", + name: "Invalid oomBumpUpRatio (less than 1)", vpaJSON: `{ "apiVersion": "autoscaling.k8s.io/v1", "kind": "VerticalPodAutoscaler", @@ -973,16 +973,16 @@ var _ = AdmissionControllerE2eDescribe("Admission-controller", func() { "resourcePolicy": { "containerPolicies": [{ "containerName": "*", - "oomBumpUpRatio": "not-a-number", + "oomBumpUpRatio": 0.5, "oomMinBumpUp": 104857600 }] } } }`, - expectedErr: "admission webhook \"vpa\\.k8s\\.io\" denied the request: quantities must match the regular expression", + expectedErr: "admission webhook \"vpa.k8s.io\" denied the request: oomBumpUpRatio must be greater than or equal to 1.0, got 0.5", }, { - name: "Invalid oomBumpUpRatio (less than 1)", + name: "Invalid oomMinBumpUp (negative value)", vpaJSON: `{ "apiVersion": "autoscaling.k8s.io/v1", "kind": "VerticalPodAutoscaler", @@ -999,16 +999,16 @@ var _ = AdmissionControllerE2eDescribe("Admission-controller", func() { "resourcePolicy": { "containerPolicies": [{ "containerName": "*", - "oomBumpUpRatio": 0.5, - "oomMinBumpUp": 104857600 + "oomBumpUpRatio": 2, + "oomMinBumpUp": -1 }] } } }`, - expectedErr: "admission webhook \"vpa.k8s.io\" denied the request: oomBumpUpRatio must be greater than or equal to 1.0, got 0.5", + expectedErr: "admission webhook \"vpa.k8s.io\" denied the request: oomMinBumpUp must be greater than or equal to 0, got -1 bytes", }, { - name: "Invalid oomMinBumpUp (negative value)", + name: "Invalid oomBumpUpRatio (string value)", vpaJSON: `{ "apiVersion": "autoscaling.k8s.io/v1", "kind": "VerticalPodAutoscaler", @@ -1025,13 +1025,13 @@ var _ = AdmissionControllerE2eDescribe("Admission-controller", func() { "resourcePolicy": { "containerPolicies": [{ "containerName": "*", - "oomBumpUpRatio": 2, - "oomMinBumpUp": -1 + "oomBumpUpRatio": "not-a-number", + "oomMinBumpUp": 104857600 }] } } }`, - expectedErr: "admission webhook \"vpa.k8s.io\" denied the request: oomMinBumpUp must be greater than or equal to 0, got -1 bytes", + expectedErr: "admission webhook \"vpa.k8s.io\" denied the request: quantities must match the regular expression", }, } for _, tc := range testCases { @@ -1111,238 +1111,3 @@ func waitForVpaWebhookRegistration(f *framework.Framework) { return false }, 3*time.Minute, 5*time.Second).Should(gomega.BeTrue(), "Webhook was not registered in the cluster") } - -var _ = AdmissionControllerE2eDescribe("Admission-controller", func() { - f := framework.NewDefaultFramework("vertical-pod-autoscaling") - f.NamespacePodSecurityEnforceLevel = podsecurity.LevelBaseline - - ginkgo.BeforeEach(func() { - waitForVpaWebhookRegistration(f) - }) - - f.It("accepts valid and rejects invalid VPA object", framework.WithFeatureGate(features.PerVPAConfig), func() { - ginkgo.By("Setting up valid VPA object") - validVPA := []byte(`{ - "kind": "VerticalPodAutoscaler", - "apiVersion": "autoscaling.k8s.io/v1", - "metadata": {"name": "hamster-vpa-valid"}, - "spec": { - "targetRef": { - "apiVersion": "apps/v1", - "kind": "Deployment", - "name":"hamster" - }, - "resourcePolicy": { - "containerPolicies": [{"containerName": "*", "minAllowed":{"cpu":"50m"}}] - } - } - }`) - err := InstallRawVPA(f, validVPA) - gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Valid VPA object rejected") - - ginkgo.By("Setting up invalid VPA objects") - testCases := []struct { - name string - vpaJSON string - expectedErr string - }{ - { - name: "Invalid oomBumpUpRatio (negative value)", - vpaJSON: `{ - "apiVersion": "autoscaling.k8s.io/v1", - "kind": "VerticalPodAutoscaler", - "metadata": {"name": "oom-test-vpa"}, - "spec": { - "targetRef": { - "apiVersion": "apps/v1", - "kind": "Deployment", - "name": "oom-test" - }, - "updatePolicy": { - "updateMode": "Auto" - }, - "resourcePolicy": { - "containerPolicies": [{ - "containerName": "*", - "oomBumpUpRatio": -1, - "oomMinBumpUp": 104857600 - }] - } - } - }`, - expectedErr: "admission webhook \"vpa.k8s.io\" denied the request: oomBumpUpRatio must be greater than or equal to 1.0, got -1", - }, - { - name: "Invalid oomBumpUpRatio (string value)", - vpaJSON: `{ - "apiVersion": "autoscaling.k8s.io/v1", - "kind": "VerticalPodAutoscaler", - "metadata": {"name": "oom-test-vpa"}, - "spec": { - "targetRef": { - "apiVersion": "apps/v1", - "kind": "Deployment", - "name": "oom-test" - }, - "updatePolicy": { - "updateMode": "Auto" - }, - "resourcePolicy": { - "containerPolicies": [{ - "containerName": "*", - "oomBumpUpRatio": "not-a-number", - "oomMinBumpUp": 104857600 - }] - } - } - }`, - expectedErr: "admission webhook \"vpa\\.k8s\\.io\" denied the request: quantities must match the regular expression", - }, - { - name: "Invalid oomBumpUpRatio (less than 1)", - vpaJSON: `{ - "apiVersion": "autoscaling.k8s.io/v1", - "kind": "VerticalPodAutoscaler", - "metadata": {"name": "oom-test-vpa"}, - "spec": { - "targetRef": { - "apiVersion": "apps/v1", - "kind": "Deployment", - "name": "oom-test" - }, - "updatePolicy": { - "updateMode": "Auto" - }, - "resourcePolicy": { - "containerPolicies": [{ - "containerName": "*", - "oomBumpUpRatio": 0.5, - "oomMinBumpUp": 104857600 - }] - } - } - }`, - expectedErr: "admission webhook \"vpa.k8s.io\" denied the request: oomBumpUpRatio must be greater than or equal to 1.0, got 0.5", - }, - { - name: "Invalid oomMinBumpUp (negative value)", - vpaJSON: `{ - "apiVersion": "autoscaling.k8s.io/v1", - "kind": "VerticalPodAutoscaler", - "metadata": {"name": "oom-test-vpa"}, - "spec": { - "targetRef": { - "apiVersion": "apps/v1", - "kind": "Deployment", - "name": "oom-test" - }, - "updatePolicy": { - "updateMode": "Auto" - }, - "resourcePolicy": { - "containerPolicies": [{ - "containerName": "*", - "oomBumpUpRatio": 2, - "oomMinBumpUp": -1 - }] - } - } - }`, - expectedErr: "admission webhook \"vpa\\.k8s\\.io\" denied the request: oomMinBumpUp must be greater than or equal to 0, got -1 bytes", - }, - { - name: "Invalid evictAfterOOMThreshold (negative duration)", - vpaJSON: `{ - "apiVersion": "autoscaling.k8s.io/v1", - "kind": "VerticalPodAutoscaler", - "metadata": {"name": "evict-threshold-vpa"}, - "spec": { - "targetRef": { - "apiVersion": "apps/v1", - "kind": "Deployment", - "name": "hamster" - }, - "updatePolicy": { - "updateMode": "Auto", - "evictAfterOOMThreshold": "-5m" - } - } - }`, - expectedErr: "spec\\.updatePolicy\\.evictAfterOOMThreshold: Invalid value:.*evictAfterOOMThreshold must be greater than 0", - }, - { - name: "Invalid evictAfterOOMThreshold (invalid format)", - vpaJSON: `{ - "apiVersion": "autoscaling.k8s.io/v1", - "kind": "VerticalPodAutoscaler", - "metadata": {"name": "evict-threshold-vpa"}, - "spec": { - "targetRef": { - "apiVersion": "apps/v1", - "kind": "Deployment", - "name": "hamster" - }, - "updatePolicy": { - "updateMode": "Auto", - "evictAfterOOMThreshold": "not-a-duration" - } - } - }`, - expectedErr: "admission webhook.*denied the request:.*invalid duration", - }, - { - name: "Invalid evictAfterOOMThreshold (invalid unit)", - vpaJSON: `{ - "apiVersion": "autoscaling.k8s.io/v1", - "kind": "VerticalPodAutoscaler", - "metadata": {"name": "evict-threshold-vpa"}, - "spec": { - "targetRef": { - "apiVersion": "apps/v1", - "kind": "Deployment", - "name": "hamster" - }, - "updatePolicy": { - "updateMode": "Auto", - "evictAfterOOMThreshold": "5x" - } - } - }`, - expectedErr: "admission webhook.*denied the request:.*unknown unit.*in duration", - }, - { - name: "Invalid minAllowed (invalid requests field)", - vpaJSON: `{ - "apiVersion": "autoscaling.k8s.io/v1", - "kind": "VerticalPodAutoscaler", - "metadata": {"name": "hamster-vpa-invalid"}, - "spec": { - "targetRef": { - "apiVersion": "apps/v1", - "kind": "Deployment", - "name": "hamster" - }, - "resourcePolicy": { - "containerPolicies": [{ - "containerName": "*", - "minAllowed": { - "requests": { - "cpu": "50m" - } - } - }] - } - } - }`, - expectedErr: "admission webhook .*vpa.* denied the request:", - }, - } - for _, tc := range testCases { - err := InstallRawVPA(f, []byte(tc.vpaJSON)) - gomega.Expect(err).To(gomega.HaveOccurred(), - fmt.Sprintf("Test case '%s': Invalid VPA object accepted", tc.name)) - gomega.Expect(err.Error()).To(gomega.MatchRegexp(tc.expectedErr), - fmt.Sprintf("Test case '%s': Expected error pattern not matched. Got error: %v", tc.name, err)) - } - }) -}) diff --git a/vertical-pod-autoscaler/e2e/v1/updater.go b/vertical-pod-autoscaler/e2e/v1/updater.go index 14a1c4d142c8..c069312f1d40 100644 --- a/vertical-pod-autoscaler/e2e/v1/updater.go +++ b/vertical-pod-autoscaler/e2e/v1/updater.go @@ -248,7 +248,6 @@ var _ = UpdaterE2eDescribe("Updater with PerVPAConfig", func() { gomega.Expect(err).NotTo(gomega.HaveOccurred()) gomega.Expect(len(podList.Items)).To(gomega.BeNumerically(">", 0)) - // verySmallThreshold := 1 * time.Nanosecond disabledThreshold := 0 * time.Second // Disable quick OOM eviction ginkgo.By("Setting up a VPA CRD with very short evictAfterOOMThreshold (1ns)") targetRef := &autoscaling.CrossVersionObjectReference{