Skip to content

Commit 2eb783b

Browse files
committed
move common functions from v1 to utils
1 parent bf86702 commit 2eb783b

File tree

7 files changed

+498
-439
lines changed

7 files changed

+498
-439
lines changed
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package utils
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
"time"
24+
25+
ginkgo "github.com/onsi/ginkgo/v2"
26+
"github.com/onsi/gomega"
27+
appsv1 "k8s.io/api/apps/v1"
28+
autoscaling "k8s.io/api/autoscaling/v1"
29+
apiv1 "k8s.io/api/core/v1"
30+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/apimachinery/pkg/types"
32+
"k8s.io/apimachinery/pkg/util/wait"
33+
"k8s.io/kubernetes/test/e2e/framework"
34+
35+
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
36+
vpa_clientset "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/client/clientset/versioned"
37+
framework_deployment "k8s.io/kubernetes/test/e2e/framework/deployment"
38+
)
39+
40+
const (
41+
recommenderComponent = "recommender"
42+
43+
// RecommenderDeploymentName is VPA recommender deployment name
44+
RecommenderDeploymentName = "vpa-recommender"
45+
// RecommenderNamespace is namespace to deploy VPA recommender
46+
RecommenderNamespace = "kube-system"
47+
// PollInterval is interval for polling
48+
PollInterval = 10 * time.Second
49+
// PollTimeout is timeout for polling
50+
PollTimeout = 15 * time.Minute
51+
52+
// DefaultHamsterReplicas is replicas of hamster deployment
53+
DefaultHamsterReplicas = int32(3)
54+
// DefaultHamsterBackoffLimit is BackoffLimit of hamster app
55+
DefaultHamsterBackoffLimit = int32(10)
56+
)
57+
58+
// HamsterTargetRef is CrossVersionObjectReference of hamster app
59+
var HamsterTargetRef = &autoscaling.CrossVersionObjectReference{
60+
APIVersion: "apps/v1",
61+
Kind: "Deployment",
62+
Name: "hamster-deployment",
63+
}
64+
65+
// RecommenderLabels are labels of VPA recommender
66+
var RecommenderLabels = map[string]string{"app": "vpa-recommender"}
67+
68+
// HamsterLabels are labels of hamster app
69+
var HamsterLabels = map[string]string{"app": "hamster"}
70+
71+
// SIGDescribe adds sig-autoscaling tag to test description.
72+
// Takes args that are passed to ginkgo.Describe.
73+
func SIGDescribe(scenario, name string, args ...interface{}) bool {
74+
full := fmt.Sprintf("[sig-autoscaling] [VPA] [%s] [v1] %s", scenario, name)
75+
return ginkgo.Describe(full, args...)
76+
}
77+
78+
// RecommenderE2eDescribe describes a VPA recommender e2e test.
79+
func RecommenderE2eDescribe(name string, args ...interface{}) bool {
80+
return SIGDescribe(recommenderComponent, name, args...)
81+
}
82+
83+
// GetHamsterContainerNameByIndex returns name of i-th hamster container.
84+
func GetHamsterContainerNameByIndex(i int) string {
85+
switch {
86+
case i < 0:
87+
panic("negative index")
88+
case i == 0:
89+
return "hamster"
90+
default:
91+
return fmt.Sprintf("hamster%d", i+1)
92+
}
93+
}
94+
95+
// GetVpaClientSet return a VpaClientSet
96+
func GetVpaClientSet(f *framework.Framework) vpa_clientset.Interface {
97+
config, err := framework.LoadConfig()
98+
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "unexpected error loading framework")
99+
return vpa_clientset.NewForConfigOrDie(config)
100+
}
101+
102+
// InstallVPA installs a VPA object in the test cluster.
103+
func InstallVPA(f *framework.Framework, vpa *vpa_types.VerticalPodAutoscaler) {
104+
vpaClientSet := GetVpaClientSet(f)
105+
_, err := vpaClientSet.AutoscalingV1().VerticalPodAutoscalers(f.Namespace.Name).Create(context.TODO(), vpa, metav1.CreateOptions{})
106+
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "unexpected error creating VPA")
107+
// apiserver ignore status in vpa create, so need to update status
108+
if !isStatusEmpty(&vpa.Status) {
109+
if vpa.Status.Recommendation != nil {
110+
PatchVpaRecommendation(f, vpa, vpa.Status.Recommendation)
111+
}
112+
}
113+
}
114+
115+
func isStatusEmpty(status *vpa_types.VerticalPodAutoscalerStatus) bool {
116+
if status == nil {
117+
return true
118+
}
119+
120+
if len(status.Conditions) == 0 && status.Recommendation == nil {
121+
return true
122+
}
123+
return false
124+
}
125+
126+
// PatchRecord used for patch action
127+
type PatchRecord struct {
128+
Op string `json:"op,inline"`
129+
Path string `json:"path,inline"`
130+
Value interface{} `json:"value"`
131+
}
132+
133+
// PatchVpaRecommendation installs a new recommendation for VPA object.
134+
func PatchVpaRecommendation(f *framework.Framework, vpa *vpa_types.VerticalPodAutoscaler,
135+
recommendation *vpa_types.RecommendedPodResources) {
136+
newStatus := vpa.Status.DeepCopy()
137+
newStatus.Recommendation = recommendation
138+
bytes, err := json.Marshal([]PatchRecord{{
139+
Op: "replace",
140+
Path: "/status",
141+
Value: *newStatus,
142+
}})
143+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
144+
_, err = GetVpaClientSet(f).AutoscalingV1().VerticalPodAutoscalers(f.Namespace.Name).Patch(context.TODO(), vpa.Name, types.JSONPatchType, bytes, metav1.PatchOptions{}, "status")
145+
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to patch VPA.")
146+
}
147+
148+
// NewNHamstersDeployment creates a simple hamster deployment with n containers
149+
// for e2e test purposes.
150+
func NewNHamstersDeployment(f *framework.Framework, n int) *appsv1.Deployment {
151+
if n < 1 {
152+
panic("container count should be greater than 0")
153+
}
154+
d := framework_deployment.NewDeployment(
155+
"hamster-deployment", /*deploymentName*/
156+
DefaultHamsterReplicas, /*replicas*/
157+
HamsterLabels, /*podLabels*/
158+
GetHamsterContainerNameByIndex(0), /*imageName*/
159+
"registry.k8s.io/ubuntu-slim:0.14", /*image*/
160+
appsv1.RollingUpdateDeploymentStrategyType, /*strategyType*/
161+
)
162+
d.ObjectMeta.Namespace = f.Namespace.Name
163+
d.Spec.Template.Spec.Containers[0].Command = []string{"/bin/sh"}
164+
d.Spec.Template.Spec.Containers[0].Args = []string{"-c", "/usr/bin/yes >/dev/null"}
165+
for i := 1; i < n; i++ {
166+
d.Spec.Template.Spec.Containers = append(d.Spec.Template.Spec.Containers, d.Spec.Template.Spec.Containers[0])
167+
d.Spec.Template.Spec.Containers[i].Name = GetHamsterContainerNameByIndex(i)
168+
}
169+
return d
170+
}
171+
172+
// StartDeploymentPods start and wait for a deployment to complete
173+
func StartDeploymentPods(f *framework.Framework, deployment *appsv1.Deployment) *apiv1.PodList {
174+
// Apiserver watch can lag depending on cached object count and apiserver resource usage.
175+
// We assume that watch can lag up to 5 seconds.
176+
const apiserverWatchLag = 5 * time.Second
177+
// In admission controller e2e tests a recommendation is created before deployment.
178+
// Creating deployment with size greater than 0 would create a race between information
179+
// about pods and information about deployment getting to the admission controller.
180+
// Any pods that get processed by AC before it receives information about the deployment
181+
// don't receive recommendation.
182+
// To avoid this create deployment with size 0, then scale it up to the desired size.
183+
desiredPodCount := *deployment.Spec.Replicas
184+
zero := int32(0)
185+
deployment.Spec.Replicas = &zero
186+
c, ns := f.ClientSet, f.Namespace.Name
187+
deployment, err := c.AppsV1().Deployments(ns).Create(context.TODO(), deployment, metav1.CreateOptions{})
188+
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "when creating deployment with size 0")
189+
190+
err = framework_deployment.WaitForDeploymentComplete(c, deployment)
191+
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "when waiting for empty deployment to create")
192+
// If admission controller receives pod before controller it will not apply recommendation and test will fail.
193+
// Wait after creating deployment to ensure VPA knows about it, then scale up.
194+
// Normally watch lag is not a problem in terms of correctness:
195+
// - Mode "Auto": created pod without assigned resources will be handled by the eviction loop.
196+
// - Mode "Initial": calculating recommendations takes more than potential ectd lag.
197+
// - Mode "Off": pods are not handled by the admission controller.
198+
// In e2e admission controller tests we want to focus on scenarios without considering watch lag.
199+
// TODO(#2631): Remove sleep when issue is fixed.
200+
time.Sleep(apiserverWatchLag)
201+
202+
scale := autoscaling.Scale{
203+
ObjectMeta: metav1.ObjectMeta{
204+
Name: deployment.ObjectMeta.Name,
205+
Namespace: deployment.ObjectMeta.Namespace,
206+
},
207+
Spec: autoscaling.ScaleSpec{
208+
Replicas: desiredPodCount,
209+
},
210+
}
211+
afterScale, err := c.AppsV1().Deployments(ns).UpdateScale(context.TODO(), deployment.Name, &scale, metav1.UpdateOptions{})
212+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
213+
gomega.Expect(afterScale.Spec.Replicas).To(gomega.Equal(desiredPodCount), fmt.Sprintf("expected %d replicas after scaling", desiredPodCount))
214+
215+
// After scaling deployment we need to retrieve current version with updated replicas count.
216+
deployment, err = c.AppsV1().Deployments(ns).Get(context.TODO(), deployment.Name, metav1.GetOptions{})
217+
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "when getting scaled deployment")
218+
err = framework_deployment.WaitForDeploymentComplete(c, deployment)
219+
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "when waiting for deployment to resize")
220+
221+
podList, err := framework_deployment.GetPodsForDeployment(context.TODO(), c, deployment)
222+
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "when listing pods after deployment resize")
223+
return podList
224+
}
225+
226+
// WaitForRecommendationPresent pools VPA object until recommendations are not empty. Returns
227+
// polled vpa object. On timeout returns error.
228+
func WaitForRecommendationPresent(c vpa_clientset.Interface, vpa *vpa_types.VerticalPodAutoscaler) (*vpa_types.VerticalPodAutoscaler, error) {
229+
return WaitForVPAMatch(c, vpa, func(vpa *vpa_types.VerticalPodAutoscaler) bool {
230+
return vpa.Status.Recommendation != nil && len(vpa.Status.Recommendation.ContainerRecommendations) != 0
231+
})
232+
}
233+
234+
// WaitForVPAMatch pools VPA object until match function returns true. Returns
235+
// polled vpa object. On timeout returns error.
236+
func WaitForVPAMatch(c vpa_clientset.Interface, vpa *vpa_types.VerticalPodAutoscaler, match func(vpa *vpa_types.VerticalPodAutoscaler) bool) (*vpa_types.VerticalPodAutoscaler, error) {
237+
var polledVpa *vpa_types.VerticalPodAutoscaler
238+
err := wait.PollUntilContextTimeout(context.Background(), PollInterval, PollTimeout, true, func(ctx context.Context) (done bool, err error) {
239+
polledVpa, err = c.AutoscalingV1().VerticalPodAutoscalers(vpa.Namespace).Get(context.TODO(), vpa.Name, metav1.GetOptions{})
240+
if err != nil {
241+
return false, err
242+
}
243+
244+
if match(polledVpa) {
245+
return true, nil
246+
}
247+
248+
return false, nil
249+
})
250+
251+
if err != nil {
252+
return nil, fmt.Errorf("error waiting for recommendation present in %v: %v", vpa.Name, err)
253+
}
254+
return polledVpa, nil
255+
}

0 commit comments

Comments
 (0)