Skip to content

Commit de6ab05

Browse files
committed
feat: sync containers resources inplace resize on host cluster
Increase k8s version in devspace.yaml for k8s distro to v1.35.0
1 parent ad7f975 commit de6ab05

File tree

5 files changed

+215
-4
lines changed

5 files changed

+215
-4
lines changed

chart/templates/role.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ rules:
4040
resources: ["pods/status", "pods/ephemeralcontainers"]
4141
verbs: ["patch", "update"]
4242
{{- end }}
43+
- apiGroups: [""]
44+
resources: ["pods/resize"]
45+
verbs: ["patch"]
4346
- apiGroups: ["apps"]
4447
resources: ["statefulsets", "replicasets", "deployments"]
4548
verbs: ["get", "list", "watch"]

chart/tests/role_test.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ tests:
5757
count: 1
5858
- lengthEqual:
5959
path: rules
60-
count: 10
60+
count: 11
6161
- contains:
6262
path: rules
6363
count: 1
@@ -86,7 +86,7 @@ tests:
8686
count: 1
8787
- lengthEqual:
8888
path: rules
89-
count: 9
89+
count: 10
9090
- contains:
9191
path: rules
9292
count: 1
@@ -304,7 +304,7 @@ tests:
304304
value: Role
305305
- lengthEqual:
306306
path: rules
307-
count: 12
307+
count: 13
308308
- contains:
309309
path: rules
310310
content:

devspace.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ deployments:
3232
distro:
3333
k8s:
3434
enabled: true
35+
image:
36+
tag: v1.35.0
3537
statefulSet:
3638
image:
3739
registry: ""

pkg/controllers/resources/pods/syncer.go

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ package pods
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"reflect"
78
"slices"
89
"time"
910

1011
nodev1 "k8s.io/api/node/v1"
1112
schedulingv1 "k8s.io/api/scheduling/v1"
12-
1313
utilerrors "k8s.io/apimachinery/pkg/util/errors"
14+
utilversion "k8s.io/apimachinery/pkg/util/version"
1415
"k8s.io/apimachinery/pkg/util/wait"
1516
"k8s.io/klog/v2"
1617

@@ -392,6 +393,12 @@ func (s *podSyncer) Sync(ctx *synccontext.SyncContext, event *synccontext.SyncEv
392393
}
393394
}()
394395

396+
// resize the host pod container resources in place if the pod spec has changed
397+
err = s.resizeHostPodContainerResourcesInPlace(ctx, event)
398+
if err != nil {
399+
return ctrl.Result{}, err
400+
}
401+
395402
// update the virtual pod if the spec has changed
396403
err = s.podTranslator.Diff(ctx, event)
397404
if err != nil {
@@ -405,6 +412,36 @@ func (s *podSyncer) Sync(ctx *synccontext.SyncContext, event *synccontext.SyncEv
405412
return ctrl.Result{}, nil
406413
}
407414

415+
func (s *podSyncer) resizeHostPodContainerResourcesInPlace(ctx *synccontext.SyncContext, event *synccontext.SyncEvent[*corev1.Pod]) error {
416+
hostClusterVersion, err := ctx.Config.HostClient.Discovery().ServerVersion()
417+
if err != nil {
418+
return fmt.Errorf("failed to get host cluster version : %w", err)
419+
}
420+
421+
// We execute the inplace container resources resize only if the host cluster version is greater than 1.35.0
422+
if hostClusterVersion == nil {
423+
return fmt.Errorf("failed to get host cluster version : hostClusterVersion is nil")
424+
}
425+
hostVersion, err := utilversion.ParseSemantic(hostClusterVersion.String())
426+
if err != nil {
427+
return fmt.Errorf("failed to parse host cluster version : %w", err)
428+
}
429+
if hostVersion.LessThan(utilversion.MustParseSemantic("1.35.0")) {
430+
return nil
431+
}
432+
433+
resizePatch, err := buildHostPodContainersResourcesResizePatch(event.Virtual, event.Host)
434+
if err != nil {
435+
return err
436+
}
437+
if resizePatch != nil {
438+
if err := s.applyResizeSubresource(ctx, event.Host, resizePatch); err != nil {
439+
return err
440+
}
441+
}
442+
return nil
443+
}
444+
408445
func (s *podSyncer) SyncToVirtual(ctx *synccontext.SyncContext, event *synccontext.SyncToVirtualEvent[*corev1.Pod]) (_ ctrl.Result, retErr error) {
409446
if event.VirtualOld != nil || translate.ShouldDeleteHostObject(event.Host) {
410447
// virtual object is not here anymore, so we delete
@@ -445,6 +482,59 @@ func setSATokenSecretAsOwner(ctx *synccontext.SyncContext, pClient client.Client
445482
return nil
446483
}
447484

485+
type resizePatch struct {
486+
Spec resizePatchSpec `json:"spec"`
487+
}
488+
489+
type resizePatchSpec struct {
490+
Containers []resizeContainer `json:"containers"`
491+
}
492+
493+
type resizeContainer struct {
494+
Name string `json:"name"`
495+
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
496+
}
497+
498+
func buildHostPodContainersResourcesResizePatch(vPod, pPod *corev1.Pod) ([]byte, error) {
499+
hostContainers := map[string]corev1.Container{}
500+
for _, c := range pPod.Spec.Containers {
501+
hostContainers[c.Name] = c
502+
}
503+
504+
var patchContainers []resizeContainer
505+
for _, v := range vPod.Spec.Containers {
506+
p, ok := hostContainers[v.Name]
507+
if !ok {
508+
continue
509+
}
510+
if equality.Semantic.DeepEqual(p.Resources, v.Resources) {
511+
continue
512+
}
513+
514+
var resources corev1.ResourceRequirements
515+
v.Resources.DeepCopyInto(&resources)
516+
patchContainers = append(patchContainers, resizeContainer{
517+
Name: v.Name,
518+
Resources: resources,
519+
})
520+
}
521+
522+
if len(patchContainers) == 0 {
523+
return nil, nil
524+
}
525+
526+
// TODO: Improve this to potentially integrate pod level resource requests and limits inplace resize when it wil be in GA
527+
return json.Marshal(resizePatch{
528+
Spec: resizePatchSpec{
529+
Containers: patchContainers,
530+
},
531+
})
532+
}
533+
534+
func (s *podSyncer) applyResizeSubresource(ctx *synccontext.SyncContext, hostPod *corev1.Pod, patch []byte) error {
535+
return ctx.HostClient.SubResource("resize").Patch(ctx, hostPod, client.RawPatch(types.StrategicMergePatchType, patch))
536+
}
537+
448538
func (s *podSyncer) ensureNode(ctx *synccontext.SyncContext, pObj *corev1.Pod, vObj *corev1.Pod) (bool, error) {
449539
if vObj.Spec.NodeName != pObj.Spec.NodeName && vObj.Spec.NodeName != "" {
450540
// node of virtual and physical pod are different, we delete the virtual pod to try to recover from this state

pkg/controllers/resources/pods/syncer_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package pods
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"maps"
67
"testing"
78

89
"gotest.tools/assert"
910
corev1 "k8s.io/api/core/v1"
1011
schedulingv1 "k8s.io/api/scheduling/v1"
12+
"k8s.io/apimachinery/pkg/api/resource"
1113
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1214
"k8s.io/apimachinery/pkg/runtime"
1315
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -691,3 +693,117 @@ func TestSync(t *testing.T) {
691693
},
692694
})
693695
}
696+
697+
func TestBuildResizePatch(t *testing.T) {
698+
makePod := func(memory string) *corev1.Pod {
699+
return &corev1.Pod{
700+
ObjectMeta: metav1.ObjectMeta{
701+
Name: "test-pod",
702+
Namespace: "default",
703+
},
704+
Spec: corev1.PodSpec{
705+
Containers: []corev1.Container{
706+
{
707+
Name: "c1",
708+
Image: "nginx",
709+
Resources: corev1.ResourceRequirements{
710+
Requests: corev1.ResourceList{
711+
corev1.ResourceCPU: resource.MustParse("100m"),
712+
corev1.ResourceMemory: resource.MustParse(memory),
713+
},
714+
Limits: corev1.ResourceList{
715+
corev1.ResourceMemory: resource.MustParse(memory),
716+
},
717+
},
718+
},
719+
},
720+
},
721+
}
722+
}
723+
724+
t.Run("creates patch when resources differ", func(t *testing.T) {
725+
vPod := makePod("30Mi")
726+
pPod := makePod("20Mi")
727+
728+
patchBytes, err := buildHostPodContainersResourcesResizePatch(vPod, pPod)
729+
assert.NilError(t, err)
730+
assert.Assert(t, patchBytes != nil)
731+
732+
var patch resizePatch
733+
err = json.Unmarshal(patchBytes, &patch)
734+
assert.NilError(t, err)
735+
assert.Equal(t, len(patch.Spec.Containers), 1)
736+
assert.Equal(t, patch.Spec.Containers[0].Name, "c1")
737+
got := patch.Spec.Containers[0].Resources.Requests[corev1.ResourceMemory]
738+
assert.Assert(t, got.Cmp(resource.MustParse("30Mi")) == 0)
739+
})
740+
741+
t.Run("returns nil when resources are equal", func(t *testing.T) {
742+
vPod := makePod("20Mi")
743+
pPod := makePod("20Mi")
744+
745+
patchBytes, err := buildHostPodContainersResourcesResizePatch(vPod, pPod)
746+
assert.NilError(t, err)
747+
assert.Assert(t, patchBytes == nil)
748+
})
749+
750+
t.Run("includes only containers with resource diffs", func(t *testing.T) {
751+
vPod := makePod("30Mi")
752+
vPod.Spec.Containers = append(vPod.Spec.Containers, corev1.Container{
753+
Name: "c2",
754+
Image: "busybox",
755+
Resources: corev1.ResourceRequirements{
756+
Requests: corev1.ResourceList{
757+
corev1.ResourceCPU: resource.MustParse("50m"),
758+
corev1.ResourceMemory: resource.MustParse("10Mi"),
759+
},
760+
},
761+
})
762+
pPod := makePod("20Mi")
763+
pPod.Spec.Containers = append(pPod.Spec.Containers, corev1.Container{
764+
Name: "c2",
765+
Image: "busybox",
766+
Resources: corev1.ResourceRequirements{
767+
Requests: corev1.ResourceList{
768+
corev1.ResourceCPU: resource.MustParse("50m"),
769+
corev1.ResourceMemory: resource.MustParse("10Mi"),
770+
},
771+
},
772+
})
773+
774+
patchBytes, err := buildHostPodContainersResourcesResizePatch(vPod, pPod)
775+
assert.NilError(t, err)
776+
assert.Assert(t, patchBytes != nil)
777+
778+
var patch resizePatch
779+
err = json.Unmarshal(patchBytes, &patch)
780+
assert.NilError(t, err)
781+
assert.Equal(t, len(patch.Spec.Containers), 1)
782+
assert.Equal(t, patch.Spec.Containers[0].Name, "c1")
783+
})
784+
785+
t.Run("skips containers missing on host", func(t *testing.T) {
786+
vPod := makePod("30Mi")
787+
vPod.Spec.Containers = append(vPod.Spec.Containers, corev1.Container{
788+
Name: "c2",
789+
Image: "busybox",
790+
Resources: corev1.ResourceRequirements{
791+
Requests: corev1.ResourceList{
792+
corev1.ResourceCPU: resource.MustParse("50m"),
793+
corev1.ResourceMemory: resource.MustParse("10Mi"),
794+
},
795+
},
796+
})
797+
pPod := makePod("20Mi")
798+
799+
patchBytes, err := buildHostPodContainersResourcesResizePatch(vPod, pPod)
800+
assert.NilError(t, err)
801+
assert.Assert(t, patchBytes != nil)
802+
803+
var patch resizePatch
804+
err = json.Unmarshal(patchBytes, &patch)
805+
assert.NilError(t, err)
806+
assert.Equal(t, len(patch.Spec.Containers), 1)
807+
assert.Equal(t, patch.Spec.Containers[0].Name, "c1")
808+
})
809+
}

0 commit comments

Comments
 (0)