From ce0888ee440eed402a42a0ecd5d83960d39cd73c Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 22 Jan 2026 16:49:25 +0800 Subject: [PATCH 1/5] issue 9343: include PV topology to data mover pod affinities Signed-off-by: Lyndon-Li --- pkg/exposer/csi_snapshot.go | 13 +- pkg/exposer/csi_snapshot_priority_test.go | 2 + pkg/exposer/generic_restore.go | 2 +- pkg/repository/maintenance/maintenance.go | 3 +- pkg/util/kube/pod.go | 39 +-- pkg/util/kube/pod_test.go | 294 +++++++++++++++--- pkg/util/kube/pvc_pv.go | 22 ++ test/e2e/nodeagentconfig/node-agent-config.go | 4 +- 8 files changed, 312 insertions(+), 67 deletions(-) diff --git a/pkg/exposer/csi_snapshot.go b/pkg/exposer/csi_snapshot.go index 5acb229d2..637cd7c07 100644 --- a/pkg/exposer/csi_snapshot.go +++ b/pkg/exposer/csi_snapshot.go @@ -124,6 +124,15 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O "owner": ownerObject.Name, }) + volumeTopology, err := kube.GetVolumeTopology(ctx, e.kubeClient.CoreV1(), e.kubeClient.StorageV1(), csiExposeParam.SourcePVName, csiExposeParam.StorageClass) + if err != nil { + return errors.Wrapf(err, "error getting volume topology for PV %s, storage class %s", csiExposeParam.SourcePVName, csiExposeParam.StorageClass) + } + + if volumeTopology != nil { + curLog.Infof("Using volume topology %v", volumeTopology) + } + curLog.Info("Exposing CSI snapshot") volumeSnapshot, err := csi.WaitVolumeSnapshotReady(ctx, e.csiSnapshotClient, csiExposeParam.SnapshotName, csiExposeParam.SourceNamespace, csiExposeParam.ExposeTimeout, curLog) @@ -254,6 +263,7 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O csiExposeParam.NodeOS, csiExposeParam.PriorityClassName, intoleratableNodes, + volumeTopology, ) if err != nil { return errors.Wrap(err, "error to create backup pod") @@ -588,6 +598,7 @@ func (e *csiSnapshotExposer) createBackupPod( nodeOS string, priorityClassName string, intoleratableNodes []string, + volumeTopology *corev1api.NodeSelector, ) (*corev1api.Pod, error) { podName := ownerObject.Name @@ -701,7 +712,7 @@ func (e *csiSnapshotExposer) createBackupPod( } if affinity != nil { - podAffinity = kube.ToSystemAffinity([]*kube.LoadAffinity{affinity}) + podAffinity = kube.ToSystemAffinity(affinity, volumeTopology) } pod := &corev1api.Pod{ diff --git a/pkg/exposer/csi_snapshot_priority_test.go b/pkg/exposer/csi_snapshot_priority_test.go index 345d5b327..d1ffa4700 100644 --- a/pkg/exposer/csi_snapshot_priority_test.go +++ b/pkg/exposer/csi_snapshot_priority_test.go @@ -154,6 +154,7 @@ func TestCreateBackupPodWithPriorityClass(t *testing.T) { kube.NodeOSLinux, tc.expectedPriorityClass, nil, + nil, ) require.NoError(t, err, tc.description) @@ -239,6 +240,7 @@ func TestCreateBackupPodWithMissingConfigMap(t *testing.T) { kube.NodeOSLinux, "", // empty priority class since config map is missing nil, + nil, ) // Should succeed even when config map is missing diff --git a/pkg/exposer/generic_restore.go b/pkg/exposer/generic_restore.go index c10370072..f7cf24d8d 100644 --- a/pkg/exposer/generic_restore.go +++ b/pkg/exposer/generic_restore.go @@ -498,7 +498,7 @@ func (e *genericRestoreExposer) createRestorePod( e.log.Infof("No selected node for restore pod. Try to get affinity from the node-agent config.") if affinity != nil { - podAffinity = kube.ToSystemAffinity([]*kube.LoadAffinity{affinity}) + podAffinity = kube.ToSystemAffinity(affinity, nil) } } diff --git a/pkg/repository/maintenance/maintenance.go b/pkg/repository/maintenance/maintenance.go index 496d07703..426cb44d4 100644 --- a/pkg/repository/maintenance/maintenance.go +++ b/pkg/repository/maintenance/maintenance.go @@ -671,8 +671,7 @@ func buildJob( } if config != nil && len(config.LoadAffinities) > 0 { - // Maintenance job only takes the first loadAffinity. - affinity := kube.ToSystemAffinity([]*kube.LoadAffinity{config.LoadAffinities[0]}) + affinity := kube.ToSystemAffinity(config.LoadAffinities[0], nil) job.Spec.Template.Spec.Affinity = affinity } diff --git a/pkg/util/kube/pod.go b/pkg/util/kube/pod.go index 2aeb45a1c..b5cd9a62c 100644 --- a/pkg/util/kube/pod.go +++ b/pkg/util/kube/pod.go @@ -230,14 +230,9 @@ func CollectPodLogs(ctx context.Context, podGetter corev1client.CoreV1Interface, return nil } -func ToSystemAffinity(loadAffinities []*LoadAffinity) *corev1api.Affinity { - if len(loadAffinities) == 0 { - return nil - } - nodeSelectorTermList := make([]corev1api.NodeSelectorTerm, 0) - - for _, loadAffinity := range loadAffinities { - requirements := []corev1api.NodeSelectorRequirement{} +func ToSystemAffinity(loadAffinity *LoadAffinity, volumeTopolpogy *corev1api.NodeSelector) *corev1api.Affinity { + requirements := []corev1api.NodeSelectorRequirement{} + if loadAffinity != nil { for k, v := range loadAffinity.NodeSelector.MatchLabels { requirements = append(requirements, corev1api.NodeSelectorRequirement{ Key: k, @@ -253,25 +248,25 @@ func ToSystemAffinity(loadAffinities []*LoadAffinity) *corev1api.Affinity { Operator: corev1api.NodeSelectorOperator(exp.Operator), }) } - - nodeSelectorTermList = append( - nodeSelectorTermList, - corev1api.NodeSelectorTerm{ - MatchExpressions: requirements, - }, - ) } - if len(nodeSelectorTermList) > 0 { - result := new(corev1api.Affinity) - result.NodeAffinity = new(corev1api.NodeAffinity) - result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution = new(corev1api.NodeSelector) - result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms = nodeSelectorTermList + result := new(corev1api.Affinity) + result.NodeAffinity = new(corev1api.NodeAffinity) + result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution = new(corev1api.NodeSelector) - return result + if volumeTopolpogy != nil { + result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms = append(result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, volumeTopolpogy.NodeSelectorTerms...) + } else if len(requirements) > 0 { + result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms = make([]corev1api.NodeSelectorTerm, 1) + } else { + return nil } - return nil + for i := range result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms { + result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[i].MatchExpressions = append(result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[i].MatchExpressions, requirements...) + } + + return result } func DiagnosePod(pod *corev1api.Pod, events *corev1api.EventList) string { diff --git a/pkg/util/kube/pod_test.go b/pkg/util/kube/pod_test.go index 6751e8b6e..aa8d4db99 100644 --- a/pkg/util/kube/pod_test.go +++ b/pkg/util/kube/pod_test.go @@ -747,24 +747,23 @@ func TestCollectPodLogs(t *testing.T) { func TestToSystemAffinity(t *testing.T) { tests := []struct { name string - loadAffinities []*LoadAffinity + loadAffinity *LoadAffinity + volumeTopology *corev1api.NodeSelector expected *corev1api.Affinity }{ { name: "loadAffinity is nil", }, { - name: "loadAffinity is empty", - loadAffinities: []*LoadAffinity{}, + name: "loadAffinity is empty", + loadAffinity: &LoadAffinity{}, }, { name: "with match label", - loadAffinities: []*LoadAffinity{ - { - NodeSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{ - "key-1": "value-1", - }, + loadAffinity: &LoadAffinity{ + NodeSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "key-1": "value-1", }, }, }, @@ -788,23 +787,21 @@ func TestToSystemAffinity(t *testing.T) { }, { name: "with match expression", - loadAffinities: []*LoadAffinity{ - { - NodeSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{ - "key-2": "value-2", + loadAffinity: &LoadAffinity{ + NodeSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "key-2": "value-2", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "key-3", + Values: []string{"value-3-1", "value-3-2"}, + Operator: metav1.LabelSelectorOpNotIn, }, - MatchExpressions: []metav1.LabelSelectorRequirement{ - { - Key: "key-3", - Values: []string{"value-3-1", "value-3-2"}, - Operator: metav1.LabelSelectorOpNotIn, - }, - { - Key: "key-4", - Values: []string{"value-4-1", "value-4-2", "value-4-3"}, - Operator: metav1.LabelSelectorOpDoesNotExist, - }, + { + Key: "key-4", + Values: []string{"value-4-1", "value-4-2", "value-4-3"}, + Operator: metav1.LabelSelectorOpDoesNotExist, }, }, }, @@ -838,19 +835,49 @@ func TestToSystemAffinity(t *testing.T) { }, }, { - name: "multiple load affinities", - loadAffinities: []*LoadAffinity{ - { - NodeSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{ - "key-1": "value-1", + name: "with olume topology", + volumeTopology: &corev1api.NodeSelector{ + NodeSelectorTerms: []corev1api.NodeSelectorTerm{ + { + MatchExpressions: []corev1api.NodeSelectorRequirement{ + { + Key: "key-5", + Values: []string{"value-5-1", "value-5-2", "value-5-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + { + Key: "key-6", + Values: []string{"value-5-1", "value-5-2", "value-5-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, }, }, - }, - { - NodeSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{ - "key-2": "value-2", + { + MatchExpressions: []corev1api.NodeSelectorRequirement{ + { + Key: "key-7", + Values: []string{"value-7-1", "value-7-2", "value-7-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + { + Key: "key-8", + Values: []string{"value-8-1", "value-8-2", "value-8-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + }, + }, + { + MatchFields: []corev1api.NodeSelectorRequirement{ + { + Key: "key-9", + Values: []string{"value-9-1", "value-9-2", "value-9-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + { + Key: "key-a", + Values: []string{"value-a-1", "value-a-2", "value-a-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, }, }, }, @@ -862,10 +889,177 @@ func TestToSystemAffinity(t *testing.T) { { MatchExpressions: []corev1api.NodeSelectorRequirement{ { - Key: "key-1", - Values: []string{"value-1"}, + Key: "key-5", + Values: []string{"value-5-1", "value-5-2", "value-5-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + { + Key: "key-6", + Values: []string{"value-5-1", "value-5-2", "value-5-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + }, + }, + { + MatchExpressions: []corev1api.NodeSelectorRequirement{ + { + Key: "key-7", + Values: []string{"value-7-1", "value-7-2", "value-7-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + { + Key: "key-8", + Values: []string{"value-8-1", "value-8-2", "value-8-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + }, + }, + { + MatchFields: []corev1api.NodeSelectorRequirement{ + { + Key: "key-9", + Values: []string{"value-9-1", "value-9-2", "value-9-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + { + Key: "key-a", + Values: []string{"value-a-1", "value-a-2", "value-a-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "with match expression and volume topology", + loadAffinity: &LoadAffinity{ + NodeSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "key-2": "value-2", + }, + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "key-3", + Values: []string{"value-3-1", "value-3-2"}, + Operator: metav1.LabelSelectorOpNotIn, + }, + { + Key: "key-4", + Values: []string{"value-4-1", "value-4-2", "value-4-3"}, + Operator: metav1.LabelSelectorOpDoesNotExist, + }, + }, + }, + }, + volumeTopology: &corev1api.NodeSelector{ + NodeSelectorTerms: []corev1api.NodeSelectorTerm{ + { + MatchExpressions: []corev1api.NodeSelectorRequirement{ + { + Key: "key-5", + Values: []string{"value-5-1", "value-5-2", "value-5-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + { + Key: "key-6", + Values: []string{"value-5-1", "value-5-2", "value-5-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + }, + }, + { + MatchExpressions: []corev1api.NodeSelectorRequirement{ + { + Key: "key-7", + Values: []string{"value-7-1", "value-7-2", "value-7-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + { + Key: "key-8", + Values: []string{"value-8-1", "value-8-2", "value-8-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + }, + }, + { + MatchFields: []corev1api.NodeSelectorRequirement{ + { + Key: "key-9", + Values: []string{"value-9-1", "value-9-2", "value-9-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + { + Key: "key-a", + Values: []string{"value-a-1", "value-a-2", "value-a-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + }, + }, + }, + }, + expected: &corev1api.Affinity{ + NodeAffinity: &corev1api.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{ + NodeSelectorTerms: []corev1api.NodeSelectorTerm{ + { + MatchExpressions: []corev1api.NodeSelectorRequirement{ + { + Key: "key-5", + Values: []string{"value-5-1", "value-5-2", "value-5-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + { + Key: "key-6", + Values: []string{"value-5-1", "value-5-2", "value-5-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + { + Key: "key-2", + Values: []string{"value-2"}, Operator: corev1api.NodeSelectorOpIn, }, + { + Key: "key-3", + Values: []string{"value-3-1", "value-3-2"}, + Operator: corev1api.NodeSelectorOpNotIn, + }, + { + Key: "key-4", + Values: []string{"value-4-1", "value-4-2", "value-4-3"}, + Operator: corev1api.NodeSelectorOpDoesNotExist, + }, + }, + }, + { + MatchExpressions: []corev1api.NodeSelectorRequirement{ + { + Key: "key-7", + Values: []string{"value-7-1", "value-7-2", "value-7-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + { + Key: "key-8", + Values: []string{"value-8-1", "value-8-2", "value-8-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + { + Key: "key-2", + Values: []string{"value-2"}, + Operator: corev1api.NodeSelectorOpIn, + }, + { + Key: "key-3", + Values: []string{"value-3-1", "value-3-2"}, + Operator: corev1api.NodeSelectorOpNotIn, + }, + { + Key: "key-4", + Values: []string{"value-4-1", "value-4-2", "value-4-3"}, + Operator: corev1api.NodeSelectorOpDoesNotExist, + }, }, }, { @@ -875,6 +1069,28 @@ func TestToSystemAffinity(t *testing.T) { Values: []string{"value-2"}, Operator: corev1api.NodeSelectorOpIn, }, + { + Key: "key-3", + Values: []string{"value-3-1", "value-3-2"}, + Operator: corev1api.NodeSelectorOpNotIn, + }, + { + Key: "key-4", + Values: []string{"value-4-1", "value-4-2", "value-4-3"}, + Operator: corev1api.NodeSelectorOpDoesNotExist, + }, + }, + MatchFields: []corev1api.NodeSelectorRequirement{ + { + Key: "key-9", + Values: []string{"value-9-1", "value-9-2", "value-9-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, + { + Key: "key-a", + Values: []string{"value-a-1", "value-a-2", "value-a-3"}, + Operator: corev1api.NodeSelectorOpGt, + }, }, }, }, @@ -886,7 +1102,7 @@ func TestToSystemAffinity(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - affinity := ToSystemAffinity(test.loadAffinities) + affinity := ToSystemAffinity(test.loadAffinity, test.volumeTopology) assert.True(t, reflect.DeepEqual(affinity, test.expected)) }) } diff --git a/pkg/util/kube/pvc_pv.go b/pkg/util/kube/pvc_pv.go index 5209cf6ea..c82040ed4 100644 --- a/pkg/util/kube/pvc_pv.go +++ b/pkg/util/kube/pvc_pv.go @@ -580,3 +580,25 @@ func GetPVAttachedNodes(ctx context.Context, pv string, storageClient storagev1. return nodes, nil } + +func GetVolumeTopology(ctx context.Context, volumeClient corev1client.CoreV1Interface, storageClient storagev1.StorageV1Interface, pvName string, scName string) (*corev1api.NodeSelector, error) { + sc, err := storageClient.StorageClasses().Get(ctx, scName, metav1.GetOptions{}) + if err != nil { + return nil, errors.Wrapf(err, "error getting storage class %s", scName) + } + + if sc.VolumeBindingMode == nil || *sc.VolumeBindingMode != storagev1api.VolumeBindingWaitForFirstConsumer { + return nil, nil + } + + pv, err := volumeClient.PersistentVolumes().Get(ctx, pvName, metav1.GetOptions{}) + if err != nil { + return nil, errors.Wrapf(err, "error getting PV %s", pvName) + } + + if pv.Spec.NodeAffinity == nil { + return nil, nil + } + + return pv.Spec.NodeAffinity.Required, nil +} diff --git a/test/e2e/nodeagentconfig/node-agent-config.go b/test/e2e/nodeagentconfig/node-agent-config.go index 1b46eed65..01cc6e38c 100644 --- a/test/e2e/nodeagentconfig/node-agent-config.go +++ b/test/e2e/nodeagentconfig/node-agent-config.go @@ -240,7 +240,7 @@ func (n *NodeAgentConfigTestCase) Backup() error { Expect(backupPodList.Items[0].Spec.PriorityClassName).To(Equal(n.nodeAgentConfigs.PriorityClassName)) // In backup, only the second element of LoadAffinity array should be used. - expectedAffinity := velerokubeutil.ToSystemAffinity(n.nodeAgentConfigs.LoadAffinity[1:]) + expectedAffinity := velerokubeutil.ToSystemAffinity(n.nodeAgentConfigs.LoadAffinity[1], nil) Expect(backupPodList.Items[0].Spec.Affinity).To(Equal(expectedAffinity)) @@ -317,7 +317,7 @@ func (n *NodeAgentConfigTestCase) Restore() error { Expect(restorePodList.Items[0].Spec.PriorityClassName).To(Equal(n.nodeAgentConfigs.PriorityClassName)) // In restore, only the first element of LoadAffinity array should be used. - expectedAffinity := velerokubeutil.ToSystemAffinity(n.nodeAgentConfigs.LoadAffinity[:1]) + expectedAffinity := velerokubeutil.ToSystemAffinity(n.nodeAgentConfigs.LoadAffinity[0], nil) Expect(restorePodList.Items[0].Spec.Affinity).To(Equal(expectedAffinity)) From c30164c355a0c3403cf01f663e5560df500a8f7c Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 10 Mar 2026 15:53:39 +0800 Subject: [PATCH 2/5] issue 9343: include PV topology to data mover pod affinities Signed-off-by: Lyndon-Li --- changelogs/unreleased/9532-Lyndon-Li‎ | 1 + pkg/exposer/csi_snapshot_test.go | 66 +++++++++++++ pkg/util/kube/pvc_pv.go | 4 + pkg/util/kube/pvc_pv_test.go | 140 +++++++++++++++++++++++++++ 4 files changed, 211 insertions(+) create mode 100644 changelogs/unreleased/9532-Lyndon-Li‎ diff --git a/changelogs/unreleased/9532-Lyndon-Li‎ b/changelogs/unreleased/9532-Lyndon-Li‎ new file mode 100644 index 000000000..0d5094c22 --- /dev/null +++ b/changelogs/unreleased/9532-Lyndon-Li‎ @@ -0,0 +1 @@ +Fix issue #9343, include PV topology to data mover pod affinities \ No newline at end of file diff --git a/pkg/exposer/csi_snapshot_test.go b/pkg/exposer/csi_snapshot_test.go index b4dd92c3f..4ec1d6d9d 100644 --- a/pkg/exposer/csi_snapshot_test.go +++ b/pkg/exposer/csi_snapshot_test.go @@ -68,6 +68,12 @@ func TestExpose(t *testing.T) { var restoreSize int64 = 123456 + scObj := &storagev1api.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-sc", + }, + } + snapshotClass := "fake-snapshot-class" vsObject := &snapshotv1api.VolumeSnapshot{ ObjectMeta: metav1.ObjectMeta{ @@ -199,6 +205,17 @@ func TestExpose(t *testing.T) { expectedAffinity *corev1api.Affinity expectedPVCAnnotation map[string]string }{ + { + name: "get volume topology fail", + ownerBackup: backup, + exposeParam: CSISnapshotExposeParam{ + SnapshotName: "fake-vs", + OperationTimeout: time.Millisecond, + ExposeTimeout: time.Millisecond, + StorageClass: "fake-sc", + }, + err: "error getting volume topology for PV , storage class fake-sc: error getting storage class fake-sc: storageclasses.storage.k8s.io \"fake-sc\" not found", + }, { name: "wait vs ready fail", ownerBackup: backup, @@ -206,6 +223,10 @@ func TestExpose(t *testing.T) { SnapshotName: "fake-vs", OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, + StorageClass: "fake-sc", + }, + kubeClientObj: []runtime.Object{ + scObj, }, err: "error wait volume snapshot ready: error to get VolumeSnapshot /fake-vs: volumesnapshots.snapshot.storage.k8s.io \"fake-vs\" not found", }, @@ -217,10 +238,14 @@ func TestExpose(t *testing.T) { SourceNamespace: "fake-ns", OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, + StorageClass: "fake-sc", }, snapshotClientObj: []runtime.Object{ vsObject, }, + kubeClientObj: []runtime.Object{ + scObj, + }, err: "error to get volume snapshot content: error getting volume snapshot content from API: volumesnapshotcontents.snapshot.storage.k8s.io \"fake-vsc\" not found", }, { @@ -231,6 +256,7 @@ func TestExpose(t *testing.T) { SourceNamespace: "fake-ns", OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, + StorageClass: "fake-sc", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -245,6 +271,9 @@ func TestExpose(t *testing.T) { }, }, }, + kubeClientObj: []runtime.Object{ + scObj, + }, err: "error to delete volume snapshot: error to delete volume snapshot: fake-delete-error", }, { @@ -255,6 +284,7 @@ func TestExpose(t *testing.T) { SourceNamespace: "fake-ns", OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, + StorageClass: "fake-sc", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -269,6 +299,9 @@ func TestExpose(t *testing.T) { }, }, }, + kubeClientObj: []runtime.Object{ + scObj, + }, err: "error to delete volume snapshot content: error to delete volume snapshot content: fake-delete-error", }, { @@ -279,6 +312,7 @@ func TestExpose(t *testing.T) { SourceNamespace: "fake-ns", OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, + StorageClass: "fake-sc", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -293,6 +327,9 @@ func TestExpose(t *testing.T) { }, }, }, + kubeClientObj: []runtime.Object{ + scObj, + }, err: "error to create backup volume snapshot: fake-create-error", }, { @@ -303,6 +340,7 @@ func TestExpose(t *testing.T) { SourceNamespace: "fake-ns", OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, + StorageClass: "fake-sc", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -317,6 +355,9 @@ func TestExpose(t *testing.T) { }, }, }, + kubeClientObj: []runtime.Object{ + scObj, + }, err: "error to create backup volume snapshot content: fake-create-error", }, { @@ -326,11 +367,15 @@ func TestExpose(t *testing.T) { SnapshotName: "fake-vs", SourceNamespace: "fake-ns", AccessMode: "fake-mode", + StorageClass: "fake-sc", }, snapshotClientObj: []runtime.Object{ vsObject, vscObj, }, + kubeClientObj: []runtime.Object{ + scObj, + }, err: "error to create backup pvc: unsupported access mode fake-mode", }, { @@ -342,6 +387,7 @@ func TestExpose(t *testing.T) { OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, AccessMode: AccessModeFileSystem, + StorageClass: "fake-sc", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -356,6 +402,9 @@ func TestExpose(t *testing.T) { }, }, }, + kubeClientObj: []runtime.Object{ + scObj, + }, err: "error to create backup pvc: error to create pvc: fake-create-error", }, { @@ -367,6 +416,7 @@ func TestExpose(t *testing.T) { AccessMode: AccessModeFileSystem, OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, + StorageClass: "fake-sc", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -374,6 +424,7 @@ func TestExpose(t *testing.T) { }, kubeClientObj: []runtime.Object{ daemonSet, + scObj, }, kubeReactors: []reactor{ { @@ -395,6 +446,7 @@ func TestExpose(t *testing.T) { AccessMode: AccessModeFileSystem, OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, + StorageClass: "fake-sc", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -402,6 +454,7 @@ func TestExpose(t *testing.T) { }, kubeClientObj: []runtime.Object{ daemonSet, + scObj, }, }, { @@ -413,6 +466,7 @@ func TestExpose(t *testing.T) { AccessMode: AccessModeFileSystem, OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, + StorageClass: "fake-sc", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -420,6 +474,7 @@ func TestExpose(t *testing.T) { }, kubeClientObj: []runtime.Object{ daemonSet, + scObj, }, }, { @@ -432,6 +487,7 @@ func TestExpose(t *testing.T) { OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, VolumeSize: *resource.NewQuantity(567890, ""), + StorageClass: "fake-sc", }, snapshotClientObj: []runtime.Object{ vsObjectWithoutRestoreSize, @@ -439,6 +495,7 @@ func TestExpose(t *testing.T) { }, kubeClientObj: []runtime.Object{ daemonSet, + scObj, }, expectedVolumeSize: resource.NewQuantity(567890, ""), }, @@ -465,6 +522,7 @@ func TestExpose(t *testing.T) { }, kubeClientObj: []runtime.Object{ daemonSet, + scObj, }, expectedReadOnlyPVC: true, }, @@ -491,6 +549,7 @@ func TestExpose(t *testing.T) { }, kubeClientObj: []runtime.Object{ daemonSet, + scObj, }, expectedReadOnlyPVC: true, expectedBackupPVCStorageClass: "fake-sc-read-only", @@ -517,6 +576,7 @@ func TestExpose(t *testing.T) { }, kubeClientObj: []runtime.Object{ daemonSet, + scObj, }, expectedBackupPVCStorageClass: "fake-sc-read-only", }, @@ -551,6 +611,7 @@ func TestExpose(t *testing.T) { }, kubeClientObj: []runtime.Object{ daemonSet, + scObj, }, expectedAffinity: &corev1api.Affinity{ NodeAffinity: &corev1api.NodeAffinity{ @@ -606,6 +667,7 @@ func TestExpose(t *testing.T) { }, kubeClientObj: []runtime.Object{ daemonSet, + scObj, }, expectedBackupPVCStorageClass: "fake-sc-read-only", expectedAffinity: &corev1api.Affinity{ @@ -649,6 +711,7 @@ func TestExpose(t *testing.T) { }, kubeClientObj: []runtime.Object{ daemonSet, + scObj, }, expectedBackupPVCStorageClass: "fake-sc-read-only", expectedAffinity: nil, @@ -677,6 +740,7 @@ func TestExpose(t *testing.T) { }, kubeClientObj: []runtime.Object{ daemonSet, + scObj, }, kubeReactors: []reactor{ { @@ -714,6 +778,7 @@ func TestExpose(t *testing.T) { }, kubeClientObj: []runtime.Object{ daemonSet, + scObj, }, expectedAffinity: nil, expectedPVCAnnotation: map[string]string{util.VSphereCNSFastCloneAnno: "true"}, @@ -744,6 +809,7 @@ func TestExpose(t *testing.T) { daemonSet, volumeAttachement1, volumeAttachement2, + scObj, }, expectedAffinity: &corev1api.Affinity{ NodeAffinity: &corev1api.NodeAffinity{ diff --git a/pkg/util/kube/pvc_pv.go b/pkg/util/kube/pvc_pv.go index c82040ed4..d5d2e2041 100644 --- a/pkg/util/kube/pvc_pv.go +++ b/pkg/util/kube/pvc_pv.go @@ -582,6 +582,10 @@ func GetPVAttachedNodes(ctx context.Context, pv string, storageClient storagev1. } func GetVolumeTopology(ctx context.Context, volumeClient corev1client.CoreV1Interface, storageClient storagev1.StorageV1Interface, pvName string, scName string) (*corev1api.NodeSelector, error) { + if pvName == "" || scName == "" { + return nil, errors.Errorf("invalid parameter, pv %s, sc %s", pvName, scName) + } + sc, err := storageClient.StorageClasses().Get(ctx, scName, metav1.GetOptions{}) if err != nil { return nil, errors.Wrapf(err, "error getting storage class %s", scName) diff --git a/pkg/util/kube/pvc_pv_test.go b/pkg/util/kube/pvc_pv_test.go index d94efa62e..63b8e1edd 100644 --- a/pkg/util/kube/pvc_pv_test.go +++ b/pkg/util/kube/pvc_pv_test.go @@ -1909,3 +1909,143 @@ func TestGetPVCAttachingNodeOS(t *testing.T) { }) } } + +func TestGetVolumeTopology(t *testing.T) { + pvWithoutNodeAffinity := &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pv", + }, + } + + pvWithNodeAffinity := &corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pv", + }, + Spec: corev1api.PersistentVolumeSpec{ + NodeAffinity: &corev1api.VolumeNodeAffinity{ + Required: &corev1api.NodeSelector{ + NodeSelectorTerms: []corev1api.NodeSelectorTerm{ + { + MatchExpressions: []corev1api.NodeSelectorRequirement{ + { + Key: "fake-key", + }, + }, + }, + }, + }, + }, + }, + } + + scObjWithoutVolumeBind := &storagev1api.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-storage-class", + }, + } + + volumeBindImmediate := storagev1api.VolumeBindingImmediate + scObjWithImeediateBind := &storagev1api.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-storage-class", + }, + VolumeBindingMode: &volumeBindImmediate, + } + + volumeBindWffc := storagev1api.VolumeBindingWaitForFirstConsumer + scObjWithWffcBind := &storagev1api.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-storage-class", + }, + VolumeBindingMode: &volumeBindWffc, + } + + tests := []struct { + name string + pvName string + scName string + kubeClientObj []runtime.Object + expectedErr string + expected *corev1api.NodeSelector + }{ + { + name: "invalid pvName", + scName: "fake-storage-class", + expectedErr: "invalid parameter, pv , sc fake-storage-class", + }, + { + name: "invalid scName", + pvName: "fake-pv", + expectedErr: "invalid parameter, pv fake-pv, sc ", + }, + { + name: "no sc", + pvName: "fake-pv", + scName: "fake-storage-class", + expectedErr: "error getting storage class fake-storage-class: storageclasses.storage.k8s.io \"fake-storage-class\" not found", + }, + { + name: "sc without binding mode", + pvName: "fake-pv", + scName: "fake-storage-class", + kubeClientObj: []runtime.Object{scObjWithoutVolumeBind}, + }, + { + name: "sc without immediate binding mode", + pvName: "fake-pv", + scName: "fake-storage-class", + kubeClientObj: []runtime.Object{scObjWithImeediateBind}, + }, + { + name: "get pv fail", + pvName: "fake-pv", + scName: "fake-storage-class", + kubeClientObj: []runtime.Object{scObjWithWffcBind}, + expectedErr: "error getting PV fake-pv: persistentvolumes \"fake-pv\" not found", + }, + { + name: "pv with no affinity", + pvName: "fake-pv", + scName: "fake-storage-class", + kubeClientObj: []runtime.Object{ + scObjWithWffcBind, + pvWithoutNodeAffinity, + }, + }, + { + name: "pv with affinity", + pvName: "fake-pv", + scName: "fake-storage-class", + kubeClientObj: []runtime.Object{ + scObjWithWffcBind, + pvWithNodeAffinity, + }, + expected: &corev1api.NodeSelector{ + NodeSelectorTerms: []corev1api.NodeSelectorTerm{ + { + MatchExpressions: []corev1api.NodeSelectorRequirement{ + { + Key: "fake-key", + }, + }, + }, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...) + + var kubeClient kubernetes.Interface = fakeKubeClient + + affinity, err := GetVolumeTopology(t.Context(), kubeClient.CoreV1(), kubeClient.StorageV1(), test.pvName, test.scName) + + if test.expectedErr != "" { + assert.EqualError(t, err, test.expectedErr) + } else { + assert.Equal(t, test.expected, affinity) + } + }) + } +} From d39285be32bca11e374b67a9e7afb0b4387a8ef8 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Mon, 9 Feb 2026 19:01:19 +0800 Subject: [PATCH 3/5] issue 9343: include PV topology to data mover pod affinitiesq Signed-off-by: Lyndon-Li --- .../{9532-Lyndon-Li‎ => 9594-Lyndon-Li‎} | 0 pkg/exposer/csi_snapshot_test.go | 21 ++++++++++++++++++- pkg/util/kube/pod.go | 6 +++--- 3 files changed, 23 insertions(+), 4 deletions(-) rename changelogs/unreleased/{9532-Lyndon-Li‎ => 9594-Lyndon-Li‎} (100%) diff --git a/changelogs/unreleased/9532-Lyndon-Li‎ b/changelogs/unreleased/9594-Lyndon-Li‎ similarity index 100% rename from changelogs/unreleased/9532-Lyndon-Li‎ rename to changelogs/unreleased/9594-Lyndon-Li‎ diff --git a/pkg/exposer/csi_snapshot_test.go b/pkg/exposer/csi_snapshot_test.go index 4ec1d6d9d..04cf2b8b6 100644 --- a/pkg/exposer/csi_snapshot_test.go +++ b/pkg/exposer/csi_snapshot_test.go @@ -213,8 +213,9 @@ func TestExpose(t *testing.T) { OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, StorageClass: "fake-sc", + SourcePVName: "fake-pv", }, - err: "error getting volume topology for PV , storage class fake-sc: error getting storage class fake-sc: storageclasses.storage.k8s.io \"fake-sc\" not found", + err: "error getting volume topology for PV fake-pv, storage class fake-sc: error getting storage class fake-sc: storageclasses.storage.k8s.io \"fake-sc\" not found", }, { name: "wait vs ready fail", @@ -224,6 +225,7 @@ func TestExpose(t *testing.T) { OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, StorageClass: "fake-sc", + SourcePVName: "fake-pv", }, kubeClientObj: []runtime.Object{ scObj, @@ -239,6 +241,7 @@ func TestExpose(t *testing.T) { OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, StorageClass: "fake-sc", + SourcePVName: "fake-pv", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -257,6 +260,7 @@ func TestExpose(t *testing.T) { OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, StorageClass: "fake-sc", + SourcePVName: "fake-pv", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -285,6 +289,7 @@ func TestExpose(t *testing.T) { OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, StorageClass: "fake-sc", + SourcePVName: "fake-pv", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -313,6 +318,7 @@ func TestExpose(t *testing.T) { OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, StorageClass: "fake-sc", + SourcePVName: "fake-pv", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -341,6 +347,7 @@ func TestExpose(t *testing.T) { OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, StorageClass: "fake-sc", + SourcePVName: "fake-pv", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -368,6 +375,7 @@ func TestExpose(t *testing.T) { SourceNamespace: "fake-ns", AccessMode: "fake-mode", StorageClass: "fake-sc", + SourcePVName: "fake-pv", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -388,6 +396,7 @@ func TestExpose(t *testing.T) { ExposeTimeout: time.Millisecond, AccessMode: AccessModeFileSystem, StorageClass: "fake-sc", + SourcePVName: "fake-pv", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -417,6 +426,7 @@ func TestExpose(t *testing.T) { OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, StorageClass: "fake-sc", + SourcePVName: "fake-pv", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -447,6 +457,7 @@ func TestExpose(t *testing.T) { OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, StorageClass: "fake-sc", + SourcePVName: "fake-pv", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -467,6 +478,7 @@ func TestExpose(t *testing.T) { OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, StorageClass: "fake-sc", + SourcePVName: "fake-pv", }, snapshotClientObj: []runtime.Object{ vsObject, @@ -488,6 +500,7 @@ func TestExpose(t *testing.T) { ExposeTimeout: time.Millisecond, VolumeSize: *resource.NewQuantity(567890, ""), StorageClass: "fake-sc", + SourcePVName: "fake-pv", }, snapshotClientObj: []runtime.Object{ vsObjectWithoutRestoreSize, @@ -506,6 +519,7 @@ func TestExpose(t *testing.T) { SnapshotName: "fake-vs", SourceNamespace: "fake-ns", StorageClass: "fake-sc", + SourcePVName: "fake-pv", AccessMode: AccessModeFileSystem, OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, @@ -533,6 +547,7 @@ func TestExpose(t *testing.T) { SnapshotName: "fake-vs", SourceNamespace: "fake-ns", StorageClass: "fake-sc", + SourcePVName: "fake-pv", AccessMode: AccessModeFileSystem, OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, @@ -561,6 +576,7 @@ func TestExpose(t *testing.T) { SnapshotName: "fake-vs", SourceNamespace: "fake-ns", StorageClass: "fake-sc", + SourcePVName: "fake-pv", AccessMode: AccessModeFileSystem, OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, @@ -587,6 +603,7 @@ func TestExpose(t *testing.T) { SnapshotName: "fake-vs", SourceNamespace: "fake-ns", StorageClass: "fake-sc", + SourcePVName: "fake-pv", AccessMode: AccessModeFileSystem, OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, @@ -638,6 +655,7 @@ func TestExpose(t *testing.T) { SnapshotName: "fake-vs", SourceNamespace: "fake-ns", StorageClass: "fake-sc", + SourcePVName: "fake-pv", AccessMode: AccessModeFileSystem, OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, @@ -695,6 +713,7 @@ func TestExpose(t *testing.T) { SnapshotName: "fake-vs", SourceNamespace: "fake-ns", StorageClass: "fake-sc", + SourcePVName: "fake-pv", AccessMode: AccessModeFileSystem, OperationTimeout: time.Millisecond, ExposeTimeout: time.Millisecond, diff --git a/pkg/util/kube/pod.go b/pkg/util/kube/pod.go index b5cd9a62c..a57e2cea9 100644 --- a/pkg/util/kube/pod.go +++ b/pkg/util/kube/pod.go @@ -230,7 +230,7 @@ func CollectPodLogs(ctx context.Context, podGetter corev1client.CoreV1Interface, return nil } -func ToSystemAffinity(loadAffinity *LoadAffinity, volumeTopolpogy *corev1api.NodeSelector) *corev1api.Affinity { +func ToSystemAffinity(loadAffinity *LoadAffinity, volumeTopology *corev1api.NodeSelector) *corev1api.Affinity { requirements := []corev1api.NodeSelectorRequirement{} if loadAffinity != nil { for k, v := range loadAffinity.NodeSelector.MatchLabels { @@ -254,8 +254,8 @@ func ToSystemAffinity(loadAffinity *LoadAffinity, volumeTopolpogy *corev1api.Nod result.NodeAffinity = new(corev1api.NodeAffinity) result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution = new(corev1api.NodeSelector) - if volumeTopolpogy != nil { - result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms = append(result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, volumeTopolpogy.NodeSelectorTerms...) + if volumeTopology != nil { + result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms = append(result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, volumeTopology.NodeSelectorTerms...) } else if len(requirements) > 0 { result.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms = make([]corev1api.NodeSelectorTerm, 1) } else { From 62a24ece5027c9eb67f7fc8dc5a173bf0e9ad6a4 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Mon, 9 Feb 2026 16:10:00 +0800 Subject: [PATCH 4/5] If BIA return updateObj with SkipFromBackupAnnotation, treat it as skip the resource from backup. Signed-off-by: Xun Jiang --- changelogs/unreleased/9547-blackpiglet | 1 + pkg/apis/velero/v1/labels_annotations.go | 9 +++++++++ pkg/backup/item_backupper.go | 13 +++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 changelogs/unreleased/9547-blackpiglet diff --git a/changelogs/unreleased/9547-blackpiglet b/changelogs/unreleased/9547-blackpiglet new file mode 100644 index 000000000..f1e546be6 --- /dev/null +++ b/changelogs/unreleased/9547-blackpiglet @@ -0,0 +1 @@ +If BIA return updateObj with SkipFromBackupAnnotation, treat it as skip the resource from backup. \ No newline at end of file diff --git a/pkg/apis/velero/v1/labels_annotations.go b/pkg/apis/velero/v1/labels_annotations.go index c1431d3cc..85d8b05aa 100644 --- a/pkg/apis/velero/v1/labels_annotations.go +++ b/pkg/apis/velero/v1/labels_annotations.go @@ -102,6 +102,15 @@ const ( // even if the resource contains a matching selector label. ExcludeFromBackupLabel = "velero.io/exclude-from-backup" + // SkipFromBackupAnnotation is the annotation used by internal BackupItemActions + // to indicate that a resource should be skipped from backup, + // even if it doesn't have the ExcludeFromBackupLabel. + // This is used in cases where we want to skip backup of a resource based on some logic in a plugin. + // + // Notice: SkipFromBackupAnnotation's priority is higher than MustIncludeAdditionalItemAnnotation. + // If SkipFromBackupAnnotation is set, the resource will be skipped even if MustIncludeAdditionalItemAnnotation is set. + SkipFromBackupAnnotation = "velero.io/skip-from-backup" + // defaultVGSLabelKey is the default label key used to group PVCs under a VolumeGroupSnapshot DefaultVGSLabelKey = "velero.io/volume-group" diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index feae0e01c..770b1ed41 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -244,6 +244,12 @@ func (ib *itemBackupper) backupItemInternal(logger logrus.FieldLogger, obj runti return false, itemFiles, kubeerrs.NewAggregate(backupErrs) } + // If err is nil and updatedObj is nil, it means the item is skipped by plugin action, + // we should return here to avoid backing up the item, and avoid potential NPE in the following code. + if updatedObj == nil { + return false, itemFiles, nil + } + itemFiles = append(itemFiles, additionalItemFiles...) obj = updatedObj if metadata, err = meta.Accessor(obj); err != nil { @@ -398,6 +404,13 @@ func (ib *itemBackupper) executeActions( } u := &unstructured.Unstructured{Object: updatedItem.UnstructuredContent()} + + if _, ok := u.GetAnnotations()[velerov1api.SkipFromBackupAnnotation]; ok { + log.Infof("Resource (groupResource=%s, namespace=%s, name=%s) is skipped from backup by action %s.", + groupResource.String(), namespace, name, actionName) + return nil, itemFiles, nil + } + if actionName == csiBIAPluginName { if additionalItemIdentifiers == nil && u.GetAnnotations()[velerov1api.SkippedNoCSIPVAnnotation] == "true" { // snapshot was skipped by CSI plugin From 9a39cbfbf57220ce3eaa4f5a19825a901570c2e8 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Fri, 27 Feb 2026 00:28:49 +0800 Subject: [PATCH 5/5] Remove the skipped item from the resource list when it's skipped by BIA. Signed-off-by: Xun Jiang --- .../unreleased/{9547-blackpiglet => 9597-blackpiglet} | 0 pkg/backup/backed_up_items_map.go | 8 ++++++++ pkg/backup/item_backupper.go | 2 ++ 3 files changed, 10 insertions(+) rename changelogs/unreleased/{9547-blackpiglet => 9597-blackpiglet} (100%) diff --git a/changelogs/unreleased/9547-blackpiglet b/changelogs/unreleased/9597-blackpiglet similarity index 100% rename from changelogs/unreleased/9547-blackpiglet rename to changelogs/unreleased/9597-blackpiglet diff --git a/pkg/backup/backed_up_items_map.go b/pkg/backup/backed_up_items_map.go index f5764cd8e..174a50e1e 100644 --- a/pkg/backup/backed_up_items_map.go +++ b/pkg/backup/backed_up_items_map.go @@ -98,6 +98,14 @@ func (m *backedUpItemsMap) AddItem(key itemKey) { m.totalItems[key] = struct{}{} } +func (m *backedUpItemsMap) DeleteItem(key itemKey) { + m.Lock() + defer m.Unlock() + + delete(m.backedUpItems, key) + delete(m.totalItems, key) +} + func (m *backedUpItemsMap) AddItemToTotal(key itemKey) { m.Lock() defer m.Unlock() diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index 770b1ed41..b50f4e119 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -247,6 +247,8 @@ func (ib *itemBackupper) backupItemInternal(logger logrus.FieldLogger, obj runti // If err is nil and updatedObj is nil, it means the item is skipped by plugin action, // we should return here to avoid backing up the item, and avoid potential NPE in the following code. if updatedObj == nil { + log.Infof("Remove item from the backup's backupItems list and totalItems list because it's skipped by plugin action.") + ib.backupRequest.BackedUpItems.DeleteItem(key) return false, itemFiles, nil }