Skip to content

Commit dc3c4ed

Browse files
committed
pod resize support in LimitRanger admission plugin
1 parent 1b98fe6 commit dc3c4ed

File tree

3 files changed

+182
-99
lines changed

3 files changed

+182
-99
lines changed

plugin/pkg/admission/limitranger/admission.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,12 @@ func (d *DefaultLimitRangerActions) ValidateLimit(limitRange *corev1.LimitRange,
415415
// SupportsAttributes ignores all calls that do not deal with pod resources or storage requests (PVCs).
416416
// Also ignores any call that has a subresource defined.
417417
func (d *DefaultLimitRangerActions) SupportsAttributes(a admission.Attributes) bool {
418+
// Handle the special case for in-place pod vertical scaling
419+
if a.GetSubresource() == "resize" && a.GetKind().GroupKind() == api.Kind("Pod") && a.GetOperation() == admission.Update {
420+
return true
421+
}
422+
423+
// No other subresources are supported
418424
if a.GetSubresource() != "" {
419425
return false
420426
}

plugin/pkg/admission/limitranger/admission_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,13 @@ import (
3434
"k8s.io/apiserver/pkg/admission"
3535
genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
3636
admissiontesting "k8s.io/apiserver/pkg/admission/testing"
37+
utilfeature "k8s.io/apiserver/pkg/util/feature"
3738
"k8s.io/client-go/informers"
3839
clientset "k8s.io/client-go/kubernetes"
3940
"k8s.io/client-go/kubernetes/fake"
4041
core "k8s.io/client-go/testing"
42+
featuregatetesting "k8s.io/component-base/featuregate/testing"
43+
"k8s.io/kubernetes/pkg/features"
4144

4245
api "k8s.io/kubernetes/pkg/apis/core"
4346
v1 "k8s.io/kubernetes/pkg/apis/core/v1"
@@ -751,7 +754,23 @@ func TestLimitRangerIgnoresSubresource(t *testing.T) {
751754
if err != nil {
752755
t.Errorf("Should have ignored calls to any subresource of pod %v", err)
753756
}
757+
}
758+
759+
func TestLimitRangerAllowPodResize(t *testing.T) {
760+
limitRange := validLimitRangeNoDefaults()
761+
mockClient := newMockClientForTest([]corev1.LimitRange{limitRange})
762+
handler, informerFactory, err := newHandlerForTest(mockClient)
763+
if err != nil {
764+
t.Errorf("unexpected error initializing handler: %v", err)
765+
}
766+
informerFactory.Start(wait.NeverStop)
754767

768+
testPod := validPod("testPod", 1, api.ResourceRequirements{})
769+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.InPlacePodVerticalScaling, true)
770+
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)
771+
if err == nil {
772+
t.Errorf("expect error, but got nil")
773+
}
755774
}
756775

757776
func TestLimitRangerAdmitPod(t *testing.T) {

test/e2e/node/pod_resize.go

Lines changed: 157 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -37,107 +37,165 @@ import (
3737
"github.com/onsi/gomega"
3838
)
3939

40-
func doPodResizeResourceQuotaTests(f *framework.Framework) {
41-
ginkgo.It("pod-resize-resource-quota-test", func(ctx context.Context) {
42-
podClient := e2epod.NewPodClient(f)
43-
resourceQuota := v1.ResourceQuota{
44-
ObjectMeta: metav1.ObjectMeta{
45-
Name: "resize-resource-quota",
46-
Namespace: f.Namespace.Name,
47-
},
48-
Spec: v1.ResourceQuotaSpec{
49-
Hard: v1.ResourceList{
50-
v1.ResourceCPU: resource.MustParse("800m"),
51-
v1.ResourceMemory: resource.MustParse("800Mi"),
52-
},
53-
},
54-
}
55-
containers := []e2epod.ResizableContainerInfo{
56-
{
57-
Name: "c1",
58-
Resources: &e2epod.ContainerResources{CPUReq: "300m", CPULim: "300m", MemReq: "300Mi", MemLim: "300Mi"},
59-
},
60-
}
61-
patchString := `{"spec":{"containers":[
62-
{"name":"c1", "resources":{"requests":{"cpu":"400m","memory":"400Mi"},"limits":{"cpu":"400m","memory":"400Mi"}}}
63-
]}}`
64-
expected := []e2epod.ResizableContainerInfo{
65-
{
66-
Name: "c1",
67-
Resources: &e2epod.ContainerResources{CPUReq: "400m", CPULim: "400m", MemReq: "400Mi", MemLim: "400Mi"},
68-
},
69-
}
70-
patchStringExceedCPU := `{"spec":{"containers":[
71-
{"name":"c1", "resources":{"requests":{"cpu":"600m","memory":"200Mi"},"limits":{"cpu":"600m","memory":"200Mi"}}}
72-
]}}`
73-
patchStringExceedMemory := `{"spec":{"containers":[
74-
{"name":"c1", "resources":{"requests":{"cpu":"250m","memory":"750Mi"},"limits":{"cpu":"250m","memory":"750Mi"}}}
75-
]}}`
76-
77-
ginkgo.By("Creating a ResourceQuota")
78-
_, rqErr := f.ClientSet.CoreV1().ResourceQuotas(f.Namespace.Name).Create(ctx, &resourceQuota, metav1.CreateOptions{})
79-
framework.ExpectNoError(rqErr, "failed to create resource quota")
80-
81-
tStamp := strconv.Itoa(time.Now().Nanosecond())
82-
e2epod.InitDefaultResizePolicy(containers)
83-
e2epod.InitDefaultResizePolicy(expected)
84-
testPod1 := e2epod.MakePodWithResizableContainers(f.Namespace.Name, "testpod1", tStamp, containers)
85-
testPod1 = e2epod.MustMixinRestrictedPodSecurity(testPod1)
86-
testPod2 := e2epod.MakePodWithResizableContainers(f.Namespace.Name, "testpod2", tStamp, containers)
87-
testPod2 = e2epod.MustMixinRestrictedPodSecurity(testPod2)
88-
89-
ginkgo.By("creating pods")
90-
newPod1 := podClient.CreateSync(ctx, testPod1)
91-
newPod2 := podClient.CreateSync(ctx, testPod2)
92-
93-
ginkgo.By("verifying initial pod resources, and policy are as expected")
94-
e2epod.VerifyPodResources(newPod1, containers)
95-
96-
ginkgo.By("patching pod for resize within resource quota")
97-
patchedPod, pErr := f.ClientSet.CoreV1().Pods(newPod1.Namespace).Patch(ctx, newPod1.Name,
98-
types.StrategicMergePatchType, []byte(patchString), metav1.PatchOptions{}, "resize")
99-
framework.ExpectNoError(pErr, "failed to patch pod for resize")
100-
101-
ginkgo.By("verifying pod patched for resize within resource quota")
102-
e2epod.VerifyPodResources(patchedPod, expected)
103-
104-
ginkgo.By("waiting for resize to be actuated")
105-
resizedPod := e2epod.WaitForPodResizeActuation(ctx, f, podClient, newPod1)
106-
e2epod.ExpectPodResized(ctx, f, resizedPod, expected)
107-
108-
ginkgo.By("verifying pod resources after resize")
109-
e2epod.VerifyPodResources(resizedPod, expected)
110-
111-
ginkgo.By("patching pod for resize with memory exceeding resource quota")
112-
_, pErrExceedMemory := f.ClientSet.CoreV1().Pods(resizedPod.Namespace).Patch(ctx,
113-
resizedPod.Name, types.StrategicMergePatchType, []byte(patchStringExceedMemory), metav1.PatchOptions{}, "resize")
114-
gomega.Expect(pErrExceedMemory).To(gomega.HaveOccurred(), "exceeded quota: %s, requested: memory=350Mi, used: memory=700Mi, limited: memory=800Mi",
115-
resourceQuota.Name)
116-
117-
ginkgo.By("verifying pod patched for resize exceeding memory resource quota remains unchanged")
118-
patchedPodExceedMemory, pErrEx2 := podClient.Get(ctx, resizedPod.Name, metav1.GetOptions{})
119-
framework.ExpectNoError(pErrEx2, "failed to get pod post exceed memory resize")
120-
e2epod.VerifyPodResources(patchedPodExceedMemory, expected)
121-
framework.ExpectNoError(e2epod.VerifyPodStatusResources(patchedPodExceedMemory, expected))
40+
func doPodResizeAdmissionPluginsTests(f *framework.Framework) {
41+
testcases := []struct {
42+
name string
43+
enableAdmissionPlugin func(ctx context.Context, f *framework.Framework)
44+
wantMemoryError string
45+
wantCPUError string
46+
}{
47+
{
48+
name: "pod-resize-resource-quota-test",
49+
enableAdmissionPlugin: func(ctx context.Context, f *framework.Framework) {
50+
resourceQuota := v1.ResourceQuota{
51+
ObjectMeta: metav1.ObjectMeta{
52+
Name: "resize-resource-quota",
53+
Namespace: f.Namespace.Name,
54+
},
55+
Spec: v1.ResourceQuotaSpec{
56+
Hard: v1.ResourceList{
57+
v1.ResourceCPU: resource.MustParse("800m"),
58+
v1.ResourceMemory: resource.MustParse("800Mi"),
59+
},
60+
},
61+
}
12262

123-
ginkgo.By(fmt.Sprintf("patching pod %s for resize with CPU exceeding resource quota", resizedPod.Name))
124-
_, pErrExceedCPU := f.ClientSet.CoreV1().Pods(resizedPod.Namespace).Patch(ctx,
125-
resizedPod.Name, types.StrategicMergePatchType, []byte(patchStringExceedCPU), metav1.PatchOptions{}, "resize")
126-
gomega.Expect(pErrExceedCPU).To(gomega.HaveOccurred(), "exceeded quota: %s, requested: cpu=200m, used: cpu=700m, limited: cpu=800m",
127-
resourceQuota.Name)
63+
ginkgo.By("Creating a ResourceQuota")
64+
_, rqErr := f.ClientSet.CoreV1().ResourceQuotas(f.Namespace.Name).Create(ctx, &resourceQuota, metav1.CreateOptions{})
65+
framework.ExpectNoError(rqErr, "failed to create resource quota")
66+
},
67+
wantMemoryError: "exceeded quota: resize-resource-quota, requested: memory=350Mi, used: memory=700Mi, limited: memory=800Mi",
68+
wantCPUError: "exceeded quota: resize-resource-quota, requested: cpu=200m, used: cpu=700m, limited: cpu=800m",
69+
},
70+
{
71+
name: "pod-resize-limit-ranger-test",
72+
enableAdmissionPlugin: func(ctx context.Context, f *framework.Framework) {
73+
lr := v1.LimitRange{
74+
ObjectMeta: metav1.ObjectMeta{
75+
Name: "resize-limit-ranger",
76+
Namespace: f.Namespace.Name,
77+
},
78+
Spec: v1.LimitRangeSpec{
79+
Limits: []v1.LimitRangeItem{
80+
{
81+
Type: v1.LimitTypeContainer,
82+
Max: v1.ResourceList{
83+
v1.ResourceCPU: resource.MustParse("500m"),
84+
v1.ResourceMemory: resource.MustParse("500Mi"),
85+
},
86+
Min: v1.ResourceList{
87+
v1.ResourceCPU: resource.MustParse("50m"),
88+
v1.ResourceMemory: resource.MustParse("50Mi"),
89+
},
90+
Default: v1.ResourceList{
91+
v1.ResourceCPU: resource.MustParse("100m"),
92+
v1.ResourceMemory: resource.MustParse("100Mi"),
93+
},
94+
DefaultRequest: v1.ResourceList{
95+
v1.ResourceCPU: resource.MustParse("50m"),
96+
v1.ResourceMemory: resource.MustParse("50Mi"),
97+
},
98+
},
99+
},
100+
},
101+
}
128102

129-
ginkgo.By("verifying pod patched for resize exceeding CPU resource quota remains unchanged")
130-
patchedPodExceedCPU, pErrEx1 := podClient.Get(ctx, resizedPod.Name, metav1.GetOptions{})
131-
framework.ExpectNoError(pErrEx1, "failed to get pod post exceed CPU resize")
132-
e2epod.VerifyPodResources(patchedPodExceedCPU, expected)
133-
framework.ExpectNoError(e2epod.VerifyPodStatusResources(patchedPodExceedMemory, expected))
103+
ginkgo.By("Creating a LimitRanger")
104+
_, lrErr := f.ClientSet.CoreV1().LimitRanges(f.Namespace.Name).Create(ctx, &lr, metav1.CreateOptions{})
105+
framework.ExpectNoError(lrErr, "failed to create limit ranger")
106+
},
107+
wantMemoryError: "forbidden: maximum memory usage per Container is 500Mi, but limit is 750Mi",
108+
wantCPUError: "forbidden: maximum cpu usage per Container is 500m, but limit is 600m",
109+
},
110+
}
111+
112+
for _, tc := range testcases {
113+
ginkgo.It(tc.name, func(ctx context.Context) {
114+
containers := []e2epod.ResizableContainerInfo{
115+
{
116+
Name: "c1",
117+
Resources: &e2epod.ContainerResources{CPUReq: "300m", CPULim: "300m", MemReq: "300Mi", MemLim: "300Mi"},
118+
},
119+
}
120+
patchString := `{"spec":{"containers":[
121+
{"name":"c1", "resources":{"requests":{"cpu":"400m","memory":"400Mi"},"limits":{"cpu":"400m","memory":"400Mi"}}}
122+
]}}`
123+
expected := []e2epod.ResizableContainerInfo{
124+
{
125+
Name: "c1",
126+
Resources: &e2epod.ContainerResources{CPUReq: "400m", CPULim: "400m", MemReq: "400Mi", MemLim: "400Mi"},
127+
},
128+
}
129+
patchStringExceedCPU := `{"spec":{"containers":[
130+
{"name":"c1", "resources":{"requests":{"cpu":"600m","memory":"200Mi"},"limits":{"cpu":"600m","memory":"200Mi"}}}
131+
]}}`
132+
patchStringExceedMemory := `{"spec":{"containers":[
133+
{"name":"c1", "resources":{"requests":{"cpu":"250m","memory":"750Mi"},"limits":{"cpu":"250m","memory":"750Mi"}}}
134+
]}}`
135+
136+
tc.enableAdmissionPlugin(ctx, f)
137+
138+
tStamp := strconv.Itoa(time.Now().Nanosecond())
139+
e2epod.InitDefaultResizePolicy(containers)
140+
e2epod.InitDefaultResizePolicy(expected)
141+
testPod1 := e2epod.MakePodWithResizableContainers(f.Namespace.Name, "testpod1", tStamp, containers)
142+
testPod1 = e2epod.MustMixinRestrictedPodSecurity(testPod1)
143+
testPod2 := e2epod.MakePodWithResizableContainers(f.Namespace.Name, "testpod2", tStamp, containers)
144+
testPod2 = e2epod.MustMixinRestrictedPodSecurity(testPod2)
145+
146+
ginkgo.By("creating pods")
147+
podClient := e2epod.NewPodClient(f)
148+
newPod1 := podClient.CreateSync(ctx, testPod1)
149+
newPod2 := podClient.CreateSync(ctx, testPod2)
150+
151+
ginkgo.By("verifying initial pod resources, and policy are as expected")
152+
e2epod.VerifyPodResources(newPod1, containers)
153+
154+
ginkgo.By("patching pod for resize within resource quota")
155+
patchedPod, pErr := f.ClientSet.CoreV1().Pods(newPod1.Namespace).Patch(ctx, newPod1.Name,
156+
types.StrategicMergePatchType, []byte(patchString), metav1.PatchOptions{}, "resize")
157+
framework.ExpectNoError(pErr, "failed to patch pod for resize")
158+
159+
ginkgo.By("verifying pod patched for resize within resource quota")
160+
e2epod.VerifyPodResources(patchedPod, expected)
161+
162+
ginkgo.By("waiting for resize to be actuated")
163+
resizedPod := e2epod.WaitForPodResizeActuation(ctx, f, podClient, newPod1)
164+
e2epod.ExpectPodResized(ctx, f, resizedPod, expected)
165+
166+
ginkgo.By("verifying pod resources after resize")
167+
e2epod.VerifyPodResources(resizedPod, expected)
168+
169+
ginkgo.By("patching pod for resize with memory exceeding resource quota")
170+
_, pErrExceedMemory := f.ClientSet.CoreV1().Pods(resizedPod.Namespace).Patch(ctx,
171+
resizedPod.Name, types.StrategicMergePatchType, []byte(patchStringExceedMemory), metav1.PatchOptions{}, "resize")
172+
gomega.Expect(pErrExceedMemory).To(gomega.HaveOccurred(), tc.wantMemoryError)
173+
174+
ginkgo.By("verifying pod patched for resize exceeding memory resource quota remains unchanged")
175+
patchedPodExceedMemory, pErrEx2 := podClient.Get(ctx, resizedPod.Name, metav1.GetOptions{})
176+
framework.ExpectNoError(pErrEx2, "failed to get pod post exceed memory resize")
177+
e2epod.VerifyPodResources(patchedPodExceedMemory, expected)
178+
framework.ExpectNoError(e2epod.VerifyPodStatusResources(patchedPodExceedMemory, expected))
179+
180+
ginkgo.By(fmt.Sprintf("patching pod %s for resize with CPU exceeding resource quota", resizedPod.Name))
181+
_, pErrExceedCPU := f.ClientSet.CoreV1().Pods(resizedPod.Namespace).Patch(ctx,
182+
resizedPod.Name, types.StrategicMergePatchType, []byte(patchStringExceedCPU), metav1.PatchOptions{}, "resize")
183+
gomega.Expect(pErrExceedCPU).To(gomega.HaveOccurred(), tc.wantCPUError)
184+
185+
ginkgo.By("verifying pod patched for resize exceeding CPU resource quota remains unchanged")
186+
patchedPodExceedCPU, pErrEx1 := podClient.Get(ctx, resizedPod.Name, metav1.GetOptions{})
187+
framework.ExpectNoError(pErrEx1, "failed to get pod post exceed CPU resize")
188+
e2epod.VerifyPodResources(patchedPodExceedCPU, expected)
189+
framework.ExpectNoError(e2epod.VerifyPodStatusResources(patchedPodExceedMemory, expected))
190+
191+
ginkgo.By("deleting pods")
192+
delErr1 := e2epod.DeletePodWithWait(ctx, f.ClientSet, newPod1)
193+
framework.ExpectNoError(delErr1, "failed to delete pod %s", newPod1.Name)
194+
delErr2 := e2epod.DeletePodWithWait(ctx, f.ClientSet, newPod2)
195+
framework.ExpectNoError(delErr2, "failed to delete pod %s", newPod2.Name)
196+
})
197+
}
134198

135-
ginkgo.By("deleting pods")
136-
delErr1 := e2epod.DeletePodWithWait(ctx, f.ClientSet, newPod1)
137-
framework.ExpectNoError(delErr1, "failed to delete pod %s", newPod1.Name)
138-
delErr2 := e2epod.DeletePodWithWait(ctx, f.ClientSet, newPod2)
139-
framework.ExpectNoError(delErr2, "failed to delete pod %s", newPod2.Name)
140-
})
141199
}
142200

143201
func doPodResizeSchedulerTests(f *framework.Framework) {
@@ -324,5 +382,5 @@ var _ = SIGDescribe("Pod InPlace Resize Container", feature.InPlacePodVerticalSc
324382
e2eskipper.Skipf("runtime does not support InPlacePodVerticalScaling -- skipping")
325383
}
326384
})
327-
doPodResizeResourceQuotaTests(f)
385+
doPodResizeAdmissionPluginsTests(f)
328386
})

0 commit comments

Comments
 (0)