@@ -19,6 +19,8 @@ package autoscaling
1919import (
2020 "context"
2121 "fmt"
22+ "strconv"
23+ "strings"
2224 "time"
2325
2426 autoscaling "k8s.io/api/autoscaling/v1"
@@ -30,6 +32,7 @@ import (
3032 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/test"
3133 "k8s.io/kubernetes/test/e2e/framework"
3234 podsecurity "k8s.io/pod-security-admission/api"
35+ "k8s.io/utils/ptr"
3336
3437 ginkgo "github.com/onsi/ginkgo/v2"
3538 "github.com/onsi/gomega"
@@ -205,8 +208,156 @@ var _ = UpdaterE2eDescribe("Updater", func() {
205208 err := WaitForPodsUpdatedWithoutEviction (f , initialPods )
206209 gomega .Expect (err ).NotTo (gomega .HaveOccurred ())
207210 })
211+
212+ framework .It ("filters pods using the --pod-label-selectors flag" , framework .WithSerial (), func () {
213+ const (
214+ testLabelKey = "vpa-updater-test"
215+ testLabelValueMatch = "enabled"
216+ matchingReplicas = 3
217+ nonMatchingReplicas = 2
218+ )
219+ testNamespace := f .Namespace .Name
220+
221+ ginkgo .By ("Creating pods with non-matching labels" )
222+ nonMatchingDeployment := utils .NewNHamstersDeployment (f , 1 )
223+ nonMatchingDeployment .Name = "non-matching-hamster"
224+ nonMatchingDeployment .Spec .Replicas = ptr .To (int32 (nonMatchingReplicas ))
225+ nonMatchingDeployment .Spec .Template .Labels [testLabelKey ] = "disabled"
226+ nonMatchingDeployment .Spec .Template .Labels ["app" ] = "non-matching"
227+ nonMatchingDeployment .Spec .Selector .MatchLabels [testLabelKey ] = "disabled"
228+ nonMatchingDeployment .Spec .Selector .MatchLabels ["app" ] = "non-matching"
229+ utils .StartDeploymentPods (f , nonMatchingDeployment )
230+
231+ ginkgo .By ("Creating pods with matching labels" )
232+ matchingDeployment := utils .NewNHamstersDeployment (f , 1 )
233+ matchingDeployment .Name = "matching-hamster"
234+ matchingDeployment .Spec .Replicas = ptr .To (int32 (matchingReplicas ))
235+ matchingDeployment .Spec .Template .Labels [testLabelKey ] = testLabelValueMatch
236+ matchingDeployment .Spec .Template .Labels ["app" ] = "matching"
237+ matchingDeployment .Spec .Selector .MatchLabels [testLabelKey ] = testLabelValueMatch
238+ matchingDeployment .Spec .Selector .MatchLabels ["app" ] = "matching"
239+ utils .StartDeploymentPods (f , matchingDeployment )
240+
241+ ginkgo .By ("Creating VPAs for both deployments" )
242+ containerName := utils .GetHamsterContainerNameByIndex (0 )
243+ nonMatchingVPA := test .VerticalPodAutoscaler ().
244+ WithName ("non-matching-vpa" ).
245+ WithNamespace (testNamespace ).
246+ WithTargetRef (& autoscaling.CrossVersionObjectReference {
247+ APIVersion : "apps/v1" ,
248+ Kind : "Deployment" ,
249+ Name : nonMatchingDeployment .Name ,
250+ }).
251+ WithContainer (containerName ).
252+ WithUpdateMode (vpa_types .UpdateModeRecreate ).
253+ Get ()
254+ utils .InstallVPA (f , nonMatchingVPA )
255+
256+ matchingVPA := test .VerticalPodAutoscaler ().
257+ WithName ("matching-vpa" ).
258+ WithNamespace (testNamespace ).
259+ WithTargetRef (& autoscaling.CrossVersionObjectReference {
260+ APIVersion : "apps/v1" ,
261+ Kind : "Deployment" ,
262+ Name : matchingDeployment .Name ,
263+ }).
264+ WithContainer (containerName ).
265+ WithUpdateMode (vpa_types .UpdateModeRecreate ).
266+ Get ()
267+ utils .InstallVPA (f , matchingVPA )
268+
269+ ginkgo .By ("Setting up custom updater deployment with --pod-label-selectors flag" )
270+ // we swap the namespace to kube-system and then back to the test namespace
271+ // so our custom updater deployment can use the deployed RBAC
272+ originalNamespace := f .Namespace .Name
273+ f .Namespace .Name = utils .UpdaterNamespace
274+ deploymentName := "vpa-updater-with-pod-label-selectors"
275+ updaterDeployment := utils .NewUpdaterDeployment (f , deploymentName , []string {
276+ "--updater-interval=10s" ,
277+ "--use-admission-controller-status=false" ,
278+ fmt .Sprintf ("--pod-label-selectors=%s=%s" , testLabelKey , testLabelValueMatch ),
279+ fmt .Sprintf ("--vpa-object-namespace=%s" , testNamespace ),
280+ })
281+ utils .StartDeploymentPods (f , updaterDeployment )
282+ f .Namespace .Name = originalNamespace
283+
284+ defer func () {
285+ ginkgo .By ("Cleaning up custom updater deployment" )
286+ f .ClientSet .AppsV1 ().Deployments (utils .UpdaterNamespace ).Delete (context .TODO (), deploymentName , metav1.DeleteOptions {})
287+ }()
288+
289+ ginkgo .By ("Waiting for custom updater to report controlled pods count via metrics" )
290+ gomega .Eventually (func () (float64 , error ) {
291+ return getMetricValue (f , utils .UpdaterNamespace , "vpa_updater_controlled_pods_total" , map [string ]string {
292+ "update_mode" : string (vpa_types .UpdateModeRecreate ),
293+ })
294+ }, 2 * time .Minute , 5 * time .Second ).Should (gomega .Equal (float64 (matchingReplicas )),
295+ "Custom updater should only see %d matching pods (not the %d non-matching pods)" ,
296+ matchingReplicas , nonMatchingReplicas )
297+ })
208298})
209299
300+ func getMetricValue (f * framework.Framework , namespace , metricName string , labels map [string ]string ) (float64 , error ) {
301+ // Port forward to the updater pod
302+ pods , err := f .ClientSet .CoreV1 ().Pods (namespace ).List (context .TODO (), metav1.ListOptions {
303+ LabelSelector : "app=custom-vpa-updater" ,
304+ })
305+ if err != nil || len (pods .Items ) == 0 {
306+ return 0 , fmt .Errorf ("updater pod not found: %v" , err )
307+ }
308+
309+ // Use kubectl port-forward via exec in the pod
310+ req := f .ClientSet .CoreV1 ().RESTClient ().Get ().
311+ Namespace (namespace ).
312+ Resource ("pods" ).
313+ Name (pods .Items [0 ].Name ).
314+ SubResource ("proxy" ).
315+ Suffix ("metrics" )
316+
317+ result := req .Do (context .TODO ())
318+ body , err := result .Raw ()
319+ if err != nil {
320+ return 0 , fmt .Errorf ("failed to get metrics: %v" , err )
321+ }
322+
323+ // Parse Prometheus metrics format
324+ lines := strings .Split (string (body ), "\n " )
325+ for _ , line := range lines {
326+ if strings .HasPrefix (line , "#" ) {
327+ continue
328+ }
329+ if ! strings .HasPrefix (line , metricName ) {
330+ continue
331+ }
332+
333+ // Match labels
334+ if len (labels ) > 0 {
335+ allLabelsMatch := true
336+ for k , v := range labels {
337+ labelPattern := fmt .Sprintf (`%s="%s"` , k , v )
338+ if ! strings .Contains (line , labelPattern ) {
339+ allLabelsMatch = false
340+ break
341+ }
342+ }
343+ if ! allLabelsMatch {
344+ continue
345+ }
346+ }
347+
348+ // Extract value from end of line
349+ parts := strings .Fields (line )
350+ if len (parts ) >= 2 {
351+ value , err := strconv .ParseFloat (parts [len (parts )- 1 ], 64 )
352+ if err == nil {
353+ return value , nil
354+ }
355+ }
356+ }
357+
358+ return 0 , fmt .Errorf ("metric %s not found" , metricName )
359+ }
360+
210361func setupPodsForUpscalingEviction (f * framework.Framework ) * apiv1.PodList {
211362 return setupPodsForEviction (f , "100m" , "100Mi" , nil )
212363}
0 commit comments