Preserve PVC selected-node annotation via carrier annotation for in-place restore

For in-place volume data restore, the existing PVC is deleted and
recreated. For StorageClasses with the WaitForFirstConsumer volume
binding mode, losing the volume.kubernetes.io/selected-node annotation
could let the scheduler place the recreated workload Pod in a different
zone than the original PV, leaving it stuck in ContainerCreating.

Instead of relying on RestoreItemAction execution order (the generic
PVC RIA unconditionally strips the selected-node annotation), the PVC
CSI RIA now captures the annotation from the existing PVC right before
deleting it and carries it on the target PVC via the Velero-internal
restore.velero.io/inplace-restore-selected-node annotation. The restore
engine translates the carrier back to the Kubernetes annotation after
all RestoreItemActions have run and always strips the carrier so it
never lands on the cluster.

This makes the behavior independent of RIA ordering: the Kubernetes
annotation is stripped by default on every path (including when the
target PVC does not exist and Velero falls back to provisioning a new
PVC), and preservation only happens when the CSI RIA explicitly
captured a value from the existing PVC.

Signed-off-by: chlins <chlins.zhang@gmail.com>
This commit is contained in:
chlins
2026-08-26 10:53:03 +08:00
committed by Wenkai Yin(尹文开)
parent 9eafc7a862
commit 2a288c605c
7 changed files with 359 additions and 10 deletions
+1
View File
@@ -0,0 +1 @@
Preserve PVC selected-node annotation via carrier annotation for in-place restore
@@ -212,7 +212,9 @@ Users must manage the lifecycle of their workloads before starting the restore.
When performing an in-place restore, Velero deletes the existing target PVC and recreates it. For StorageClasses using the `WaitForFirstConsumer` volume binding mode, this recreation resets the scheduling lifecycle. Even though Velero adds a selector to the PVC spec to ensure it binds exclusively to the original PV, a scheduling issue can still occur. If the target PVC loses its node affinity, the Kubernetes Scheduler might schedule the recreated business Pod to a different availability zone. Because the original PV is physically constrained to its original zone, the Pod will fail to mount the volume and remain stuck in the `ContainerCreating` state with an attachment error.
**Solution**:
During the PVC Restore Item Action (RIA), Velero must extract the `volume.kubernetes.io/selected-node` annotation from the original PVC. When Velero recreates the target PVC, it must inject this annotation back into the PVC spec.
During the PVC CSI Restore Item Action (RIA), right before deleting the existing PVC, Velero extracts the `volume.kubernetes.io/selected-node` annotation from that PVC and carries it on the PVC to be restored via a Velero-internal carrier annotation (`restore.velero.io/inplace-restore-selected-node`). After all Restore Item Actions have run, the restore engine translates the carrier back to the `volume.kubernetes.io/selected-node` annotation and strips the carrier so it never lands on the cluster.
A carrier annotation is used instead of the Kubernetes annotation directly because the generic PVC RIA unconditionally strips the `selected-node` annotation during restore, and the execution order of Restore Item Actions is not a documented contract. With the carrier, the behavior is independent of the RIA execution order: the Kubernetes annotation is stripped by default on every path (including when the target PVC does not exist and Velero falls back to provisioning a new PVC), and preservation only happens when the PVC CSI RIA explicitly captured a value from the existing PVC.
By preserving the `selected-node` annotation, the Kubernetes Scheduler is forced to schedule the recreated business Pod to the original node/zone, ensuring it successfully mounts the restored PV.
### Namespace Mapping
@@ -260,10 +262,8 @@ This section outlines the step-by-step control path and data path workflows for
**Control Path**
PVC RIA:
- Preserve the `volume.kubernetes.io/selected-node` annotation to ensure correct scheduling during target PVC recreation.
PVC CSI RIA:
- Capture the `volume.kubernetes.io/selected-node` annotation from the existing PVC into the Velero-internal carrier annotation before deleting the PVC, so the restore engine can re-apply it to the recreated target PVC (see [Handling Cross-Zone Scheduling](#handling-cross-zone-scheduling-waitforfirstconsumer)).
- Create a snapshot of the existing `PVC` to serve as the baseline for CBT delta calculations.
- Patch the existing PV's reclaim policy to `Retain`.
- Delete the existing PVC.
@@ -308,10 +308,8 @@ The workflow is identical to the **In-place Incremental Restore for CSI Snapshot
**Control Path**
PVC RIA:
- Preserve the `volume.kubernetes.io/selected-node` annotation to ensure correct scheduling during target PVC recreation.
PVC CSI RIA:
- Capture the `volume.kubernetes.io/selected-node` annotation from the existing PVC into the Velero-internal carrier annotation before deleting the PVC, so the restore engine can re-apply it to the recreated target PVC (see [Handling Cross-Zone Scheduling](#handling-cross-zone-scheduling-waitforfirstconsumer)).
- Create a snapshot of the existing `PVC` to serve as the baseline for CBT delta calculations.
- Patch the existing `PV` to set its `persistentVolumeReclaimPolicy` to `Retain`.
- Delete the existing `PVC`.
+11
View File
@@ -174,6 +174,17 @@ const (
// Notice: SkipRestore on the Execute output takes precedence. If SkipRestore is true, the
// annotation is never inspected and AdditionalItems are not processed.
MustIncludeAdditionalItemRestoreAnnotation = "restore.velero.io/must-include-additional-items"
// InplaceRestoreSelectedNodeAnnotation is a Velero-internal carrier annotation set by the
// PVC CSI RestoreItemAction during an in-place volume data restore. It carries the
// "volume.kubernetes.io/selected-node" value captured from the existing PVC right before
// that PVC is deleted, so the restore engine can re-apply it to the recreated target PVC
// after all RestoreItemActions have run. This keeps the recreated PVC (and the workload
// Pod, for WaitForFirstConsumer StorageClasses) scheduled to the original node/zone.
// The annotation is always translated and stripped by the restore engine; it never lands
// on the cluster. Using a carrier annotation avoids any dependency on the execution order
// of RestoreItemActions.
InplaceRestoreSelectedNodeAnnotation = "restore.velero.io/inplace-restore-selected-node"
// SkippedNoCSIPVAnnotation - Velero checks this annotation on processed PVC to
// find out if the snapshot was skipped b/c the PV is not provisioned via CSI
SkippedNoCSIPVAnnotation = "backup.velero.io/skipped-no-csi-pv"
+8 -3
View File
@@ -597,14 +597,19 @@ func (p *pvcRestoreItemAction) prepareForInplaceRestore(ctx context.Context, log
return nil, errors.New("ExistingVolumeDataPolicy is in-place restore, but the existing PVC is not bound.")
}
// set the "selected-node" annotation to target PVC to make sure the target pod is scheduled to the same node
// Capture the "selected-node" annotation from the existing PVC before it is deleted below,
// and carry it on the target PVC via a Velero-internal carrier annotation. The restore
// engine translates the carrier back to the Kubernetes "selected-node" annotation after
// all RestoreItemActions have run, so the recreated target PVC keeps the same scheduling
// constraint regardless of the order in which RestoreItemActions execute (the generic PVC
// RIA unconditionally strips the Kubernetes annotation).
selectedNode, exists := existingPVC.Annotations[kube.KubeAnnSelectedNode]
if exists {
logger.Infof("Setting %q annotation to %q for target PVC to keep the same selected node as the existing PVC", kube.KubeAnnSelectedNode, existingPVC.Annotations[kube.KubeAnnSelectedNode])
logger.Infof("Carrying %q annotation with value %q for target PVC to keep the same selected node as the existing PVC", kube.KubeAnnSelectedNode, selectedNode)
if targetPVC.Annotations == nil {
targetPVC.Annotations = map[string]string{}
}
targetPVC.Annotations[kube.KubeAnnSelectedNode] = selectedNode
targetPVC.Annotations[velerov1api.InplaceRestoreSelectedNodeAnnotation] = selectedNode
}
var err error
+123
View File
@@ -28,6 +28,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -596,6 +597,128 @@ func TestExecute(t *testing.T) {
}
}
// TestPrepareForInplaceRestoreSelectedNode verifies that prepareForInplaceRestore captures
// the selected-node annotation from the existing PVC into the Velero-internal carrier
// annotation (not the Kubernetes annotation) on the target PVC, before deleting the PVC.
func TestPrepareForInplaceRestoreSelectedNode(t *testing.T) {
tests := []struct {
name string
existingPVC *corev1api.PersistentVolumeClaim
expectedCarrier string
expectCarrierSet bool
expectKubeAnnoSet bool
}{
{
name: "existing PVC with selected-node sets carrier annotation only",
existingPVC: builder.ForPersistentVolumeClaim("ns-1", "pvc-1").
ObjectMeta(builder.WithAnnotations(AnnSelectedNode, "node-1")).
VolumeName("pv-1").
Phase(corev1api.ClaimBound).Result(),
expectedCarrier: "node-1",
expectCarrierSet: true,
expectKubeAnnoSet: false,
},
{
name: "existing PVC without selected-node sets neither annotation",
existingPVC: builder.ForPersistentVolumeClaim("ns-1", "pvc-1").
VolumeName("pv-1").
Phase(corev1api.ClaimBound).Result(),
expectCarrierSet: false,
expectKubeAnnoSet: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
pv := builder.ForPersistentVolume("pv-1").Result()
kubeClient := fake.NewSimpleClientset(tc.existingPVC, pv)
pvcRIA := pvcRestoreItemAction{
log: logrus.New(),
crClient: velerotest.NewFakeControllerRuntimeClient(t, pv),
kubeClient: kubeClient,
}
targetPVC := builder.ForPersistentVolumeClaim("ns-1", "pvc-1").Result()
returnedPV, err := pvcRIA.prepareForInplaceRestore(
t.Context(), logrus.New().WithField("test", tc.name),
targetPVC, tc.existingPVC, time.Minute)
require.NoError(t, err)
require.Equal(t, "pv-1", returnedPV.Name)
carrier, carrierOK := targetPVC.Annotations[velerov1api.InplaceRestoreSelectedNodeAnnotation]
require.Equal(t, tc.expectCarrierSet, carrierOK)
if tc.expectCarrierSet {
require.Equal(t, tc.expectedCarrier, carrier)
}
_, kubeAnnoOK := targetPVC.Annotations[AnnSelectedNode]
require.Equal(t, tc.expectKubeAnnoSet, kubeAnnoOK)
})
}
}
// TestExecuteInplaceRestore exercises the public Execute() entry for an in-place restore
// with an existing PVC: the carrier annotation must be emitted on the returned item, the
// Kubernetes selected-node annotation must not be set by this RIA, the existing PVC must be
// deleted, and a DataDownload with the in-place restoreType must be created.
func TestExecuteInplaceRestore(t *testing.T) {
existingPVC := builder.ForPersistentVolumeClaim("velero", "testPVC").
ObjectMeta(builder.WithAnnotations(AnnSelectedNode, "node-1")).
VolumeName("testPV").
Phase(corev1api.ClaimBound).Result()
existingPV := builder.ForPersistentVolume("testPV").Result()
backup := builder.ForBackup("velero", "testBackup").SnapshotMoveData(true).Result()
restore := builder.ForRestore("velero", "testRestore").Backup("testBackup").
ObjectMeta(builder.WithUID("uid")).ExistingVolumeDataPolicy("full").Result()
pvcFromBackup := builder.ForPersistentVolumeClaim("velero", "testPVC").
ObjectMeta(builder.WithAnnotations(
velerov1api.VolumeSnapshotLabel, "vsName",
velerov1api.DataUploadNameAnnotation, "velero/testDU",
)).Result()
dataUploadResult := builder.ForConfigMap("velero", "testCM").Data("uid", "{}").
ObjectMeta(builder.WithLabels(
velerov1api.RestoreUIDLabel, "uid",
velerov1api.PVCNamespaceNameLabel, "velero.testPVC",
velerov1api.ResourceUsageLabel, label.GetValidName(string(velerov1api.VeleroResourceUsageDataUploadResult)),
)).Result()
pvcRIA := pvcRestoreItemAction{
log: logrus.New(),
crClient: velerotest.NewFakeControllerRuntimeClient(t, existingPVC, existingPV, backup, dataUploadResult),
kubeClient: fake.NewSimpleClientset(existingPVC, existingPV),
}
pvcMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pvcFromBackup.DeepCopy())
require.NoError(t, err)
pvcFromBackupMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pvcFromBackup)
require.NoError(t, err)
output, err := pvcRIA.Execute(&velero.RestoreItemActionExecuteInput{
Item: &unstructured.Unstructured{Object: pvcMap},
ItemFromBackup: &unstructured.Unstructured{Object: pvcFromBackupMap},
Restore: restore,
})
require.NoError(t, err)
updatedPVC := new(corev1api.PersistentVolumeClaim)
require.NoError(t, runtime.DefaultUnstructuredConverter.FromUnstructured(
output.UpdatedItem.UnstructuredContent(), updatedPVC))
// Carrier annotation carries the captured value; the Kubernetes annotation is not set by this RIA.
require.Equal(t, "node-1", updatedPVC.Annotations[velerov1api.InplaceRestoreSelectedNodeAnnotation])
require.NotContains(t, updatedPVC.Annotations, AnnSelectedNode)
// The existing PVC is deleted so the exposer can bind a temporary PVC to the PV.
_, err = pvcRIA.kubeClient.CoreV1().PersistentVolumeClaims("velero").Get(t.Context(), "testPVC", metav1.GetOptions{})
require.True(t, apierrors.IsNotFound(err))
// A DataDownload with the in-place restoreType referencing the existing PV is created.
dataDownloadList := new(velerov2alpha1.DataDownloadList)
require.NoError(t, pvcRIA.crClient.List(t.Context(), dataDownloadList, &crclient.ListOptions{}))
require.Len(t, dataDownloadList.Items, 1)
require.Equal(t, "full", dataDownloadList.Items[0].Spec.RestoreType)
require.Equal(t, "testPV", dataDownloadList.Items[0].Spec.TargetVolume.PV)
}
func TestPVCAppliesTo(t *testing.T) {
p := pvcRestoreItemAction{
log: logrus.StandardLogger(),
+30
View File
@@ -1636,6 +1636,19 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
return warnings, errs, itemExists
}
// Strip any pre-existing Velero-internal in-place restore carrier annotation coming from
// the backup metadata before RestoreItemActions run. The carrier is only trusted when it
// is set by a RestoreItemAction (the PVC CSI RIA) during this restore; a stale carrier
// baked into the backup must not be translated into the Kubernetes "selected-node"
// annotation, which could pin a newly provisioned PVC to a stale node.
if annotations := obj.GetAnnotations(); annotations != nil {
if _, present := annotations[velerov1api.InplaceRestoreSelectedNodeAnnotation]; present {
restoreLogger.Infof("Removing pre-existing %q annotation from backup metadata", velerov1api.InplaceRestoreSelectedNodeAnnotation)
delete(annotations, velerov1api.InplaceRestoreSelectedNodeAnnotation)
obj.SetAnnotations(annotations)
}
}
restoreLogger.Infof("restore status includes excludes: %+v", ctx.resourceStatusIncludesExcludes)
for _, action := range ctx.getApplicableActions(groupResource, namespace) {
@@ -1768,6 +1781,23 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
}
}
// Translate the Velero-internal carrier annotation (set by the PVC CSI RestoreItemAction
// during an in-place volume data restore) back to the Kubernetes "selected-node" annotation.
// This runs after all RestoreItemActions so the result does not depend on the order in which
// the actions executed: the generic PVC RIA unconditionally strips the Kubernetes annotation,
// while the carrier annotation passes through untouched. The carrier itself is always
// stripped so it never lands on the cluster.
if annotations := obj.GetAnnotations(); annotations != nil {
if selectedNode, present := annotations[velerov1api.InplaceRestoreSelectedNodeAnnotation]; present {
if selectedNode != "" {
restoreLogger.Infof("Restoring %q annotation with value %q from in-place restore carrier annotation", kube.KubeAnnSelectedNode, selectedNode)
annotations[kube.KubeAnnSelectedNode] = selectedNode
}
delete(annotations, velerov1api.InplaceRestoreSelectedNodeAnnotation)
obj.SetAnnotations(annotations)
}
}
// This comes after running item actions because we have built-in actions that restore
// a PVC's associated PV (if applicable). As part of the PV being restored, the 'pvsToProvision'
// set may be inserted into, and this needs to happen *before* running the following block of logic.
+181
View File
@@ -44,6 +44,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/dynamic"
k8sfake "k8s.io/client-go/kubernetes/fake"
kubetesting "k8s.io/client-go/testing"
"github.com/vmware-tanzu/velero/internal/volume"
@@ -60,6 +61,7 @@ import (
vsv1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/volumesnapshotter/v1"
"github.com/vmware-tanzu/velero/pkg/podvolume"
uploadermocks "github.com/vmware-tanzu/velero/pkg/podvolume/mocks"
riav1 "github.com/vmware-tanzu/velero/pkg/restore/actions"
"github.com/vmware-tanzu/velero/pkg/test"
"github.com/vmware-tanzu/velero/pkg/types"
"github.com/vmware-tanzu/velero/pkg/util/kube"
@@ -2852,6 +2854,185 @@ func TestRestoreMustIncludeAdditionalItems(t *testing.T) {
})
}
// TestRestoreInplaceSelectedNodeCarrierAnnotation verifies the engine translates the
// Velero-internal in-place restore carrier annotation into the Kubernetes selected-node
// annotation after all RestoreItemActions have run, and always strips the carrier.
func TestRestoreInplaceSelectedNodeCarrierAnnotation(t *testing.T) {
t.Run("carrier annotation is translated to selected-node and stripped", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.PVCs())
data := &Request{
Log: h.log,
Restore: defaultRestore().Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
// Simulates the PVC CSI RIA setting the carrier during an in-place restore.
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.InplaceRestoreSelectedNodeAnnotation] = "node-1"
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{UpdatedItem: item}, nil
},
},
// The real generic PVC RIA (velero.io/pvc), which unconditionally strips the
// Kubernetes selected-node annotation. Running it after the carrier-setting
// action proves the carrier survives the real strip regardless of action order.
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
clientset := k8sfake.NewSimpleClientset()
return riav1.NewPVCAction(
h.log,
clientset.CoreV1().ConfigMaps("velero"),
clientset.CoreV1().Nodes(),
).Execute(input)
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
got, err := h.DynamicClient.Resource(test.PVCs().GVR()).Namespace("ns-1").Get(t.Context(), "pvc-1", metav1.GetOptions{})
require.NoError(t, err)
annotations := got.GetAnnotations()
assert.Equal(t, "node-1", annotations["volume.kubernetes.io/selected-node"])
assert.NotContains(t, annotations, velerov1api.InplaceRestoreSelectedNodeAnnotation)
})
t.Run("empty carrier annotation is stripped without setting selected-node", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.PVCs())
data := &Request{
Log: h.log,
Restore: defaultRestore().Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1").Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[velerov1api.InplaceRestoreSelectedNodeAnnotation] = ""
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{UpdatedItem: item}, nil
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
got, err := h.DynamicClient.Resource(test.PVCs().GVR()).Namespace("ns-1").Get(t.Context(), "pvc-1", metav1.GetOptions{})
require.NoError(t, err)
annotations := got.GetAnnotations()
assert.NotContains(t, annotations, "volume.kubernetes.io/selected-node")
assert.NotContains(t, annotations, velerov1api.InplaceRestoreSelectedNodeAnnotation)
})
t.Run("no carrier annotation leaves selected-node stripped (PVC-absent fallback)", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.PVCs())
data := &Request{
Log: h.log,
Restore: defaultRestore().Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1").
ObjectMeta(builder.WithAnnotations("volume.kubernetes.io/selected-node", "stale-node")).Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
[]riav2.RestoreItemAction{
// Simulates the generic PVC RIA stripping the annotation; no action sets the
// carrier (as when the target PVC does not exist and Velero falls back to
// provisioning a new PVC).
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
annotations := item.GetAnnotations()
delete(annotations, "volume.kubernetes.io/selected-node")
item.SetAnnotations(annotations)
return &velero.RestoreItemActionExecuteOutput{UpdatedItem: item}, nil
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
got, err := h.DynamicClient.Resource(test.PVCs().GVR()).Namespace("ns-1").Get(t.Context(), "pvc-1", metav1.GetOptions{})
require.NoError(t, err)
assert.NotContains(t, got.GetAnnotations(), "volume.kubernetes.io/selected-node")
})
t.Run("carrier annotation baked into backup metadata is not trusted when no action sets it", func(t *testing.T) {
h := newHarness(t)
h.AddItems(t, test.PVCs())
data := &Request{
Log: h.log,
Restore: defaultRestore().Result(),
Backup: defaultBackup().Result(),
BackupReader: test.NewTarWriter(t).
AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1").
ObjectMeta(builder.WithAnnotations(velerov1api.InplaceRestoreSelectedNodeAnnotation, "stale-node")).Result()).
Done(),
}
warnings, errs := h.restorer.Restore(
data,
// No action sets the carrier during this restore (as in the PVC-absent fallback
// path where a new PVC is dynamically provisioned), so the carrier from the
// backup metadata must be stripped and never translated into selected-node.
[]riav2.RestoreItemAction{
&pluggableAction{
executeFunc: func(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
item := input.Item.(*unstructured.Unstructured)
// The stale carrier from the backup must already be gone before
// RestoreItemActions execute.
assert.NotContains(t, item.GetAnnotations(), velerov1api.InplaceRestoreSelectedNodeAnnotation)
return &velero.RestoreItemActionExecuteOutput{UpdatedItem: item}, nil
},
},
},
nil,
)
assertEmptyResults(t, warnings, errs)
got, err := h.DynamicClient.Resource(test.PVCs().GVR()).Namespace("ns-1").Get(t.Context(), "pvc-1", metav1.GetOptions{})
require.NoError(t, err)
annotations := got.GetAnnotations()
assert.NotContains(t, annotations, "volume.kubernetes.io/selected-node")
assert.NotContains(t, annotations, velerov1api.InplaceRestoreSelectedNodeAnnotation)
})
}
// TestShouldRestore runs the ShouldRestore function for various permutations of
// existing/nonexisting/being-deleted PVs, PVCs, and namespaces, and verifies the
// result/error matches expectations.