Fail the in-place restore pre-flight check when the backed-up pod already exists on the file system restore path (#10550)

PodVolumeRestores are only created for pods that Velero creates, so when
the pod already exists in the cluster the PVC-not-in-use pre-flight check
never runs and the volume data restore is skipped silently, while the
existing pod keeps consuming the PVC. Report an explicit pre-flight error
for such pods, aligned with the PVC CSI RIA behavior.

Signed-off-by: chlins <chlins.zhang@gmail.com>
This commit is contained in:
Chlins Zhang
2026-09-22 11:53:35 +08:00
committed by GitHub
parent 36935902b9
commit 3793d9ab4a
4 changed files with 121 additions and 0 deletions
+25
View File
@@ -1945,6 +1945,31 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
itemStatus := ctx.restoredItems[itemKey]
itemStatus.itemExists = itemExists
ctx.restoredItems[itemKey] = itemStatus
// PodVolumeRestores are only created for pods Velero creates, so an
// existing pod silently skips the volume data restore. For an in-place
// restore this fails the pre-flight check: the pod is still consuming
// the PVCs that were supposed to be restored in place. Otherwise it is
// only worth a warning.
if newGR == kuberesource.Pods {
pod := new(corev1api.Pod)
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), pod); err != nil {
errs.Add(namespace, err)
return warnings, errs, itemExists
}
if len(podvolume.GetVolumeBackupsForPod(ctx.podVolumeBackups, pod, originalNamespace)) > 0 {
if ctx.restore.IsVolumeDataInplaceRestore() {
err := errors.Errorf("in-place restore pre-flight check failed, skipping volume data restore: pod %s already exists and is still using the backed-up volumes: delete the pod and its owning workload and retry", kube.NamespaceAndName(obj))
restoreLogger.Error(err.Error())
errs.Add(namespace, err)
} else {
err := errors.Errorf("skipping volume data restore: pod %s already exists, its PodVolumeBackups will not be restored", kube.NamespaceAndName(obj))
restoreLogger.Warn(err.Error())
warnings.Add(namespace, err)
}
}
}
// Remove insubstantial metadata.
fromCluster, err = resetMetadataAndStatus(fromCluster)
if err != nil {
+93
View File
@@ -4333,6 +4333,99 @@ func TestRestoreWithPodVolume(t *testing.T) {
}
}
// TestRestoreInplaceExistingPodWithPodVolumeBackups verifies that an in-place
// restore reports an error when the backed-up pod has PodVolumeBackups to
// restore but already exists in the cluster: the PodVolumeRestores are never
// created for an existing pod, so the volume data restore must not be skipped
// silently. A regular restore keeps the plain "already exists" warning.
func TestRestoreInplaceExistingPodWithPodVolumeBackups(t *testing.T) {
pvbs := []*velerov1api.PodVolumeBackup{
builder.ForPodVolumeBackup("velero", "pvb-1").PodName("pod-1").PodNamespace("ns-1").Volume("data").SnapshotID("foo").Result(),
}
backedUpPod := builder.ForPod("ns-1", "pod-1").
Volumes(builder.ForVolume("data").PersistentVolumeClaimSource("pvc-1").Result()).
Result()
existingPod := backedUpPod.DeepCopy()
existingPod.Spec.NodeName = "node-1"
tests := []struct {
name string
restore *velerov1api.Restore
pvbs []*velerov1api.PodVolumeBackup
wantErrs bool
wantWarnings bool
wantPVBSkip bool
}{
{
name: "in-place restore with an existing pod consuming the backed-up volumes fails the pre-flight check",
restore: defaultRestore().ExistingVolumeDataPolicy(string(velerov1api.VolumeDataPolicyTypeFull)).Result(),
pvbs: pvbs,
wantErrs: true,
},
{
name: "in-place restore with existingResourcePolicy=update and an existing pod still fails the pre-flight check",
restore: defaultRestore().ExistingVolumeDataPolicy(string(velerov1api.VolumeDataPolicyTypeFull)).ExistingResourcePolicy(string(velerov1api.ResourcePolicyTypeUpdate)).Result(),
pvbs: pvbs,
wantErrs: true,
},
{
name: "in-place restore with an existing pod without PodVolumeBackups only warns",
restore: defaultRestore().ExistingVolumeDataPolicy(string(velerov1api.VolumeDataPolicyTypeIncremental)).Result(),
pvbs: nil,
wantWarnings: true,
},
{
name: "regular restore with an existing pod warns that the PodVolumeBackups are not restored",
restore: defaultRestore().Result(),
pvbs: pvbs,
wantWarnings: true,
wantPVBSkip: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h := newHarness(t)
restorer := new(uploadermocks.Restorer)
defer restorer.AssertExpectations(t)
h.restorer.podVolumeRestorerFactory = &fakePodVolumeRestorerFactory{restorer: restorer}
h.AddItems(t, test.Pods(existingPod))
tarball := test.NewTarWriter(t)
tarball.AddItems("pods", backedUpPod)
warnings, errs := h.restorer.Restore(
&Request{
Log: h.log,
Restore: tc.restore,
Backup: defaultBackup().Result(),
PodVolumeBackups: tc.pvbs,
BackupReader: tarball.Done(),
},
nil,
nil,
)
if tc.wantErrs {
require.Len(t, errs.Namespaces["ns-1"], 1)
assert.Contains(t, errs.Namespaces["ns-1"][0], "in-place restore pre-flight check failed")
assert.Contains(t, errs.Namespaces["ns-1"][0], "pod ns-1/pod-1 already exists")
} else {
assert.Empty(t, errs.Namespaces)
}
if tc.wantPVBSkip {
require.Len(t, warnings.Namespaces["ns-1"], 2)
assert.Contains(t, warnings.Namespaces["ns-1"][0], "PodVolumeBackups will not be restored")
assert.Contains(t, warnings.Namespaces["ns-1"][1], "already exists")
} else if tc.wantWarnings {
require.Len(t, warnings.Namespaces["ns-1"], 1)
assert.Contains(t, warnings.Namespaces["ns-1"][0], "already exists")
}
})
}
}
func TestResetMetadata(t *testing.T) {
tests := []struct {
name string