Skip to content

Commit d67e654

Browse files
authored
Merge pull request kubernetes#124227 from iholder101/in-pod-vertical-scaling/extended-resources
[FG:InPlacePodVerticalScaling] Add extended resources to ContainerStatuses[i].Resources
2 parents a8fd407 + 85068bc commit d67e654

File tree

3 files changed

+264
-10
lines changed

3 files changed

+264
-10
lines changed

pkg/kubelet/kubelet_pods.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2094,6 +2094,18 @@ func (kl *Kubelet) convertToAPIContainerStatuses(pod *v1.Pod, podStatus *kubecon
20942094
if oldStatus.Resources == nil {
20952095
oldStatus.Resources = &v1.ResourceRequirements{}
20962096
}
2097+
2098+
convertCustomResources := func(inResources, outResources v1.ResourceList) {
2099+
for resourceName, resourceQuantity := range inResources {
2100+
if resourceName == v1.ResourceCPU || resourceName == v1.ResourceMemory ||
2101+
resourceName == v1.ResourceStorage || resourceName == v1.ResourceEphemeralStorage {
2102+
continue
2103+
}
2104+
2105+
outResources[resourceName] = resourceQuantity.DeepCopy()
2106+
}
2107+
}
2108+
20972109
// Convert Limits
20982110
if container.Resources.Limits != nil {
20992111
limits = make(v1.ResourceList)
@@ -2110,6 +2122,11 @@ func (kl *Kubelet) convertToAPIContainerStatuses(pod *v1.Pod, podStatus *kubecon
21102122
if ephemeralStorage, found := container.Resources.Limits[v1.ResourceEphemeralStorage]; found {
21112123
limits[v1.ResourceEphemeralStorage] = ephemeralStorage.DeepCopy()
21122124
}
2125+
if storage, found := container.Resources.Limits[v1.ResourceStorage]; found {
2126+
limits[v1.ResourceStorage] = storage.DeepCopy()
2127+
}
2128+
2129+
convertCustomResources(container.Resources.Limits, limits)
21132130
}
21142131
// Convert Requests
21152132
if status.AllocatedResources != nil {
@@ -2125,9 +2142,13 @@ func (kl *Kubelet) convertToAPIContainerStatuses(pod *v1.Pod, podStatus *kubecon
21252142
if ephemeralStorage, found := status.AllocatedResources[v1.ResourceEphemeralStorage]; found {
21262143
requests[v1.ResourceEphemeralStorage] = ephemeralStorage.DeepCopy()
21272144
}
2145+
if storage, found := status.AllocatedResources[v1.ResourceStorage]; found {
2146+
requests[v1.ResourceStorage] = storage.DeepCopy()
2147+
}
2148+
2149+
convertCustomResources(status.AllocatedResources, requests)
21282150
}
2129-
//TODO(vinaykul,derekwaynecarr,InPlacePodVerticalScaling): Update this to include extended resources in
2130-
// addition to CPU, memory, ephemeral storage. Add test case for extended resources.
2151+
21312152
resources := &v1.ResourceRequirements{
21322153
Limits: limits,
21332154
Requests: requests,

pkg/kubelet/kubelet_pods_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4585,8 +4585,24 @@ func TestConvertToAPIContainerStatusesForResources(t *testing.T) {
45854585
CPU2AndMem2G := v1.ResourceList{v1.ResourceCPU: resource.MustParse("2"), v1.ResourceMemory: resource.MustParse("2Gi")}
45864586
CPU1AndMem1GAndStorage2G := CPU1AndMem1G.DeepCopy()
45874587
CPU1AndMem1GAndStorage2G[v1.ResourceEphemeralStorage] = resource.MustParse("2Gi")
4588+
CPU1AndMem1GAndStorage2G[v1.ResourceStorage] = resource.MustParse("2Gi")
45884589
CPU2AndMem2GAndStorage2G := CPU2AndMem2G.DeepCopy()
45894590
CPU2AndMem2GAndStorage2G[v1.ResourceEphemeralStorage] = resource.MustParse("2Gi")
4591+
CPU2AndMem2GAndStorage2G[v1.ResourceStorage] = resource.MustParse("2Gi")
4592+
4593+
addExtendedResource := func(list v1.ResourceList) v1.ResourceList {
4594+
const stubCustomResource = v1.ResourceName("dummy.io/dummy")
4595+
4596+
withExtendedResource := list.DeepCopy()
4597+
for _, resourceName := range []v1.ResourceName{v1.ResourceMemory, v1.ResourceCPU} {
4598+
if _, exists := withExtendedResource[resourceName]; !exists {
4599+
withExtendedResource[resourceName] = resource.MustParse("0")
4600+
}
4601+
}
4602+
4603+
withExtendedResource[stubCustomResource] = resource.MustParse("1")
4604+
return withExtendedResource
4605+
}
45904606

45914607
testKubelet := newTestKubelet(t, false)
45924608
defer testKubelet.Cleanup()
@@ -4734,6 +4750,98 @@ func TestConvertToAPIContainerStatusesForResources(t *testing.T) {
47344750
},
47354751
},
47364752
},
4753+
"BestEffort QoSPod with extended resources": {
4754+
Resources: []v1.ResourceRequirements{{Requests: addExtendedResource(v1.ResourceList{})}},
4755+
OldStatus: []v1.ContainerStatus{
4756+
{
4757+
Name: testContainerName,
4758+
Image: "img",
4759+
ImageID: "img1234",
4760+
State: v1.ContainerState{Running: &v1.ContainerStateRunning{}},
4761+
Resources: &v1.ResourceRequirements{},
4762+
},
4763+
},
4764+
Expected: []v1.ContainerStatus{
4765+
{
4766+
Name: testContainerName,
4767+
ContainerID: testContainerID.String(),
4768+
Image: "img",
4769+
ImageID: "img1234",
4770+
State: v1.ContainerState{Running: &v1.ContainerStateRunning{StartedAt: metav1.NewTime(nowTime)}},
4771+
AllocatedResources: addExtendedResource(v1.ResourceList{}),
4772+
Resources: &v1.ResourceRequirements{Requests: addExtendedResource(v1.ResourceList{})},
4773+
},
4774+
},
4775+
},
4776+
"BurstableQoSPod with extended resources": {
4777+
Resources: []v1.ResourceRequirements{{Requests: addExtendedResource(CPU1AndMem1G)}},
4778+
OldStatus: []v1.ContainerStatus{
4779+
{
4780+
Name: testContainerName,
4781+
Image: "img",
4782+
ImageID: "img1234",
4783+
State: v1.ContainerState{Running: &v1.ContainerStateRunning{}},
4784+
Resources: &v1.ResourceRequirements{},
4785+
},
4786+
},
4787+
Expected: []v1.ContainerStatus{
4788+
{
4789+
Name: testContainerName,
4790+
ContainerID: testContainerID.String(),
4791+
Image: "img",
4792+
ImageID: "img1234",
4793+
State: v1.ContainerState{Running: &v1.ContainerStateRunning{StartedAt: metav1.NewTime(nowTime)}},
4794+
AllocatedResources: addExtendedResource(CPU1AndMem1G),
4795+
Resources: &v1.ResourceRequirements{Requests: addExtendedResource(CPU1AndMem1G)},
4796+
},
4797+
},
4798+
},
4799+
"BurstableQoSPod with storage, ephemeral storage and extended resources": {
4800+
Resources: []v1.ResourceRequirements{{Requests: addExtendedResource(CPU1AndMem1GAndStorage2G)}},
4801+
OldStatus: []v1.ContainerStatus{
4802+
{
4803+
Name: testContainerName,
4804+
Image: "img",
4805+
ImageID: "img1234",
4806+
State: v1.ContainerState{Running: &v1.ContainerStateRunning{}},
4807+
Resources: &v1.ResourceRequirements{},
4808+
},
4809+
},
4810+
Expected: []v1.ContainerStatus{
4811+
{
4812+
Name: testContainerName,
4813+
ContainerID: testContainerID.String(),
4814+
Image: "img",
4815+
ImageID: "img1234",
4816+
State: v1.ContainerState{Running: &v1.ContainerStateRunning{StartedAt: metav1.NewTime(nowTime)}},
4817+
AllocatedResources: addExtendedResource(CPU1AndMem1GAndStorage2G),
4818+
Resources: &v1.ResourceRequirements{Requests: addExtendedResource(CPU1AndMem1GAndStorage2G)},
4819+
},
4820+
},
4821+
},
4822+
"GuaranteedQoSPod with extended resources": {
4823+
Resources: []v1.ResourceRequirements{{Requests: addExtendedResource(CPU1AndMem1G), Limits: addExtendedResource(CPU1AndMem1G)}},
4824+
OldStatus: []v1.ContainerStatus{
4825+
{
4826+
Name: testContainerName,
4827+
Image: "img",
4828+
ImageID: "img1234",
4829+
State: v1.ContainerState{Running: &v1.ContainerStateRunning{}},
4830+
Resources: &v1.ResourceRequirements{},
4831+
},
4832+
},
4833+
Expected: []v1.ContainerStatus{
4834+
{
4835+
Name: testContainerName,
4836+
ContainerID: testContainerID.String(),
4837+
Image: "img",
4838+
ImageID: "img1234",
4839+
State: v1.ContainerState{Running: &v1.ContainerStateRunning{StartedAt: metav1.NewTime(nowTime)}},
4840+
AllocatedResources: addExtendedResource(CPU1AndMem1G),
4841+
Resources: &v1.ResourceRequirements{Requests: addExtendedResource(CPU1AndMem1G), Limits: addExtendedResource(CPU1AndMem1G)},
4842+
},
4843+
},
4844+
},
47374845
} {
47384846
tPod := testPod.DeepCopy()
47394847
tPod.Name = fmt.Sprintf("%s-%d", testPod.Name, idx)

test/e2e/node/pod_resize.go

Lines changed: 133 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3232
"k8s.io/apimachinery/pkg/labels"
3333
"k8s.io/apimachinery/pkg/types"
34+
"k8s.io/apimachinery/pkg/util/strategicpatch"
3435
clientset "k8s.io/client-go/kubernetes"
3536
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
3637
resourceapi "k8s.io/kubernetes/pkg/api/v1/resource"
@@ -63,14 +64,16 @@ const (
6364

6465
PollInterval time.Duration = 2 * time.Second
6566
PollTimeout time.Duration = 4 * time.Minute
67+
68+
fakeExtendedResource = "dummy.com/dummy"
6669
)
6770

6871
type ContainerResources struct {
69-
CPUReq, CPULim, MemReq, MemLim, EphStorReq, EphStorLim string
72+
CPUReq, CPULim, MemReq, MemLim, EphStorReq, EphStorLim, ExtendedResourceReq, ExtendedResourceLim string
7073
}
7174

7275
type ContainerAllocations struct {
73-
CPUAlloc, MemAlloc, ephStorAlloc string
76+
CPUAlloc, MemAlloc, ephStorAlloc, ExtendedResourceAlloc string
7477
}
7578

7679
type TestContainerInfo struct {
@@ -146,6 +149,9 @@ func getTestResourceInfo(tcInfo TestContainerInfo) (v1.ResourceRequirements, v1.
146149
if tcInfo.Resources.EphStorLim != "" {
147150
lim[v1.ResourceEphemeralStorage] = resource.MustParse(tcInfo.Resources.EphStorLim)
148151
}
152+
if tcInfo.Resources.ExtendedResourceLim != "" {
153+
lim[fakeExtendedResource] = resource.MustParse(tcInfo.Resources.ExtendedResourceLim)
154+
}
149155
if tcInfo.Resources.CPUReq != "" {
150156
req[v1.ResourceCPU] = resource.MustParse(tcInfo.Resources.CPUReq)
151157
}
@@ -155,6 +161,9 @@ func getTestResourceInfo(tcInfo TestContainerInfo) (v1.ResourceRequirements, v1.
155161
if tcInfo.Resources.EphStorReq != "" {
156162
req[v1.ResourceEphemeralStorage] = resource.MustParse(tcInfo.Resources.EphStorReq)
157163
}
164+
if tcInfo.Resources.ExtendedResourceReq != "" {
165+
req[fakeExtendedResource] = resource.MustParse(tcInfo.Resources.ExtendedResourceReq)
166+
}
158167
res = v1.ResourceRequirements{Limits: lim, Requests: req}
159168
}
160169
if tcInfo.Allocations != nil {
@@ -168,7 +177,9 @@ func getTestResourceInfo(tcInfo TestContainerInfo) (v1.ResourceRequirements, v1.
168177
if tcInfo.Allocations.ephStorAlloc != "" {
169178
alloc[v1.ResourceEphemeralStorage] = resource.MustParse(tcInfo.Allocations.ephStorAlloc)
170179
}
171-
180+
if tcInfo.Allocations.ExtendedResourceAlloc != "" {
181+
alloc[fakeExtendedResource] = resource.MustParse(tcInfo.Allocations.ExtendedResourceAlloc)
182+
}
172183
}
173184
if tcInfo.CPUPolicy != nil {
174185
cpuPol := v1.ContainerResizePolicy{ResourceName: v1.ResourceCPU, RestartPolicy: *tcInfo.CPUPolicy}
@@ -318,7 +329,8 @@ func verifyPodAllocations(pod *v1.Pod, tcInfo []TestContainerInfo, flagError boo
318329
cStatus := cStatusMap[ci.Name]
319330
if ci.Allocations == nil {
320331
if ci.Resources != nil {
321-
alloc := &ContainerAllocations{CPUAlloc: ci.Resources.CPUReq, MemAlloc: ci.Resources.MemReq}
332+
alloc := &ContainerAllocations{CPUAlloc: ci.Resources.CPUReq, MemAlloc: ci.Resources.MemReq,
333+
ExtendedResourceAlloc: ci.Resources.ExtendedResourceReq}
322334
ci.Allocations = alloc
323335
defer func() {
324336
ci.Allocations = nil
@@ -571,18 +583,92 @@ func genPatchString(containers []TestContainerInfo) (string, error) {
571583
return string(patchBytes), nil
572584
}
573585

586+
func patchNode(ctx context.Context, client clientset.Interface, old *v1.Node, new *v1.Node) error {
587+
oldData, err := json.Marshal(old)
588+
if err != nil {
589+
return err
590+
}
591+
592+
newData, err := json.Marshal(new)
593+
if err != nil {
594+
return err
595+
}
596+
patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, &v1.Node{})
597+
if err != nil {
598+
return fmt.Errorf("failed to create merge patch for node %q: %w", old.Name, err)
599+
}
600+
_, err = client.CoreV1().Nodes().Patch(ctx, old.Name, types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{}, "status")
601+
return err
602+
}
603+
604+
func addExtendedResource(clientSet clientset.Interface, nodeName, extendedResourceName string, extendedResourceQuantity resource.Quantity) {
605+
extendedResource := v1.ResourceName(extendedResourceName)
606+
607+
ginkgo.By("Adding a custom resource")
608+
OriginalNode, err := clientSet.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{})
609+
framework.ExpectNoError(err)
610+
611+
node := OriginalNode.DeepCopy()
612+
node.Status.Capacity[extendedResource] = extendedResourceQuantity
613+
node.Status.Allocatable[extendedResource] = extendedResourceQuantity
614+
err = patchNode(context.Background(), clientSet, OriginalNode.DeepCopy(), node)
615+
framework.ExpectNoError(err)
616+
617+
gomega.Eventually(func() error {
618+
node, err = clientSet.CoreV1().Nodes().Get(context.Background(), node.Name, metav1.GetOptions{})
619+
framework.ExpectNoError(err)
620+
621+
fakeResourceCapacity, exists := node.Status.Capacity[extendedResource]
622+
if !exists {
623+
return fmt.Errorf("node %s has no %s resource capacity", node.Name, extendedResourceName)
624+
}
625+
if expectedResource := resource.MustParse("123"); fakeResourceCapacity.Cmp(expectedResource) != 0 {
626+
return fmt.Errorf("node %s has resource capacity %s, expected: %s", node.Name, fakeResourceCapacity.String(), expectedResource.String())
627+
}
628+
629+
return nil
630+
}).WithTimeout(30 * time.Second).WithPolling(time.Second).ShouldNot(gomega.HaveOccurred())
631+
}
632+
633+
func removeExtendedResource(clientSet clientset.Interface, nodeName, extendedResourceName string) {
634+
extendedResource := v1.ResourceName(extendedResourceName)
635+
636+
ginkgo.By("Removing a custom resource")
637+
originalNode, err := clientSet.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{})
638+
framework.ExpectNoError(err)
639+
640+
node := originalNode.DeepCopy()
641+
delete(node.Status.Capacity, extendedResource)
642+
delete(node.Status.Allocatable, extendedResource)
643+
err = patchNode(context.Background(), clientSet, originalNode.DeepCopy(), node)
644+
framework.ExpectNoError(err)
645+
646+
gomega.Eventually(func() error {
647+
node, err = clientSet.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{})
648+
framework.ExpectNoError(err)
649+
650+
if _, exists := node.Status.Capacity[extendedResource]; exists {
651+
return fmt.Errorf("node %s has resource capacity %s which is expected to be removed", node.Name, extendedResourceName)
652+
}
653+
654+
return nil
655+
}).WithTimeout(30 * time.Second).WithPolling(time.Second).ShouldNot(gomega.HaveOccurred())
656+
}
657+
574658
func doPodResizeTests() {
575659
f := framework.NewDefaultFramework("pod-resize")
576660
var podClient *e2epod.PodClient
661+
577662
ginkgo.BeforeEach(func() {
578663
podClient = e2epod.NewPodClient(f)
579664
})
580665

581666
type testCase struct {
582-
name string
583-
containers []TestContainerInfo
584-
patchString string
585-
expected []TestContainerInfo
667+
name string
668+
containers []TestContainerInfo
669+
patchString string
670+
expected []TestContainerInfo
671+
addExtendedResource bool
586672
}
587673

588674
noRestart := v1.NotRequired
@@ -1284,6 +1370,31 @@ func doPodResizeTests() {
12841370
},
12851371
},
12861372
},
1373+
{
1374+
name: "Guaranteed QoS pod, one container - increase CPU & memory with an extended resource",
1375+
containers: []TestContainerInfo{
1376+
{
1377+
Name: "c1",
1378+
Resources: &ContainerResources{CPUReq: "100m", CPULim: "100m", MemReq: "200Mi", MemLim: "200Mi",
1379+
ExtendedResourceReq: "1", ExtendedResourceLim: "1"},
1380+
CPUPolicy: &noRestart,
1381+
MemPolicy: &noRestart,
1382+
},
1383+
},
1384+
patchString: `{"spec":{"containers":[
1385+
{"name":"c1", "resources":{"requests":{"cpu":"200m","memory":"400Mi"},"limits":{"cpu":"200m","memory":"400Mi"}}}
1386+
]}}`,
1387+
expected: []TestContainerInfo{
1388+
{
1389+
Name: "c1",
1390+
Resources: &ContainerResources{CPUReq: "200m", CPULim: "200m", MemReq: "400Mi", MemLim: "400Mi",
1391+
ExtendedResourceReq: "1", ExtendedResourceLim: "1"},
1392+
CPUPolicy: &noRestart,
1393+
MemPolicy: &noRestart,
1394+
},
1395+
},
1396+
addExtendedResource: true,
1397+
},
12871398
}
12881399

12891400
for idx := range tests {
@@ -1297,6 +1408,20 @@ func doPodResizeTests() {
12971408
initDefaultResizePolicy(tc.expected)
12981409
testPod = makeTestPod(f.Namespace.Name, "testpod", tStamp, tc.containers)
12991410

1411+
if tc.addExtendedResource {
1412+
nodes, err := e2enode.GetReadySchedulableNodes(context.Background(), f.ClientSet)
1413+
framework.ExpectNoError(err)
1414+
1415+
for _, node := range nodes.Items {
1416+
addExtendedResource(f.ClientSet, node.Name, fakeExtendedResource, resource.MustParse("123"))
1417+
}
1418+
defer func() {
1419+
for _, node := range nodes.Items {
1420+
removeExtendedResource(f.ClientSet, node.Name, fakeExtendedResource)
1421+
}
1422+
}()
1423+
}
1424+
13001425
ginkgo.By("creating pod")
13011426
newPod := podClient.CreateSync(ctx, testPod)
13021427

0 commit comments

Comments
 (0)