pod resize support in LimitRanger admission plugin

This commit is contained in:
Anish Shah 2024-10-24 16:26:09 -07:00
parent 1b98fe6079
commit dc3c4ed559
3 changed files with 170 additions and 87 deletions

View File

@ -415,6 +415,12 @@ func (d *DefaultLimitRangerActions) ValidateLimit(limitRange *corev1.LimitRange,
// SupportsAttributes ignores all calls that do not deal with pod resources or storage requests (PVCs). // SupportsAttributes ignores all calls that do not deal with pod resources or storage requests (PVCs).
// Also ignores any call that has a subresource defined. // Also ignores any call that has a subresource defined.
func (d *DefaultLimitRangerActions) SupportsAttributes(a admission.Attributes) bool { func (d *DefaultLimitRangerActions) SupportsAttributes(a admission.Attributes) bool {
// Handle the special case for in-place pod vertical scaling
if a.GetSubresource() == "resize" && a.GetKind().GroupKind() == api.Kind("Pod") && a.GetOperation() == admission.Update {
return true
}
// No other subresources are supported
if a.GetSubresource() != "" { if a.GetSubresource() != "" {
return false return false
} }

View File

@ -34,10 +34,13 @@ import (
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer" genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
admissiontesting "k8s.io/apiserver/pkg/admission/testing" admissiontesting "k8s.io/apiserver/pkg/admission/testing"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
core "k8s.io/client-go/testing" core "k8s.io/client-go/testing"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/pkg/features"
api "k8s.io/kubernetes/pkg/apis/core" api "k8s.io/kubernetes/pkg/apis/core"
v1 "k8s.io/kubernetes/pkg/apis/core/v1" v1 "k8s.io/kubernetes/pkg/apis/core/v1"
@ -751,7 +754,23 @@ func TestLimitRangerIgnoresSubresource(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("Should have ignored calls to any subresource of pod %v", err) t.Errorf("Should have ignored calls to any subresource of pod %v", err)
} }
}
func TestLimitRangerAllowPodResize(t *testing.T) {
limitRange := validLimitRangeNoDefaults()
mockClient := newMockClientForTest([]corev1.LimitRange{limitRange})
handler, informerFactory, err := newHandlerForTest(mockClient)
if err != nil {
t.Errorf("unexpected error initializing handler: %v", err)
}
informerFactory.Start(wait.NeverStop)
testPod := validPod("testPod", 1, api.ResourceRequirements{})
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, true)
err = handler.Validate(context.TODO(), admission.NewAttributesRecord(&testPod, nil, api.Kind("Pod").WithVersion("version"), limitRange.Namespace, "testPod", api.Resource("pods").WithVersion("version"), "resize", admission.Update, &metav1.UpdateOptions{}, false, nil), nil)
if err == nil {
t.Errorf("expect error, but got nil")
}
} }
func TestLimitRangerAdmitPod(t *testing.T) { func TestLimitRangerAdmitPod(t *testing.T) {

View File

@ -37,9 +37,16 @@ import (
"github.com/onsi/gomega" "github.com/onsi/gomega"
) )
func doPodResizeResourceQuotaTests(f *framework.Framework) { func doPodResizeAdmissionPluginsTests(f *framework.Framework) {
ginkgo.It("pod-resize-resource-quota-test", func(ctx context.Context) { testcases := []struct {
podClient := e2epod.NewPodClient(f) name string
enableAdmissionPlugin func(ctx context.Context, f *framework.Framework)
wantMemoryError string
wantCPUError string
}{
{
name: "pod-resize-resource-quota-test",
enableAdmissionPlugin: func(ctx context.Context, f *framework.Framework) {
resourceQuota := v1.ResourceQuota{ resourceQuota := v1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "resize-resource-quota", Name: "resize-resource-quota",
@ -52,6 +59,58 @@ func doPodResizeResourceQuotaTests(f *framework.Framework) {
}, },
}, },
} }
ginkgo.By("Creating a ResourceQuota")
_, rqErr := f.ClientSet.CoreV1().ResourceQuotas(f.Namespace.Name).Create(ctx, &resourceQuota, metav1.CreateOptions{})
framework.ExpectNoError(rqErr, "failed to create resource quota")
},
wantMemoryError: "exceeded quota: resize-resource-quota, requested: memory=350Mi, used: memory=700Mi, limited: memory=800Mi",
wantCPUError: "exceeded quota: resize-resource-quota, requested: cpu=200m, used: cpu=700m, limited: cpu=800m",
},
{
name: "pod-resize-limit-ranger-test",
enableAdmissionPlugin: func(ctx context.Context, f *framework.Framework) {
lr := v1.LimitRange{
ObjectMeta: metav1.ObjectMeta{
Name: "resize-limit-ranger",
Namespace: f.Namespace.Name,
},
Spec: v1.LimitRangeSpec{
Limits: []v1.LimitRangeItem{
{
Type: v1.LimitTypeContainer,
Max: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("500m"),
v1.ResourceMemory: resource.MustParse("500Mi"),
},
Min: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("50m"),
v1.ResourceMemory: resource.MustParse("50Mi"),
},
Default: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("100m"),
v1.ResourceMemory: resource.MustParse("100Mi"),
},
DefaultRequest: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("50m"),
v1.ResourceMemory: resource.MustParse("50Mi"),
},
},
},
},
}
ginkgo.By("Creating a LimitRanger")
_, lrErr := f.ClientSet.CoreV1().LimitRanges(f.Namespace.Name).Create(ctx, &lr, metav1.CreateOptions{})
framework.ExpectNoError(lrErr, "failed to create limit ranger")
},
wantMemoryError: "forbidden: maximum memory usage per Container is 500Mi, but limit is 750Mi",
wantCPUError: "forbidden: maximum cpu usage per Container is 500m, but limit is 600m",
},
}
for _, tc := range testcases {
ginkgo.It(tc.name, func(ctx context.Context) {
containers := []e2epod.ResizableContainerInfo{ containers := []e2epod.ResizableContainerInfo{
{ {
Name: "c1", Name: "c1",
@ -74,9 +133,7 @@ func doPodResizeResourceQuotaTests(f *framework.Framework) {
{"name":"c1", "resources":{"requests":{"cpu":"250m","memory":"750Mi"},"limits":{"cpu":"250m","memory":"750Mi"}}} {"name":"c1", "resources":{"requests":{"cpu":"250m","memory":"750Mi"},"limits":{"cpu":"250m","memory":"750Mi"}}}
]}}` ]}}`
ginkgo.By("Creating a ResourceQuota") tc.enableAdmissionPlugin(ctx, f)
_, rqErr := f.ClientSet.CoreV1().ResourceQuotas(f.Namespace.Name).Create(ctx, &resourceQuota, metav1.CreateOptions{})
framework.ExpectNoError(rqErr, "failed to create resource quota")
tStamp := strconv.Itoa(time.Now().Nanosecond()) tStamp := strconv.Itoa(time.Now().Nanosecond())
e2epod.InitDefaultResizePolicy(containers) e2epod.InitDefaultResizePolicy(containers)
@ -87,6 +144,7 @@ func doPodResizeResourceQuotaTests(f *framework.Framework) {
testPod2 = e2epod.MustMixinRestrictedPodSecurity(testPod2) testPod2 = e2epod.MustMixinRestrictedPodSecurity(testPod2)
ginkgo.By("creating pods") ginkgo.By("creating pods")
podClient := e2epod.NewPodClient(f)
newPod1 := podClient.CreateSync(ctx, testPod1) newPod1 := podClient.CreateSync(ctx, testPod1)
newPod2 := podClient.CreateSync(ctx, testPod2) newPod2 := podClient.CreateSync(ctx, testPod2)
@ -111,8 +169,7 @@ func doPodResizeResourceQuotaTests(f *framework.Framework) {
ginkgo.By("patching pod for resize with memory exceeding resource quota") ginkgo.By("patching pod for resize with memory exceeding resource quota")
_, pErrExceedMemory := f.ClientSet.CoreV1().Pods(resizedPod.Namespace).Patch(ctx, _, pErrExceedMemory := f.ClientSet.CoreV1().Pods(resizedPod.Namespace).Patch(ctx,
resizedPod.Name, types.StrategicMergePatchType, []byte(patchStringExceedMemory), metav1.PatchOptions{}, "resize") resizedPod.Name, types.StrategicMergePatchType, []byte(patchStringExceedMemory), metav1.PatchOptions{}, "resize")
gomega.Expect(pErrExceedMemory).To(gomega.HaveOccurred(), "exceeded quota: %s, requested: memory=350Mi, used: memory=700Mi, limited: memory=800Mi", gomega.Expect(pErrExceedMemory).To(gomega.HaveOccurred(), tc.wantMemoryError)
resourceQuota.Name)
ginkgo.By("verifying pod patched for resize exceeding memory resource quota remains unchanged") ginkgo.By("verifying pod patched for resize exceeding memory resource quota remains unchanged")
patchedPodExceedMemory, pErrEx2 := podClient.Get(ctx, resizedPod.Name, metav1.GetOptions{}) patchedPodExceedMemory, pErrEx2 := podClient.Get(ctx, resizedPod.Name, metav1.GetOptions{})
@ -123,8 +180,7 @@ func doPodResizeResourceQuotaTests(f *framework.Framework) {
ginkgo.By(fmt.Sprintf("patching pod %s for resize with CPU exceeding resource quota", resizedPod.Name)) ginkgo.By(fmt.Sprintf("patching pod %s for resize with CPU exceeding resource quota", resizedPod.Name))
_, pErrExceedCPU := f.ClientSet.CoreV1().Pods(resizedPod.Namespace).Patch(ctx, _, pErrExceedCPU := f.ClientSet.CoreV1().Pods(resizedPod.Namespace).Patch(ctx,
resizedPod.Name, types.StrategicMergePatchType, []byte(patchStringExceedCPU), metav1.PatchOptions{}, "resize") resizedPod.Name, types.StrategicMergePatchType, []byte(patchStringExceedCPU), metav1.PatchOptions{}, "resize")
gomega.Expect(pErrExceedCPU).To(gomega.HaveOccurred(), "exceeded quota: %s, requested: cpu=200m, used: cpu=700m, limited: cpu=800m", gomega.Expect(pErrExceedCPU).To(gomega.HaveOccurred(), tc.wantCPUError)
resourceQuota.Name)
ginkgo.By("verifying pod patched for resize exceeding CPU resource quota remains unchanged") ginkgo.By("verifying pod patched for resize exceeding CPU resource quota remains unchanged")
patchedPodExceedCPU, pErrEx1 := podClient.Get(ctx, resizedPod.Name, metav1.GetOptions{}) patchedPodExceedCPU, pErrEx1 := podClient.Get(ctx, resizedPod.Name, metav1.GetOptions{})
@ -140,6 +196,8 @@ func doPodResizeResourceQuotaTests(f *framework.Framework) {
}) })
} }
}
func doPodResizeSchedulerTests(f *framework.Framework) { func doPodResizeSchedulerTests(f *framework.Framework) {
ginkgo.It("pod-resize-scheduler-tests", func(ctx context.Context) { ginkgo.It("pod-resize-scheduler-tests", func(ctx context.Context) {
podClient := e2epod.NewPodClient(f) podClient := e2epod.NewPodClient(f)
@ -324,5 +382,5 @@ var _ = SIGDescribe("Pod InPlace Resize Container", feature.InPlacePodVerticalSc
e2eskipper.Skipf("runtime does not support InPlacePodVerticalScaling -- skipping") e2eskipper.Skipf("runtime does not support InPlacePodVerticalScaling -- skipping")
} }
}) })
doPodResizeResourceQuotaTests(f) doPodResizeAdmissionPluginsTests(f)
}) })