Repository: apecloud/kubeblocks

Issue:
[BUG]Restore Cluster does not respect InstanceTemplate's ScheduingPolicy
**Describe the bug**
Restore Cluster does not respect InstanceTemplate's ScheduingPolicy. Which may cause Restore Job provision PV to some node's that the instance pod can not scheduled(like available zone or local-pv).

**To Reproduce**
Steps to reproduce the behavior:
1. create a restore cluster using InstanceTemplate with SchedulingPolicy, likes below
```
apiVersion: apps.kubeblocks.io/v1
kind: Cluster
metadata:
  annotations:
    kubeblocks.io/crd-api-version: apps.kubeblocks.io/v1
    # kubeblocks.io/restore-from-backup: '{"mysql":{"encryptedSystemAccounts":"abc","name":"t-vip-mysql57-xaz-backup-qz4nw","namespace":"thomas-demo","volumeRestorePolicy":"Parallel"}}'
    kubeblocks.io/restore-from-backup: '{"mysql":{"encryptedSystemAccounts":"abc","name":"t-vip-mysql57-xaz-backup-qz4nw","namespace":"thomas-demo","volumeRestorePolicy":"Serial"}}'
  name: mysql57-restore
  namespace: thomas-demo
spec:
  clusterDef: vip-mysql
  componentSpecs:
  - componentDef: vip-mysql-5.7-1.0.0-alpha.0
    instanceUpdateStrategy:
      type: OnDelete
    instances:
    - annotations:
        tpl: q9n0
      name: q9n0
      ordinals:
        ranges:
        - end: 299
          start: 0
      replicas: 2
      schedulingPolicy:
        affinity:
          nodeAffinity:
            requiredDuringSchedulingIgnoredDuringExecution:
              nodeSelectorTerms:
              - matchExpressions:
                - key: foo
                  operator: In
                  values:
                  - bar
    - annotations:
        tpl: iph1
      name: iph1
      ordinals:
        ranges:
        - end: 599
          start: 300
      replicas: 1
    - annotations:
        tpl: wte2
      name: wte2
      ordinals:
        ranges:
        - end: 899
          start: 600
      replicas: 2
    name: mysql
    persistentVolumeClaimRetentionPolicy:
      whenDeleted: Delete
      whenScaled: Retain
    replicas: 5
    resources:
      limits:
        cpu: "2"
        memory: 4G
      requests:
        cpu: "2"
        memory: 4G
    # serviceVersion: 5.7.44-vip.1
    volumeClaimTemplates:
    - name: data
      spec:
        accessModes:
        - ReadWriteOnce
        resources:
          requests:
            storage: 10G
        # storageClassName: open-local-lvm
  terminationPolicy: Delete
  # topology: replset
```
2. check the restore task, it does not has InstanceTemplate's SchedulingPolicy, likes below.
```
apiVersion: dataprotection.kubeblocks.io/v1alpha1
kind: Restore
metadata:
  creationTimestamp: "2025-07-30T01:41:08Z"
  finalizers:
  - dataprotection.kubeblocks.io/finalizer
  generation: 1
  labels:
    app.kubernetes.io/instance: mysql57-restore
    app.kubernetes.io/managed-by: kubeblocks
    apps.kubeblocks.io/component-name: mysql
  name: mysql57-restore-mysql-561898ef-preparedata-q9n0
  namespace: thomas-demo
  ownerReferences:
  - apiVersion: apps.kubeblocks.io/v1
    blockOwnerDeletion: true
    controller: true
    kind: Component
    name: mysql57-restore-mysql
    uid: 2d587bf9-98cc-4044-881d-f57ea67ce994
  resourceVersion: "112911207"
  uid: 4b4a5317-2412-444f-80d0-565ced3d666b
spec:
  backup:
    name: t-vip-mysql57-xaz-backup-qz4nw
    namespace: thomas-demo
  containerResources: {}
  prepareDataConfig:
    schedulingSpec: {}
    volumeClaimRestorePolicy: Serial
    volumeClaimsTemplate:
      replicas: 2
      templates:
      - metadata:
          labels:
            app.kubernetes.io/component: vip-mysql-5.7-1.0.0-alpha.0
            app.kubernetes.io/instance: mysql57-restore
            app.kubernetes.io/managed-by: kubeblocks
            apps.kubeblocks.io/component-name: mysql
            apps.kubeblocks.io/instance-template: q9n0
            apps.kubeblocks.io/vct-name: data
            componentdefinition.kubeblocks.io/name: vip-mysql-5.7-1.0.0-alpha.0
          name: data-mysql57-restore-mysql-q9n0
        volumeClaimSpec:
          accessModes:
          - ReadWriteOnce
          resources:
            requests:
              storage: 10G
        volumeSource: data
```


**Expected behavior**
Restore task should using the same SchedulingPolicy with InstanceTemplate.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - KB branch(release-1.0)

**Additional context**
Add any other context about the problem here.


Discussion:

Solution patch:
diff --git a/pkg/controller/plan/restore.go b/pkg/controller/plan/restore.go
index e0982af41..492068427 100644
--- a/pkg/controller/plan/restore.go
+++ b/pkg/controller/plan/restore.go
@@ -223,7 +223,7 @@ func (r *RestoreManager) BuildPrepareDataRestore(comp *component.SynthesizedComp
 			Parameters:  r.parameters,
 			PrepareDataConfig: &dpv1alpha1.PrepareDataConfig{
 				RequiredPolicyForAllPodSelection: r.buildRequiredPolicy(sourceTarget),
-				SchedulingSpec:                   r.buildSchedulingSpec(comp),
+				SchedulingSpec:                   r.buildSchedulingSpec(comp, template),
 				VolumeClaimRestorePolicy:         r.volumeRestorePolicy,
 				RestoreVolumeClaimsTemplate: &dpv1alpha1.RestoreVolumeClaimsTemplate{
 					Replicas:      r.replicas,
@@ -328,7 +328,19 @@ func (r *RestoreManager) buildRequiredPolicy(sourceTarget *dpv1alpha1.BackupStat
 	return requiredPolicy
 }
 
-func (r *RestoreManager) buildSchedulingSpec(comp *component.SynthesizedComponent) dpv1alpha1.SchedulingSpec {
+func (r *RestoreManager) buildSchedulingSpec(comp *component.SynthesizedComponent, template *appsv1.InstanceTemplate) dpv1alpha1.SchedulingSpec {
+	// if instance template provided and scheduling policy is set, use it
+	if template != nil && template.SchedulingPolicy != nil {
+		return dpv1alpha1.SchedulingSpec{
+			Affinity:                  template.SchedulingPolicy.Affinity,
+			Tolerations:               template.SchedulingPolicy.Tolerations,
+			TopologySpreadConstraints: template.SchedulingPolicy.TopologySpreadConstraints,
+			NodeSelector:              template.SchedulingPolicy.NodeSelector,
+			SchedulerName:             template.SchedulingPolicy.SchedulerName,
+			NodeName:                  template.SchedulingPolicy.NodeName,
+		}
+	}
+	// otherwise, fallback to component's pod spec
 	if comp.PodSpec == nil {
 		return dpv1alpha1.SchedulingSpec{}
 	}
@@ -336,6 +348,9 @@ func (r *RestoreManager) buildSchedulingSpec(comp *component.SynthesizedComponen
 		Affinity:                  comp.PodSpec.Affinity,
 		Tolerations:               comp.PodSpec.Tolerations,
 		TopologySpreadConstraints: comp.PodSpec.TopologySpreadConstraints,
+		NodeSelector:              comp.PodSpec.NodeSelector,
+		SchedulerName:             comp.PodSpec.SchedulerName,
+		NodeName:                  comp.PodSpec.NodeName,
 	}
 }
 



Test patch:
diff --git a/pkg/controller/plan/restore_test.go b/pkg/controller/plan/restore_test.go
index 3f8a85132..d884b5559 100644
--- a/pkg/controller/plan/restore_test.go
+++ b/pkg/controller/plan/restore_test.go
@@ -263,6 +263,8 @@ var _ = Describe("Restore", func() {
 			topologyKey     = "testTopologyKey"
 			labelKey        = "testNodeLabelKey"
 			labelValue      = "testLabelValue"
+			istLabelKey     = "testISTLabelKey"
+			istLabelValue   = "testISTLabelValue"
 		)
 
 		var (
@@ -317,8 +319,26 @@ var _ = Describe("Restore", func() {
 			By("create actionset of full backup")
 			fullBackupActionSet = testapps.CreateCustomizedObj(&testCtx, "backup/actionset.yaml", &dpv1alpha1.ActionSet{}, testapps.RandomizedObjName())
 			fullBackupActionSetName = fullBackupActionSet.Name
+			podSpec := &compDef.Spec.Runtime
+			podSpec.Affinity = &corev1.Affinity{
+				NodeAffinity: &corev1.NodeAffinity{
+					RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+						NodeSelectorTerms: []corev1.NodeSelectorTerm{
+							{
+								MatchExpressions: []corev1.NodeSelectorRequirement{
+									{
+										Key:      labelKey,
+										Operator: corev1.NodeSelectorOpIn,
+										Values:   []string{labelValue},
+									},
+								},
+							},
+						},
+					},
+				},
+			}
 			synthesizedComponent = &component.SynthesizedComponent{
-				PodSpec:              &compDef.Spec.Runtime,
+				PodSpec:              podSpec,
 				VolumeClaimTemplates: intctrlutil.ToCoreV1PVCTs(cluster.Spec.ComponentSpecs[0].VolumeClaimTemplates),
 				Name:                 defaultCompName,
 				Replicas:             3,
@@ -335,6 +355,25 @@ var _ = Describe("Restore", func() {
 				Instances: []appsv1.InstanceTemplate{{
 					Name:     "foo",
 					Replicas: func() *int32 { replicas := int32(1); return &replicas }(),
+					SchedulingPolicy: &appsv1.SchedulingPolicy{
+						Affinity: &corev1.Affinity{
+							NodeAffinity: &corev1.NodeAffinity{
+								RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
+									NodeSelectorTerms: []corev1.NodeSelectorTerm{
+										{
+											MatchExpressions: []corev1.NodeSelectorRequirement{
+												{
+													Key:      istLabelKey,
+													Operator: corev1.NodeSelectorOpIn,
+													Values:   []string{istLabelValue},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
 					Ordinals: appsv1.Ordinals{
 						Ranges: []appsv1.Range{
 							{Start: startIndex, End: 20},
@@ -400,6 +439,8 @@ var _ = Describe("Restore", func() {
 			Expect(k8sClient.Get(ctx, namedspace, restore)).Should(Succeed())
 			Expect(restore.Spec.PrepareDataConfig.RestoreVolumeClaimsTemplate.StartingIndex).Should(Equal(startIndex))
 			Expect(restore.Spec.PrepareDataConfig.RestoreVolumeClaimsTemplate.Replicas).Should(Equal(int32(1)))
+			Expect(restore.Spec.PrepareDataConfig.SchedulingSpec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions[0].Key).Should(Equal(istLabelKey))
+			Expect(restore.Spec.PrepareDataConfig.SchedulingSpec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions[0].Values[0]).Should(Equal(istLabelValue))
 
 			By("mock template restore of prepareData stage to Completed")
 			Expect(testapps.GetAndChangeObjStatus(&testCtx, namedspace, func(restore *dpv1alpha1.Restore) {
@@ -417,6 +458,8 @@ var _ = Describe("Restore", func() {
 			Expect(k8sClient.Get(ctx, defaultNs, defaultRestore)).Should(Succeed())
 			Expect(defaultRestore.Spec.PrepareDataConfig.RestoreVolumeClaimsTemplate.StartingIndex).Should(Equal(int32(0)))
 			Expect(defaultRestore.Spec.PrepareDataConfig.RestoreVolumeClaimsTemplate.Replicas).Should(Equal(int32(2)))
+			Expect(defaultRestore.Spec.PrepareDataConfig.SchedulingSpec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions[0].Key).Should(Equal(labelKey))
+			Expect(defaultRestore.Spec.PrepareDataConfig.SchedulingSpec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions[0].Values[0]).Should(Equal(labelValue))
 
 			By("mock default restore of prepareData stage to Completed")
 			Expect(testapps.GetAndChangeObjStatus(&testCtx, defaultNs, func(restore *dpv1alpha1.Restore) {