Skip to content

Commit 1680325

Browse files
committed
VPA: add InPlaceOrRecreate e2e tests
Signed-off-by: Max Cao <[email protected]>
1 parent 80b0ffe commit 1680325

File tree

7 files changed

+664
-60
lines changed

7 files changed

+664
-60
lines changed

vertical-pod-autoscaler/e2e/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ require (
1414
k8s.io/apimachinery v0.32.0
1515
k8s.io/autoscaler/vertical-pod-autoscaler v1.2.1
1616
k8s.io/client-go v0.32.0
17-
k8s.io/component-base v0.32.0
17+
k8s.io/component-base v0.32.2
1818
k8s.io/klog/v2 v2.130.1
1919
k8s.io/kubernetes v1.32.0
2020
k8s.io/pod-security-admission v0.32.0

vertical-pod-autoscaler/e2e/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
9292
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
9393
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
9494
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
95+
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
96+
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
9597
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
9698
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
9799
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=

vertical-pod-autoscaler/e2e/v1/actuation.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"k8s.io/apimachinery/pkg/util/wait"
3636
"k8s.io/autoscaler/vertical-pod-autoscaler/e2e/utils"
3737
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
38+
restriction "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/updater/restriction"
3839
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations"
3940
clientset "k8s.io/client-go/kubernetes"
4041
"k8s.io/kubernetes/test/e2e/framework"
@@ -50,6 +51,237 @@ import (
5051
"github.com/onsi/gomega"
5152
)
5253

54+
var _ = ActuationSuiteE2eDescribe("Actuation [InPlaceOrRecreate]", func() {
55+
f := framework.NewDefaultFramework("vertical-pod-autoscaling")
56+
f.NamespacePodSecurityEnforceLevel = podsecurity.LevelBaseline
57+
58+
ginkgo.BeforeEach(func() {
59+
checkInPlaceOrRecreateTestsEnabled(f, true, true)
60+
})
61+
62+
ginkgo.It("still applies recommendations on restart when update mode is InPlaceOrRecreate", func() {
63+
ginkgo.By("Setting up a hamster deployment")
64+
SetupHamsterDeployment(f, "100m", "100Mi", defaultHamsterReplicas)
65+
podList, err := GetHamsterPods(f)
66+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
67+
podSet := MakePodSet(podList)
68+
69+
ginkgo.By("Setting up a VPA CRD in mode InPlaceOrRecreate")
70+
containerName := GetHamsterContainerNameByIndex(0)
71+
vpaCRD := test.VerticalPodAutoscaler().
72+
WithName("hamster-vpa").
73+
WithNamespace(f.Namespace.Name).
74+
WithTargetRef(hamsterTargetRef).
75+
WithUpdateMode(vpa_types.UpdateModeInPlaceOrRecreate).
76+
WithContainer(containerName).
77+
AppendRecommendation(
78+
test.Recommendation().
79+
WithContainer(containerName).
80+
WithTarget("200m", "").
81+
WithLowerBound("200m", "").
82+
WithUpperBound("200m", "").
83+
GetContainerResources()).
84+
Get()
85+
86+
InstallVPA(f, vpaCRD)
87+
updatedCPURequest := ParseQuantityOrDie("200m")
88+
89+
ginkgo.By(fmt.Sprintf("Waiting for pods to be evicted, hoping it won't happen, sleep for %s", VpaEvictionTimeout.String()))
90+
CheckNoPodsEvicted(f, podSet)
91+
ginkgo.By("Forcefully killing one pod")
92+
killPod(f, podList)
93+
94+
ginkgo.By("Checking that request was modified after forceful restart")
95+
updatedPodList, _ := GetHamsterPods(f)
96+
var foundUpdated int32
97+
for _, pod := range updatedPodList.Items {
98+
podRequest := getCPURequest(pod.Spec)
99+
framework.Logf("podReq: %v", podRequest)
100+
if podRequest.Cmp(updatedCPURequest) == 0 {
101+
foundUpdated += 1
102+
}
103+
}
104+
gomega.Expect(foundUpdated).To(gomega.Equal(defaultHamsterReplicas))
105+
})
106+
107+
ginkgo.It("applies in-place updates to all containers when update mode is InPlaceOrRecreate", func() {
108+
ginkgo.By("Setting up a hamster deployment")
109+
d := NewNHamstersDeployment(f, 2 /*number of containers*/)
110+
d.Spec.Template.Spec.Containers[0].Resources.Requests = apiv1.ResourceList{
111+
apiv1.ResourceCPU: ParseQuantityOrDie("100m"),
112+
apiv1.ResourceMemory: ParseQuantityOrDie("100Mi"),
113+
}
114+
d.Spec.Template.Spec.Containers[1].Resources.Requests = apiv1.ResourceList{
115+
apiv1.ResourceCPU: ParseQuantityOrDie("100m"),
116+
apiv1.ResourceMemory: ParseQuantityOrDie("100Mi"),
117+
}
118+
targetCPU := "200m"
119+
_ = startDeploymentPods(f, d) // 3 replicas
120+
container1Name := GetHamsterContainerNameByIndex(0)
121+
container2Name := GetHamsterContainerNameByIndex(1)
122+
podList, err := GetHamsterPods(f)
123+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
124+
125+
ginkgo.By("Setting up a VPA CRD")
126+
vpaCRD := test.VerticalPodAutoscaler().
127+
WithName("hamster-vpa").
128+
WithNamespace(f.Namespace.Name).
129+
WithTargetRef(hamsterTargetRef).
130+
WithContainer(container1Name).
131+
WithContainer(container2Name).
132+
WithUpdateMode(vpa_types.UpdateModeInPlaceOrRecreate).
133+
AppendRecommendation(
134+
test.Recommendation().
135+
WithContainer(container1Name).
136+
WithTarget(targetCPU, "").
137+
WithLowerBound("200m", "").
138+
WithUpperBound("200m", "").
139+
GetContainerResources()).
140+
AppendRecommendation(
141+
test.Recommendation().
142+
WithContainer(container2Name).
143+
WithTarget(targetCPU, "").
144+
WithLowerBound("200m", "").
145+
WithUpperBound("200m", "").
146+
GetContainerResources()).
147+
Get()
148+
149+
InstallVPA(f, vpaCRD)
150+
151+
ginkgo.By("Checking that resources were modified due to in-place update, not due to evictions")
152+
err = WaitForPodsUpdatedWithoutEviction(f, podList)
153+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
154+
155+
ginkgo.By("Checking that container resources were actually updated")
156+
gomega.Eventually(func() error {
157+
updatedPodList, err := GetHamsterPods(f)
158+
if err != nil {
159+
return err
160+
}
161+
for _, pod := range updatedPodList.Items {
162+
for _, container := range pod.Spec.Containers {
163+
if container.Name == container1Name {
164+
request := container.Resources.Requests[apiv1.ResourceCPU]
165+
if request.Cmp(ParseQuantityOrDie(targetCPU)) != 0 {
166+
framework.Logf("%v/%v has not been updated to %v yet: currently=%v", pod.Name, container.Name, targetCPU, request.String())
167+
return fmt.Errorf("%s CPU request not updated", container1Name)
168+
}
169+
}
170+
if container.Name == container2Name {
171+
request := container.Resources.Requests[apiv1.ResourceCPU]
172+
if request.Cmp(ParseQuantityOrDie(targetCPU)) != 0 {
173+
framework.Logf("%v/%v has not been updated to %v yet: currently=%v", pod.Name, container.Name, targetCPU, request.String())
174+
return fmt.Errorf("%s CPU request not updated", container2Name)
175+
}
176+
}
177+
}
178+
}
179+
return nil
180+
}, VpaInPlaceTimeout*3, 15*time.Second).Should(gomega.Succeed())
181+
})
182+
183+
ginkgo.It("falls back to evicting pods when in-place update is Infeasible when update mode is InPlaceOrRecreate", func() {
184+
ginkgo.By("Setting up a hamster deployment")
185+
replicas := int32(2)
186+
SetupHamsterDeployment(f, "100m", "100Mi", replicas)
187+
podList, err := GetHamsterPods(f)
188+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
189+
190+
ginkgo.By("Setting up a VPA CRD")
191+
containerName := GetHamsterContainerNameByIndex(0)
192+
updatedCPU := "999" // infeasible target
193+
vpaCRD := test.VerticalPodAutoscaler().
194+
WithName("hamster-vpa").
195+
WithNamespace(f.Namespace.Name).
196+
WithTargetRef(hamsterTargetRef).
197+
WithContainer(containerName).
198+
WithUpdateMode(vpa_types.UpdateModeInPlaceOrRecreate).
199+
AppendRecommendation(
200+
test.Recommendation().
201+
WithContainer(containerName).
202+
WithTarget(updatedCPU, "").
203+
WithLowerBound("200m", "").
204+
WithUpperBound("200m", "").
205+
GetContainerResources()).
206+
Get()
207+
208+
InstallVPA(f, vpaCRD)
209+
210+
ginkgo.By("Waiting for pods to be evicted")
211+
err = WaitForPodsEvicted(f, podList)
212+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
213+
})
214+
215+
ginkgo.It("falls back to evicting pods when resize is Deferred and more than 5 minute has elapsed since last in-place update when update mode is InPlaceOrRecreate", func() {
216+
ginkgo.By("Setting up a hamster deployment")
217+
replicas := int32(2)
218+
SetupHamsterDeployment(f, "100m", "100Mi", replicas)
219+
podList, err := GetHamsterPods(f)
220+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
221+
222+
ginkgo.By("Setting up a VPA CRD")
223+
containerName := GetHamsterContainerNameByIndex(0)
224+
225+
// get node name
226+
nodeName := podList.Items[0].Spec.NodeName
227+
node, err := f.ClientSet.CoreV1().Nodes().Get(context.TODO(), nodeName, metav1.GetOptions{})
228+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
229+
allocatableCPU := node.Status.Allocatable[apiv1.ResourceCPU]
230+
updatedCPU := allocatableCPU.String()
231+
232+
vpaCRD := test.VerticalPodAutoscaler().
233+
WithName("hamster-vpa").
234+
WithNamespace(f.Namespace.Name).
235+
WithTargetRef(hamsterTargetRef).
236+
WithContainer(containerName).
237+
WithUpdateMode(vpa_types.UpdateModeInPlaceOrRecreate).
238+
AppendRecommendation(
239+
test.Recommendation().
240+
WithContainer(containerName).
241+
WithTarget(updatedCPU, "").
242+
WithLowerBound("200m", "").
243+
WithUpperBound("200m", "").
244+
GetContainerResources()).
245+
Get()
246+
247+
InstallVPA(f, vpaCRD)
248+
249+
ginkgo.By("Waiting for status to be Deferred")
250+
gomega.Eventually(func() error {
251+
updatedPodList, err := GetHamsterPods(f)
252+
if err != nil {
253+
return err
254+
}
255+
for _, pod := range updatedPodList.Items {
256+
if pod.Status.Resize == apiv1.PodResizeStatusDeferred {
257+
return nil
258+
}
259+
}
260+
return fmt.Errorf("status not deferred")
261+
}, VpaInPlaceTimeout, 5*time.Second).Should(gomega.Succeed())
262+
263+
ginkgo.By("Making sure pods are not evicted yet")
264+
gomega.Consistently(func() error {
265+
updatedPodList, err := GetHamsterPods(f)
266+
if err != nil {
267+
return fmt.Errorf("failed to get pods: %v", err)
268+
}
269+
for _, pod := range updatedPodList.Items {
270+
request := getCPURequestFromStatus(pod.Status)
271+
if request.Cmp(ParseQuantityOrDie(updatedCPU)) == 0 {
272+
framework.Logf("%v/%v updated to %v, that wasn't supposed to happen this early", pod.Name, containerName, updatedCPU)
273+
return fmt.Errorf("%s CPU request should not have been updated", containerName)
274+
}
275+
}
276+
return nil
277+
}, restriction.DeferredResizeUpdateTimeout, 10*time.Second).Should(gomega.Succeed())
278+
279+
ginkgo.By("Waiting for pods to be evicted")
280+
err = WaitForPodsEvicted(f, podList)
281+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
282+
})
283+
})
284+
53285
var _ = ActuationSuiteE2eDescribe("Actuation", func() {
54286
f := framework.NewDefaultFramework("vertical-pod-autoscaling")
55287
f.NamespacePodSecurityEnforceLevel = podsecurity.LevelBaseline
@@ -519,6 +751,10 @@ func getCPURequest(podSpec apiv1.PodSpec) resource.Quantity {
519751
return podSpec.Containers[0].Resources.Requests[apiv1.ResourceCPU]
520752
}
521753

754+
func getCPURequestFromStatus(podStatus apiv1.PodStatus) resource.Quantity {
755+
return podStatus.ContainerStatuses[0].Resources.Requests[apiv1.ResourceCPU]
756+
}
757+
522758
func killPod(f *framework.Framework, podList *apiv1.PodList) {
523759
f.ClientSet.CoreV1().Pods(f.Namespace.Name).Delete(context.TODO(), podList.Items[0].Name, metav1.DeleteOptions{})
524760
err := WaitForPodsRestarted(f, podList)

vertical-pod-autoscaler/e2e/v1/admission_controller.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,19 @@ import (
3737
"github.com/onsi/gomega"
3838
)
3939

40+
const (
41+
webhookConfigName = "vpa-webhook-config"
42+
webhookName = "vpa.k8s.io"
43+
)
44+
4045
var _ = AdmissionControllerE2eDescribe("Admission-controller", func() {
4146
f := framework.NewDefaultFramework("vertical-pod-autoscaling")
4247
f.NamespacePodSecurityEnforceLevel = podsecurity.LevelBaseline
4348

49+
ginkgo.BeforeEach(func() {
50+
waitForVpaWebhookRegistration(f)
51+
})
52+
4453
ginkgo.It("starts pods with new recommended request", func() {
4554
d := NewHamsterDeploymentWithResources(f, ParseQuantityOrDie("100m") /*cpu*/, ParseQuantityOrDie("100Mi") /*memory*/)
4655

@@ -908,6 +917,40 @@ var _ = AdmissionControllerE2eDescribe("Admission-controller", func() {
908917
gomega.Expect(err.Error()).To(gomega.MatchRegexp(`.*admission webhook .*vpa.* denied the request: .*`), "Admission controller did not inspect the object")
909918
})
910919

920+
ginkgo.It("starts pods with new recommended request with InPlaceOrRecreate mode", func() {
921+
checkInPlaceOrRecreateTestsEnabled(f, true, false)
922+
923+
d := NewHamsterDeploymentWithResources(f, ParseQuantityOrDie("100m") /*cpu*/, ParseQuantityOrDie("100Mi") /*memory*/)
924+
925+
ginkgo.By("Setting up a VPA CRD")
926+
containerName := GetHamsterContainerNameByIndex(0)
927+
vpaCRD := test.VerticalPodAutoscaler().
928+
WithName("hamster-vpa").
929+
WithNamespace(f.Namespace.Name).
930+
WithTargetRef(hamsterTargetRef).
931+
WithContainer(containerName).
932+
WithUpdateMode(vpa_types.UpdateModeInPlaceOrRecreate).
933+
AppendRecommendation(
934+
test.Recommendation().
935+
WithContainer(containerName).
936+
WithTarget("250m", "200Mi").
937+
WithLowerBound("250m", "200Mi").
938+
WithUpperBound("250m", "200Mi").
939+
GetContainerResources()).
940+
Get()
941+
942+
InstallVPA(f, vpaCRD)
943+
944+
ginkgo.By("Setting up a hamster deployment")
945+
podList := startDeploymentPods(f, d)
946+
947+
// Originally Pods had 100m CPU, 100Mi of memory, but admission controller
948+
// should change it to recommended 250m CPU and 200Mi of memory.
949+
for _, pod := range podList.Items {
950+
gomega.Expect(pod.Spec.Containers[0].Resources.Requests[apiv1.ResourceCPU]).To(gomega.Equal(ParseQuantityOrDie("250m")))
951+
gomega.Expect(pod.Spec.Containers[0].Resources.Requests[apiv1.ResourceMemory]).To(gomega.Equal(ParseQuantityOrDie("200Mi")))
952+
}
953+
})
911954
})
912955

913956
func startDeploymentPods(f *framework.Framework, deployment *appsv1.Deployment) *apiv1.PodList {
@@ -962,3 +1005,17 @@ func startDeploymentPods(f *framework.Framework, deployment *appsv1.Deployment)
9621005
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "when listing pods after deployment resize")
9631006
return podList
9641007
}
1008+
1009+
func waitForVpaWebhookRegistration(f *framework.Framework) {
1010+
ginkgo.By("Waiting for VPA webhook registration")
1011+
gomega.Eventually(func() bool {
1012+
webhook, err := f.ClientSet.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(context.TODO(), webhookConfigName, metav1.GetOptions{})
1013+
if err != nil {
1014+
return false
1015+
}
1016+
if webhook != nil && len(webhook.Webhooks) > 0 && webhook.Webhooks[0].Name == webhookName {
1017+
return true
1018+
}
1019+
return false
1020+
}, 3*time.Minute, 5*time.Second).Should(gomega.BeTrue(), "Webhook was not registered in the cluster")
1021+
}

0 commit comments

Comments
 (0)