Skip to content

Commit 7f00816

Browse files
committed
VPA: test multi VPA behaviour.
Test that in a scenario where multiple VPAs match one Pod, one VPA has `updateMode: InPlace` and all others have `updateMode: Off`, VPA updater always applies the in place resize from the correct VPA. Also adds a timestamp to a similar test in the admissions controller to make the test stronger. Updates #10158 Signed-off-by: Irbe Krumina <irbekrm@gmail.com>
1 parent 55c37cd commit 7f00816

2 files changed

Lines changed: 158 additions & 2 deletions

File tree

vertical-pod-autoscaler/pkg/admission-controller/resource/vpa/matcher_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package vpa
1919
import (
2020
"context"
2121
"testing"
22+
"time"
2223

2324
"github.com/stretchr/testify/assert"
2425
"go.uber.org/mock/gomock"
@@ -126,8 +127,8 @@ func TestGetMatchingVpa(t *testing.T) {
126127
name: "two vpas, one in off mode",
127128
pod: podBuilder.Get(),
128129
vpas: []*vpa_types.VerticalPodAutoscaler{
129-
vpaBuilder.WithUpdateMode(vpa_types.UpdateModeOff).WithName("off-vpa").WithTargetRef(targetRef).Get(),
130-
vpaBuilder.WithUpdateMode(vpa_types.UpdateModeRecreate).WithName("recreate-vpa").WithTargetRef(targetRef).Get(),
130+
vpaBuilder.WithUpdateMode(vpa_types.UpdateModeOff).WithName("off-vpa").WithCreationTimestamp(time.Now().Add(-time.Hour)).WithTargetRef(targetRef).Get(),
131+
vpaBuilder.WithUpdateMode(vpa_types.UpdateModeRecreate).WithName("recreate-vpa").WithCreationTimestamp(time.Now()).WithTargetRef(targetRef).Get(),
131132
},
132133
labelSelector: "app = test",
133134
expectedFound: true,

vertical-pod-autoscaler/pkg/updater/logic/updater_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

5362
func 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+
373528
func TestRunOnceNotingToProcess(t *testing.T) {
374529
eviction := &test.PodsEvictionRestrictionMock{}
375530
inplace := &test.PodsInPlaceRestrictionMock{}

0 commit comments

Comments
 (0)