Repository: apecloud/kubeblocks

Issue:
[BUG] In place pod resource resize does not work on K8s 1.32, which launches 'resize' resubsource.
```bash
Kubernetes: v1.32.2
```

KubeBlocks cannot modify pod resources directly, instead, patch `resize` subresource to update.

```text
The Pod "mypod" is invalid: spec: Forbidden: pod updates may not change fields other than `spec.containers[*].image`,`spec.initContainers[*].image`,`spec.activeDeadlineSeconds`,`spec.tolerations` (only additions to existing tolerations),`spec.terminationGracePeriodSeconds` (allow it to be set to 1 if it was previously negative)
  core.PodSpec{
  	Volumes:        {{Name: "kube-api-access-79z4n", VolumeSource: {Projected: &{Sources: {{ServiceAccountToken: &{ExpirationSeconds: 3607, Path: "token"}}, {ConfigMap: &{LocalObjectReference: {Name: "kube-root-ca.crt"}, Items: {{Key: "ca.crt", Path: "ca.crt"}}}}, {DownwardAPI: &{Items: {{Path: "namespace", FieldRef: &{APIVersion: "v1", FieldPath: "metadata.namespace"}}}}}}, DefaultMode: &420}}}},
  	InitContainers: nil,
  	Containers: []core.Container{
  		{
  			... // 6 identical fields
  			EnvFrom: nil,
  			Env:     nil,
  			Resources: core.ResourceRequirements{
  				Limits: core.ResourceList{
  					s"cpu":    {i: {...}, s: "1", Format: "DecimalSI"},
- 					s"memory": {i: resource.int64Amount{value: 1073741824}, s: "1Gi", Format: "BinarySI"},
+ 					s"memory": {i: resource.int64Amount{value: 524288000}, s: "500Mi", Format: "BinarySI"},
  				},
  				Requests: core.ResourceList{
  					s"cpu":    {i: {...}, s: "1", Format: "DecimalSI"},
- 					s"memory": {i: resource.int64Amount{value: 1073741824}, s: "1Gi", Format: "BinarySI"},
+ 					s"memory": {i: resource.int64Amount{value: 524288000}, s: "500Mi", Format: "BinarySI"},
  				},
  				Claims: nil,
  			},
  			ResizePolicy:  {{ResourceName: s"cpu", RestartPolicy: "NotRequired"}, {ResourceName: s"memory", RestartPolicy: "NotRequired"}},
  			RestartPolicy: nil,
  			... // 13 identical fields
  		},
  	},
  	EphemeralContainers: nil,
  	RestartPolicy:       "Always",
  	... // 29 identical fields
  }
```

Discussion:
@github-actions: This issue has been marked as stale because it has been open for 30 days with no activity

Solution patch:
diff --git a/controllers/dataprotection/backup_controller.go b/controllers/dataprotection/backup_controller.go
index 721201d4d..e7ef045d7 100644
--- a/controllers/dataprotection/backup_controller.go
+++ b/controllers/dataprotection/backup_controller.go
@@ -268,6 +268,12 @@ func (r *BackupReconciler) handleDeletingPhase(reqCtx intctrlutil.RequestCtx, ba
 		return intctrlutil.Reconciled()
 	}
 
+	if cleaned, err := r.waitForBackupPodsDeleted(reqCtx, backup); err != nil {
+		return intctrlutil.RequeueWithError(err, reqCtx.Log, "")
+	} else if !cleaned {
+		return intctrlutil.Reconciled()
+	}
+
 	if err := r.deleteVolumeSnapshots(reqCtx, backup); err != nil {
 		return intctrlutil.RequeueWithError(err, reqCtx.Log, "")
 	}
@@ -751,6 +757,20 @@ func (r *BackupReconciler) deleteVolumeSnapshots(reqCtx intctrlutil.RequestCtx,
 	return deleter.DeleteVolumeSnapshots(backup)
 }
 
+func (r *BackupReconciler) waitForBackupPodsDeleted(reqCtx intctrlutil.RequestCtx, backup *dpv1alpha1.Backup) (bool, error) {
+	podList := &corev1.PodList{}
+	if err := r.Client.List(reqCtx.Ctx, podList, client.InNamespace(backup.Namespace),
+		client.MatchingLabels(map[string]string{
+			dptypes.BackupNameLabelKey: backup.Name,
+		})); err != nil {
+		return false, err
+	}
+	if len(podList.Items) == 0 {
+		return true, nil
+	}
+	return false, nil
+}
+
 // deleteExternalResources deletes the external workloads that execute backup.
 // Currently, it only supports two types of workloads: job, statefulSet
 func (r *BackupReconciler) deleteExternalResources(
diff --git a/go.mod b/go.mod
index 9b543c4d2..52c85b81a 100644
--- a/go.mod
+++ b/go.mod
@@ -37,7 +37,6 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.19.0
 	github.com/replicatedhq/troubleshoot v0.57.0
-	github.com/rogpeppe/go-internal v1.12.0
 	github.com/russross/blackfriday/v2 v2.1.0
 	github.com/sethvargo/go-password v0.2.0
 	github.com/shirou/gopsutil/v3 v3.23.6
@@ -54,6 +53,7 @@ require (
 	go.uber.org/zap v1.27.0
 	golang.org/x/crypto v0.31.0
 	golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
+	golang.org/x/mod v0.17.0
 	golang.org/x/net v0.33.0
 	golang.org/x/text v0.21.0
 	google.golang.org/grpc v1.63.2
@@ -192,6 +192,7 @@ require (
 	github.com/prometheus/procfs v0.12.0 // indirect
 	github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 // indirect
 	github.com/rivo/uniseg v0.4.6 // indirect
+	github.com/rogpeppe/go-internal v1.12.0 // indirect
 	github.com/rubenv/sql-migrate v1.5.2 // indirect
 	github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
 	github.com/sergi/go-diff v1.2.0 // indirect
@@ -223,7 +224,6 @@ require (
 	go.opentelemetry.io/proto/otlp v1.1.0 // indirect
 	go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/mod v0.17.0 // indirect
 	golang.org/x/oauth2 v0.19.0 // indirect
 	golang.org/x/sync v0.10.0 // indirect
 	golang.org/x/sys v0.28.0 // indirect
diff --git a/pkg/controller/instanceset/reconciler_update.go b/pkg/controller/instanceset/reconciler_update.go
index 53be1933f..70a519ef8 100644
--- a/pkg/controller/instanceset/reconciler_update.go
+++ b/pkg/controller/instanceset/reconciler_update.go
@@ -203,10 +203,23 @@ func (r *updateReconciler) Reconcile(tree *kubebuilderx.ObjectTree) (kubebuilder
 				return kubebuilderx.Continue, err
 			}
 			newPod := copyAndMerge(pod, newInstance.pod)
-			if err = r.switchover(tree, its, newPod.(*corev1.Pod)); err != nil {
+			supportResizeSubResource, err := intctrlutil.SupportResizeSubResource()
+			if err != nil {
+				tree.Logger.Error(err, "check support resize sub resource error")
 				return kubebuilderx.Continue, err
 			}
-			if err = tree.Update(newPod); err != nil {
+
+			// if already updating using subresource, don't update it again, because without subresource, those fields are considered immutable.
+			// Another reconciliation will be triggered since pod status will be updated.
+			if !equalResourcesInPlaceFields(pod, newInstance.pod) && supportResizeSubResource {
+				err = tree.Update(newPod, kubebuilderx.WithSubResource("resize"))
+			} else {
+				if err = r.switchover(tree, its, newPod.(*corev1.Pod)); err != nil {
+					return kubebuilderx.Continue, err
+				}
+				err = tree.Update(newPod)
+			}
+			if err != nil {
 				return kubebuilderx.Continue, err
 			}
 			updatingPods++
diff --git a/pkg/controller/kubebuilderx/plan_builder.go b/pkg/controller/kubebuilderx/plan_builder.go
index d3737ca8e..0e9aa876b 100644
--- a/pkg/controller/kubebuilderx/plan_builder.go
+++ b/pkg/controller/kubebuilderx/plan_builder.go
@@ -28,6 +28,7 @@ import (
 
 	"github.com/go-logr/logr"
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/equality"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/client-go/tools/record"
@@ -93,7 +94,7 @@ func (b *PlanBuilder) AddTransformer(_ ...graph.Transformer) graph.PlanBuilder {
 }
 
 func (b *PlanBuilder) Build() (graph.Plan, error) {
-	vertices := buildOrderedVertices(b.transCtx.GetContext(), b.currentTree, b.desiredTree)
+	vertices := buildOrderedVertices(b.transCtx, b.currentTree, b.desiredTree)
 	plan := &Plan{
 		walkFunc: b.defaultWalkFunc,
 		vertices: vertices,
@@ -101,7 +102,8 @@ func (b *PlanBuilder) Build() (graph.Plan, error) {
 	return plan, nil
 }
 
-func buildOrderedVertices(ctx context.Context, currentTree *ObjectTree, desiredTree *ObjectTree) []*model.ObjectVertex {
+func buildOrderedVertices(transCtx *transformContext, currentTree *ObjectTree, desiredTree *ObjectTree) []*model.ObjectVertex {
+	ctx := transCtx.GetContext()
 	getStatusField := func(obj client.Object) interface{} {
 		objValue := reflect.ValueOf(obj)
 		if objValue.Kind() != reflect.Ptr || objValue.Elem().Kind() != reflect.Struct {
@@ -172,8 +174,19 @@ func buildOrderedVertices(ctx context.Context, currentTree *ObjectTree, desiredT
 		for name := range updateSet {
 			oldObj := oldSnapshot[name]
 			newObj := newSnapshot[name]
-			if !reflect.DeepEqual(oldObj, newObj) {
-				v := model.NewObjectVertex(oldObj, newObj, model.ActionUpdatePtr(), inDataContext4G())
+			if !equality.Semantic.DeepEqual(oldObj, newObj) {
+				name, err := model.GetGVKName(newObj)
+				if err != nil {
+					transCtx.logger.Error(err, "can't get GVKName from object", "object", newObj.GetName())
+					return
+				}
+				var v *model.ObjectVertex
+				subResource := desiredTree.childrenOptions[*name].SubResource
+				if subResource != "" {
+					v = model.NewObjectVertex(oldObj, newObj, model.ActionUpdatePtr(), inDataContext4G(), model.WithSubResource(subResource))
+				} else {
+					v = model.NewObjectVertex(oldObj, newObj, model.ActionUpdatePtr(), inDataContext4G())
+				}
 				findAndAppend(v)
 			}
 		}
@@ -266,21 +279,37 @@ func (b *PlanBuilder) createObject(ctx context.Context, vertex *model.ObjectVert
 }
 
 func (b *PlanBuilder) updateObject(ctx context.Context, vertex *model.ObjectVertex) error {
-	err := b.cli.Update(ctx, vertex.Obj, clientOption(vertex))
+	var err error
+	var reason string
+	if vertex.SubResource != "" {
+		err = b.cli.SubResource(vertex.SubResource).Update(ctx, vertex.Obj, clientOption(vertex))
+		reason = "SuccessfulUpdateSubResource"
+	} else {
+		err = b.cli.Update(ctx, vertex.Obj, clientOption(vertex))
+		reason = "SuccessfulUpdate"
+	}
 	if err != nil && !apierrors.IsNotFound(err) {
 		return err
 	}
-	b.emitEvent(vertex.Obj, "SuccessfulUpdate", model.UPDATE)
+	b.emitEvent(vertex.Obj, reason, model.UPDATE)
 	return nil
 }
 
 func (b *PlanBuilder) patchObject(ctx context.Context, vertex *model.ObjectVertex) error {
+	var err error
+	var reason string
 	patch := client.MergeFrom(vertex.OriObj)
-	err := b.cli.Patch(ctx, vertex.Obj, patch, clientOption(vertex))
+	if vertex.SubResource != "" {
+		err = b.cli.SubResource(vertex.SubResource).Patch(ctx, vertex.Obj, patch, clientOption(vertex))
+		reason = "SuccessfulPatchSubResource"
+	} else {
+		err = b.cli.Patch(ctx, vertex.Obj, patch, clientOption(vertex))
+		reason = "SuccessfulPatch"
+	}
 	if err != nil && !apierrors.IsNotFound(err) {
 		return err
 	}
-	b.emitEvent(vertex.Obj, "SuccessfulUpdate", model.UPDATE)
+	b.emitEvent(vertex.Obj, reason, model.PATCH)
 	return nil
 }
 
diff --git a/pkg/controller/kubebuilderx/reconciler.go b/pkg/controller/kubebuilderx/reconciler.go
index 18bbe993f..3d6a11a7f 100644
--- a/pkg/controller/kubebuilderx/reconciler.go
+++ b/pkg/controller/kubebuilderx/reconciler.go
@@ -33,14 +33,30 @@ import (
 	"github.com/apecloud/kubeblocks/pkg/controller/model"
 )
 
+type ObjectOption interface {
+	ApplyToObject(*ObjectOptions)
+}
+
+type ObjectOptions struct {
+	// if not empty, action should be done on the specified subresource
+	SubResource string
+}
+
+type WithSubResource string
+
+func (w WithSubResource) ApplyToObject(opts *ObjectOptions) {
+	opts.SubResource = string(w)
+}
+
 type ObjectTree struct {
 	// TODO(free6om): should find a better place to hold these two params?
 	context.Context
 	record.EventRecorder
 	logr.Logger
 
-	root     client.Object
-	children model.ObjectSnapshot
+	root            client.Object
+	children        model.ObjectSnapshot
+	childrenOptions map[model.GVKNObjKey]ObjectOptions
 
 	// finalizer to protect all objects of this tree
 	finalizer string
@@ -124,7 +140,12 @@ func (t *ObjectTree) DeepCopy() (*ObjectTree, error) {
 		}
 		children[key] = childCopied
 	}
+	childrenOptions := make(map[model.GVKNObjKey]ObjectOptions, len(t.childrenOptions))
+	for key, options := range t.childrenOptions {
+		childrenOptions[key] = options
+	}
 	out.children = children
+	out.childrenOptions = childrenOptions
 	out.finalizer = t.finalizer
 	out.Context = t.Context
 	out.EventRecorder = t.EventRecorder
@@ -140,6 +161,14 @@ func (t *ObjectTree) Get(object client.Object) (client.Object, error) {
 	return t.children[*name], nil
 }
 
+func (t *ObjectTree) GetWithOption(object client.Object) (client.Object, ObjectOptions, error) {
+	name, err := model.GetGVKName(object)
+	if err != nil {
+		return nil, ObjectOptions{}, err
+	}
+	return t.children[*name], t.childrenOptions[*name], nil
+}
+
 func (t *ObjectTree) GetRoot() client.Object {
 	return t.root
 }
@@ -175,19 +204,25 @@ func (t *ObjectTree) GetSecondaryObjects() model.ObjectSnapshot {
 }
 
 func (t *ObjectTree) Add(objects ...client.Object) error {
-	return t.replace(objects...)
+	return t.replace(objects)
 }
 
-func (t *ObjectTree) Update(objects ...client.Object) error {
-	return t.replace(objects...)
+func (t *ObjectTree) Update(object client.Object, options ...ObjectOption) error {
+	return t.replace([]client.Object{object}, options...)
 }
 
-func (t *ObjectTree) replace(objects ...client.Object) error {
+func (t *ObjectTree) replace(objects []client.Object, options ...ObjectOption) error {
+	option := ObjectOptions{}
+	for _, opt := range options {
+		opt.ApplyToObject(&option)
+	}
+
 	for _, object := range objects {
 		name, err := model.GetGVKName(object)
 		if err != nil {
 			return err
 		}
+		t.childrenOptions[*name] = option
 		t.children[*name] = object
 	}
 	return nil
@@ -218,6 +253,7 @@ func (t *ObjectTree) GetFinalizer() string {
 
 func NewObjectTree() *ObjectTree {
 	return &ObjectTree{
-		children: make(model.ObjectSnapshot),
+		children:        make(model.ObjectSnapshot),
+		childrenOptions: make(map[model.GVKNObjKey]ObjectOptions),
 	}
 }
diff --git a/pkg/controller/model/graph_options.go b/pkg/controller/model/graph_options.go
index 912108a9d..46ac5666a 100644
--- a/pkg/controller/model/graph_options.go
+++ b/pkg/controller/model/graph_options.go
@@ -26,6 +26,7 @@ type GraphOptions struct {
 	haveDifferentTypeWith bool
 	clientOpt             any
 	propagationPolicy     client.PropagationPolicy
+	subResource           string
 }
 
 type GraphOption interface {
@@ -82,3 +83,19 @@ func WithPropagationPolicy(policy client.PropagationPolicy) GraphOption {
 		propagationPolicy: policy,
 	}
 }
+
+type subResourceOption struct {
+	subResource string
+}
+
+func (o *subResourceOption) ApplyTo(opts *GraphOptions) {
+	opts.subResource = o.subResource
+}
+
+var _ GraphOption = &subResourceOption{}
+
+func WithSubResource(subResource string) GraphOption {
+	return &subResourceOption{
+		subResource: subResource,
+	}
+}
diff --git a/pkg/controller/model/transform_types.go b/pkg/controller/model/transform_types.go
index aa41dd598..ad21a2a4a 100644
--- a/pkg/controller/model/transform_types.go
+++ b/pkg/controller/model/transform_types.go
@@ -69,6 +69,7 @@ type ObjectVertex struct {
 	Obj               client.Object
 	OriObj            client.Object
 	Action            *Action
+	SubResource       string
 	ClientOpt         any
 	PropagationPolicy client.PropagationPolicy
 }
@@ -86,10 +87,11 @@ func NewObjectVertex(oldObj, newObj client.Object, action *Action, opts ...Graph
 		opt.ApplyTo(graphOpts)
 	}
 	return &ObjectVertex{
-		Obj:       newObj,
-		OriObj:    oldObj,
-		Action:    action,
-		ClientOpt: graphOpts.clientOpt,
+		Obj:         newObj,
+		OriObj:      oldObj,
+		Action:      action,
+		SubResource: graphOpts.subResource,
+		ClientOpt:   graphOpts.clientOpt,
 	}
 }
 
diff --git a/pkg/controllerutil/util.go b/pkg/controllerutil/util.go
index be99eae8a..236fdb477 100644
--- a/pkg/controllerutil/util.go
+++ b/pkg/controllerutil/util.go
@@ -27,9 +27,11 @@ import (
 	"slices"
 	"strings"
 
+	"golang.org/x/mod/semver"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/version"
 	"k8s.io/client-go/rest"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -43,7 +45,7 @@ import (
 	"github.com/apecloud/kubeblocks/pkg/constant"
 	"github.com/apecloud/kubeblocks/pkg/generics"
 	viper "github.com/apecloud/kubeblocks/pkg/viperx"
-	"github.com/apecloud/kubeblocks/version"
+	kbversion "github.com/apecloud/kubeblocks/version"
 )
 
 var (
@@ -166,7 +168,7 @@ func GetKubeRestConfig(userAgent string) *rest.Config {
 }
 
 func defaultUserAgent() string {
-	return fmt.Sprintf("KubeBlocks %s (%s/%s)", version.GitVersion, gruntime.GOOS, gruntime.GOARCH)
+	return fmt.Sprintf("KubeBlocks %s (%s/%s)", kbversion.GitVersion, gruntime.GOOS, gruntime.GOARCH)
 }
 
 // DeleteOwnedResources deletes the matched resources which are owned by the owner.
@@ -211,3 +213,30 @@ func MergeList[E any](src, dst *[]E, f func(E) func(E) bool) {
 		}
 	}
 }
+
+// GetKubeVersion get the version of Kubernetes and return the gitVersion
+func GetKubeVersion() (string, error) {
+	verInfo := viper.Get(constant.CfgKeyServerInfo)
+	ver, ok := verInfo.(version.Info)
+	if !ok {
+		return "", fmt.Errorf("failed to get kubernetes version, version info %v", verInfo)
+	}
+	if !semver.IsValid(ver.GitVersion) {
+		return "", fmt.Errorf("kubernetes version is not a valid semver, version info %v", verInfo)
+	}
+	return semver.MajorMinor(ver.GitVersion), nil
+}
+
+// SupportResizeSubResource is for the ease of tests
+var SupportResizeSubResource = supportResizeSubResourceImpl
+
+func supportResizeSubResourceImpl() (bool, error) {
+	kubeVersion, err := GetKubeVersion()
+	if err != nil {
+		return false, err
+	}
+	if semver.Compare(kubeVersion, "v1.32") < 0 {
+		return false, nil
+	}
+	return true, nil
+}
diff --git a/pkg/dataprotection/backup/utils.go b/pkg/dataprotection/backup/utils.go
index 1d4310af7..bc611eb32 100644
--- a/pkg/dataprotection/backup/utils.go
+++ b/pkg/dataprotection/backup/utils.go
@@ -27,7 +27,7 @@ import (
 	"slices"
 	"strings"
 
-	"github.com/rogpeppe/go-internal/semver"
+	"golang.org/x/mod/semver"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -36,9 +36,9 @@ import (
 
 	dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
 	"github.com/apecloud/kubeblocks/pkg/constant"
+	intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil"
 	"github.com/apecloud/kubeblocks/pkg/dataprotection/action"
 	"github.com/apecloud/kubeblocks/pkg/dataprotection/types"
-	dputils "github.com/apecloud/kubeblocks/pkg/dataprotection/utils"
 )
 
 func getVolumesByNames(pod *corev1.Pod, volumeNames []string) []corev1.Volume {
@@ -315,7 +315,7 @@ func SetExpirationTime(backup *dpv1alpha1.Backup) error {
 // The kube-controller-manager interprets schedules relative to its local time zone.
 func BuildCronJobSchedule(cronExpression string) (*string, string) {
 	timeZone := "UTC"
-	ver, err := dputils.GetKubeVersion()
+	ver, err := intctrlutil.GetKubeVersion()
 	if err != nil {
 		return nil, cronExpression
 	}
diff --git a/pkg/dataprotection/utils/utils.go b/pkg/dataprotection/utils/utils.go
index 69d9a19a4..55622508d 100644
--- a/pkg/dataprotection/utils/utils.go
+++ b/pkg/dataprotection/utils/utils.go
@@ -27,14 +27,13 @@ import (
 	"sort"
 	"strconv"
 
-	"github.com/rogpeppe/go-internal/semver"
+	"golang.org/x/mod/semver"
 	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/json"
-	"k8s.io/apimachinery/pkg/version"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
@@ -316,21 +315,8 @@ func GetPodByName(podList *corev1.PodList, name string) *corev1.Pod {
 	return nil
 }
 
-// GetKubeVersion get the version of Kubernetes and return the gitVersion
-func GetKubeVersion() (string, error) {
-	verInfo := viper.Get(constant.CfgKeyServerInfo)
-	ver, ok := verInfo.(version.Info)
-	if !ok {
-		return "", fmt.Errorf("failed to get kubernetes version, version info %v", verInfo)
-	}
-	if !semver.IsValid(ver.GitVersion) {
-		return "", fmt.Errorf("kubernetes version is not a valid semver, version info %v", verInfo)
-	}
-	return semver.MajorMinor(ver.GitVersion), nil
-}
-
 func SupportsCronJobV1() bool {
-	kubeVersion, err := GetKubeVersion()
+	kubeVersion, err := intctrlutil.GetKubeVersion()
 	if err != nil {
 		return true
 	}
diff --git a/pkg/operations/rebuild_instance.go b/pkg/operations/rebuild_instance.go
index 6f9c2b893..6ee2176bf 100644
--- a/pkg/operations/rebuild_instance.go
+++ b/pkg/operations/rebuild_instance.go
@@ -566,8 +566,8 @@ func (r rebuildInstanceOpsHandler) prepareInplaceRebuildHelper(reqCtx intctrluti
 		if err = cli.Get(reqCtx.Ctx, client.ObjectKey{Name: rebuildInstance.BackupName, Namespace: opsRes.Cluster.Namespace}, backup); err != nil {
 			return nil, err
 		}
-		if backup.Labels[dptypes.BackupTypeLabelKey] != string(dpv1alpha1.BackupTypeFull) {
-			return nil, intctrlutil.NewFatalError(fmt.Sprintf(`the backup "%s" is not a Full backup`, rebuildInstance.BackupName))
+		if !slices.Contains([]string{string(dpv1alpha1.BackupTypeFull), string(dpv1alpha1.BackupTypeIncremental)}, backup.Labels[dptypes.BackupTypeLabelKey]) {
+			return nil, intctrlutil.NewFatalError(fmt.Sprintf(`the backup "%s" is not a Full or Incremental backup`, rebuildInstance.BackupName))
 		}
 		if backup.Status.Phase != dpv1alpha1.BackupPhaseCompleted {
 			return nil, intctrlutil.NewFatalError(fmt.Sprintf(`the backup "%s" phase is not Completed`, rebuildInstance.BackupName))



Test patch:
diff --git a/pkg/controller/instanceset/reconciler_update_test.go b/pkg/controller/instanceset/reconciler_update_test.go
index d5520e298..76ebe8ea7 100644
--- a/pkg/controller/instanceset/reconciler_update_test.go
+++ b/pkg/controller/instanceset/reconciler_update_test.go
@@ -28,14 +28,19 @@ import (
 
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/intstr"
+	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	kbappsv1 "github.com/apecloud/kubeblocks/apis/apps/v1"
 	workloads "github.com/apecloud/kubeblocks/apis/workloads/v1"
+	"github.com/apecloud/kubeblocks/pkg/constant"
 	"github.com/apecloud/kubeblocks/pkg/controller/builder"
 	"github.com/apecloud/kubeblocks/pkg/controller/kubebuilderx"
+	intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil"
+	viper "github.com/apecloud/kubeblocks/pkg/viperx"
 )
 
 var _ = Describe("update reconciler test", func() {
@@ -47,7 +52,7 @@ var _ = Describe("update reconciler test", func() {
 			SetUID(uid).
 			SetReplicas(replicas).
 			SetSelectorMatchLabel(selectors).
-			SetTemplate(template).
+			SetTemplate(*template.DeepCopy()).
 			SetVolumeClaimTemplates(volumeClaimTemplates...).
 			SetMinReadySeconds(minReadySeconds).
 			GetObject()
@@ -351,5 +356,59 @@ var _ = Describe("update reconciler test", func() {
 			Expect(res).Should(Equal(kubebuilderx.Continue))
 			expectUpdatedPods(tree, []string{"bar-1"})
 		})
+
+		testInplacePodVerticalScaling := func(useSubResource bool) {
+			oldFeatureGate := viper.GetBool(constant.FeatureGateInPlacePodVerticalScaling)
+			defer viper.Set(constant.FeatureGateInPlacePodVerticalScaling, oldFeatureGate)
+			viper.Set(constant.FeatureGateInPlacePodVerticalScaling, true)
+
+			// Mock intctrlutil.SupportResizeSubResource
+			origSupportResize := intctrlutil.SupportResizeSubResource
+			if useSubResource {
+				intctrlutil.SupportResizeSubResource = func() (bool, error) { return true, nil }
+			} else {
+				intctrlutil.SupportResizeSubResource = func() (bool, error) { return false, nil }
+			}
+			defer func() { intctrlutil.SupportResizeSubResource = origSupportResize }()
+
+			tree := kubebuilderx.NewObjectTree()
+			its.Spec.PodManagementPolicy = appsv1.ParallelPodManagement
+			its.Spec.Replicas = ptr.To[int32](1)
+			tree.SetRoot(its)
+
+			prepareForUpdate(tree)
+
+			pods := tree.List(&corev1.Pod{})
+			Expect(pods).Should(HaveLen(1))
+			pod := pods[0].(*corev1.Pod)
+			// mark available
+			pod.Status.Phase = corev1.PodRunning
+			pod.Status.Conditions = append(pod.Status.Conditions, getPodReadyCondition())
+
+			its.Spec.Template.Spec.Containers[0].Resources.Limits[corev1.ResourceCPU] = resource.MustParse("1")
+			reconciler = NewUpdateReconciler()
+			res, err := reconciler.Reconcile(tree)
+			Expect(err).Should(BeNil())
+			Expect(res).Should(Equal(kubebuilderx.Continue))
+			pods = tree.List(&corev1.Pod{})
+			pod = pods[0].(*corev1.Pod)
+			Expect(pod.Spec.Containers[0].Resources.Limits[corev1.ResourceCPU]).
+				Should(Equal(resource.MustParse("1")))
+			_, option, err := tree.GetWithOption(pod)
+			Expect(err).NotTo(HaveOccurred())
+			if useSubResource {
+				Expect(option.SubResource).Should(Equal("resize"))
+			} else {
+				Expect(option.SubResource).Should(BeEmpty())
+			}
+		}
+
+		It("inplace updates pod resource", func() {
+			testInplacePodVerticalScaling(false)
+		})
+
+		It("inplace updates pod resource using resize subresource", func() {
+			testInplacePodVerticalScaling(true)
+		})
 	})
 })
diff --git a/pkg/controller/kubebuilderx/plan_builder_test.go b/pkg/controller/kubebuilderx/plan_builder_test.go
index 999d92a61..0d8bc43a0 100644
--- a/pkg/controller/kubebuilderx/plan_builder_test.go
+++ b/pkg/controller/kubebuilderx/plan_builder_test.go
@@ -233,7 +233,7 @@ var _ = Describe("plan builder test", func() {
 				currentTree.SetRoot(its)
 				desiredTree.SetRoot(itsCopy)
 				Expect(desiredTree.Add(pod, headlessSvc, svc, env)).Should(Succeed())
-				vertices := buildOrderedVertices(ctx, currentTree, desiredTree)
+				vertices := buildOrderedVertices(transCtx, currentTree, desiredTree)
 
 				// compare vertices
 				Expect(vertices).Should(HaveLen(len(verticesExpected)))
@@ -246,6 +246,45 @@ var _ = Describe("plan builder test", func() {
 					})).Should(BeNumerically(">=", 0))
 				}
 			})
+
+			It("should append a vertex with subresource when Pod resource has the corresponding options", func() {
+				// Setup: two pods with different container resources
+				oldPod := builder.NewPodBuilder("ns", "pod").
+					AddContainer(corev1.Container{
+						Name:  "foo",
+						Image: "bar",
+						Resources: corev1.ResourceRequirements{
+							Requests: corev1.ResourceList{
+								corev1.ResourceCPU:    resource.MustParse("100m"),
+								corev1.ResourceMemory: resource.MustParse("128Mi"),
+							},
+						},
+					}).
+					GetObject()
+				newPod := oldPod.DeepCopy()
+				newPod.Spec.Containers[0].Resources.Requests[corev1.ResourceCPU] = resource.MustParse("200m")
+
+				// Setup trees
+				subResource := "foo"
+				currentTree := NewObjectTree()
+				desiredTree := NewObjectTree()
+				currentTree.SetRoot(builder.NewInstanceSetBuilder("ns", "root").GetObject())
+				desiredTree.SetRoot(currentTree.GetRoot())
+				Expect(currentTree.Add(oldPod)).Should(Succeed())
+				Expect(desiredTree.Update(newPod, WithSubResource(subResource))).Should(Succeed())
+
+				vertices := buildOrderedVertices(transCtx, currentTree, desiredTree)
+
+				// Find the vertex with subresource
+				found := false
+				for _, v := range vertices {
+					if pod, ok := v.Obj.(*corev1.Pod); ok && pod.Name == "pod" && v.SubResource == subResource {
+						found = true
+						Expect(*v.Action).To(Equal(model.UPDATE))
+					}
+				}
+				Expect(found).To(BeTrue())
+			})
 		})
 	})
 })
diff --git a/pkg/controller/kubebuilderx/reconciler_test.go b/pkg/controller/kubebuilderx/reconciler_test.go
index e6c00f2cd..242c1ba06 100644
--- a/pkg/controller/kubebuilderx/reconciler_test.go
+++ b/pkg/controller/kubebuilderx/reconciler_test.go
@@ -38,7 +38,7 @@ var _ = Describe("reconciler test", func() {
 	Context("ObjectTree methods", func() {
 		It("should work well", func() {
 			By("NewObjectTree")
-			expectedTree := &ObjectTree{children: make(model.ObjectSnapshot)}
+			expectedTree := &ObjectTree{children: make(model.ObjectSnapshot), childrenOptions: make(map[model.GVKNObjKey]ObjectOptions)}
 			tree := NewObjectTree()
 			Expect(tree).Should(Equal(expectedTree))
 
diff --git a/pkg/controller/kubebuilderx/suite_test.go b/pkg/controller/kubebuilderx/suite_test.go
index 8bf390e46..8be93eb1f 100644
--- a/pkg/controller/kubebuilderx/suite_test.go
+++ b/pkg/controller/kubebuilderx/suite_test.go
@@ -42,6 +42,7 @@ var (
 	k8sMock        *mocks.MockClient
 	ctx            context.Context
 	logger         logr.Logger
+	transCtx       *transformContext
 )
 
 func init() {
@@ -57,6 +58,11 @@ var _ = BeforeSuite(func() {
 	mockController, k8sMock = testutil.SetupK8sMock()
 	ctx = context.Background()
 	logger = logf.FromContext(ctx).WithValues("kubebuilderx-test", "foo")
+	transCtx = &transformContext{
+		ctx:    ctx,
+		cli:    k8sMock,
+		logger: logger,
+	}
 
 	go func() {
 		defer GinkgoRecover()
diff --git a/pkg/controllerutil/util_test.go b/pkg/controllerutil/util_test.go
index 08c148ad7..d881e7499 100644
--- a/pkg/controllerutil/util_test.go
+++ b/pkg/controllerutil/util_test.go
@@ -27,9 +27,13 @@ import (
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
-
+	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/version"
 	"k8s.io/client-go/tools/record"
+
+	"github.com/apecloud/kubeblocks/pkg/constant"
+	viper "github.com/apecloud/kubeblocks/pkg/viperx"
 )
 
 var _ = Describe("utils test", func() {
@@ -109,3 +113,57 @@ func TestRequestCtxMisc(t *testing.T) {
 		Recorder: record.NewFakeRecorder(100),
 	})
 }
+
+func TestGetKubeVersion(t *testing.T) {
+	tests := []struct {
+		name        string
+		versionInfo interface{}
+		expected    string
+		withError   bool
+	}{
+		{
+			name:        "valid version info",
+			versionInfo: version.Info{GitVersion: "v1.20"},
+			expected:    "v1.20",
+			withError:   false,
+		},
+		{
+			name:        "invalid version info",
+			versionInfo: "invalid",
+			expected:    "",
+			withError:   true,
+		},
+		{
+			name:        "invalid major version",
+			versionInfo: version.Info{GitVersion: "vmajor.20"},
+			expected:    "",
+			withError:   true,
+		},
+		{
+			name:        "invalid minor version",
+			versionInfo: version.Info{GitVersion: "v1.minor"},
+			expected:    "",
+			withError:   true,
+		},
+		{
+			name:        "version with suffix",
+			versionInfo: version.Info{GitVersion: "v1.20.0-rc1"},
+			expected:    "v1.20",
+			withError:   false,
+		},
+	}
+
+	defer viper.Set(constant.CfgKeyServerInfo, nil)
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			viper.Set(constant.CfgKeyServerInfo, tt.versionInfo)
+			ver, err := GetKubeVersion()
+			assert.Equal(t, tt.expected, ver)
+			if tt.withError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
diff --git a/pkg/dataprotection/utils/utils_test.go b/pkg/dataprotection/utils/utils_test.go
index 40ac30d68..baa63f98c 100644
--- a/pkg/dataprotection/utils/utils_test.go
+++ b/pkg/dataprotection/utils/utils_test.go
@@ -23,67 +23,11 @@ import (
 	"errors"
 	"testing"
 
-	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
-	"k8s.io/apimachinery/pkg/version"
 
 	dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1"
-	"github.com/apecloud/kubeblocks/pkg/constant"
 )
 
-func TestGetKubeVersion(t *testing.T) {
-	tests := []struct {
-		name        string
-		versionInfo interface{}
-		expected    string
-		withError   bool
-	}{
-		{
-			name:        "valid version info",
-			versionInfo: version.Info{GitVersion: "v1.20"},
-			expected:    "v1.20",
-			withError:   false,
-		},
-		{
-			name:        "invalid version info",
-			versionInfo: "invalid",
-			expected:    "",
-			withError:   true,
-		},
-		{
-			name:        "invalid major version",
-			versionInfo: version.Info{GitVersion: "vmajor.20"},
-			expected:    "",
-			withError:   true,
-		},
-		{
-			name:        "invalid minor version",
-			versionInfo: version.Info{GitVersion: "v1.minor"},
-			expected:    "",
-			withError:   true,
-		},
-		{
-			name:        "version with suffix",
-			versionInfo: version.Info{GitVersion: "v1.20.0-rc1"},
-			expected:    "v1.20",
-			withError:   false,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			viper.Set(constant.CfgKeyServerInfo, tt.versionInfo)
-			ver, err := GetKubeVersion()
-			assert.Equal(t, tt.expected, ver)
-			if tt.withError {
-				assert.Error(t, err)
-			} else {
-				assert.NoError(t, err)
-			}
-		})
-	}
-}
-
 func TestGetBackupStatusTarget(t *testing.T) {
 	sourceTargetName := "test-target"
 	backupTarget := dpv1alpha1.BackupStatusTarget{