@@ -24,8 +24,10 @@ import (
2424 "time"
2525
2626 "github.com/stretchr/testify/assert"
27+ "github.com/stretchr/testify/require"
2728 "go.uber.org/mock/gomock"
2829 "golang.org/x/time/rate"
30+ appsv1 "k8s.io/api/apps/v1"
2931 autoscalingv1 "k8s.io/api/autoscaling/v1"
3032 corev1 "k8s.io/api/core/v1"
3133 apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -34,20 +36,27 @@ import (
3436 "k8s.io/apimachinery/pkg/labels"
3537 "k8s.io/apimachinery/pkg/runtime"
3638 "k8s.io/apimachinery/pkg/types"
39+ "k8s.io/client-go/informers"
3740 "k8s.io/client-go/kubernetes/fake"
41+ "k8s.io/client-go/tools/record"
3842 featuregatetesting "k8s.io/component-base/featuregate/testing"
3943 "k8s.io/utils/set"
4044
45+ "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch"
46+ "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation"
4147 vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
4248 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/features"
4349 controllerfetcher "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/target/controller_fetcher"
4450 target_mock "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/target/mock"
51+ "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/updater/inplace"
4552 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/updater/priority"
4653 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/updater/restriction"
4754 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/updater/utils"
4855 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations"
56+ "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/limitrange"
4957 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/status"
5058 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/test"
59+ vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
5160)
5261
5362func parseLabelSelector (selector string ) labels.Selector {
@@ -370,6 +379,152 @@ func testRunOnceBase(
370379 inplace .AssertNumberOfCalls (t , "InPlaceUpdate" , expectedInPlacedCount )
371380}
372381
382+ // TestRunOnce_MultipleVPAs tests that if multiple VPAs target the same Pod and only one of them is 'active' (does NOT have `spec.updateMode: Off`):
383+ // - the applied recommendation is always the one from the 'active' VPA
384+ // - the 'active' VPA is always selected as the 'controlling' VPA for the Pod
385+ func TestRunOnce_MultipleVPAs (t * testing.T ) {
386+ featuregatetesting .SetFeatureGateDuringTest (t , features .MutableFeatureGate , features .InPlace , true )
387+ ctrl := gomock .NewController (t )
388+ defer ctrl .Finish ()
389+
390+ podLabels := map [string ]string {"app" : "test" }
391+ selector := parseLabelSelector ("app = test" )
392+ containerName := "test"
393+ replicas := int32 (1 )
394+ rs := appsv1.ReplicaSet {
395+ TypeMeta : metav1.TypeMeta {
396+ Kind : "ReplicaSet" ,
397+ APIVersion : "apps/v1" ,
398+ },
399+ ObjectMeta : metav1.ObjectMeta {
400+ Name : "rs" ,
401+ Namespace : "default" ,
402+ },
403+ Spec : appsv1.ReplicaSetSpec {
404+ Replicas : & replicas ,
405+ },
406+ }
407+
408+ pod := test .Pod ().WithName ("test-pod" ).
409+ AddContainer (test .Container ().WithName (containerName ).
410+ WithCPURequest (resource .MustParse ("1" )).
411+ WithMemRequest (resource .MustParse ("100M" )).
412+ WithCPULimit (resource .MustParse ("1" )).
413+ WithMemLimit (resource .MustParse ("100M" )).Get ()).
414+ WithCreator (& rs .ObjectMeta , & rs .TypeMeta ).
415+ Get ()
416+ pod .Labels = podLabels
417+
418+ targetRef := & autoscalingv1.CrossVersionObjectReference {
419+ Kind : rs .Kind ,
420+ Name : rs .Name ,
421+ APIVersion : rs .APIVersion ,
422+ }
423+
424+ // If multiple VPAs match a Pod, a 'controlling' VPA is selected using VPA
425+ // timestamps.
426+ // The selector should be given a list that does NOT include any VPAs with
427+ // `updateMode: Off` and no startup boost, so the test here would catch if
428+ // that logic changed.
429+ now := time .Now ()
430+ VPAInPlace := test .VerticalPodAutoscaler ().
431+ WithName ("vpa-inplace" ).
432+ WithNamespace ("default" ).
433+ WithCreationTimestamp (now ).
434+ WithContainer (containerName ).
435+ WithUpdateMode (vpa_types .UpdateModeInPlace ).
436+ WithTarget ("3" , "300M" ).
437+ WithTargetRef (targetRef ).
438+ Get ()
439+
440+ VPAOff1 := test .VerticalPodAutoscaler ().
441+ WithName ("vpa-off-1" ).
442+ WithNamespace ("default" ).
443+ WithCreationTimestamp (now .Add (- time .Hour )). // older than the InPlace VPA
444+ WithContainer (containerName ).
445+ WithUpdateMode (vpa_types .UpdateModeOff ).
446+ WithTarget ("9" , "900M" ).
447+ WithTargetRef (targetRef ).
448+ Get ()
449+
450+ VPAOff2 := test .VerticalPodAutoscaler ().
451+ WithName ("vpa-off-2" ).
452+ WithNamespace ("default" ).
453+ WithCreationTimestamp (now ). // timestamp matches the InPlace VPA
454+ WithContainer (containerName ).
455+ WithUpdateMode (vpa_types .UpdateModeOff ).
456+ WithTarget ("5" , "500M" ).
457+ WithTargetRef (targetRef ).
458+ Get ()
459+
460+ VPAOff3 := test .VerticalPodAutoscaler ().
461+ WithName ("vpa-off-3" ).
462+ WithNamespace ("default" ).
463+ WithCreationTimestamp (now .Add (time .Hour )). // newer than the InPlace VPA
464+ WithContainer (containerName ).
465+ WithUpdateMode (vpa_types .UpdateModeOff ).
466+ WithTarget ("7" , "700M" ).
467+ WithTargetRef (targetRef ).
468+ Get ()
469+
470+ kubeClient := fake .NewSimpleClientset (pod )
471+
472+ informerFactory := informers .NewSharedInformerFactory (kubeClient , 0 )
473+ require .NoError (t , informerFactory .Apps ().V1 ().ReplicaSets ().Informer ().GetStore ().Add (& rs ))
474+
475+ limitRangeCalculator := limitrange .NewNoopLimitsCalculator ()
476+ recommendationProvider := recommendation .NewProvider (limitRangeCalculator , vpa_api_util .NewCappingRecommendationProcessor (limitRangeCalculator ))
477+ calculators := []patch.Calculator {inplace .NewResourceInPlaceUpdatesCalculator (recommendationProvider )}
478+ restrictionFactory := restriction .NewPodsRestrictionFactory (kubeClient , informerFactory , 0 , 0 , calculators , false )
479+
480+ vpaLister := & test.VerticalPodAutoscalerListerMock {}
481+ vpaLister .On ("List" ).Return ([]* vpa_types.VerticalPodAutoscaler {VPAInPlace , VPAOff1 , VPAOff2 , VPAOff3 }, nil ).Once ()
482+
483+ podLister := & test.PodListerMock {}
484+ podLister .On ("List" ).Return ([]* corev1.Pod {pod }, nil )
485+
486+ mockSelectorFetcher := target_mock .NewMockVpaTargetSelectorFetcher (ctrl )
487+ mockSelectorFetcher .EXPECT ().Fetch (gomock .Eq (VPAInPlace )).Return (selector , nil )
488+
489+ u := & updater {
490+ vpaLister : vpaLister ,
491+ podLister : podLister ,
492+ restrictionFactory : restrictionFactory ,
493+ evictionRateLimiter : rate .NewLimiter (rate .Inf , 0 ),
494+ inPlaceRateLimiter : rate .NewLimiter (rate .Inf , 0 ),
495+ evictionAdmission : priority .NewDefaultPodEvictionAdmission (),
496+ recommendationProcessor : & test.FakeRecommendationProcessor {},
497+ selectorFetcher : mockSelectorFetcher ,
498+ controllerFetcher : controllerfetcher.FakeControllerFetcher {},
499+ useAdmissionControllerStatus : true ,
500+ statusValidator : newFakeValidator (true ),
501+ priorityProcessor : priority .NewProcessor (),
502+ eventRecorder : record .NewFakeRecorder (10 ),
503+ }
504+
505+ u .RunOnce (context .Background ())
506+
507+ updatedPod , err := kubeClient .CoreV1 ().Pods ("default" ).Get (context .Background (), pod .Name , metav1.GetOptions {})
508+ require .NoError (t , err )
509+
510+ // Test that the Pod got patched with the recommendation from the VPA with `updateMode: InPlaceOrRecreate`.
511+ gotCPUReq := updatedPod .Spec .Containers [0 ].Resources .Requests [corev1 .ResourceCPU ]
512+ assert .True (t , resource .MustParse ("3" ).Equal (gotCPUReq ),
513+ "Container's CPU request should be patched with the recommendation from %q (3 CPU), got %s" , VPAInPlace .Name , gotCPU .String ())
514+
515+ gotMemReq := updatedPod .Spec .Containers [0 ].Resources .Requests [corev1 .ResourceMemory ]
516+ assert .True (t , resource .MustParse ("300M" ).Equal (gotMemReq ),
517+ "Container's memory request should be patched with the recommendation from %q (300M), got %s" , VPAInPlace .Name , gotMem .String ())
518+
519+ gotCPULimit := updatedPod .Spec .Containers [0 ].Resources .Limits [corev1 .ResourceCPU ]
520+ assert .True (t , resource .MustParse ("3" ).Equal (gotCPULimit ),
521+ "Container's CPU limit should be patched with the recommendation from %q (3 CPU), got %s" , VPAInPlace .Name , gotCPULimit .String ())
522+
523+ gotMemLimit := updatedPod .Spec .Containers [0 ].Resources .Limits [corev1 .ResourceMemory ]
524+ assert .True (t , resource .MustParse ("300M" ).Equal (gotMemLimit ),
525+ "Container's memory limit should be patched with the recommendation from %q (300M), got %s" , VPAInPlace .Name , gotMemLimit .String ())
526+ }
527+
373528func TestRunOnceNotingToProcess (t * testing.T ) {
374529 eviction := & test.PodsEvictionRestrictionMock {}
375530 inplace := & test.PodsInPlaceRestrictionMock {}
0 commit comments