From f6f1af218aa3548eba3afaceb0a6a7f91145cbae Mon Sep 17 00:00:00 2001 From: Chris Motley Date: Tue, 4 Nov 2025 18:54:09 -0800 Subject: [PATCH 1/3] fix: ensure vpa_recommender_vpa_objects_count UpdateModeInPlaceOrRecreate is reset --- .../resource/vpa/handler.go | 10 +-- .../pkg/apis/autoscaling.k8s.io/v1/types.go | 11 +++ .../utils/metrics/recommender/recommender.go | 15 +--- .../metrics/recommender/recommender_test.go | 71 +++++++++++++++++++ 4 files changed, 85 insertions(+), 22 deletions(-) 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..8b5906d2ce17 100644 --- a/vertical-pod-autoscaler/pkg/admission-controller/resource/vpa/handler.go +++ b/vertical-pod-autoscaler/pkg/admission-controller/resource/vpa/handler.go @@ -34,14 +34,6 @@ import ( ) var ( - possibleUpdateModes = map[vpa_types.UpdateMode]interface{}{ - vpa_types.UpdateModeOff: struct{}{}, - vpa_types.UpdateModeInitial: struct{}{}, - vpa_types.UpdateModeRecreate: struct{}{}, - vpa_types.UpdateModeAuto: struct{}{}, - vpa_types.UpdateModeInPlaceOrRecreate: struct{}{}, - } - possibleScalingModes = map[vpa_types.ContainerScalingMode]interface{}{ vpa_types.ContainerScalingModeAuto: struct{}{}, vpa_types.ContainerScalingModeOff: struct{}{}, @@ -120,7 +112,7 @@ func ValidateVPA(vpa *vpa_types.VerticalPodAutoscaler, isCreate bool) error { if mode == nil { return fmt.Errorf("updateMode is required if UpdatePolicy is used") } - if _, found := possibleUpdateModes[*mode]; !found { + if _, found := vpa_types.GetUpdateModes()[*mode]; !found { return fmt.Errorf("unexpected UpdateMode value %s", *mode) } if (*mode == vpa_types.UpdateModeInPlaceOrRecreate) && !features.Enabled(features.InPlaceOrRecreate) && isCreate { 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..905c8d6c1479 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 @@ -184,6 +184,17 @@ const ( UpdateModeInPlaceOrRecreate UpdateMode = "InPlaceOrRecreate" ) +// GetUpdateModes returns all supported UpdateModes +func GetUpdateModes() map[UpdateMode]any { + return map[UpdateMode]any{ + UpdateModeOff: nil, + UpdateModeInitial: nil, + UpdateModeRecreate: nil, + UpdateModeAuto: nil, + UpdateModeInPlaceOrRecreate: nil, + } +} + // PodResourcePolicy controls how autoscaler computes the recommended resources // for containers belonging to the pod. There can be at most one entry for every // named container and optionally a single wildcard entry with `containerName` = '*', diff --git a/vertical-pod-autoscaler/pkg/utils/metrics/recommender/recommender.go b/vertical-pod-autoscaler/pkg/utils/metrics/recommender/recommender.go index 9a621cda38b6..1a2d5aee45b4 100644 --- a/vertical-pod-autoscaler/pkg/utils/metrics/recommender/recommender.go +++ b/vertical-pod-autoscaler/pkg/utils/metrics/recommender/recommender.go @@ -36,17 +36,6 @@ const ( metricsNamespace = metrics.TopMetricsNamespace + "recommender" ) -var ( - // TODO: unify this list with the types defined in the VPA handler to avoid - // drift if one file is changed and the other one is missed. - modes = []string{ - string(vpa_types.UpdateModeOff), - string(vpa_types.UpdateModeInitial), - string(vpa_types.UpdateModeRecreate), - string(vpa_types.UpdateModeAuto), - } -) - type apiVersion string const ( @@ -156,13 +145,13 @@ func NewObjectCounter() *ObjectCounter { } // initialize with empty data so we can clean stale gauge values in Observe - for _, m := range modes { + for m := range vpa_types.GetUpdateModes() { for _, h := range []bool{false, true} { for _, api := range []apiVersion{v1beta1, v1beta2, v1} { for _, mp := range []bool{false, true} { for _, uc := range []bool{false, true} { obj.cnt[objectCounterKey{ - mode: m, + mode: string(m), has: h, apiVersion: api, matchesPods: mp, diff --git a/vertical-pod-autoscaler/pkg/utils/metrics/recommender/recommender_test.go b/vertical-pod-autoscaler/pkg/utils/metrics/recommender/recommender_test.go index 30cfedca76d8..a8017275112f 100644 --- a/vertical-pod-autoscaler/pkg/utils/metrics/recommender/recommender_test.go +++ b/vertical-pod-autoscaler/pkg/utils/metrics/recommender/recommender_test.go @@ -33,6 +33,7 @@ func TestObjectCounter(t *testing.T) { updateModeInitial := vpa_types.UpdateModeInitial updateModeRecreate := vpa_types.UpdateModeRecreate updateModeAuto := vpa_types.UpdateModeAuto + updateModeInPlaceOrRecreate := vpa_types.UpdateModeInPlaceOrRecreate // We verify that other update modes are handled correctly as validation // may not happen if there are issues with the admission controller. updateModeUserDefined := vpa_types.UpdateMode("userDefined") @@ -146,6 +147,18 @@ func TestObjectCounter(t *testing.T) { "api=v1,has_recommendation=false,matches_pods=true,unsupported_config=false,update_mode=Off,": 1, }, }, + { + name: "report update mode InPlaceOrRecreate", + add: []*model.Vpa{ + { + APIVersion: "v1", + UpdateMode: &updateModeInPlaceOrRecreate, + }, + }, + wantMetrics: map[string]float64{ + "api=v1,has_recommendation=false,matches_pods=true,unsupported_config=false,update_mode=InPlaceOrRecreate,": 1, + }, + }, { name: "report update mode user defined", add: []*model.Vpa{ @@ -321,3 +334,61 @@ func labelsToKey(labels []*dto.LabelPair) string { } return key.String() } + +func TestObjectCounterResetsAllUpdateModes(t *testing.T) { + updatesModes := []vpa_types.UpdateMode{ + vpa_types.UpdateModeOff, + vpa_types.UpdateModeInitial, + vpa_types.UpdateModeAuto, + vpa_types.UpdateModeRecreate, + vpa_types.UpdateModeInPlaceOrRecreate, + } + + for _, mode := range updatesModes { + t.Run(string(mode), func(t *testing.T) { + t.Cleanup(func() { + vpaObjectCount.Reset() + }) + + key := "api=v1,has_recommendation=false,matches_pods=true,unsupported_config=false,update_mode=" + string(mode) + "," + + // first loop add VPAs to increment the counter + counter1 := NewObjectCounter() + for range 3 { + vpa := model.Vpa{ + APIVersion: "v1", + UpdateMode: &mode, + } + counter1.Add(&vpa) + } + counter1.Observe() + collectMetricsAndVerifyCount(t, key, 3) + + // next loop no VPAs + counter2 := NewObjectCounter() + counter2.Observe() + collectMetricsAndVerifyCount(t, key, 0) + }) + } +} + +func collectMetricsAndVerifyCount(t *testing.T, key string, expectedCount float64) { + metrics := make(chan prometheus.Metric) + go func() { + vpaObjectCount.Collect(metrics) + close(metrics) + }() + + liveMetrics := make(map[string]float64) + for metric := range metrics { + var metricProto dto.Metric + if err := metric.Write(&metricProto); err != nil { + t.Errorf("failed to write metric: %v", err) + } + liveMetrics[labelsToKey(metricProto.GetLabel())] = *metricProto.GetGauge().Value + } + + if actualCount := liveMetrics[key]; actualCount != expectedCount { + t.Errorf("key=%s expectedCount=%v actualCount=%v", key, expectedCount, actualCount) + } +} From aefde4d14fe5c56ccf82f93e569962e9f3baa1b8 Mon Sep 17 00:00:00 2001 From: Chris Motley Date: Mon, 10 Nov 2025 08:06:42 -0800 Subject: [PATCH 2/3] move GetUpdateModes() to helpers.go --- .../pkg/apis/autoscaling.k8s.io/v1/helpers.go | 28 +++++++++++++++++++ .../pkg/apis/autoscaling.k8s.io/v1/types.go | 11 -------- 2 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/helpers.go diff --git a/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/helpers.go b/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/helpers.go new file mode 100644 index 000000000000..5cd9eb10b0b9 --- /dev/null +++ b/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/helpers.go @@ -0,0 +1,28 @@ +/* +Copyright 2019 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 v1 + +// GetUpdateModes returns all supported UpdateModes +func GetUpdateModes() map[UpdateMode]any { + return map[UpdateMode]any{ + UpdateModeOff: nil, + UpdateModeInitial: nil, + UpdateModeRecreate: nil, + UpdateModeAuto: nil, + UpdateModeInPlaceOrRecreate: nil, + } +} 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 905c8d6c1479..dc365b8c686f 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 @@ -184,17 +184,6 @@ const ( UpdateModeInPlaceOrRecreate UpdateMode = "InPlaceOrRecreate" ) -// GetUpdateModes returns all supported UpdateModes -func GetUpdateModes() map[UpdateMode]any { - return map[UpdateMode]any{ - UpdateModeOff: nil, - UpdateModeInitial: nil, - UpdateModeRecreate: nil, - UpdateModeAuto: nil, - UpdateModeInPlaceOrRecreate: nil, - } -} - // PodResourcePolicy controls how autoscaler computes the recommended resources // for containers belonging to the pod. There can be at most one entry for every // named container and optionally a single wildcard entry with `containerName` = '*', From f457f09efbee5e74b008fa97986237d3657793b4 Mon Sep 17 00:00:00 2001 From: Chris Motley Date: Fri, 14 Nov 2025 08:11:45 -0800 Subject: [PATCH 3/3] update copyright Co-authored-by: Adrian Moisey --- .../pkg/apis/autoscaling.k8s.io/v1/helpers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/helpers.go b/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/helpers.go index 5cd9eb10b0b9..abb815f59a34 100644 --- a/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/helpers.go +++ b/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/helpers.go @@ -1,5 +1,5 @@ /* -Copyright 2019 The Kubernetes Authors. +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.