Remove e2e tests for v1beta1
This commit is contained in:
parent
a258103f8e
commit
9420e790a0
|
|
@ -1,173 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 The Kubernetes Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package autoscaling
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
|
||||||
apiv1 "k8s.io/api/core/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/api/resource"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
|
||||||
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1beta1"
|
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
|
||||||
"k8s.io/kubernetes/test/e2e/framework"
|
|
||||||
|
|
||||||
"github.com/onsi/ginkgo"
|
|
||||||
"github.com/onsi/gomega"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ = ActuationSuiteE2eDescribe("Actuation", func() {
|
|
||||||
f := framework.NewDefaultFramework("vertical-pod-autoscaling")
|
|
||||||
|
|
||||||
ginkgo.It("stops when pods get pending", func() {
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
d := SetupHamsterDeployment(f, "100m", "100Mi", defaultHamsterReplicas)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD with ridiculous request")
|
|
||||||
SetupVPA(f, "9999", vpa_types.UpdateModeAuto) // Request 9999 CPUs to make POD pending
|
|
||||||
|
|
||||||
ginkgo.By("Waiting for pods to be restarted and stuck pending")
|
|
||||||
err := assertPodsPendingForDuration(f.ClientSet, d, 1, 2*time.Minute)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("never applies recommendations when update mode is Off", func() {
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
d := SetupHamsterDeployment(f, "100m", "100Mi", defaultHamsterReplicas)
|
|
||||||
cpuRequest := getCPURequest(d.Spec.Template.Spec)
|
|
||||||
podList, err := GetHamsterPods(f)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
podSet := MakePodSet(podList)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD in mode Off")
|
|
||||||
SetupVPA(f, "200m", vpa_types.UpdateModeOff)
|
|
||||||
|
|
||||||
ginkgo.By(fmt.Sprintf("Waiting for pods to be evicted, hoping it won't happen, sleep for %s", VpaEvictionTimeout.String()))
|
|
||||||
CheckNoPodsEvicted(f, podSet)
|
|
||||||
ginkgo.By("Forcefully killing one pod")
|
|
||||||
killPod(f, podList)
|
|
||||||
|
|
||||||
ginkgo.By("Checking the requests were not modified")
|
|
||||||
updatedPodList, err := GetHamsterPods(f)
|
|
||||||
for _, pod := range updatedPodList.Items {
|
|
||||||
gomega.Expect(getCPURequest(pod.Spec)).To(gomega.Equal(cpuRequest))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("applies recommendations only on restart when update mode is Initial", func() {
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
SetupHamsterDeployment(f, "100m", "100Mi", defaultHamsterReplicas)
|
|
||||||
podList, err := GetHamsterPods(f)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
podSet := MakePodSet(podList)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD in mode Initial")
|
|
||||||
SetupVPA(f, "200m", vpa_types.UpdateModeInitial)
|
|
||||||
updatedCPURequest := ParseQuantityOrDie("200m")
|
|
||||||
|
|
||||||
ginkgo.By(fmt.Sprintf("Waiting for pods to be evicted, hoping it won't happen, sleep for %s", VpaEvictionTimeout.String()))
|
|
||||||
CheckNoPodsEvicted(f, podSet)
|
|
||||||
ginkgo.By("Forcefully killing one pod")
|
|
||||||
killPod(f, podList)
|
|
||||||
|
|
||||||
ginkgo.By("Checking that request was modified after forceful restart")
|
|
||||||
updatedPodList, err := GetHamsterPods(f)
|
|
||||||
foundUpdated := 0
|
|
||||||
for _, pod := range updatedPodList.Items {
|
|
||||||
podRequest := getCPURequest(pod.Spec)
|
|
||||||
framework.Logf("podReq: %v", podRequest)
|
|
||||||
if podRequest.Cmp(updatedCPURequest) == 0 {
|
|
||||||
foundUpdated += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gomega.Expect(foundUpdated).To(gomega.Equal(1))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
func getCPURequest(podSpec apiv1.PodSpec) resource.Quantity {
|
|
||||||
return podSpec.Containers[0].Resources.Requests[apiv1.ResourceCPU]
|
|
||||||
}
|
|
||||||
|
|
||||||
func killPod(f *framework.Framework, podList *apiv1.PodList) {
|
|
||||||
f.ClientSet.CoreV1().Pods(f.Namespace.Name).Delete(podList.Items[0].Name, &metav1.DeleteOptions{})
|
|
||||||
err := WaitForPodsRestarted(f, podList)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
}
|
|
||||||
|
|
||||||
// assertPodsPendingForDuration checks that at most pendingPodsNum pods are pending for pendingDuration
|
|
||||||
func assertPodsPendingForDuration(c clientset.Interface, deployment *appsv1.Deployment, pendingPodsNum int, pendingDuration time.Duration) error {
|
|
||||||
|
|
||||||
pendingPods := make(map[string]time.Time)
|
|
||||||
|
|
||||||
err := wait.PollImmediate(pollInterval, pollTimeout+pendingDuration, func() (bool, error) {
|
|
||||||
var err error
|
|
||||||
currentPodList, err := framework.GetPodsForDeployment(c, deployment)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
missingPods := make(map[string]bool)
|
|
||||||
for podName := range pendingPods {
|
|
||||||
missingPods[podName] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
for _, pod := range currentPodList.Items {
|
|
||||||
delete(missingPods, pod.Name)
|
|
||||||
switch pod.Status.Phase {
|
|
||||||
case apiv1.PodPending:
|
|
||||||
_, ok := pendingPods[pod.Name]
|
|
||||||
if !ok {
|
|
||||||
pendingPods[pod.Name] = now
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
delete(pendingPods, pod.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for missingPod := range missingPods {
|
|
||||||
delete(pendingPods, missingPod)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pendingPods) < pendingPodsNum {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pendingPods) > pendingPodsNum {
|
|
||||||
return false, fmt.Errorf("%v pending pods seen - expecting %v", len(pendingPods), pendingPodsNum)
|
|
||||||
}
|
|
||||||
|
|
||||||
for p, t := range pendingPods {
|
|
||||||
fmt.Println("task", now, p, t, now.Sub(t), pendingDuration)
|
|
||||||
if now.Sub(t) < pendingDuration {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("assertion failed for pending pods in %v: %v", deployment.Name, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,531 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 The Kubernetes Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package autoscaling
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
|
||||||
apiv1 "k8s.io/api/core/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1beta1"
|
|
||||||
"k8s.io/kubernetes/test/e2e/framework"
|
|
||||||
|
|
||||||
"github.com/onsi/ginkgo"
|
|
||||||
"github.com/onsi/gomega"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ = AdmissionControllerE2eDescribe("Admission-controller", func() {
|
|
||||||
f := framework.NewDefaultFramework("vertical-pod-autoscaling")
|
|
||||||
|
|
||||||
ginkgo.It("starts pods with new recommended request", func() {
|
|
||||||
d := NewHamsterDeploymentWithResources(f, ParseQuantityOrDie("100m") /*cpu*/, ParseQuantityOrDie("100Mi") /*memory*/)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
vpaCRD := NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: d.Spec.Template.Labels,
|
|
||||||
})
|
|
||||||
vpaCRD.Status.Recommendation = &vpa_types.RecommendedPodResources{
|
|
||||||
ContainerRecommendations: []vpa_types.RecommendedContainerResources{{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
Target: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("250m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("200Mi"),
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
InstallVPA(f, vpaCRD)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
// Originally Pods had 100m CPU, 100Mi of memory, but admission controller
|
|
||||||
// should change it to recommended 250m CPU and 200Mi of memory.
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Requests[apiv1.ResourceCPU]).To(gomega.Equal(ParseQuantityOrDie("250m")))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Requests[apiv1.ResourceMemory]).To(gomega.Equal(ParseQuantityOrDie("200Mi")))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("doesn't block patches", func() {
|
|
||||||
d := NewHamsterDeploymentWithResources(f, ParseQuantityOrDie("100m") /*cpu*/, ParseQuantityOrDie("100Mi") /*memory*/)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
vpaCRD := NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: d.Spec.Template.Labels,
|
|
||||||
})
|
|
||||||
vpaCRD.Status.Recommendation = &vpa_types.RecommendedPodResources{
|
|
||||||
ContainerRecommendations: []vpa_types.RecommendedContainerResources{{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
Target: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("250m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("200Mi"),
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
InstallVPA(f, vpaCRD)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
// Originally Pods had 100m CPU, 100Mi of memory, but admission controller
|
|
||||||
// should change it to recommended 250m CPU and 200Mi of memory.
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Requests[apiv1.ResourceCPU]).To(gomega.Equal(ParseQuantityOrDie("250m")))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Requests[apiv1.ResourceMemory]).To(gomega.Equal(ParseQuantityOrDie("200Mi")))
|
|
||||||
}
|
|
||||||
|
|
||||||
ginkgo.By("Modifying recommendation.")
|
|
||||||
PatchVpaRecommendation(f, vpaCRD, &vpa_types.RecommendedPodResources{
|
|
||||||
ContainerRecommendations: []vpa_types.RecommendedContainerResources{{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
Target: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("100m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("100Mi"),
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
})
|
|
||||||
|
|
||||||
podName := podList.Items[0].Name
|
|
||||||
ginkgo.By(fmt.Sprintf("Modifying pod %v.", podName))
|
|
||||||
AnnotatePod(f, podName, "someAnnotation", "someValue")
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("keeps limits equal to request", func() {
|
|
||||||
d := NewHamsterDeploymentWithGuaranteedResources(f, ParseQuantityOrDie("100m") /*cpu*/, ParseQuantityOrDie("100Mi") /*memory*/)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
vpaCRD := NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: d.Spec.Template.Labels,
|
|
||||||
})
|
|
||||||
vpaCRD.Status.Recommendation = &vpa_types.RecommendedPodResources{
|
|
||||||
ContainerRecommendations: []vpa_types.RecommendedContainerResources{{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
Target: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("250m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("200Mi"),
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
InstallVPA(f, vpaCRD)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
// Originally Pods had 100m CPU, 100Mi of memory, but admission controller
|
|
||||||
// should change it to 250m CPU and 200Mi of memory. Limits and requests should stay equal.
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Requests[apiv1.ResourceCPU]).To(gomega.Equal(ParseQuantityOrDie("250m")))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Requests[apiv1.ResourceMemory]).To(gomega.Equal(ParseQuantityOrDie("200Mi")))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Limits[apiv1.ResourceCPU]).To(gomega.Equal(ParseQuantityOrDie("250m")))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Limits[apiv1.ResourceMemory]).To(gomega.Equal(ParseQuantityOrDie("200Mi")))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("keeps limits to request ratio constant", func() {
|
|
||||||
d := NewHamsterDeploymentWithResourcesAndLimits(f,
|
|
||||||
ParseQuantityOrDie("100m") /*cpu request*/, ParseQuantityOrDie("100Mi"), /*memory request*/
|
|
||||||
ParseQuantityOrDie("150m") /*cpu limit*/, ParseQuantityOrDie("200Mi") /*memory limit*/)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
vpaCRD := NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: d.Spec.Template.Labels,
|
|
||||||
})
|
|
||||||
vpaCRD.Status.Recommendation = &vpa_types.RecommendedPodResources{
|
|
||||||
ContainerRecommendations: []vpa_types.RecommendedContainerResources{{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
Target: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("250m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("200Mi"),
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
InstallVPA(f, vpaCRD)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
// Originally Pods had 100m CPU, 100Mi of memory, but admission controller
|
|
||||||
// should change it to 250m CPU and 200Mi of memory. Limits to request ratio should stay unchanged.
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
gomega.Expect(*pod.Spec.Containers[0].Resources.Requests.Cpu()).To(gomega.Equal(ParseQuantityOrDie("250m")))
|
|
||||||
gomega.Expect(*pod.Spec.Containers[0].Resources.Requests.Memory()).To(gomega.Equal(ParseQuantityOrDie("200Mi")))
|
|
||||||
gomega.Expect(float64(pod.Spec.Containers[0].Resources.Limits.Cpu().MilliValue()) / float64(pod.Spec.Containers[0].Resources.Requests.Cpu().MilliValue())).To(gomega.BeNumerically("~", 1.5))
|
|
||||||
gomega.Expect(float64(pod.Spec.Containers[0].Resources.Limits.Memory().Value()) / float64(pod.Spec.Containers[0].Resources.Requests.Memory().Value())).To(gomega.BeNumerically("~", 2.))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("caps request according to container max limit set in LimitRange", func() {
|
|
||||||
d := NewHamsterDeploymentWithResourcesAndLimits(f,
|
|
||||||
ParseQuantityOrDie("100m") /*cpu request*/, ParseQuantityOrDie("100Mi"), /*memory request*/
|
|
||||||
ParseQuantityOrDie("150m") /*cpu limit*/, ParseQuantityOrDie("200Mi") /*memory limit*/)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
vpaCRD := NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: d.Spec.Template.Labels,
|
|
||||||
})
|
|
||||||
vpaCRD.Status.Recommendation = &vpa_types.RecommendedPodResources{
|
|
||||||
ContainerRecommendations: []vpa_types.RecommendedContainerResources{{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
Target: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("250m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("200Mi"),
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
InstallVPA(f, vpaCRD)
|
|
||||||
|
|
||||||
// Max CPU limit is 300m and ratio is 1.5, so max request is 200m, while
|
|
||||||
// recommendation is 250m
|
|
||||||
// Max memory limit is 1Gi and ratio is 2., so max request is 0.5Gi
|
|
||||||
InstallLimitRangeWithMax(f, "300m", "1Gi", apiv1.LimitTypeContainer)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
// Originally Pods had 100m CPU, 100Mi of memory, but admission controller
|
|
||||||
// should change it to 200m CPU (as this is the recommendation
|
|
||||||
// capped according to max limit in LimitRange) and 200Mi of memory,
|
|
||||||
// which is uncapped. Limit to request ratio should stay unchanged.
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
gomega.Expect(*pod.Spec.Containers[0].Resources.Requests.Cpu()).To(gomega.Equal(ParseQuantityOrDie("200m")))
|
|
||||||
gomega.Expect(*pod.Spec.Containers[0].Resources.Requests.Memory()).To(gomega.Equal(ParseQuantityOrDie("200Mi")))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Limits.Cpu().MilliValue()).To(gomega.BeNumerically("<=", 300))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Limits.Memory().Value()).To(gomega.BeNumerically("<=", 1024*1024*1024))
|
|
||||||
gomega.Expect(float64(pod.Spec.Containers[0].Resources.Limits.Cpu().MilliValue()) / float64(pod.Spec.Containers[0].Resources.Requests.Cpu().MilliValue())).To(gomega.BeNumerically("~", 1.5))
|
|
||||||
gomega.Expect(float64(pod.Spec.Containers[0].Resources.Limits.Memory().Value()) / float64(pod.Spec.Containers[0].Resources.Requests.Memory().Value())).To(gomega.BeNumerically("~", 2.))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("raises request according to container min limit set in LimitRange", func() {
|
|
||||||
d := NewHamsterDeploymentWithResourcesAndLimits(f,
|
|
||||||
ParseQuantityOrDie("100m") /*cpu request*/, ParseQuantityOrDie("200Mi"), /*memory request*/
|
|
||||||
ParseQuantityOrDie("150m") /*cpu limit*/, ParseQuantityOrDie("400Mi") /*memory limit*/)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
vpaCRD := NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: d.Spec.Template.Labels,
|
|
||||||
})
|
|
||||||
vpaCRD.Status.Recommendation = &vpa_types.RecommendedPodResources{
|
|
||||||
ContainerRecommendations: []vpa_types.RecommendedContainerResources{{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
Target: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("250m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("100Mi"), // memory is downscaled
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
InstallVPA(f, vpaCRD)
|
|
||||||
|
|
||||||
// Min CPU from limit range is 50m and ratio is 1.5. Min applies to both limit and request so min
|
|
||||||
// request is 50m and min limit is 75
|
|
||||||
// Min memory limit is 250Mi and it applies to both limit and request. Recommendation is 100Mi.
|
|
||||||
// It should be scaled up to 250Mi.
|
|
||||||
InstallLimitRangeWithMin(f, "50m", "250Mi", apiv1.LimitTypeContainer)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
// Originally Pods had 100m CPU, 200Mi of memory, but admission controller
|
|
||||||
// should change it to 250m CPU and 125Mi of memory, since this is the lowest
|
|
||||||
// request that limitrange allows.
|
|
||||||
// Limit to request ratio should stay unchanged.
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
gomega.Expect(*pod.Spec.Containers[0].Resources.Requests.Cpu()).To(gomega.Equal(ParseQuantityOrDie("250m")))
|
|
||||||
gomega.Expect(*pod.Spec.Containers[0].Resources.Requests.Memory()).To(gomega.Equal(ParseQuantityOrDie("250Mi")))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Limits.Cpu().MilliValue()).To(gomega.BeNumerically(">=", 75))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Limits.Memory().Value()).To(gomega.BeNumerically(">=", 250*1024*1024))
|
|
||||||
gomega.Expect(float64(pod.Spec.Containers[0].Resources.Limits.Cpu().MilliValue()) / float64(pod.Spec.Containers[0].Resources.Requests.Cpu().MilliValue())).To(gomega.BeNumerically("~", 1.5))
|
|
||||||
gomega.Expect(float64(pod.Spec.Containers[0].Resources.Limits.Memory().Value()) / float64(pod.Spec.Containers[0].Resources.Requests.Memory().Value())).To(gomega.BeNumerically("~", 2.))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("caps request according to pod max limit set in LimitRange", func() {
|
|
||||||
d := NewHamsterDeploymentWithResourcesAndLimits(f,
|
|
||||||
ParseQuantityOrDie("100m") /*cpu request*/, ParseQuantityOrDie("100Mi"), /*memory request*/
|
|
||||||
ParseQuantityOrDie("150m") /*cpu limit*/, ParseQuantityOrDie("200Mi") /*memory limit*/)
|
|
||||||
d.Spec.Template.Spec.Containers = append(d.Spec.Template.Spec.Containers, d.Spec.Template.Spec.Containers[0])
|
|
||||||
d.Spec.Template.Spec.Containers[1].Name = "hamster2"
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
vpaCRD := NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: d.Spec.Template.Labels,
|
|
||||||
})
|
|
||||||
vpaCRD.Status.Recommendation = &vpa_types.RecommendedPodResources{
|
|
||||||
ContainerRecommendations: []vpa_types.RecommendedContainerResources{
|
|
||||||
{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
Target: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("250m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("200Mi"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ContainerName: "hamster2",
|
|
||||||
Target: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("250m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("200Mi"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
InstallVPA(f, vpaCRD)
|
|
||||||
|
|
||||||
// Max CPU limit is 600m for pod, 300 per container and ratio is 1.5, so max request is 200m,
|
|
||||||
// while recommendation is 250m
|
|
||||||
// Max memory limit is 1Gi and ratio is 2., so max request is 0.5Gi
|
|
||||||
InstallLimitRangeWithMax(f, "600m", "1Gi", apiv1.LimitTypePod)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
// Originally Pods had 100m CPU, 100Mi of memory, but admission controller
|
|
||||||
// should change it to 200m CPU (as this is the recommendation
|
|
||||||
// capped according to max limit in LimitRange) and 200Mi of memory,
|
|
||||||
// which is uncapped. Limit to request ratio should stay unchanged.
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
gomega.Expect(*pod.Spec.Containers[0].Resources.Requests.Cpu()).To(gomega.Equal(ParseQuantityOrDie("200m")))
|
|
||||||
gomega.Expect(*pod.Spec.Containers[0].Resources.Requests.Memory()).To(gomega.Equal(ParseQuantityOrDie("200Mi")))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Limits.Cpu().MilliValue()).To(gomega.BeNumerically("<=", 300))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Limits.Memory().Value()).To(gomega.BeNumerically("<=", 1024*1024*1024))
|
|
||||||
gomega.Expect(float64(pod.Spec.Containers[0].Resources.Limits.Cpu().MilliValue()) / float64(pod.Spec.Containers[0].Resources.Requests.Cpu().MilliValue())).To(gomega.BeNumerically("~", 1.5))
|
|
||||||
gomega.Expect(float64(pod.Spec.Containers[0].Resources.Limits.Memory().Value()) / float64(pod.Spec.Containers[0].Resources.Requests.Memory().Value())).To(gomega.BeNumerically("~", 2.))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("raises request according to pod min limit set in LimitRange", func() {
|
|
||||||
d := NewHamsterDeploymentWithResourcesAndLimits(f,
|
|
||||||
ParseQuantityOrDie("100m") /*cpu request*/, ParseQuantityOrDie("200Mi"), /*memory request*/
|
|
||||||
ParseQuantityOrDie("150m") /*cpu limit*/, ParseQuantityOrDie("400Mi") /*memory limit*/)
|
|
||||||
d.Spec.Template.Spec.Containers = append(d.Spec.Template.Spec.Containers, d.Spec.Template.Spec.Containers[0])
|
|
||||||
d.Spec.Template.Spec.Containers[1].Name = "hamster2"
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
vpaCRD := NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: d.Spec.Template.Labels,
|
|
||||||
})
|
|
||||||
vpaCRD.Status.Recommendation = &vpa_types.RecommendedPodResources{
|
|
||||||
ContainerRecommendations: []vpa_types.RecommendedContainerResources{
|
|
||||||
{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
Target: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("120m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("100Mi"), // memory is downscaled
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ContainerName: "hamster2",
|
|
||||||
Target: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("120m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("100Mi"), // memory is downscaled
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
InstallVPA(f, vpaCRD)
|
|
||||||
|
|
||||||
// Min CPU from limit range is 100m, 50m per pod and ratio is 1.5. Min applies to both limit and
|
|
||||||
// request so min request is 50m and min limit is 75
|
|
||||||
// Min memory limit is 500Mi per pod, 250 per container and it applies to both limit and request.
|
|
||||||
// Recommendation is 100Mi it should be scaled up to 250Mi.
|
|
||||||
InstallLimitRangeWithMin(f, "100m", "500Mi", apiv1.LimitTypePod)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
// Originally Pods had 100m CPU, 200Mi of memory, but admission controller
|
|
||||||
// should change it to 250m CPU and 125Mi of memory, since this is the lowest
|
|
||||||
// request that limitrange allows.
|
|
||||||
// Limit to request ratio should stay unchanged.
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
gomega.Expect(*pod.Spec.Containers[0].Resources.Requests.Cpu()).To(gomega.Equal(ParseQuantityOrDie("120m")))
|
|
||||||
gomega.Expect(*pod.Spec.Containers[0].Resources.Requests.Memory()).To(gomega.Equal(ParseQuantityOrDie("250Mi")))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Limits.Cpu().MilliValue()).To(gomega.BeNumerically(">=", 75))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Limits.Memory().Value()).To(gomega.BeNumerically(">=", 250*1024*1024))
|
|
||||||
gomega.Expect(float64(pod.Spec.Containers[0].Resources.Limits.Cpu().MilliValue()) / float64(pod.Spec.Containers[0].Resources.Requests.Cpu().MilliValue())).To(gomega.BeNumerically("~", 1.5))
|
|
||||||
gomega.Expect(float64(pod.Spec.Containers[0].Resources.Limits.Memory().Value()) / float64(pod.Spec.Containers[0].Resources.Requests.Memory().Value())).To(gomega.BeNumerically("~", 2.))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("caps request to max set in VPA", func() {
|
|
||||||
d := NewHamsterDeploymentWithResources(f, ParseQuantityOrDie("100m") /*cpu*/, ParseQuantityOrDie("100Mi") /*memory*/)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
vpaCRD := NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: d.Spec.Template.Labels,
|
|
||||||
})
|
|
||||||
vpaCRD.Status.Recommendation = &vpa_types.RecommendedPodResources{
|
|
||||||
ContainerRecommendations: []vpa_types.RecommendedContainerResources{{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
Target: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("250m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("200Mi"),
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
vpaCRD.Spec.ResourcePolicy = &vpa_types.PodResourcePolicy{
|
|
||||||
ContainerPolicies: []vpa_types.ContainerResourcePolicy{{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
MaxAllowed: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("233m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("150Mi"),
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
InstallVPA(f, vpaCRD)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
// Originally Pods had 100m CPU, 100Mi of memory, but admission controller
|
|
||||||
// should change it to 233m CPU and 150Mi of memory (as this is the recommendation
|
|
||||||
// capped to max specified in VPA)
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Requests[apiv1.ResourceCPU]).To(gomega.Equal(ParseQuantityOrDie("233m")))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Requests[apiv1.ResourceMemory]).To(gomega.Equal(ParseQuantityOrDie("150Mi")))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("raises request to min set in VPA", func() {
|
|
||||||
d := NewHamsterDeploymentWithResources(f, ParseQuantityOrDie("100m") /*cpu*/, ParseQuantityOrDie("100Mi") /*memory*/)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
vpaCRD := NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: d.Spec.Template.Labels,
|
|
||||||
})
|
|
||||||
vpaCRD.Status.Recommendation = &vpa_types.RecommendedPodResources{
|
|
||||||
ContainerRecommendations: []vpa_types.RecommendedContainerResources{{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
Target: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("50m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("60Mi"),
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
vpaCRD.Spec.ResourcePolicy = &vpa_types.PodResourcePolicy{
|
|
||||||
ContainerPolicies: []vpa_types.ContainerResourcePolicy{{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
MinAllowed: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: ParseQuantityOrDie("90m"),
|
|
||||||
apiv1.ResourceMemory: ParseQuantityOrDie("80Mi"),
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
InstallVPA(f, vpaCRD)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
// Originally Pods had 100m CPU, 100Mi of memory, but admission controller
|
|
||||||
// should change it to recommended 90m CPU and 800Mi of memory (as this the
|
|
||||||
// recommendation raised to min specified in VPA)
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Requests[apiv1.ResourceCPU]).To(gomega.Equal(ParseQuantityOrDie("90m")))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Requests[apiv1.ResourceMemory]).To(gomega.Equal(ParseQuantityOrDie("80Mi")))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("leaves users request when no recommendation", func() {
|
|
||||||
d := NewHamsterDeploymentWithResources(f, ParseQuantityOrDie("100m") /*cpu*/, ParseQuantityOrDie("100Mi") /*memory*/)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
vpaCRD := NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: d.Spec.Template.Labels,
|
|
||||||
})
|
|
||||||
InstallVPA(f, vpaCRD)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
// VPA has no recommendation, so user's request is passed through
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Requests[apiv1.ResourceCPU]).To(gomega.Equal(ParseQuantityOrDie("100m")))
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Requests[apiv1.ResourceMemory]).To(gomega.Equal(ParseQuantityOrDie("100Mi")))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("passes empty request when no recommendation and no user-specified request", func() {
|
|
||||||
d := NewHamsterDeployment(f)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
vpaCRD := NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: d.Spec.Template.Labels,
|
|
||||||
})
|
|
||||||
InstallVPA(f, vpaCRD)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
// VPA has no recommendation, deployment has no request specified
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
gomega.Expect(pod.Spec.Containers[0].Resources.Requests).To(gomega.BeEmpty())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("accepts valid and rejects invalid VPA object", func() {
|
|
||||||
ginkgo.By("Setting up valid VPA object")
|
|
||||||
validVPA := []byte(`{
|
|
||||||
"kind": "VerticalPodAutoscaler",
|
|
||||||
"apiVersion": "autoscaling.k8s.io/v1beta1",
|
|
||||||
"metadata": {"name": "hamster-vpa-valid"},
|
|
||||||
"spec": {
|
|
||||||
"targetRef": {
|
|
||||||
"apiVersion": "apps/v1",
|
|
||||||
"kind": "Deployment",
|
|
||||||
"name":"hamster"
|
|
||||||
},
|
|
||||||
"resourcePolicy": {
|
|
||||||
"containerPolicies": [{"containerName": "*", "minAllowed":{"cpu":"50m"}}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
err := InstallRawVPA(f, validVPA)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Valid VPA object rejected")
|
|
||||||
|
|
||||||
ginkgo.By("Setting up invalid VPA object")
|
|
||||||
// The invalid object differs by name and minAllowed - there is an invalid "requests" field.
|
|
||||||
invalidVPA := []byte(`{
|
|
||||||
"kind": "VerticalPodAutoscaler",
|
|
||||||
"apiVersion": "autoscaling.k8s.io/v1beta1",
|
|
||||||
"metadata": {"name": "hamster-vpa-invalid"},
|
|
||||||
"spec": {
|
|
||||||
"targetRef": {
|
|
||||||
"apiVersion": "apps/v1",
|
|
||||||
"kind": "Deployment",
|
|
||||||
"name":"hamster"
|
|
||||||
},
|
|
||||||
"resourcePolicy": {
|
|
||||||
"containerPolicies": [{"containerName": "*", "minAllowed":{"requests":{"cpu":"50m"}}}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
err2 := InstallRawVPA(f, invalidVPA)
|
|
||||||
gomega.Expect(err2).To(gomega.HaveOccurred(), "Invalid VPA object accepted")
|
|
||||||
gomega.Expect(err2.Error()).To(gomega.MatchRegexp(`.*admission webhook .*vpa.* denied the request: .*`))
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
func startDeploymentPods(f *framework.Framework, deployment *appsv1.Deployment) *apiv1.PodList {
|
|
||||||
c, ns := f.ClientSet, f.Namespace.Name
|
|
||||||
deployment, err := c.AppsV1().Deployments(ns).Create(deployment)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
err = framework.WaitForDeploymentComplete(c, deployment)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
|
|
||||||
podList, err := framework.GetPodsForDeployment(c, deployment)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
return podList
|
|
||||||
}
|
|
||||||
|
|
@ -1,536 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 The Kubernetes Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// This is a fork of k8s.io/kubernetes/test/e2e/common/autoscaling_utils.go
|
|
||||||
|
|
||||||
package autoscaling
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
autoscalingv1 "k8s.io/api/autoscaling/v1"
|
|
||||||
"k8s.io/api/core/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/api/resource"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
"k8s.io/apimachinery/pkg/util/intstr"
|
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
|
||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
|
||||||
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
|
||||||
"k8s.io/kubernetes/test/e2e/framework"
|
|
||||||
testutils "k8s.io/kubernetes/test/utils"
|
|
||||||
|
|
||||||
ginkgo "github.com/onsi/ginkgo"
|
|
||||||
imageutils "k8s.io/kubernetes/test/utils/image"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
dynamicConsumptionTimeInSeconds = 30
|
|
||||||
staticConsumptionTimeInSeconds = 3600
|
|
||||||
dynamicRequestSizeInMillicores = 20
|
|
||||||
dynamicRequestSizeInMegabytes = 100
|
|
||||||
dynamicRequestSizeCustomMetric = 10
|
|
||||||
port = 80
|
|
||||||
targetPort = 8080
|
|
||||||
timeoutRC = 120 * time.Second
|
|
||||||
startServiceTimeout = time.Minute
|
|
||||||
startServiceInterval = 5 * time.Second
|
|
||||||
rcIsNil = "ERROR: replicationController = nil"
|
|
||||||
deploymentIsNil = "ERROR: deployment = nil"
|
|
||||||
rsIsNil = "ERROR: replicaset = nil"
|
|
||||||
invalidKind = "ERROR: invalid workload kind for resource consumer"
|
|
||||||
customMetricName = "QPS"
|
|
||||||
serviceInitializationTimeout = 2 * time.Minute
|
|
||||||
serviceInitializationInterval = 15 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
resourceConsumerImage = imageutils.GetE2EImage(imageutils.ResourceConsumer)
|
|
||||||
resourceConsumerControllerImage = imageutils.GetE2EImage(imageutils.ResourceController)
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// KindRC var
|
|
||||||
KindRC = schema.GroupVersionKind{Version: "v1", Kind: "ReplicationController"}
|
|
||||||
// KindDeployment var
|
|
||||||
KindDeployment = schema.GroupVersionKind{Group: "apps", Version: "v1beta2", Kind: "Deployment"}
|
|
||||||
// KindReplicaSet var
|
|
||||||
KindReplicaSet = schema.GroupVersionKind{Group: "apps", Version: "v1beta2", Kind: "ReplicaSet"}
|
|
||||||
subresource = "scale"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
ResourceConsumer is a tool for testing. It helps create specified usage of CPU or memory
|
|
||||||
typical use case:
|
|
||||||
rc.ConsumeCPU(600)
|
|
||||||
// ... check your assumption here
|
|
||||||
rc.ConsumeCPU(300)
|
|
||||||
// ... check your assumption here
|
|
||||||
*/
|
|
||||||
type ResourceConsumer struct {
|
|
||||||
name string
|
|
||||||
controllerName string
|
|
||||||
kind schema.GroupVersionKind
|
|
||||||
nsName string
|
|
||||||
clientSet clientset.Interface
|
|
||||||
internalClientset *internalclientset.Clientset
|
|
||||||
cpu chan int
|
|
||||||
mem chan int
|
|
||||||
customMetric chan int
|
|
||||||
stopCPU chan int
|
|
||||||
stopMem chan int
|
|
||||||
stopCustomMetric chan int
|
|
||||||
stopWaitGroup sync.WaitGroup
|
|
||||||
consumptionTimeInSeconds int
|
|
||||||
sleepTime time.Duration
|
|
||||||
requestSizeInMillicores int
|
|
||||||
requestSizeInMegabytes int
|
|
||||||
requestSizeCustomMetric int
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetResourceConsumerImage func
|
|
||||||
func GetResourceConsumerImage() string {
|
|
||||||
return resourceConsumerImage
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDynamicResourceConsumer func
|
|
||||||
func NewDynamicResourceConsumer(name, nsName string, kind schema.GroupVersionKind, replicas, initCPUTotal, initMemoryTotal, initCustomMetric int, cpuRequest, memRequest resource.Quantity, clientset clientset.Interface, internalClientset *internalclientset.Clientset) *ResourceConsumer {
|
|
||||||
return newResourceConsumer(name, nsName, kind, replicas, initCPUTotal, initMemoryTotal, initCustomMetric, dynamicConsumptionTimeInSeconds,
|
|
||||||
dynamicRequestSizeInMillicores, dynamicRequestSizeInMegabytes, dynamicRequestSizeCustomMetric, cpuRequest, memRequest, clientset, internalClientset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStaticResourceConsumer TODO this still defaults to replication controller
|
|
||||||
func NewStaticResourceConsumer(name, nsName string, replicas, initCPUTotal, initMemoryTotal, initCustomMetric int, cpuRequest, memRequest resource.Quantity, clientset clientset.Interface, internalClientset *internalclientset.Clientset) *ResourceConsumer {
|
|
||||||
return newResourceConsumer(name, nsName, KindRC, replicas, initCPUTotal, initMemoryTotal, initCustomMetric, staticConsumptionTimeInSeconds,
|
|
||||||
initCPUTotal/replicas, initMemoryTotal/replicas, initCustomMetric/replicas, cpuRequest, memRequest, clientset, internalClientset)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
NewResourceConsumer creates new ResourceConsumer
|
|
||||||
initCPUTotal argument is in millicores
|
|
||||||
initMemoryTotal argument is in megabytes
|
|
||||||
memRequest argument is in megabytes, it specifies the original Pod resource request
|
|
||||||
cpuRequest argument is in millicores, it specifies the original Pod resource request
|
|
||||||
*/
|
|
||||||
func newResourceConsumer(name, nsName string, kind schema.GroupVersionKind, replicas, initCPUTotal, initMemoryTotal, initCustomMetric, consumptionTimeInSeconds, requestSizeInMillicores,
|
|
||||||
requestSizeInMegabytes int, requestSizeCustomMetric int, cpuRequest, memRequest resource.Quantity, clientset clientset.Interface, internalClientset *internalclientset.Clientset) *ResourceConsumer {
|
|
||||||
|
|
||||||
runServiceAndWorkloadForResourceConsumer(clientset, internalClientset, nsName, name, kind, replicas, cpuRequest, memRequest)
|
|
||||||
rc := &ResourceConsumer{
|
|
||||||
name: name,
|
|
||||||
controllerName: name + "-ctrl",
|
|
||||||
kind: kind,
|
|
||||||
nsName: nsName,
|
|
||||||
clientSet: clientset,
|
|
||||||
internalClientset: internalClientset,
|
|
||||||
cpu: make(chan int),
|
|
||||||
mem: make(chan int),
|
|
||||||
customMetric: make(chan int),
|
|
||||||
stopCPU: make(chan int),
|
|
||||||
stopMem: make(chan int),
|
|
||||||
stopCustomMetric: make(chan int),
|
|
||||||
consumptionTimeInSeconds: consumptionTimeInSeconds,
|
|
||||||
sleepTime: time.Duration(consumptionTimeInSeconds) * time.Second,
|
|
||||||
requestSizeInMillicores: requestSizeInMillicores,
|
|
||||||
requestSizeInMegabytes: requestSizeInMegabytes,
|
|
||||||
requestSizeCustomMetric: requestSizeCustomMetric,
|
|
||||||
}
|
|
||||||
|
|
||||||
go rc.makeConsumeCPURequests()
|
|
||||||
rc.ConsumeCPU(initCPUTotal)
|
|
||||||
|
|
||||||
go rc.makeConsumeMemRequests()
|
|
||||||
rc.ConsumeMem(initMemoryTotal)
|
|
||||||
go rc.makeConsumeCustomMetric()
|
|
||||||
rc.ConsumeCustomMetric(initCustomMetric)
|
|
||||||
return rc
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConsumeCPU consumes given number of CPU
|
|
||||||
func (rc *ResourceConsumer) ConsumeCPU(millicores int) {
|
|
||||||
framework.Logf("RC %s: consume %v millicores in total", rc.name, millicores)
|
|
||||||
rc.cpu <- millicores
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConsumeMem consumes given number of Mem
|
|
||||||
func (rc *ResourceConsumer) ConsumeMem(megabytes int) {
|
|
||||||
framework.Logf("RC %s: consume %v MB in total", rc.name, megabytes)
|
|
||||||
rc.mem <- megabytes
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConsumeCustomMetric consumes given number of custom metric
|
|
||||||
func (rc *ResourceConsumer) ConsumeCustomMetric(amount int) {
|
|
||||||
framework.Logf("RC %s: consume custom metric %v in total", rc.name, amount)
|
|
||||||
rc.customMetric <- amount
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *ResourceConsumer) makeConsumeCPURequests() {
|
|
||||||
defer ginkgo.GinkgoRecover()
|
|
||||||
rc.stopWaitGroup.Add(1)
|
|
||||||
defer rc.stopWaitGroup.Done()
|
|
||||||
sleepTime := time.Duration(0)
|
|
||||||
millicores := 0
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case millicores = <-rc.cpu:
|
|
||||||
framework.Logf("RC %s: setting consumption to %v millicores in total", rc.name, millicores)
|
|
||||||
case <-time.After(sleepTime):
|
|
||||||
framework.Logf("RC %s: sending request to consume %d millicores", rc.name, millicores)
|
|
||||||
rc.sendConsumeCPURequest(millicores)
|
|
||||||
sleepTime = rc.sleepTime
|
|
||||||
case <-rc.stopCPU:
|
|
||||||
framework.Logf("RC %s: stopping CPU consumer", rc.name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *ResourceConsumer) makeConsumeMemRequests() {
|
|
||||||
defer ginkgo.GinkgoRecover()
|
|
||||||
rc.stopWaitGroup.Add(1)
|
|
||||||
defer rc.stopWaitGroup.Done()
|
|
||||||
sleepTime := time.Duration(0)
|
|
||||||
megabytes := 0
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case megabytes = <-rc.mem:
|
|
||||||
framework.Logf("RC %s: setting consumption to %v MB in total", rc.name, megabytes)
|
|
||||||
case <-time.After(sleepTime):
|
|
||||||
framework.Logf("RC %s: sending request to consume %d MB", rc.name, megabytes)
|
|
||||||
rc.sendConsumeMemRequest(megabytes)
|
|
||||||
sleepTime = rc.sleepTime
|
|
||||||
case <-rc.stopMem:
|
|
||||||
framework.Logf("RC %s: stopping mem consumer", rc.name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *ResourceConsumer) makeConsumeCustomMetric() {
|
|
||||||
defer ginkgo.GinkgoRecover()
|
|
||||||
rc.stopWaitGroup.Add(1)
|
|
||||||
defer rc.stopWaitGroup.Done()
|
|
||||||
sleepTime := time.Duration(0)
|
|
||||||
delta := 0
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case delta := <-rc.customMetric:
|
|
||||||
framework.Logf("RC %s: setting bump of metric %s to %d in total", rc.name, customMetricName, delta)
|
|
||||||
case <-time.After(sleepTime):
|
|
||||||
framework.Logf("RC %s: sending request to consume %d of custom metric %s", rc.name, delta, customMetricName)
|
|
||||||
rc.sendConsumeCustomMetric(delta)
|
|
||||||
sleepTime = rc.sleepTime
|
|
||||||
case <-rc.stopCustomMetric:
|
|
||||||
framework.Logf("RC %s: stopping metric consumer", rc.name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *ResourceConsumer) sendConsumeCPURequest(millicores int) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), framework.SingleCallTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err := wait.PollImmediate(serviceInitializationInterval, serviceInitializationTimeout, func() (bool, error) {
|
|
||||||
proxyRequest, err := framework.GetServicesProxyRequest(rc.clientSet, rc.clientSet.CoreV1().RESTClient().Post())
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
req := proxyRequest.Namespace(rc.nsName).
|
|
||||||
Context(ctx).
|
|
||||||
Name(rc.controllerName).
|
|
||||||
Suffix("ConsumeCPU").
|
|
||||||
Param("millicores", strconv.Itoa(millicores)).
|
|
||||||
Param("durationSec", strconv.Itoa(rc.consumptionTimeInSeconds)).
|
|
||||||
Param("requestSizeMillicores", strconv.Itoa(rc.requestSizeInMillicores))
|
|
||||||
framework.Logf("ConsumeCPU URL: %v", *req.URL())
|
|
||||||
_, err = req.DoRaw()
|
|
||||||
if err != nil {
|
|
||||||
framework.Logf("ConsumeCPU failure: %v", err)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendConsumeMemRequest sends POST request for memory consumption
|
|
||||||
func (rc *ResourceConsumer) sendConsumeMemRequest(megabytes int) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), framework.SingleCallTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err := wait.PollImmediate(serviceInitializationInterval, serviceInitializationTimeout, func() (bool, error) {
|
|
||||||
proxyRequest, err := framework.GetServicesProxyRequest(rc.clientSet, rc.clientSet.CoreV1().RESTClient().Post())
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
req := proxyRequest.Namespace(rc.nsName).
|
|
||||||
Context(ctx).
|
|
||||||
Name(rc.controllerName).
|
|
||||||
Suffix("ConsumeMem").
|
|
||||||
Param("megabytes", strconv.Itoa(megabytes)).
|
|
||||||
Param("durationSec", strconv.Itoa(rc.consumptionTimeInSeconds)).
|
|
||||||
Param("requestSizeMegabytes", strconv.Itoa(rc.requestSizeInMegabytes))
|
|
||||||
framework.Logf("ConsumeMem URL: %v", *req.URL())
|
|
||||||
_, err = req.DoRaw()
|
|
||||||
if err != nil {
|
|
||||||
framework.Logf("ConsumeMem failure: %v", err)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendConsumeCustomMetric sends POST request for custom metric consumption
|
|
||||||
func (rc *ResourceConsumer) sendConsumeCustomMetric(delta int) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), framework.SingleCallTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err := wait.PollImmediate(serviceInitializationInterval, serviceInitializationTimeout, func() (bool, error) {
|
|
||||||
proxyRequest, err := framework.GetServicesProxyRequest(rc.clientSet, rc.clientSet.CoreV1().RESTClient().Post())
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
req := proxyRequest.Namespace(rc.nsName).
|
|
||||||
Context(ctx).
|
|
||||||
Name(rc.controllerName).
|
|
||||||
Suffix("BumpMetric").
|
|
||||||
Param("metric", customMetricName).
|
|
||||||
Param("delta", strconv.Itoa(delta)).
|
|
||||||
Param("durationSec", strconv.Itoa(rc.consumptionTimeInSeconds)).
|
|
||||||
Param("requestSizeMetrics", strconv.Itoa(rc.requestSizeCustomMetric))
|
|
||||||
framework.Logf("ConsumeCustomMetric URL: %v", *req.URL())
|
|
||||||
_, err = req.DoRaw()
|
|
||||||
if err != nil {
|
|
||||||
framework.Logf("ConsumeCustomMetric failure: %v", err)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
})
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetReplicas func
|
|
||||||
func (rc *ResourceConsumer) GetReplicas() int {
|
|
||||||
switch rc.kind {
|
|
||||||
case KindRC:
|
|
||||||
replicationController, err := rc.clientSet.CoreV1().ReplicationControllers(rc.nsName).Get(rc.name, metav1.GetOptions{})
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
if replicationController == nil {
|
|
||||||
framework.Failf(rcIsNil)
|
|
||||||
}
|
|
||||||
return int(replicationController.Status.ReadyReplicas)
|
|
||||||
case KindDeployment:
|
|
||||||
deployment, err := rc.clientSet.ExtensionsV1beta1().Deployments(rc.nsName).Get(rc.name, metav1.GetOptions{})
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
if deployment == nil {
|
|
||||||
framework.Failf(deploymentIsNil)
|
|
||||||
}
|
|
||||||
return int(deployment.Status.ReadyReplicas)
|
|
||||||
case KindReplicaSet:
|
|
||||||
rs, err := rc.clientSet.ExtensionsV1beta1().ReplicaSets(rc.nsName).Get(rc.name, metav1.GetOptions{})
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
if rs == nil {
|
|
||||||
framework.Failf(rsIsNil)
|
|
||||||
}
|
|
||||||
return int(rs.Status.ReadyReplicas)
|
|
||||||
default:
|
|
||||||
framework.Failf(invalidKind)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitForReplicas func
|
|
||||||
func (rc *ResourceConsumer) WaitForReplicas(desiredReplicas int, duration time.Duration) {
|
|
||||||
interval := 20 * time.Second
|
|
||||||
err := wait.PollImmediate(interval, duration, func() (bool, error) {
|
|
||||||
replicas := rc.GetReplicas()
|
|
||||||
framework.Logf("waiting for %d replicas (current: %d)", desiredReplicas, replicas)
|
|
||||||
return replicas == desiredReplicas, nil // Expected number of replicas found. Exit.
|
|
||||||
})
|
|
||||||
framework.ExpectNoErrorWithOffset(1, err, "timeout waiting %v for %d replicas", duration, desiredReplicas)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnsureDesiredReplicas func
|
|
||||||
func (rc *ResourceConsumer) EnsureDesiredReplicas(desiredReplicas int, duration time.Duration) {
|
|
||||||
interval := 10 * time.Second
|
|
||||||
err := wait.PollImmediate(interval, duration, func() (bool, error) {
|
|
||||||
replicas := rc.GetReplicas()
|
|
||||||
framework.Logf("expecting there to be %d replicas (are: %d)", desiredReplicas, replicas)
|
|
||||||
if replicas != desiredReplicas {
|
|
||||||
return false, fmt.Errorf("number of replicas changed unexpectedly")
|
|
||||||
}
|
|
||||||
return false, nil // Expected number of replicas found. Continue polling until timeout.
|
|
||||||
})
|
|
||||||
// The call above always returns an error, but if it is timeout, it's OK (condition satisfied all the time).
|
|
||||||
if err == wait.ErrWaitTimeout {
|
|
||||||
framework.Logf("Number of replicas was stable over %v", duration)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
framework.ExpectNoErrorWithOffset(1, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pause stops background goroutines responsible for consuming resources.
|
|
||||||
func (rc *ResourceConsumer) Pause() {
|
|
||||||
ginkgo.By(fmt.Sprintf("HPA pausing RC %s", rc.name))
|
|
||||||
rc.stopCPU <- 0
|
|
||||||
rc.stopMem <- 0
|
|
||||||
rc.stopCustomMetric <- 0
|
|
||||||
rc.stopWaitGroup.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resume starts background goroutines responsible for consuming resources.
|
|
||||||
func (rc *ResourceConsumer) Resume() {
|
|
||||||
ginkgo.By(fmt.Sprintf("HPA resuming RC %s", rc.name))
|
|
||||||
go rc.makeConsumeCPURequests()
|
|
||||||
go rc.makeConsumeMemRequests()
|
|
||||||
go rc.makeConsumeCustomMetric()
|
|
||||||
}
|
|
||||||
|
|
||||||
// CleanUp func
|
|
||||||
func (rc *ResourceConsumer) CleanUp() {
|
|
||||||
ginkgo.By(fmt.Sprintf("Removing consuming RC %s", rc.name))
|
|
||||||
close(rc.stopCPU)
|
|
||||||
close(rc.stopMem)
|
|
||||||
close(rc.stopCustomMetric)
|
|
||||||
rc.stopWaitGroup.Wait()
|
|
||||||
// Wait some time to ensure all child goroutines are finished.
|
|
||||||
time.Sleep(10 * time.Second)
|
|
||||||
kind := rc.kind.GroupKind()
|
|
||||||
framework.ExpectNoError(framework.DeleteResourceAndWaitForGC(rc.clientSet, kind, rc.nsName, rc.name))
|
|
||||||
framework.ExpectNoError(rc.clientSet.CoreV1().Services(rc.nsName).Delete(rc.name, nil))
|
|
||||||
framework.ExpectNoError(framework.DeleteResourceAndWaitForGC(rc.clientSet, api.Kind("ReplicationController"), rc.nsName, rc.controllerName))
|
|
||||||
framework.ExpectNoError(rc.clientSet.CoreV1().Services(rc.nsName).Delete(rc.controllerName, nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func runServiceAndWorkloadForResourceConsumer(c clientset.Interface, internalClient internalclientset.Interface, ns, name string, kind schema.GroupVersionKind, replicas int, cpuRequest, memRequest resource.Quantity) {
|
|
||||||
ginkgo.By(fmt.Sprintf("Running consuming RC %s via %s with %v replicas", name, kind, replicas))
|
|
||||||
_, err := c.CoreV1().Services(ns).Create(&v1.Service{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: name,
|
|
||||||
},
|
|
||||||
Spec: v1.ServiceSpec{
|
|
||||||
Ports: []v1.ServicePort{{
|
|
||||||
Port: port,
|
|
||||||
TargetPort: intstr.FromInt(targetPort),
|
|
||||||
}},
|
|
||||||
|
|
||||||
Selector: map[string]string{
|
|
||||||
"name": name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
|
|
||||||
rcConfig := testutils.RCConfig{
|
|
||||||
Client: c,
|
|
||||||
InternalClient: internalClient,
|
|
||||||
Image: resourceConsumerImage,
|
|
||||||
Name: name,
|
|
||||||
Namespace: ns,
|
|
||||||
Timeout: timeoutRC,
|
|
||||||
Replicas: replicas,
|
|
||||||
CpuRequest: cpuRequest.MilliValue(),
|
|
||||||
MemRequest: memRequest.Value(),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch kind {
|
|
||||||
case KindRC:
|
|
||||||
framework.ExpectNoError(framework.RunRC(rcConfig))
|
|
||||||
break
|
|
||||||
case KindDeployment:
|
|
||||||
dpConfig := testutils.DeploymentConfig{
|
|
||||||
RCConfig: rcConfig,
|
|
||||||
}
|
|
||||||
framework.ExpectNoError(framework.RunDeployment(dpConfig))
|
|
||||||
break
|
|
||||||
case KindReplicaSet:
|
|
||||||
rsConfig := testutils.ReplicaSetConfig{
|
|
||||||
RCConfig: rcConfig,
|
|
||||||
}
|
|
||||||
ginkgo.By(fmt.Sprintf("creating replicaset %s in namespace %s", rsConfig.Name, rsConfig.Namespace))
|
|
||||||
framework.ExpectNoError(framework.RunReplicaSet(rsConfig))
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
framework.Failf(invalidKind)
|
|
||||||
}
|
|
||||||
|
|
||||||
ginkgo.By(fmt.Sprintf("Running controller"))
|
|
||||||
controllerName := name + "-ctrl"
|
|
||||||
_, err = c.CoreV1().Services(ns).Create(&v1.Service{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: controllerName,
|
|
||||||
},
|
|
||||||
Spec: v1.ServiceSpec{
|
|
||||||
Ports: []v1.ServicePort{{
|
|
||||||
Port: port,
|
|
||||||
TargetPort: intstr.FromInt(targetPort),
|
|
||||||
}},
|
|
||||||
|
|
||||||
Selector: map[string]string{
|
|
||||||
"name": controllerName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
|
|
||||||
dnsClusterFirst := v1.DNSClusterFirst
|
|
||||||
controllerRcConfig := testutils.RCConfig{
|
|
||||||
Client: c,
|
|
||||||
Image: resourceConsumerControllerImage,
|
|
||||||
Name: controllerName,
|
|
||||||
Namespace: ns,
|
|
||||||
Timeout: timeoutRC,
|
|
||||||
Replicas: 1,
|
|
||||||
Command: []string{"/controller", "--consumer-service-name=" + name, "--consumer-service-namespace=" + ns, "--consumer-port=80"},
|
|
||||||
DNSPolicy: &dnsClusterFirst,
|
|
||||||
}
|
|
||||||
framework.ExpectNoError(framework.RunRC(controllerRcConfig))
|
|
||||||
|
|
||||||
// Wait for endpoints to propagate for the controller service.
|
|
||||||
framework.ExpectNoError(framework.WaitForServiceEndpointsNum(
|
|
||||||
c, ns, controllerName, 1, startServiceInterval, startServiceTimeout))
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateCPUHorizontalPodAutoscaler func
|
|
||||||
func CreateCPUHorizontalPodAutoscaler(rc *ResourceConsumer, cpu, minReplicas, maxRepl int32) *autoscalingv1.HorizontalPodAutoscaler {
|
|
||||||
hpa := &autoscalingv1.HorizontalPodAutoscaler{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: rc.name,
|
|
||||||
Namespace: rc.nsName,
|
|
||||||
},
|
|
||||||
Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
|
|
||||||
ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
|
|
||||||
APIVersion: rc.kind.GroupVersion().String(),
|
|
||||||
Kind: rc.kind.Kind,
|
|
||||||
Name: rc.name,
|
|
||||||
},
|
|
||||||
MinReplicas: &minReplicas,
|
|
||||||
MaxReplicas: maxRepl,
|
|
||||||
TargetCPUUtilizationPercentage: &cpu,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
hpa, errHPA := rc.clientSet.AutoscalingV1().HorizontalPodAutoscalers(rc.nsName).Create(hpa)
|
|
||||||
framework.ExpectNoError(errHPA)
|
|
||||||
return hpa
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteHorizontalPodAutoscaler func
|
|
||||||
func DeleteHorizontalPodAutoscaler(rc *ResourceConsumer, autoscalerName string) {
|
|
||||||
rc.clientSet.AutoscalingV1().HorizontalPodAutoscalers(rc.nsName).Delete(autoscalerName, nil)
|
|
||||||
}
|
|
||||||
|
|
@ -1,489 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 The Kubernetes Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package autoscaling
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/onsi/ginkgo"
|
|
||||||
"github.com/onsi/gomega"
|
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
|
||||||
apiv1 "k8s.io/api/core/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/api/resource"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
|
||||||
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1beta1"
|
|
||||||
vpa_clientset "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/client/clientset/versioned"
|
|
||||||
"k8s.io/kubernetes/test/e2e/framework"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
recommenderComponent = "recommender"
|
|
||||||
updateComponent = "updater"
|
|
||||||
admissionControllerComponent = "admission-controller"
|
|
||||||
fullVpaSuite = "full-vpa"
|
|
||||||
actuationSuite = "actuation"
|
|
||||||
pollInterval = 10 * time.Second
|
|
||||||
pollTimeout = 15 * time.Minute
|
|
||||||
// VpaEvictionTimeout is a timeout for VPA to restart a pod if there are no
|
|
||||||
// mechanisms blocking it (for example PDB).
|
|
||||||
VpaEvictionTimeout = 3 * time.Minute
|
|
||||||
|
|
||||||
defaultHamsterReplicas = int32(3)
|
|
||||||
)
|
|
||||||
|
|
||||||
var hamsterLabels = map[string]string{"app": "hamster"}
|
|
||||||
|
|
||||||
// SIGDescribe adds sig-autoscaling tag to test description.
|
|
||||||
func SIGDescribe(text string, body func()) bool {
|
|
||||||
return ginkgo.Describe(fmt.Sprintf("[sig-autoscaling] %v", text), body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// E2eDescribe describes a VPA e2e test.
|
|
||||||
func E2eDescribe(scenario, name string, body func()) bool {
|
|
||||||
return SIGDescribe(fmt.Sprintf("[VPA] [%s] [v1beta1] %s", scenario, name), body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecommenderE2eDescribe describes a VPA recommender e2e test.
|
|
||||||
func RecommenderE2eDescribe(name string, body func()) bool {
|
|
||||||
return E2eDescribe(recommenderComponent, name, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdaterE2eDescribe describes a VPA updater e2e test.
|
|
||||||
func UpdaterE2eDescribe(name string, body func()) bool {
|
|
||||||
return E2eDescribe(updateComponent, name, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdmissionControllerE2eDescribe describes a VPA admission controller e2e test.
|
|
||||||
func AdmissionControllerE2eDescribe(name string, body func()) bool {
|
|
||||||
return E2eDescribe(admissionControllerComponent, name, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FullVpaE2eDescribe describes a VPA full stack e2e test.
|
|
||||||
func FullVpaE2eDescribe(name string, body func()) bool {
|
|
||||||
return E2eDescribe(fullVpaSuite, name, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActuationSuiteE2eDescribe describes a VPA actuation e2e test.
|
|
||||||
func ActuationSuiteE2eDescribe(name string, body func()) bool {
|
|
||||||
return E2eDescribe(actuationSuite, name, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetupHamsterDeployment creates and installs a simple hamster deployment
|
|
||||||
// for e2e test purposes, then makes sure the deployment is running.
|
|
||||||
func SetupHamsterDeployment(f *framework.Framework, cpu, memory string, replicas int32) *appsv1.Deployment {
|
|
||||||
cpuQuantity := ParseQuantityOrDie(cpu)
|
|
||||||
memoryQuantity := ParseQuantityOrDie(memory)
|
|
||||||
|
|
||||||
d := NewHamsterDeploymentWithResources(f, cpuQuantity, memoryQuantity)
|
|
||||||
d.Spec.Replicas = &replicas
|
|
||||||
d, err := f.ClientSet.AppsV1().Deployments(f.Namespace.Name).Create(d)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
err = framework.WaitForDeploymentComplete(f.ClientSet, d)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHamsterDeployment creates a simple hamster deployment for e2e test
|
|
||||||
// purposes.
|
|
||||||
func NewHamsterDeployment(f *framework.Framework) *appsv1.Deployment {
|
|
||||||
d := framework.NewDeployment("hamster-deployment", defaultHamsterReplicas, hamsterLabels, "hamster", "k8s.gcr.io/ubuntu-slim:0.1", appsv1.RollingUpdateDeploymentStrategyType)
|
|
||||||
d.ObjectMeta.Namespace = f.Namespace.Name
|
|
||||||
d.Spec.Template.Spec.Containers[0].Command = []string{"/bin/sh"}
|
|
||||||
d.Spec.Template.Spec.Containers[0].Args = []string{"-c", "/usr/bin/yes >/dev/null"}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHamsterDeploymentWithResources creates a simple hamster deployment with specific
|
|
||||||
// resource requests for e2e test purposes.
|
|
||||||
func NewHamsterDeploymentWithResources(f *framework.Framework, cpuQuantity, memoryQuantity resource.Quantity) *appsv1.Deployment {
|
|
||||||
d := NewHamsterDeployment(f)
|
|
||||||
d.Spec.Template.Spec.Containers[0].Resources.Requests = apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: cpuQuantity,
|
|
||||||
apiv1.ResourceMemory: memoryQuantity,
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHamsterDeploymentWithGuaranteedResources creates a simple hamster deployment with specific
|
|
||||||
// resource requests for e2e test purposes. Since the container in the pod specifies resource limits
|
|
||||||
// but not resource requests K8s will set requests equal to limits and the pod will have guaranteed
|
|
||||||
// QoS class.
|
|
||||||
func NewHamsterDeploymentWithGuaranteedResources(f *framework.Framework, cpuQuantity, memoryQuantity resource.Quantity) *appsv1.Deployment {
|
|
||||||
d := NewHamsterDeployment(f)
|
|
||||||
d.Spec.Template.Spec.Containers[0].Resources.Limits = apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: cpuQuantity,
|
|
||||||
apiv1.ResourceMemory: memoryQuantity,
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHamsterDeploymentWithResourcesAndLimits creates a simple hamster deployment with specific
|
|
||||||
// resource requests and limits for e2e test purposes.
|
|
||||||
func NewHamsterDeploymentWithResourcesAndLimits(f *framework.Framework, cpuQuantityRequest, memoryQuantityRequest, cpuQuantityLimit, memoryQuantityLimit resource.Quantity) *appsv1.Deployment {
|
|
||||||
d := NewHamsterDeploymentWithResources(f, cpuQuantityRequest, memoryQuantityRequest)
|
|
||||||
d.Spec.Template.Spec.Containers[0].Resources.Limits = apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: cpuQuantityLimit,
|
|
||||||
apiv1.ResourceMemory: memoryQuantityLimit,
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHamsterPods returns running hamster pods (matched by hamsterLabels)
|
|
||||||
func GetHamsterPods(f *framework.Framework) (*apiv1.PodList, error) {
|
|
||||||
label := labels.SelectorFromSet(labels.Set(hamsterLabels))
|
|
||||||
selector := fields.ParseSelectorOrDie("status.phase!=" + string(apiv1.PodSucceeded) +
|
|
||||||
",status.phase!=" + string(apiv1.PodFailed))
|
|
||||||
options := metav1.ListOptions{LabelSelector: label.String(), FieldSelector: selector.String()}
|
|
||||||
return f.ClientSet.CoreV1().Pods(f.Namespace.Name).List(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetupVPA creates and installs a simple hamster VPA for e2e test purposes.
|
|
||||||
func SetupVPA(f *framework.Framework, cpu string, mode vpa_types.UpdateMode) {
|
|
||||||
vpaCRD := NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: hamsterLabels,
|
|
||||||
})
|
|
||||||
vpaCRD.Spec.UpdatePolicy.UpdateMode = &mode
|
|
||||||
|
|
||||||
cpuQuantity := ParseQuantityOrDie(cpu)
|
|
||||||
resourceList := apiv1.ResourceList{apiv1.ResourceCPU: cpuQuantity}
|
|
||||||
|
|
||||||
vpaCRD.Status.Recommendation = &vpa_types.RecommendedPodResources{
|
|
||||||
ContainerRecommendations: []vpa_types.RecommendedContainerResources{{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
Target: resourceList,
|
|
||||||
LowerBound: resourceList,
|
|
||||||
UpperBound: resourceList,
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
InstallVPA(f, vpaCRD)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewVPA creates a VPA object for e2e test purposes.
|
|
||||||
func NewVPA(f *framework.Framework, name string, selector *metav1.LabelSelector) *vpa_types.VerticalPodAutoscaler {
|
|
||||||
updateMode := vpa_types.UpdateModeAuto
|
|
||||||
vpa := vpa_types.VerticalPodAutoscaler{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: name,
|
|
||||||
Namespace: f.Namespace.Name,
|
|
||||||
},
|
|
||||||
Spec: vpa_types.VerticalPodAutoscalerSpec{
|
|
||||||
Selector: selector,
|
|
||||||
UpdatePolicy: &vpa_types.PodUpdatePolicy{
|
|
||||||
UpdateMode: &updateMode,
|
|
||||||
},
|
|
||||||
ResourcePolicy: &vpa_types.PodResourcePolicy{
|
|
||||||
ContainerPolicies: []vpa_types.ContainerResourcePolicy{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return &vpa
|
|
||||||
}
|
|
||||||
|
|
||||||
type patchRecord struct {
|
|
||||||
Op string `json:"op,inline"`
|
|
||||||
Path string `json:"path,inline"`
|
|
||||||
Value interface{} `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func getVpaClientSet(f *framework.Framework) vpa_clientset.Interface {
|
|
||||||
config, err := framework.LoadConfig()
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "unexpected error loading framework")
|
|
||||||
return vpa_clientset.NewForConfigOrDie(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallVPA installs a VPA object in the test cluster.
|
|
||||||
func InstallVPA(f *framework.Framework, vpa *vpa_types.VerticalPodAutoscaler) {
|
|
||||||
vpaClientSet := getVpaClientSet(f)
|
|
||||||
_, err := vpaClientSet.AutoscalingV1beta1().VerticalPodAutoscalers(f.Namespace.Name).Create(vpa)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "unexpected error creating VPA")
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallRawVPA installs a VPA object passed in as raw json in the test cluster.
|
|
||||||
func InstallRawVPA(f *framework.Framework, obj interface{}) error {
|
|
||||||
vpaClientSet := getVpaClientSet(f)
|
|
||||||
err := vpaClientSet.AutoscalingV1beta1().RESTClient().Post().
|
|
||||||
Namespace(f.Namespace.Name).
|
|
||||||
Resource("verticalpodautoscalers").
|
|
||||||
Body(obj).
|
|
||||||
Do()
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// PatchVpaRecommendation installs a new reocmmendation for VPA object.
|
|
||||||
func PatchVpaRecommendation(f *framework.Framework, vpa *vpa_types.VerticalPodAutoscaler,
|
|
||||||
recommendation *vpa_types.RecommendedPodResources) {
|
|
||||||
newStatus := vpa.Status.DeepCopy()
|
|
||||||
newStatus.Recommendation = recommendation
|
|
||||||
bytes, err := json.Marshal([]patchRecord{{
|
|
||||||
Op: "replace",
|
|
||||||
Path: "/status",
|
|
||||||
Value: *newStatus,
|
|
||||||
}})
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
_, err = getVpaClientSet(f).AutoscalingV1beta2().VerticalPodAutoscalers(f.Namespace.Name).Patch(vpa.Name, types.JSONPatchType, bytes)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to patch VPA.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnnotatePod adds annotation for an existing pod.
|
|
||||||
func AnnotatePod(f *framework.Framework, podName, annotationName, annotationValue string) {
|
|
||||||
bytes, err := json.Marshal([]patchRecord{{
|
|
||||||
Op: "add",
|
|
||||||
Path: fmt.Sprintf("/metadata/annotations/%v", annotationName),
|
|
||||||
Value: annotationValue,
|
|
||||||
}})
|
|
||||||
pod, err := f.ClientSet.CoreV1().Pods(f.Namespace.Name).Patch(podName, types.JSONPatchType, bytes)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to patch pod.")
|
|
||||||
gomega.Expect(pod.Annotations[annotationName]).To(gomega.Equal(annotationValue))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseQuantityOrDie parses quantity from string and dies with an error if
|
|
||||||
// unparsable.
|
|
||||||
func ParseQuantityOrDie(text string) resource.Quantity {
|
|
||||||
quantity, err := resource.ParseQuantity(text)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
return quantity
|
|
||||||
}
|
|
||||||
|
|
||||||
// PodSet is a simplified representation of PodList mapping names to UIDs.
|
|
||||||
type PodSet map[string]types.UID
|
|
||||||
|
|
||||||
// MakePodSet converts PodList to podset for easier comparison of pod collections.
|
|
||||||
func MakePodSet(pods *apiv1.PodList) PodSet {
|
|
||||||
result := make(PodSet)
|
|
||||||
if pods == nil {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
for _, p := range pods.Items {
|
|
||||||
result[p.Name] = p.UID
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitForPodsRestarted waits until some pods from the list are restarted.
|
|
||||||
func WaitForPodsRestarted(f *framework.Framework, podList *apiv1.PodList) error {
|
|
||||||
initialPodSet := MakePodSet(podList)
|
|
||||||
|
|
||||||
err := wait.PollImmediate(pollInterval, pollTimeout, func() (bool, error) {
|
|
||||||
currentPodList, err := GetHamsterPods(f)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
currentPodSet := MakePodSet(currentPodList)
|
|
||||||
return WerePodsSuccessfullyRestarted(currentPodSet, initialPodSet), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("waiting for set of pods changed: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitForPodsEvicted waits until some pods from the list are evicted.
|
|
||||||
func WaitForPodsEvicted(f *framework.Framework, podList *apiv1.PodList) error {
|
|
||||||
initialPodSet := MakePodSet(podList)
|
|
||||||
|
|
||||||
err := wait.PollImmediate(pollInterval, pollTimeout, func() (bool, error) {
|
|
||||||
currentPodList, err := GetHamsterPods(f)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
currentPodSet := MakePodSet(currentPodList)
|
|
||||||
return GetEvictedPodsCount(currentPodSet, initialPodSet) > 0, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("waiting for set of pods changed: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WerePodsSuccessfullyRestarted returns true if some pods from initialPodSet have been
|
|
||||||
// successfully restarted comparing to currentPodSet (pods were evicted and
|
|
||||||
// are running).
|
|
||||||
func WerePodsSuccessfullyRestarted(currentPodSet PodSet, initialPodSet PodSet) bool {
|
|
||||||
if len(currentPodSet) < len(initialPodSet) {
|
|
||||||
// If we have less pods running than in the beginning, there is a restart
|
|
||||||
// in progress - a pod was evicted but not yet recreated.
|
|
||||||
framework.Logf("Restart in progress")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
evictedCount := GetEvictedPodsCount(currentPodSet, initialPodSet)
|
|
||||||
framework.Logf("%v of initial pods were already evicted", evictedCount)
|
|
||||||
return evictedCount > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEvictedPodsCount returns the count of pods from initialPodSet that have
|
|
||||||
// been evicted comparing to currentPodSet.
|
|
||||||
func GetEvictedPodsCount(currentPodSet PodSet, initialPodSet PodSet) int {
|
|
||||||
diffs := 0
|
|
||||||
for name, initialUID := range initialPodSet {
|
|
||||||
currentUID, inCurrent := currentPodSet[name]
|
|
||||||
if !inCurrent {
|
|
||||||
diffs += 1
|
|
||||||
} else if initialUID != currentUID {
|
|
||||||
diffs += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return diffs
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckNoPodsEvicted waits for long enough period for VPA to start evicting
|
|
||||||
// pods and checks that no pods were restarted.
|
|
||||||
func CheckNoPodsEvicted(f *framework.Framework, initialPodSet PodSet) {
|
|
||||||
time.Sleep(VpaEvictionTimeout)
|
|
||||||
currentPodList, err := GetHamsterPods(f)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
restarted := GetEvictedPodsCount(MakePodSet(currentPodList), initialPodSet)
|
|
||||||
gomega.Expect(restarted).To(gomega.Equal(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitForVPAMatch pools VPA object until match function returns true. Returns
|
|
||||||
// polled vpa object. On timeout returns error.
|
|
||||||
func WaitForVPAMatch(c *vpa_clientset.Clientset, vpa *vpa_types.VerticalPodAutoscaler, match func(vpa *vpa_types.VerticalPodAutoscaler) bool) (*vpa_types.VerticalPodAutoscaler, error) {
|
|
||||||
var polledVpa *vpa_types.VerticalPodAutoscaler
|
|
||||||
err := wait.PollImmediate(pollInterval, pollTimeout, func() (bool, error) {
|
|
||||||
var err error
|
|
||||||
polledVpa, err = c.AutoscalingV1beta1().VerticalPodAutoscalers(vpa.Namespace).Get(vpa.Name, metav1.GetOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if match(polledVpa) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error waiting for recommendation present in %v: %v", vpa.Name, err)
|
|
||||||
}
|
|
||||||
return polledVpa, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitForRecommendationPresent pools VPA object until recommendations are not empty. Returns
|
|
||||||
// polled vpa object. On timeout returns error.
|
|
||||||
func WaitForRecommendationPresent(c *vpa_clientset.Clientset, vpa *vpa_types.VerticalPodAutoscaler) (*vpa_types.VerticalPodAutoscaler, error) {
|
|
||||||
return WaitForVPAMatch(c, vpa, func(vpa *vpa_types.VerticalPodAutoscaler) bool {
|
|
||||||
return vpa.Status.Recommendation != nil && len(vpa.Status.Recommendation.ContainerRecommendations) != 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitForConditionPresent pools VPA object until it contains condition with given type. On timeout returns an error.
|
|
||||||
func WaitForConditionPresent(c *vpa_clientset.Clientset, vpa *vpa_types.VerticalPodAutoscaler, expectedConditionType string) (*vpa_types.VerticalPodAutoscaler, error) {
|
|
||||||
return WaitForVPAMatch(c, vpa, func(vpa *vpa_types.VerticalPodAutoscaler) bool {
|
|
||||||
for _, condition := range vpa.Status.Conditions {
|
|
||||||
if string(condition.Type) == expectedConditionType {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func installLimitRange(f *framework.Framework, minCpuLimit, minMemoryLimit, maxCpuLimit, maxMemoryLimit *resource.Quantity, lrType apiv1.LimitType) {
|
|
||||||
lr := &apiv1.LimitRange{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Namespace: f.Namespace.Name,
|
|
||||||
Name: "hamster-lr",
|
|
||||||
},
|
|
||||||
Spec: apiv1.LimitRangeSpec{
|
|
||||||
Limits: []apiv1.LimitRangeItem{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if maxMemoryLimit != nil || maxCpuLimit != nil {
|
|
||||||
lrItem := apiv1.LimitRangeItem{
|
|
||||||
Type: lrType,
|
|
||||||
Max: apiv1.ResourceList{},
|
|
||||||
}
|
|
||||||
if maxCpuLimit != nil {
|
|
||||||
lrItem.Max[apiv1.ResourceCPU] = *maxCpuLimit
|
|
||||||
}
|
|
||||||
if maxMemoryLimit != nil {
|
|
||||||
lrItem.Max[apiv1.ResourceMemory] = *maxMemoryLimit
|
|
||||||
}
|
|
||||||
lr.Spec.Limits = append(lr.Spec.Limits, lrItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
if minMemoryLimit != nil || minCpuLimit != nil {
|
|
||||||
lrItem := apiv1.LimitRangeItem{
|
|
||||||
Type: lrType,
|
|
||||||
Min: apiv1.ResourceList{},
|
|
||||||
}
|
|
||||||
if minCpuLimit != nil {
|
|
||||||
lrItem.Min[apiv1.ResourceCPU] = *minCpuLimit
|
|
||||||
}
|
|
||||||
if minMemoryLimit != nil {
|
|
||||||
lrItem.Min[apiv1.ResourceMemory] = *minMemoryLimit
|
|
||||||
}
|
|
||||||
lr.Spec.Limits = append(lr.Spec.Limits, lrItem)
|
|
||||||
}
|
|
||||||
_, err := f.ClientSet.CoreV1().LimitRanges(f.Namespace.Name).Create(lr)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "unexpected error when creating limit range")
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallLimitRangeWithMax installs a LimitRange with a maximum limit for CPU and memory.
|
|
||||||
func InstallLimitRangeWithMax(f *framework.Framework, maxCpuLimit, maxMemoryLimit string, lrType apiv1.LimitType) {
|
|
||||||
ginkgo.By(fmt.Sprintf("Setting up LimitRange with max limits - CPU: %v, memory: %v", maxCpuLimit, maxMemoryLimit))
|
|
||||||
maxCpuLimitQuantity := ParseQuantityOrDie(maxCpuLimit)
|
|
||||||
maxMemoryLimitQuantity := ParseQuantityOrDie(maxMemoryLimit)
|
|
||||||
installLimitRange(f, nil, nil, &maxCpuLimitQuantity, &maxMemoryLimitQuantity, lrType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallLimitRangeWithMin installs a LimitRange with a minimum limit for CPU and memory.
|
|
||||||
func InstallLimitRangeWithMin(f *framework.Framework, minCpuLimit, minMemoryLimit string, lrType apiv1.LimitType) {
|
|
||||||
ginkgo.By(fmt.Sprintf("Setting up LimitRange with min limits - CPU: %v, memory: %v", minCpuLimit, minMemoryLimit))
|
|
||||||
minCpuLimitQuantity := ParseQuantityOrDie(minCpuLimit)
|
|
||||||
minMemoryLimitQuantity := ParseQuantityOrDie(minMemoryLimit)
|
|
||||||
installLimitRange(f, &minCpuLimitQuantity, &minMemoryLimitQuantity, nil, nil, lrType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetupVPAForTwoHamsters creates and installs a simple pod with two hamster containers for e2e test purposes.
|
|
||||||
func SetupVPAForTwoHamsters(f *framework.Framework, cpu string, mode vpa_types.UpdateMode) {
|
|
||||||
vpaCRD := NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: hamsterLabels,
|
|
||||||
})
|
|
||||||
vpaCRD.Spec.UpdatePolicy.UpdateMode = &mode
|
|
||||||
|
|
||||||
cpuQuantity := ParseQuantityOrDie(cpu)
|
|
||||||
resourceList := apiv1.ResourceList{apiv1.ResourceCPU: cpuQuantity}
|
|
||||||
|
|
||||||
vpaCRD.Status.Recommendation = &vpa_types.RecommendedPodResources{
|
|
||||||
ContainerRecommendations: []vpa_types.RecommendedContainerResources{
|
|
||||||
{
|
|
||||||
ContainerName: "hamster",
|
|
||||||
Target: resourceList,
|
|
||||||
LowerBound: resourceList,
|
|
||||||
UpperBound: resourceList,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ContainerName: "hamster2",
|
|
||||||
Target: resourceList,
|
|
||||||
LowerBound: resourceList,
|
|
||||||
UpperBound: resourceList,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
InstallVPA(f, vpaCRD)
|
|
||||||
}
|
|
||||||
|
|
@ -1,210 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 The Kubernetes Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package autoscaling
|
|
||||||
|
|
||||||
// This file is a cut down fork of k8s/test/e2e/e2e.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/onsi/ginkgo"
|
|
||||||
"github.com/onsi/ginkgo/config"
|
|
||||||
"github.com/onsi/ginkgo/reporters"
|
|
||||||
"github.com/onsi/gomega"
|
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
runtimeutils "k8s.io/apimachinery/pkg/util/runtime"
|
|
||||||
"k8s.io/component-base/logs"
|
|
||||||
|
|
||||||
// needed to authorize to GKE cluster
|
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
|
||||||
"k8s.io/klog"
|
|
||||||
"k8s.io/kubernetes/pkg/version"
|
|
||||||
commontest "k8s.io/kubernetes/test/e2e/common"
|
|
||||||
"k8s.io/kubernetes/test/e2e/framework"
|
|
||||||
"k8s.io/kubernetes/test/e2e/framework/ginkgowrapper"
|
|
||||||
"k8s.io/kubernetes/test/e2e/framework/metrics"
|
|
||||||
)
|
|
||||||
|
|
||||||
// There are certain operations we only want to run once per overall test invocation
|
|
||||||
// (such as deleting old namespaces, or verifying that all system pods are running.
|
|
||||||
// Because of the way Ginkgo runs tests in parallel, we must use SynchronizedBeforeSuite
|
|
||||||
// to ensure that these operations only run on the first parallel Ginkgo node.
|
|
||||||
//
|
|
||||||
// This function takes two parameters: one function which runs on only the first Ginkgo node,
|
|
||||||
// returning an opaque byte array, and then a second function which runs on all Ginkgo nodes,
|
|
||||||
// accepting the byte array.
|
|
||||||
var _ = ginkgo.SynchronizedBeforeSuite(func() []byte {
|
|
||||||
|
|
||||||
c, err := framework.LoadClientset()
|
|
||||||
if err != nil {
|
|
||||||
klog.Fatal("Error loading client: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete any namespaces except those created by the system. This ensures no
|
|
||||||
// lingering resources are left over from a previous test run.
|
|
||||||
if framework.TestContext.CleanStart {
|
|
||||||
deleted, err := framework.DeleteNamespaces(c, nil, /* deleteFilter */
|
|
||||||
[]string{
|
|
||||||
metav1.NamespaceSystem,
|
|
||||||
metav1.NamespaceDefault,
|
|
||||||
metav1.NamespacePublic,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
framework.Failf("Error deleting orphaned namespaces: %v", err)
|
|
||||||
}
|
|
||||||
klog.Infof("Waiting for deletion of the following namespaces: %v", deleted)
|
|
||||||
if err := framework.WaitForNamespacesDeleted(c, deleted, framework.NamespaceCleanupTimeout); err != nil {
|
|
||||||
framework.Failf("Failed to delete orphaned namespaces %v: %v", deleted, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In large clusters we may get to this point but still have a bunch
|
|
||||||
// of nodes without Routes created. Since this would make a node
|
|
||||||
// unschedulable, we need to wait until all of them are schedulable.
|
|
||||||
framework.ExpectNoError(framework.WaitForAllNodesSchedulable(c, framework.TestContext.NodeSchedulableTimeout))
|
|
||||||
|
|
||||||
// Ensure all pods are running and ready before starting tests (otherwise,
|
|
||||||
// cluster infrastructure pods that are being pulled or started can block
|
|
||||||
// test pods from running, and tests that ensure all pods are running and
|
|
||||||
// ready will fail).
|
|
||||||
podStartupTimeout := framework.TestContext.SystemPodsStartupTimeout
|
|
||||||
// TODO: In large clusters, we often observe a non-starting pods due to
|
|
||||||
// #41007. To avoid those pods preventing the whole test runs (and just
|
|
||||||
// wasting the whole run), we allow for some not-ready pods (with the
|
|
||||||
// number equal to the number of allowed not-ready nodes).
|
|
||||||
if err := framework.WaitForPodsRunningReady(c, metav1.NamespaceSystem, int32(framework.TestContext.MinStartupPods), int32(framework.TestContext.AllowedNotReadyNodes), podStartupTimeout, map[string]string{}); err != nil {
|
|
||||||
framework.DumpAllNamespaceInfo(c, metav1.NamespaceSystem)
|
|
||||||
framework.LogFailedContainers(c, metav1.NamespaceSystem, framework.Logf)
|
|
||||||
// runKubernetesServiceTestContainer(c, metav1.NamespaceDefault)
|
|
||||||
framework.Failf("Error waiting for all pods to be running and ready: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := framework.WaitForDaemonSets(c, metav1.NamespaceSystem, int32(framework.TestContext.AllowedNotReadyNodes), framework.TestContext.SystemDaemonsetStartupTimeout); err != nil {
|
|
||||||
framework.Logf("WARNING: Waiting for all daemonsets to be ready failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the version of the server and this client.
|
|
||||||
framework.Logf("e2e test version: %s", version.Get().GitVersion)
|
|
||||||
|
|
||||||
dc := c.DiscoveryClient
|
|
||||||
|
|
||||||
serverVersion, serverErr := dc.ServerVersion()
|
|
||||||
if serverErr != nil {
|
|
||||||
framework.Logf("Unexpected server error retrieving version: %v", serverErr)
|
|
||||||
}
|
|
||||||
if serverVersion != nil {
|
|
||||||
framework.Logf("kube-apiserver version: %s", serverVersion.GitVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reference common test to make the import valid.
|
|
||||||
commontest.CurrentSuite = commontest.E2E
|
|
||||||
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}, func(data []byte) {
|
|
||||||
framework.Logf("No cloud config support.")
|
|
||||||
})
|
|
||||||
|
|
||||||
// Similar to SynchronizedBeforeSuite, we want to run some operations only once (such as collecting cluster logs).
|
|
||||||
// Here, the order of functions is reversed; first, the function which runs everywhere,
|
|
||||||
// and then the function that only runs on the first Ginkgo node.
|
|
||||||
var _ = ginkgo.SynchronizedAfterSuite(func() {
|
|
||||||
// Run on all Ginkgo nodes
|
|
||||||
framework.Logf("Running AfterSuite actions on all node")
|
|
||||||
framework.RunCleanupActions()
|
|
||||||
}, func() {
|
|
||||||
// Run only Ginkgo on node 1
|
|
||||||
framework.Logf("Running AfterSuite actions on node 1")
|
|
||||||
if framework.TestContext.ReportDir != "" {
|
|
||||||
framework.CoreDump(framework.TestContext.ReportDir)
|
|
||||||
}
|
|
||||||
if framework.TestContext.GatherSuiteMetricsAfterTest {
|
|
||||||
if err := gatherTestSuiteMetrics(); err != nil {
|
|
||||||
framework.Logf("Error gathering metrics: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
func gatherTestSuiteMetrics() error {
|
|
||||||
framework.Logf("Gathering metrics")
|
|
||||||
c, err := framework.LoadClientset()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error loading client: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab metrics for apiserver, scheduler, controller-manager, kubelet (for non-kubemark case) and cluster autoscaler (optionally).
|
|
||||||
grabber, err := metrics.NewMetricsGrabber(c, nil, !framework.ProviderIs("kubemark"), true, true, true, framework.TestContext.IncludeClusterAutoscalerMetrics)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create MetricsGrabber: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
received, err := grabber.Grab()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to grab metrics: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
metricsForE2E := (*framework.MetricsForE2E)(&received)
|
|
||||||
metricsJSON := metricsForE2E.PrintJSON()
|
|
||||||
if framework.TestContext.ReportDir != "" {
|
|
||||||
filePath := path.Join(framework.TestContext.ReportDir, "MetricsForE2ESuite_"+time.Now().Format(time.RFC3339)+".json")
|
|
||||||
if err := ioutil.WriteFile(filePath, []byte(metricsJSON), 0644); err != nil {
|
|
||||||
return fmt.Errorf("error writing to %q: %v", filePath, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
framework.Logf("\n\nTest Suite Metrics:\n%s\n\n", metricsJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunE2ETests checks configuration parameters (specified through flags) and then runs
|
|
||||||
// E2E tests using the Ginkgo runner.
|
|
||||||
// If a "report directory" is specified, one or more JUnit test reports will be
|
|
||||||
// generated in this directory, and cluster logs will also be saved.
|
|
||||||
// This function is called on each Ginkgo node in parallel mode.
|
|
||||||
func RunE2ETests(t *testing.T) {
|
|
||||||
runtimeutils.ReallyCrash = true
|
|
||||||
logs.InitLogs()
|
|
||||||
defer logs.FlushLogs()
|
|
||||||
|
|
||||||
gomega.RegisterFailHandler(ginkgowrapper.Fail)
|
|
||||||
// Disable skipped tests unless they are explicitly requested.
|
|
||||||
if config.GinkgoConfig.FocusString == "" && config.GinkgoConfig.SkipString == "" {
|
|
||||||
config.GinkgoConfig.SkipString = `\[Flaky\]|\[Feature:.+\]`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run tests through the Ginkgo runner with output to console + JUnit for Jenkins
|
|
||||||
var r []ginkgo.Reporter
|
|
||||||
if framework.TestContext.ReportDir != "" {
|
|
||||||
// TODO: we should probably only be trying to create this directory once
|
|
||||||
// rather than once-per-Ginkgo-node.
|
|
||||||
if err := os.MkdirAll(framework.TestContext.ReportDir, 0755); err != nil {
|
|
||||||
klog.Errorf("Failed creating report directory: %v", err)
|
|
||||||
} else {
|
|
||||||
r = append(r, reporters.NewJUnitReporter(path.Join(framework.TestContext.ReportDir, "junit_02.xml")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
klog.Infof("Starting e2e run %q on Ginkgo node %d", framework.RunId, config.GinkgoConfig.ParallelNode)
|
|
||||||
|
|
||||||
ginkgo.RunSpecsWithDefaultAndCustomReporters(t, "Kubernetes e2e suite", r)
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 The Kubernetes Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package autoscaling
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"k8s.io/kubernetes/test/e2e/framework"
|
|
||||||
"k8s.io/kubernetes/test/e2e/framework/viperconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
framework.HandleFlags()
|
|
||||||
if err := viperconfig.ViperizeFlags("", "e2e"); err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
framework.AfterReadingAllFlags(&framework.TestContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVpaE2E(t *testing.T) {
|
|
||||||
RunE2ETests(t)
|
|
||||||
}
|
|
||||||
|
|
@ -1,160 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 The Kubernetes Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package autoscaling
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
apiv1 "k8s.io/api/core/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/api/resource"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
|
||||||
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1beta1"
|
|
||||||
vpa_clientset "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/client/clientset/versioned"
|
|
||||||
e2e_common "k8s.io/kubernetes/test/e2e/common"
|
|
||||||
"k8s.io/kubernetes/test/e2e/framework"
|
|
||||||
|
|
||||||
"github.com/onsi/ginkgo"
|
|
||||||
"github.com/onsi/gomega"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
minimalCPULowerBound = "20m"
|
|
||||||
minimalCPUUpperBound = "100m"
|
|
||||||
minimalMemoryLowerBound = "20Mi"
|
|
||||||
minimalMemoryUpperBound = "300Mi"
|
|
||||||
// the initial values should be outside minimal bounds
|
|
||||||
initialCPU = "10m"
|
|
||||||
initialMemory = "10Mi"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ = FullVpaE2eDescribe("Pods under VPA", func() {
|
|
||||||
var (
|
|
||||||
rc *ResourceConsumer
|
|
||||||
vpaClientSet *vpa_clientset.Clientset
|
|
||||||
vpaCRD *vpa_types.VerticalPodAutoscaler
|
|
||||||
)
|
|
||||||
replicas := 3
|
|
||||||
|
|
||||||
ginkgo.AfterEach(func() {
|
|
||||||
rc.CleanUp()
|
|
||||||
})
|
|
||||||
|
|
||||||
// This schedules AfterEach block that needs to run after the AfterEach above and
|
|
||||||
// BeforeEach that needs to run before the BeforeEach below - thus the order of these matters.
|
|
||||||
f := framework.NewDefaultFramework("vertical-pod-autoscaling")
|
|
||||||
|
|
||||||
ginkgo.BeforeEach(func() {
|
|
||||||
ns := f.Namespace.Name
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
rc = NewDynamicResourceConsumer("hamster", ns, e2e_common.KindDeployment,
|
|
||||||
replicas,
|
|
||||||
1, /*initCPUTotal*/
|
|
||||||
10, /*initMemoryTotal*/
|
|
||||||
1, /*initCustomMetric*/
|
|
||||||
ParseQuantityOrDie(initialCPU), /*cpuRequest*/
|
|
||||||
ParseQuantityOrDie(initialMemory), /*memRequest*/
|
|
||||||
f.ClientSet,
|
|
||||||
f.InternalClientset)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
config, err := framework.LoadConfig()
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
|
|
||||||
vpaCRD = NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: map[string]string{
|
|
||||||
"name": "hamster",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
vpaClientSet = vpa_clientset.NewForConfigOrDie(config)
|
|
||||||
vpaClient := vpaClientSet.AutoscalingV1beta1()
|
|
||||||
_, err = vpaClient.VerticalPodAutoscalers(ns).Create(vpaCRD)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("have cpu requests growing with usage", func() {
|
|
||||||
// initial CPU usage is low so a minimal recommendation is expected
|
|
||||||
err := waitForResourceRequestInRangeInPods(
|
|
||||||
f, metav1.ListOptions{LabelSelector: "name=hamster"}, apiv1.ResourceCPU,
|
|
||||||
ParseQuantityOrDie(minimalCPULowerBound), ParseQuantityOrDie(minimalCPUUpperBound))
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
|
|
||||||
// consume more CPU to get a higher recommendation
|
|
||||||
rc.ConsumeCPU(600 * replicas)
|
|
||||||
err = waitForResourceRequestInRangeInPods(
|
|
||||||
f, metav1.ListOptions{LabelSelector: "name=hamster"}, apiv1.ResourceCPU,
|
|
||||||
ParseQuantityOrDie("500m"), ParseQuantityOrDie("900m"))
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("have memory requests growing with usage", func() {
|
|
||||||
// initial memory usage is low so a minimal recommendation is expected
|
|
||||||
err := waitForResourceRequestInRangeInPods(
|
|
||||||
f, metav1.ListOptions{LabelSelector: "name=hamster"}, apiv1.ResourceMemory,
|
|
||||||
ParseQuantityOrDie(minimalMemoryLowerBound), ParseQuantityOrDie(minimalMemoryUpperBound))
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
|
|
||||||
// consume more memory to get a higher recommendation
|
|
||||||
// NOTE: large range given due to unpredictability of actual memory usage
|
|
||||||
rc.ConsumeMem(1024 * replicas)
|
|
||||||
err = waitForResourceRequestInRangeInPods(
|
|
||||||
f, metav1.ListOptions{LabelSelector: "name=hamster"}, apiv1.ResourceMemory,
|
|
||||||
ParseQuantityOrDie("900Mi"), ParseQuantityOrDie("4000Mi"))
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
func waitForPodsMatch(f *framework.Framework, listOptions metav1.ListOptions, matcher func(pod apiv1.Pod) bool) error {
|
|
||||||
return wait.PollImmediate(pollInterval, pollTimeout, func() (bool, error) {
|
|
||||||
|
|
||||||
ns := f.Namespace.Name
|
|
||||||
c := f.ClientSet
|
|
||||||
|
|
||||||
podList, err := c.CoreV1().Pods(ns).List(listOptions)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(podList.Items) == 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
if !matcher(pod) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitForResourceRequestInRangeInPods(f *framework.Framework, listOptions metav1.ListOptions, resourceName apiv1.ResourceName, lowerBound, upperBound resource.Quantity) error {
|
|
||||||
err := waitForPodsMatch(f, listOptions,
|
|
||||||
func(pod apiv1.Pod) bool {
|
|
||||||
resourceRequest, found := pod.Spec.Containers[0].Resources.Requests[resourceName]
|
|
||||||
framework.Logf("Comparing %v request %v against range of (%v, %v)", resourceName, resourceRequest, lowerBound, upperBound)
|
|
||||||
return found && resourceRequest.MilliValue() > lowerBound.MilliValue() && resourceRequest.MilliValue() < upperBound.MilliValue()
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error waiting for %s request in range of (%v,%v) for pods: %+v", resourceName, lowerBound, upperBound, listOptions)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,254 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 The Kubernetes Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package autoscaling
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
apiv1 "k8s.io/api/core/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
|
||||||
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1beta1"
|
|
||||||
vpa_clientset "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/client/clientset/versioned"
|
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
|
||||||
"k8s.io/client-go/tools/cache"
|
|
||||||
"k8s.io/klog"
|
|
||||||
"k8s.io/kubernetes/test/e2e/framework"
|
|
||||||
|
|
||||||
"github.com/onsi/ginkgo"
|
|
||||||
"github.com/onsi/gomega"
|
|
||||||
)
|
|
||||||
|
|
||||||
type resourceRecommendation struct {
|
|
||||||
target, lower, upper int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resourceRecommendation) sub(other *resourceRecommendation) resourceRecommendation {
|
|
||||||
return resourceRecommendation{
|
|
||||||
target: r.target - other.target,
|
|
||||||
lower: r.lower - other.lower,
|
|
||||||
upper: r.upper - other.upper,
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func getResourceRecommendation(containerRecommendation *vpa_types.RecommendedContainerResources, r apiv1.ResourceName) resourceRecommendation {
|
|
||||||
getOrZero := func(resourceList apiv1.ResourceList) int64 {
|
|
||||||
value, found := resourceList[r]
|
|
||||||
if found {
|
|
||||||
return value.Value()
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return resourceRecommendation{
|
|
||||||
target: getOrZero(containerRecommendation.Target),
|
|
||||||
lower: getOrZero(containerRecommendation.LowerBound),
|
|
||||||
upper: getOrZero(containerRecommendation.UpperBound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type recommendationChange struct {
|
|
||||||
oldMissing, newMissing bool
|
|
||||||
diff resourceRecommendation
|
|
||||||
}
|
|
||||||
|
|
||||||
type observer struct {
|
|
||||||
channel chan recommendationChange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*observer) OnAdd(obj interface{}) {}
|
|
||||||
func (*observer) OnDelete(obj interface{}) {}
|
|
||||||
|
|
||||||
func (o *observer) OnUpdate(oldObj, newObj interface{}) {
|
|
||||||
get := func(vpa *vpa_types.VerticalPodAutoscaler) (result resourceRecommendation, found bool) {
|
|
||||||
if vpa.Status.Recommendation == nil || len(vpa.Status.Recommendation.ContainerRecommendations) == 0 {
|
|
||||||
found = false
|
|
||||||
result = resourceRecommendation{}
|
|
||||||
} else {
|
|
||||||
found = true
|
|
||||||
result = getResourceRecommendation(&vpa.Status.Recommendation.ContainerRecommendations[0], apiv1.ResourceCPU)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
oldVPA, _ := oldObj.(*vpa_types.VerticalPodAutoscaler)
|
|
||||||
NewVPA, _ := newObj.(*vpa_types.VerticalPodAutoscaler)
|
|
||||||
oldRecommendation, oldFound := get(oldVPA)
|
|
||||||
newRecommendation, newFound := get(NewVPA)
|
|
||||||
result := recommendationChange{
|
|
||||||
oldMissing: !oldFound,
|
|
||||||
newMissing: !newFound,
|
|
||||||
diff: newRecommendation.sub(&oldRecommendation),
|
|
||||||
}
|
|
||||||
go func() { o.channel <- result }()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getVpaObserver(vpaClientSet *vpa_clientset.Clientset) *observer {
|
|
||||||
vpaListWatch := cache.NewListWatchFromClient(vpaClientSet.AutoscalingV1beta1().RESTClient(), "verticalpodautoscalers", apiv1.NamespaceAll, fields.Everything())
|
|
||||||
vpaObserver := observer{channel: make(chan recommendationChange)}
|
|
||||||
_, controller := cache.NewIndexerInformer(vpaListWatch,
|
|
||||||
&vpa_types.VerticalPodAutoscaler{},
|
|
||||||
1*time.Hour,
|
|
||||||
&vpaObserver,
|
|
||||||
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
|
|
||||||
go controller.Run(make(chan struct{}))
|
|
||||||
if !cache.WaitForCacheSync(make(chan struct{}), controller.HasSynced) {
|
|
||||||
klog.Fatalf("Failed to sync VPA cache during initialization")
|
|
||||||
} else {
|
|
||||||
klog.Info("Initial VPA synced successfully")
|
|
||||||
}
|
|
||||||
return &vpaObserver
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = RecommenderE2eDescribe("Checkpoints", func() {
|
|
||||||
f := framework.NewDefaultFramework("vertical-pod-autoscaling")
|
|
||||||
|
|
||||||
ginkgo.It("with missing VPA objects are garbage collected", func() {
|
|
||||||
ns := f.Namespace.Name
|
|
||||||
config, err := framework.LoadConfig()
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
|
|
||||||
checkpoint := vpa_types.VerticalPodAutoscalerCheckpoint{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "test",
|
|
||||||
Namespace: ns,
|
|
||||||
},
|
|
||||||
Spec: vpa_types.VerticalPodAutoscalerCheckpointSpec{
|
|
||||||
VPAObjectName: "some-vpa",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
vpaClientSet := vpa_clientset.NewForConfigOrDie(config)
|
|
||||||
_, err = vpaClientSet.AutoscalingV1beta1().VerticalPodAutoscalerCheckpoints(ns).Create(&checkpoint)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
|
|
||||||
time.Sleep(15 * time.Minute)
|
|
||||||
|
|
||||||
list, err := vpaClientSet.AutoscalingV1beta1().VerticalPodAutoscalerCheckpoints(ns).List(metav1.ListOptions{})
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
gomega.Expect(list.Items).To(gomega.BeEmpty())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
var _ = RecommenderE2eDescribe("VPA CRD object", func() {
|
|
||||||
f := framework.NewDefaultFramework("vertical-pod-autoscaling")
|
|
||||||
|
|
||||||
var (
|
|
||||||
vpaCRD *vpa_types.VerticalPodAutoscaler
|
|
||||||
vpaClientSet *vpa_clientset.Clientset
|
|
||||||
)
|
|
||||||
|
|
||||||
ginkgo.BeforeEach(func() {
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
c := f.ClientSet
|
|
||||||
ns := f.Namespace.Name
|
|
||||||
|
|
||||||
cpuQuantity := ParseQuantityOrDie("100m")
|
|
||||||
memoryQuantity := ParseQuantityOrDie("100Mi")
|
|
||||||
|
|
||||||
d := NewHamsterDeploymentWithResources(f, cpuQuantity, memoryQuantity)
|
|
||||||
_, err := c.AppsV1().Deployments(ns).Create(d)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
err = framework.WaitForDeploymentComplete(c, d)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
config, err := framework.LoadConfig()
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
|
|
||||||
vpaCRD = NewVPA(f, "hamster-vpa", &metav1.LabelSelector{
|
|
||||||
MatchLabels: map[string]string{
|
|
||||||
"app": "hamster",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
vpaClientSet = vpa_clientset.NewForConfigOrDie(config)
|
|
||||||
vpaClient := vpaClientSet.AutoscalingV1beta1()
|
|
||||||
_, err = vpaClient.VerticalPodAutoscalers(ns).Create(vpaCRD)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("serves recommendation and is marked deprecated", func() {
|
|
||||||
ginkgo.By("Waiting for recommendation to be filled")
|
|
||||||
_, err := WaitForRecommendationPresent(vpaClientSet, vpaCRD)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
_, err = WaitForConditionPresent(vpaClientSet, vpaCRD, "ConfigDeprecated")
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("doesn't drop lower/upper after recommender's restart", func() {
|
|
||||||
|
|
||||||
o := getVpaObserver(vpaClientSet)
|
|
||||||
|
|
||||||
ginkgo.By("Waiting for recommendation to be filled")
|
|
||||||
_, err := WaitForRecommendationPresent(vpaClientSet, vpaCRD)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
ginkgo.By("Drain diffs")
|
|
||||||
out:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case recommendationDiff := <-o.channel:
|
|
||||||
fmt.Println("Dropping recommendation diff", recommendationDiff)
|
|
||||||
default:
|
|
||||||
break out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ginkgo.By("Deleting recommender")
|
|
||||||
gomega.Expect(deleteRecommender(f.ClientSet)).To(gomega.BeNil())
|
|
||||||
ginkgo.By("Accumulating diffs after restart")
|
|
||||||
time.Sleep(5 * time.Minute)
|
|
||||||
changeDetected := false
|
|
||||||
finish:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case recommendationDiff := <-o.channel:
|
|
||||||
fmt.Println("checking recommendation diff", recommendationDiff)
|
|
||||||
changeDetected = true
|
|
||||||
gomega.Expect(recommendationDiff.oldMissing).To(gomega.Equal(false))
|
|
||||||
gomega.Expect(recommendationDiff.newMissing).To(gomega.Equal(false))
|
|
||||||
gomega.Expect(recommendationDiff.diff.lower).Should(gomega.BeNumerically(">=", 0))
|
|
||||||
gomega.Expect(recommendationDiff.diff.upper).Should(gomega.BeNumerically("<=", 0))
|
|
||||||
default:
|
|
||||||
break finish
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gomega.Expect(changeDetected).To(gomega.Equal(true))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
func deleteRecommender(c clientset.Interface) error {
|
|
||||||
namespace := "kube-system"
|
|
||||||
listOptions := metav1.ListOptions{}
|
|
||||||
podList, err := c.CoreV1().Pods(namespace).List(listOptions)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Could not list pods.", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Print("Pods list items:", len(podList.Items))
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
if strings.HasPrefix(pod.Name, "vpa-recommender") {
|
|
||||||
fmt.Print("Deleting pod.", namespace, pod.Name)
|
|
||||||
err := c.CoreV1().Pods(namespace).Delete(pod.Name, &metav1.DeleteOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("vpa recommender not found")
|
|
||||||
}
|
|
||||||
|
|
@ -1,336 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 The Kubernetes Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package autoscaling
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
appsv1 "k8s.io/api/apps/v1"
|
|
||||||
apiv1 "k8s.io/api/core/v1"
|
|
||||||
policyv1beta1 "k8s.io/api/policy/v1beta1"
|
|
||||||
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/util/intstr"
|
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
|
||||||
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1beta1"
|
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
|
||||||
"k8s.io/kubernetes/test/e2e/framework"
|
|
||||||
testutils "k8s.io/kubernetes/test/utils"
|
|
||||||
|
|
||||||
"github.com/onsi/ginkgo"
|
|
||||||
"github.com/onsi/gomega"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ = UpdaterE2eDescribe("Updater", func() {
|
|
||||||
f := framework.NewDefaultFramework("vertical-pod-autoscaling")
|
|
||||||
|
|
||||||
ginkgo.It("evicts pods in a Deployment", func() {
|
|
||||||
testEvictsPods(f, "Deployment")
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("evicts pods in a Replication Controller", func() {
|
|
||||||
testEvictsPods(f, "ReplicationController")
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("evicts pods in a Job", func() {
|
|
||||||
testEvictsPods(f, "Job")
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("evicts pods in a ReplicaSet", func() {
|
|
||||||
testEvictsPods(f, "ReplicaSet")
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("evicts pods in a StatefulSet", func() {
|
|
||||||
testEvictsPods(f, "StatefulSet")
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("observes pod disruption budget", func() {
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
c := f.ClientSet
|
|
||||||
ns := f.Namespace.Name
|
|
||||||
|
|
||||||
SetupHamsterDeployment(f, "10m", "10Mi", 10)
|
|
||||||
podList, err := GetHamsterPods(f)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
podSet := MakePodSet(podList)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up prohibitive PDB for hamster deployment")
|
|
||||||
pdb := setupPDB(f, "hamster-pdb", 0 /* maxUnavailable */)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
SetupVPA(f, "25m", vpa_types.UpdateModeAuto)
|
|
||||||
|
|
||||||
ginkgo.By(fmt.Sprintf("Waiting for pods to be evicted, hoping it won't happen, sleep for %s", VpaEvictionTimeout.String()))
|
|
||||||
CheckNoPodsEvicted(f, podSet)
|
|
||||||
|
|
||||||
ginkgo.By("Updating the PDB to allow for multiple pods to be evicted")
|
|
||||||
// We will check that 7 replicas are evicted in 3 minutes, which translates
|
|
||||||
// to 3 updater loops. This gives us relatively good confidence that updater
|
|
||||||
// evicts more than one pod in a loop if PDB allows it.
|
|
||||||
permissiveMaxUnavailable := 7
|
|
||||||
// Creating new PDB and removing old one, since PDBs are immutable at the moment
|
|
||||||
setupPDB(f, "hamster-pdb-2", permissiveMaxUnavailable)
|
|
||||||
err = c.PolicyV1beta1().PodDisruptionBudgets(ns).Delete(pdb.Name, &metav1.DeleteOptions{})
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
|
|
||||||
ginkgo.By(fmt.Sprintf("Waiting for pods to be evicted, sleep for %s", VpaEvictionTimeout.String()))
|
|
||||||
time.Sleep(VpaEvictionTimeout)
|
|
||||||
ginkgo.By("Checking enough pods were evicted.")
|
|
||||||
currentPodList, err := GetHamsterPods(f)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
evictedCount := GetEvictedPodsCount(MakePodSet(currentPodList), podSet)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
gomega.Expect(evictedCount >= permissiveMaxUnavailable).To(gomega.BeTrue())
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("observes container min in LimitRange", func() {
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
d := NewHamsterDeploymentWithResourcesAndLimits(f,
|
|
||||||
ParseQuantityOrDie("100m") /*cpu request*/, ParseQuantityOrDie("200Mi"), /*memory request*/
|
|
||||||
ParseQuantityOrDie("300m") /*cpu limit*/, ParseQuantityOrDie("400Mi") /*memory limit*/)
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
SetupVPA(f, "50m", vpa_types.UpdateModeAuto)
|
|
||||||
|
|
||||||
// Min CPU from limit range is 100m and ratio is 3. Min applies both to limit and request so min
|
|
||||||
// request is 100m request and 300m limit
|
|
||||||
// Min memory limit is 0 and ratio is 2., so min request is 0
|
|
||||||
InstallLimitRangeWithMin(f, "100m", "0", apiv1.LimitTypeContainer)
|
|
||||||
|
|
||||||
ginkgo.By(fmt.Sprintf("Waiting for pods to be evicted, hoping it won't happen, sleep for %s", VpaEvictionTimeout.String()))
|
|
||||||
CheckNoPodsEvicted(f, MakePodSet(podList))
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("observes pod max in LimitRange", func() {
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
d := NewHamsterDeploymentWithResourcesAndLimits(f,
|
|
||||||
ParseQuantityOrDie("100m") /*cpu request*/, ParseQuantityOrDie("200Mi"), /*memory request*/
|
|
||||||
ParseQuantityOrDie("300m") /*cpu limit*/, ParseQuantityOrDie("400Mi") /*memory limit*/)
|
|
||||||
d.Spec.Template.Spec.Containers = append(d.Spec.Template.Spec.Containers, d.Spec.Template.Spec.Containers[0])
|
|
||||||
d.Spec.Template.Spec.Containers[1].Name = "hamster2"
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
SetupVPAForTwoHamsters(f, "200m", vpa_types.UpdateModeAuto)
|
|
||||||
|
|
||||||
// Max CPU limit is 600m per pod, 300m per container and ratio is 3., so max request is 100m,
|
|
||||||
// while recommendation is 200m
|
|
||||||
// Max memory limit is 2T per pod, 1T per container and ratio is 2., so max request is 0.5T
|
|
||||||
InstallLimitRangeWithMax(f, "600m", "2T", apiv1.LimitTypePod)
|
|
||||||
|
|
||||||
ginkgo.By(fmt.Sprintf("Waiting for pods to be evicted, hoping it won't happen, sleep for %s", VpaEvictionTimeout.String()))
|
|
||||||
CheckNoPodsEvicted(f, MakePodSet(podList))
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("observes pod min in LimitRange", func() {
|
|
||||||
ginkgo.By("Setting up a hamster deployment")
|
|
||||||
d := NewHamsterDeploymentWithResourcesAndLimits(f,
|
|
||||||
ParseQuantityOrDie("100m") /*cpu request*/, ParseQuantityOrDie("200Mi"), /*memory request*/
|
|
||||||
ParseQuantityOrDie("300m") /*cpu limit*/, ParseQuantityOrDie("400Mi") /*memory limit*/)
|
|
||||||
d.Spec.Template.Spec.Containers = append(d.Spec.Template.Spec.Containers, d.Spec.Template.Spec.Containers[0])
|
|
||||||
d.Spec.Template.Spec.Containers[1].Name = "hamster2"
|
|
||||||
podList := startDeploymentPods(f, d)
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
SetupVPAForTwoHamsters(f, "50m", vpa_types.UpdateModeAuto)
|
|
||||||
|
|
||||||
// Min CPU from limit range is 200m per pod, 100m per container and ratio is 3. Min applies both
|
|
||||||
// to limit and request so min request is 100m request and 300m limit
|
|
||||||
// Min memory limit is 0 and ratio is 2., so min request is 0
|
|
||||||
InstallLimitRangeWithMin(f, "200m", "0", apiv1.LimitTypePod)
|
|
||||||
|
|
||||||
ginkgo.By(fmt.Sprintf("Waiting for pods to be evicted, hoping it won't happen, sleep for %s", VpaEvictionTimeout.String()))
|
|
||||||
CheckNoPodsEvicted(f, MakePodSet(podList))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
func testEvictsPods(f *framework.Framework, controllerKind string) {
|
|
||||||
ginkgo.By(fmt.Sprintf("Setting up a hamster %v", controllerKind))
|
|
||||||
setupHamsterController(f, controllerKind, "100m", "100Mi", defaultHamsterReplicas)
|
|
||||||
podList, err := GetHamsterPods(f)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
|
|
||||||
ginkgo.By("Setting up a VPA CRD")
|
|
||||||
SetupVPA(f, "200m", vpa_types.UpdateModeAuto)
|
|
||||||
|
|
||||||
ginkgo.By("Waiting for pods to be evicted")
|
|
||||||
err = WaitForPodsEvicted(f, podList)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupHamsterController(f *framework.Framework, controllerKind, cpu, memory string, replicas int32) *apiv1.PodList {
|
|
||||||
switch controllerKind {
|
|
||||||
case "Deployment":
|
|
||||||
SetupHamsterDeployment(f, cpu, memory, replicas)
|
|
||||||
case "ReplicationController":
|
|
||||||
setupHamsterReplicationController(f, cpu, memory, replicas)
|
|
||||||
case "Job":
|
|
||||||
setupHamsterJob(f, cpu, memory, replicas)
|
|
||||||
case "ReplicaSet":
|
|
||||||
setupHamsterRS(f, cpu, memory, replicas)
|
|
||||||
case "StatefulSet":
|
|
||||||
setupHamsterStateful(f, cpu, memory, replicas)
|
|
||||||
default:
|
|
||||||
framework.Failf("Unknown controller kind: %v", controllerKind)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
pods, err := GetHamsterPods(f)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
return pods
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupHamsterReplicationController(f *framework.Framework, cpu, memory string, replicas int32) {
|
|
||||||
hamsterContainer := setupHamsterContainer(cpu, memory)
|
|
||||||
rc := framework.RcByNameContainer("hamster-rc", replicas, "k8s.gcr.io/ubuntu-slim:0.1",
|
|
||||||
hamsterLabels, hamsterContainer, nil)
|
|
||||||
|
|
||||||
rc.Namespace = f.Namespace.Name
|
|
||||||
err := testutils.CreateRCWithRetries(f.ClientSet, f.Namespace.Name, rc)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
err = waitForRCPodsRunning(f, rc)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitForRCPodsRunning(f *framework.Framework, rc *apiv1.ReplicationController) error {
|
|
||||||
return wait.PollImmediate(pollInterval, pollTimeout, func() (bool, error) {
|
|
||||||
podList, err := GetHamsterPods(f)
|
|
||||||
if err != nil {
|
|
||||||
framework.Logf("Error listing pods, retrying: %v", err)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
podsRunning := int32(0)
|
|
||||||
for _, pod := range podList.Items {
|
|
||||||
if pod.Status.Phase == apiv1.PodRunning {
|
|
||||||
podsRunning += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return podsRunning == *rc.Spec.Replicas, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupHamsterJob(f *framework.Framework, cpu, memory string, replicas int32) {
|
|
||||||
job := framework.NewTestJob("notTerminate", "hamster-job", apiv1.RestartPolicyOnFailure,
|
|
||||||
replicas, replicas, nil, 10)
|
|
||||||
job.Spec.Template.Spec.Containers[0] = setupHamsterContainer(cpu, memory)
|
|
||||||
for label, value := range hamsterLabels {
|
|
||||||
job.Spec.Template.Labels[label] = value
|
|
||||||
}
|
|
||||||
err := testutils.CreateJobWithRetries(f.ClientSet, f.Namespace.Name, job)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
err = framework.WaitForAllJobPodsRunning(f.ClientSet, f.Namespace.Name, job.Name, replicas)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupHamsterRS(f *framework.Framework, cpu, memory string, replicas int32) {
|
|
||||||
rs := framework.NewReplicaSet("hamster-rs", f.Namespace.Name, replicas,
|
|
||||||
hamsterLabels, "", "")
|
|
||||||
rs.Spec.Template.Spec.Containers[0] = setupHamsterContainer(cpu, memory)
|
|
||||||
err := createReplicaSetWithRetries(f.ClientSet, f.Namespace.Name, rs)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
err = framework.WaitForReadyReplicaSet(f.ClientSet, f.Namespace.Name, rs.Name)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupHamsterStateful(f *framework.Framework, cpu, memory string, replicas int32) {
|
|
||||||
stateful := framework.NewStatefulSet("hamster-stateful", f.Namespace.Name,
|
|
||||||
"hamster-service", replicas, nil, nil, hamsterLabels)
|
|
||||||
|
|
||||||
stateful.Spec.Template.Spec.Containers[0] = setupHamsterContainer(cpu, memory)
|
|
||||||
err := createStatefulSetSetWithRetries(f.ClientSet, f.Namespace.Name, stateful)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
tester := framework.NewStatefulSetTester(f.ClientSet)
|
|
||||||
tester.WaitForRunningAndReady(*stateful.Spec.Replicas, stateful)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupHamsterContainer(cpu, memory string) apiv1.Container {
|
|
||||||
cpuQuantity := ParseQuantityOrDie(cpu)
|
|
||||||
memoryQuantity := ParseQuantityOrDie(memory)
|
|
||||||
|
|
||||||
return apiv1.Container{
|
|
||||||
Name: "hamster",
|
|
||||||
Image: "k8s.gcr.io/ubuntu-slim:0.1",
|
|
||||||
Resources: apiv1.ResourceRequirements{
|
|
||||||
Requests: apiv1.ResourceList{
|
|
||||||
apiv1.ResourceCPU: cpuQuantity,
|
|
||||||
apiv1.ResourceMemory: memoryQuantity,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Command: []string{"/bin/sh"},
|
|
||||||
Args: []string{"-c", "while true; do sleep 10 ; done"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupPDB(f *framework.Framework, name string, maxUnavailable int) *policyv1beta1.PodDisruptionBudget {
|
|
||||||
maxUnavailableIntstr := intstr.FromInt(maxUnavailable)
|
|
||||||
pdb := &policyv1beta1.PodDisruptionBudget{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: name,
|
|
||||||
},
|
|
||||||
Spec: policyv1beta1.PodDisruptionBudgetSpec{
|
|
||||||
MaxUnavailable: &maxUnavailableIntstr,
|
|
||||||
Selector: &metav1.LabelSelector{
|
|
||||||
MatchLabels: hamsterLabels,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, err := f.ClientSet.PolicyV1beta1().PodDisruptionBudgets(f.Namespace.Name).Create(pdb)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
return pdb
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCurrentPodSetForDeployment(c clientset.Interface, d *appsv1.Deployment) PodSet {
|
|
||||||
podList, err := framework.GetPodsForDeployment(c, d)
|
|
||||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
|
||||||
return MakePodSet(podList)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createReplicaSetWithRetries(c clientset.Interface, namespace string, obj *appsv1.ReplicaSet) error {
|
|
||||||
if obj == nil {
|
|
||||||
return fmt.Errorf("object provided to create is empty")
|
|
||||||
}
|
|
||||||
createFunc := func() (bool, error) {
|
|
||||||
_, err := c.AppsV1().ReplicaSets(namespace).Create(obj)
|
|
||||||
if err == nil || apierrs.IsAlreadyExists(err) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
if testutils.IsRetryableAPIError(err) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, fmt.Errorf("failed to create object with non-retriable error: %v", err)
|
|
||||||
}
|
|
||||||
return testutils.RetryWithExponentialBackOff(createFunc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createStatefulSetSetWithRetries(c clientset.Interface, namespace string, obj *appsv1.StatefulSet) error {
|
|
||||||
if obj == nil {
|
|
||||||
return fmt.Errorf("object provided to create is empty")
|
|
||||||
}
|
|
||||||
createFunc := func() (bool, error) {
|
|
||||||
_, err := c.AppsV1().StatefulSets(namespace).Create(obj)
|
|
||||||
if err == nil || apierrs.IsAlreadyExists(err) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
if testutils.IsRetryableAPIError(err) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, fmt.Errorf("failed to create object with non-retriable error: %v", err)
|
|
||||||
}
|
|
||||||
return testutils.RetryWithExponentialBackOff(createFunc)
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue