Add in-place restore pre-flight check: target PVC must not be in use (#10419)
Run the E2E test on kind / setup-test-matrix (push) Failing after 4s
e2e-test-kind.yaml / extract (push) Failing after 7s
Run the E2E test on kind / get-go-version (push) Failing after 7s
Run the E2E test on kind / build (push) Skipped
Run the E2E test on kind / run-e2e-test (push) Skipped
push.yml / extract (push) Failing after 5s
Main CI / get-go-version (push) Failing after 6s
Main CI / Build (push) Skipped

Check the target PVC is not used by any active pod before any side
effect, on both the CSI data mover path and the file system path.
The in-use semantics align with the pvc-protection controller:
terminal-phase pods don't block, terminating pods block with a wait
hint. On the file system path, pods gated by this restore's
restore-wait init container (identified by the restore UID in its args,
and not yet terminated) are exempted: they must mount the PVC for the
node-agent to restore the data and cannot write to the volume until the
PodVolumeRestores complete. Leftover pods, controller-recreated pods,
and pods gated by a different restore still block.

Signed-off-by: chlins <chlins.zhang@gmail.com>
This commit is contained in:
Chlins Zhang
2026-09-01 17:08:25 +08:00
committed by GitHub
parent 25b21f3c5c
commit efc69c61aa
8 changed files with 563 additions and 2 deletions
+1
View File
@@ -0,0 +1 @@
Add in-place restore pre-flight check: target PVC must not be in use
@@ -236,7 +236,13 @@ The key requirements for this approach are:
Before initiating an in-place restore for a volume, Velero performs the following pre-flight checks to ensure the operation is safe and valid:
#### 1. PVC is Not Actively Used by a Running Pod
Velero verifies that the target PVC is not currently mounted or consumed by any running Pods in the cluster. If the PVC is in use, Velero will skip the in-place restore for that volume and log an error. This enforces the prerequisite that users must completely delete consuming workloads prior to the restore, which prevents data corruption and avoids deadlocks caused by the Kubernetes `pvc-protection` finalizer during PVC recreation.
Velero verifies that the target PVC is not currently mounted or consumed by any active Pods in the cluster. If the PVC is in use, Velero will skip the in-place restore for that volume and log an error. This enforces the prerequisite that users must completely delete consuming workloads prior to the restore, which prevents data corruption and avoids deadlocks caused by the Kubernetes `pvc-protection` finalizer during PVC recreation.
The "in use" semantics align with the Kubernetes `pvc-protection` controller: Pods in a terminal phase (`Succeeded`/`Failed`) do not block the restore, all other phases do, and terminating Pods are flagged in the error message so users know to simply wait and retry.
The check runs on both restore paths before any side effect on the existing PVC/PV: in the PVC CSI RIA before deleting the existing PVC, and before creating the `PodVolumeRestore` on the file system path. On the file system path, Pods gated by this restore's `restore-wait` init container (identified by the restore UID in its args, and not yet terminated) are exempted: they must mount the PVC for the node-agent to restore the data, and they cannot write to the volume until this restore's `PodVolumeRestore`s complete. Leftover Pods, controller-recreated Pods, and Pods gated by a different restore still block.
This check is a fail-fast validation, not an atomic guarantee; the `pvc-protection` finalizer remains the actual safety gate for PVC deletion. A residual `VolumeAttachment` check (e.g. a `Failed` Pod imposed by the control plane after a non-graceful node shutdown, where the node never unmounted the volume) may be added as a future enhancement.
#### 2. PVC is Bound to the Original PV
Velero checks whether the existing PVC in the cluster is still bound to the same PersistentVolume (PV) it was bound to at the time of the backup. If the PVC is bound to a different PV, performing an in-place restore (especially an incremental one that relies on Changed Block Tracking) may be unsafe or result in unpredictable behavior. If this check fails, Velero will log an error and skip the in-place restore for that volume.
+13
View File
@@ -38,6 +38,7 @@ import (
"github.com/vmware-tanzu/velero/pkg/label"
"github.com/vmware-tanzu/velero/pkg/nodeagent"
"github.com/vmware-tanzu/velero/pkg/repository"
"github.com/vmware-tanzu/velero/pkg/restore/inplace"
uploaderutil "github.com/vmware-tanzu/velero/pkg/uploader/util"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
"github.com/vmware-tanzu/velero/pkg/util/kube"
@@ -179,6 +180,18 @@ func (r *restorer) RestorePodVolumes(data RestoreData, tracker *volume.RestoreVo
}
}
// Pre-flight checks for in-place restore. Pods gated by this
// restore's restore-wait init container are excluded: they must mount
// the PVC so the volume gets mounted on the node for the node-agent
// to write into, and they cannot write to it themselves until this
// restore's PodVolumeRestores complete.
if data.Restore.IsVolumeDataInplaceRestore() && pvc != nil {
if err := inplace.CheckPVCNotInUse(r.ctx, r.crClient, pvc, data.Restore.UID); err != nil {
errs = append(errs, err)
continue
}
}
volumeRestore := newPodVolumeRestore(data.Restore, data.Pod, data.BackupLocation, volume, backupInfo.snapshotID, backupInfo.snapshotSize, "", backupInfo.uploaderType, data.SourceNamespace, pvc)
if err := veleroclient.CreateRetryGenerateName(r.crClient, r.ctx, volumeRestore); err != nil {
errs = append(errs, errors.WithStack(err))
+103 -1
View File
@@ -37,6 +37,7 @@ import (
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/builder"
"github.com/vmware-tanzu/velero/pkg/repository"
"github.com/vmware-tanzu/velero/pkg/restorehelper"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
)
@@ -119,6 +120,21 @@ func TestGetVolumesRepositoryType(t *testing.T) {
}
}
// createGatedPodObj returns the restored pod as it exists in the cluster:
// running with the restore-wait init container injected by the given restore.
func createGatedPodObj(restoreUID string, volumeNum int) *corev1api.Pod {
pod := createPodObj(true, true, true, volumeNum)
pod.Spec.InitContainers = append([]corev1api.Container{{
Name: restorehelper.WaitInitContainer,
Args: []string{restoreUID},
}}, pod.Spec.InitContainers...)
pod.Status.InitContainerStatuses = []corev1api.ContainerStatus{{
Name: restorehelper.WaitInitContainer,
State: corev1api.ContainerState{Running: &corev1api.ContainerStateRunning{}},
}}
return pod
}
func createNodeAgentDaemonset() *appsv1api.DaemonSet {
ds := &appsv1api.DaemonSet{
ObjectMeta: metav1.ObjectMeta{
@@ -181,6 +197,7 @@ func TestRestorePodVolumes(t *testing.T) {
pvbs []*velerov1api.PodVolumeBackup
restoredPod *corev1api.Pod
sourceNamespace string
inplace bool
errs []expectError
}{
{
@@ -340,6 +357,86 @@ func TestRestorePodVolumes(t *testing.T) {
completedPVR,
},
},
{
name: "in-place restore blocked when the PVC is used by another running pod",
pvbs: []*velerov1api.PodVolumeBackup{
createPVBObj(true, true, 1, "kopia"),
},
inplace: true,
kubeClientObj: []runtime.Object{
createNodeAgentDaemonset(),
createPVCObj(1),
func() *corev1api.Pod {
pod := builder.ForPod("fake-ns", "other-pod").
Volumes(builder.ForVolume("fake-volume-1").PersistentVolumeClaimSource("fake-pvc-1").Result()).
Result()
pod.Status.Phase = corev1api.PodRunning
return pod
}(),
},
ctlClientObj: []runtime.Object{
createBackupRepoObj(),
},
restoredPod: createPodObj(true, true, true, 1),
sourceNamespace: "fake-ns",
bsl: "fake-bsl",
runtimeScheme: scheme,
errs: []expectError{
{
err: "in-place restore pre-flight check failed",
prefixOnly: true,
},
},
},
{
name: "in-place restore blocked when the pod is gated by a different restore",
pvbs: []*velerov1api.PodVolumeBackup{
createPVBObj(true, true, 1, "kopia"),
},
inplace: true,
kubeClientObj: []runtime.Object{
createNodeAgentDaemonset(),
createPVCObj(1),
createGatedPodObj("old-restore-uid", 1),
},
ctlClientObj: []runtime.Object{
createBackupRepoObj(),
},
restoredPod: createPodObj(true, true, true, 1),
sourceNamespace: "fake-ns",
bsl: "fake-bsl",
runtimeScheme: scheme,
errs: []expectError{
{
err: "in-place restore pre-flight check failed",
prefixOnly: true,
},
},
},
{
name: "in-place restore proceeds when the PVC is only used by the gated restored pod",
pvbs: []*velerov1api.PodVolumeBackup{
createPVBObj(true, true, 1, "kopia"),
},
inplace: true,
kubeClientObj: []runtime.Object{
createNodeAgentDaemonset(),
createNodeObj(),
createPVCObj(1),
createGatedPodObj("fake-restore-uid", 1),
createNodeAgentPodObj(true),
},
ctlClientObj: []runtime.Object{
createBackupRepoObj(),
},
restoredPod: createPodObj(true, true, true, 1),
sourceNamespace: "fake-ns",
bsl: "fake-bsl",
runtimeScheme: scheme,
retPVRs: []*velerov1api.PodVolumeRestore{
completedPVR,
},
},
}
for _, test := range tests {
@@ -362,7 +459,12 @@ func TestRestorePodVolumes(t *testing.T) {
ensurer := repository.NewEnsurer(fakeCRClient, velerotest.NewLogger(), time.Millisecond)
restoreObj := builder.ForRestore(velerov1api.DefaultNamespace, "fake-restore").Result()
restoreBuilder := builder.ForRestore(velerov1api.DefaultNamespace, "fake-restore").
ObjectMeta(builder.WithUID("fake-restore-uid"))
if test.inplace {
restoreBuilder = restoreBuilder.ExistingVolumeDataPolicy(string(velerov1api.VolumeDataPolicyTypeFull))
}
restoreObj := restoreBuilder.Result()
rs := newRestorer(ctx, repository.NewRepoLocker(), ensurer, pvrInformer, kubeClient, fakeCRClient, restoreObj, velerotest.NewLogger())
+7
View File
@@ -44,6 +44,7 @@ import (
plugincommon "github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
riav2 "github.com/vmware-tanzu/velero/pkg/plugin/velero/restoreitemaction/v2"
"github.com/vmware-tanzu/velero/pkg/restore/inplace"
uploaderUtil "github.com/vmware-tanzu/velero/pkg/uploader/util"
"github.com/vmware-tanzu/velero/pkg/util"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
@@ -236,6 +237,12 @@ func (p *pvcRestoreItemAction) executeWithDataMove(logger *logrus.Entry, input *
if existingPVC.Status.Phase != corev1api.ClaimBound {
return nil, errors.New("ExistingVolumeDataPolicy is in-place restore, but the existing PVC is not bound.")
}
// Pre-flight checks must pass before any side effect on the existing PVC/PV.
if err := inplace.CheckPVCNotInUse(ctx, p.crClient, existingPVC, input.Restore.UID); err != nil {
return nil, errors.WithStack(err)
}
// take a CSI snapshot of the existing PVC as the baseline of CBT
if input.Restore.IsVolumeDataInplaceIncrementalRestore() && datamover.IsVeleroBlockDataMover(dataUploadResult.DataMover) {
logger.Info("ExistingVolumeDataPolicy is in-place incremental restore and data mover is velero-block. Taking a CSI snapshot of the existing PVC as the baseline of CBT...")
@@ -741,6 +741,105 @@ func TestExecuteInplaceRestore(t *testing.T) {
require.Equal(t, "testPV", dataDownloadList.Items[0].Spec.TargetVolume.PV)
}
// TestExecuteInplaceRestorePreflight verifies the RIA fails the item without
// side effects when the pre-flight check fails. The in-use semantics are
// covered by the pkg/restore/inplace unit tests.
func TestExecuteInplaceRestorePreflight(t *testing.T) {
newPodUsingPVC := func(phase corev1api.PodPhase) *corev1api.Pod {
pod := builder.ForPod("velero", "consumer-pod").
Volumes(builder.ForVolume("data").PersistentVolumeClaimSource("testPVC").Result()).
Result()
pod.Status.Phase = phase
return pod
}
tests := []struct {
name string
pod *corev1api.Pod
expectBlock bool
}{
{
name: "no pod, restore proceeds",
},
{
name: "active pod blocks the restore",
pod: newPodUsingPVC(corev1api.PodRunning),
expectBlock: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
existingPVC := builder.ForPersistentVolumeClaim("velero", "testPVC").
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()
crObjects := []runtime.Object{existingPVC, existingPV, backup, dataUploadResult}
kubeObjects := []runtime.Object{existingPVC, existingPV}
if tc.pod != nil {
crObjects = append(crObjects, tc.pod)
kubeObjects = append(kubeObjects, tc.pod)
}
pvcRIA := pvcRestoreItemAction{
log: logrus.New(),
crClient: velerotest.NewFakeControllerRuntimeClient(t, crObjects...),
kubeClient: fake.NewSimpleClientset(kubeObjects...),
}
pvcMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pvcFromBackup.DeepCopy())
require.NoError(t, err)
pvcFromBackupMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pvcFromBackup)
require.NoError(t, err)
_, err = pvcRIA.Execute(&velero.RestoreItemActionExecuteInput{
Item: &unstructured.Unstructured{Object: pvcMap},
ItemFromBackup: &unstructured.Unstructured{Object: pvcFromBackupMap},
Restore: restore,
})
gotPVC, getErr := pvcRIA.kubeClient.CoreV1().PersistentVolumeClaims("velero").Get(t.Context(), "testPVC", metav1.GetOptions{})
dataDownloadList := new(velerov2alpha1.DataDownloadList)
require.NoError(t, pvcRIA.crClient.List(t.Context(), dataDownloadList, &crclient.ListOptions{}))
if tc.expectBlock {
require.Error(t, err)
require.Contains(t, err.Error(), "pre-flight check failed")
require.Contains(t, err.Error(), "consumer-pod")
// No side effects: PVC untouched with the original volumeName,
// PV reclaim policy not patched, no DataDownload created.
require.NoError(t, getErr)
require.Equal(t, "testPV", gotPVC.Spec.VolumeName)
gotPV, pvErr := pvcRIA.kubeClient.CoreV1().PersistentVolumes().Get(t.Context(), "testPV", metav1.GetOptions{})
require.NoError(t, pvErr)
require.Equal(t, existingPV.Spec.PersistentVolumeReclaimPolicy, gotPV.Spec.PersistentVolumeReclaimPolicy)
require.Empty(t, dataDownloadList.Items)
} else {
require.NoError(t, err)
// The in-place restore proceeded: the existing PVC is deleted
// and a DataDownload is created.
require.True(t, apierrors.IsNotFound(getErr))
require.Len(t, dataDownloadList.Items, 1)
}
})
}
}
func TestPVCAppliesTo(t *testing.T) {
p := pvcRestoreItemAction{
log: logrus.StandardLogger(),
+131
View File
@@ -0,0 +1,131 @@
/*
Copyright the Velero contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package inplace holds the pre-flight checks for in-place volume data
// restores. The checks must pass before Velero performs any side effect on
// the existing PVC/PV.
package inplace
import (
"context"
"fmt"
"strings"
"github.com/cockroachdb/errors"
corev1api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/vmware-tanzu/velero/pkg/restorehelper"
)
// CheckPVCNotInUse verifies the target PVC is not used by any active pod,
// aligned with the pvc-protection controller semantics: terminal-phase pods
// (Succeeded/Failed) don't block; all other phases do, and terminating pods
// are flagged so the message can hint the user to wait.
//
// Pods gated by this restore's restore-wait init container are exempted: on
// the file system restore path the restored pods must mount the PVC for the
// node-agent to restore the data, and an RWX PVC may be mounted by several of
// them. Such a pod cannot write to the volume since its workload containers
// are blocked until this restore's PodVolumeRestores complete (see
// gatedByThisRestore). Any other pod, including one gated by a different
// restore whose release timing is out of our control, still blocks.
func CheckPVCNotInUse(
ctx context.Context,
cli crclient.Client,
pvc *corev1api.PersistentVolumeClaim,
restoreUID types.UID,
) error {
podList := new(corev1api.PodList)
if err := cli.List(ctx, podList, &crclient.ListOptions{Namespace: pvc.Namespace}); err != nil {
return errors.Wrapf(err, "failed to check whether PVC %s/%s is in use: failed to list pods in namespace %s", pvc.Namespace, pvc.Name, pvc.Namespace)
}
podsInUse := []string{}
terminatingOnly := true
for i := range podList.Items {
pod := &podList.Items[i]
if !podUsesPVC(pod, pvc.Name) ||
pod.Status.Phase == corev1api.PodSucceeded || pod.Status.Phase == corev1api.PodFailed ||
gatedByThisRestore(pod, restoreUID) {
continue
}
state := string(pod.Status.Phase)
if pod.DeletionTimestamp != nil {
state += ", terminating"
} else {
terminatingOnly = false
}
podsInUse = append(podsInUse, fmt.Sprintf("%s (%s)", pod.Name, state))
}
if len(podsInUse) == 0 {
return nil
}
hint := "delete the workloads consuming the PVC and retry"
if terminatingOnly {
hint = "the pod(s) are terminating; retry after they are fully removed"
}
return errors.Errorf("in-place restore pre-flight check failed, skipping volume data restore: PVC %s/%s is still in use by pod(s) [%s]: %s",
pvc.Namespace, pvc.Name, strings.Join(podsInUse, ", "), hint)
}
func podUsesPVC(pod *corev1api.Pod, pvcName string) bool {
for _, vol := range pod.Spec.Volumes {
if vol.PersistentVolumeClaim != nil && vol.PersistentVolumeClaim.ClaimName == pvcName {
return true
}
}
return false
}
// gatedByThisRestore reports whether the pod is blocked by the restore-wait
// init container injected by this restore, identified by the restore UID in
// the init container's args. Such a pod cannot write to the volume: its
// workload containers won't start until this restore's PodVolumeRestores
// complete and write the done signal. A terminated init container means the
// gate is already open, so the pod is no longer exempted. Pods gated by a
// different restore are not exempted either, since their release timing is
// unrelated to this restore.
func gatedByThisRestore(pod *corev1api.Pod, restoreUID types.UID) bool {
if restoreUID == "" {
return false
}
idx := -1
for i, c := range pod.Spec.InitContainers {
if c.Name == restorehelper.WaitInitContainer {
if len(c.Args) == 0 || c.Args[0] != string(restoreUID) {
return false
}
idx = i
break
}
}
if idx < 0 {
return false
}
for _, cs := range pod.Status.InitContainerStatuses {
if cs.Name == restorehelper.WaitInitContainer {
return cs.State.Terminated == nil
}
}
// Statuses not populated yet: the init container hasn't run, so the gate
// is still closed.
return true
}
+202
View File
@@ -0,0 +1,202 @@
/*
Copyright the Velero contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package inplace
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"github.com/vmware-tanzu/velero/pkg/restorehelper"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
)
func podUsingPVC(name, pvcName string, phase corev1api.PodPhase, terminating bool) *corev1api.Pod {
pod := &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
UID: types.UID(name + "-uid"),
},
Spec: corev1api.PodSpec{
Volumes: []corev1api.Volume{
{
Name: "data",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: pvcName,
},
},
},
},
},
Status: corev1api.PodStatus{Phase: phase},
}
if terminating {
now := metav1.Now()
pod.DeletionTimestamp = &now
pod.Finalizers = []string{"fake-finalizer"}
}
return pod
}
// gatedPod adds the restore-wait init container (as injected by the
// PodVolumeRestoreAction RIA, carrying the restore UID in args) to the pod.
// terminated simulates the gate having already been released.
func gatedPod(pod *corev1api.Pod, restoreUID string, terminated bool) *corev1api.Pod {
pod.Spec.InitContainers = append([]corev1api.Container{{
Name: restorehelper.WaitInitContainer,
Args: []string{restoreUID},
}}, pod.Spec.InitContainers...)
status := corev1api.ContainerStatus{
Name: restorehelper.WaitInitContainer,
State: corev1api.ContainerState{Running: &corev1api.ContainerStateRunning{}},
}
if terminated {
status.State = corev1api.ContainerState{Terminated: &corev1api.ContainerStateTerminated{}}
}
pod.Status.InitContainerStatuses = []corev1api.ContainerStatus{status}
return pod
}
func TestCheckPVCNotInUse(t *testing.T) {
tests := []struct {
name string
pods []*corev1api.Pod
restoreUID types.UID
expectPass bool
expectMessage []string
}{
{
name: "no pods, check passes",
expectPass: true,
},
{
name: "active pod blocks with the delete hint",
pods: []*corev1api.Pod{podUsingPVC("pod-1", "pvc-1", corev1api.PodRunning, false)},
expectMessage: []string{"pod-1 (Running)", "delete the workloads"},
},
{
name: "unknown-phase pod blocks (node may be unreachable)",
pods: []*corev1api.Pod{podUsingPVC("pod-1", "pvc-1", corev1api.PodUnknown, false)},
expectMessage: []string{"pod-1 (Unknown)"},
},
{
name: "terminating pod blocks with the wait hint",
pods: []*corev1api.Pod{podUsingPVC("pod-1", "pvc-1", corev1api.PodRunning, true)},
expectMessage: []string{"pod-1 (Running, terminating)", "retry after they are fully removed"},
},
{
name: "terminal-phase pods do not block",
pods: []*corev1api.Pod{
podUsingPVC("pod-1", "pvc-1", corev1api.PodSucceeded, false),
podUsingPVC("pod-2", "pvc-1", corev1api.PodFailed, false),
},
expectPass: true,
},
{
name: "pod using another PVC does not block",
pods: []*corev1api.Pod{podUsingPVC("pod-1", "other-pvc", corev1api.PodRunning, false)},
expectPass: true,
},
{
name: "pods gated by this restore do not block, other pods still do",
pods: []*corev1api.Pod{
gatedPod(podUsingPVC("restored-pod-1", "pvc-1", corev1api.PodPending, false), "restore-uid", false),
gatedPod(podUsingPVC("restored-pod-2", "pvc-1", corev1api.PodPending, false), "restore-uid", false),
podUsingPVC("other-pod", "pvc-1", corev1api.PodRunning, false),
},
restoreUID: "restore-uid",
expectMessage: []string{"[other-pod (Running)]"},
},
{
name: "multiple pods gated by this restore pass the check",
pods: []*corev1api.Pod{
gatedPod(podUsingPVC("restored-pod-1", "pvc-1", corev1api.PodPending, false), "restore-uid", false),
gatedPod(podUsingPVC("restored-pod-2", "pvc-1", corev1api.PodPending, false), "restore-uid", false),
},
restoreUID: "restore-uid",
expectPass: true,
},
{
name: "pod gated by a different restore still blocks",
pods: []*corev1api.Pod{
gatedPod(podUsingPVC("old-restored-pod", "pvc-1", corev1api.PodPending, false), "old-restore-uid", false),
},
restoreUID: "restore-uid",
expectMessage: []string{"[old-restored-pod (Pending)]"},
},
{
name: "pod whose restore-wait already terminated still blocks",
pods: []*corev1api.Pod{
gatedPod(podUsingPVC("released-pod", "pvc-1", corev1api.PodRunning, false), "restore-uid", true),
},
restoreUID: "restore-uid",
expectMessage: []string{"[released-pod (Running)]"},
},
{
name: "gated pod without init container status yet passes the check",
pods: []*corev1api.Pod{
func() *corev1api.Pod {
pod := gatedPod(podUsingPVC("new-pod", "pvc-1", corev1api.PodPending, false), "restore-uid", false)
pod.Status.InitContainerStatuses = nil
return pod
}(),
},
restoreUID: "restore-uid",
expectPass: true,
},
{
name: "empty restore UID exempts nothing",
pods: []*corev1api.Pod{
gatedPod(podUsingPVC("restored-pod", "pvc-1", corev1api.PodPending, false), "", false),
},
restoreUID: "",
expectMessage: []string{"[restored-pod (Pending)]"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
objs := []runtime.Object{}
for _, pod := range tc.pods {
objs = append(objs, pod)
}
cli := velerotest.NewFakeControllerRuntimeClient(t, objs...)
pvc := &corev1api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{Name: "pvc-1", Namespace: "default"},
}
err := CheckPVCNotInUse(t.Context(), cli, pvc, tc.restoreUID)
if tc.expectPass {
require.NoError(t, err)
return
}
require.Error(t, err)
assert.Contains(t, err.Error(), "pre-flight check failed")
for _, fragment := range tc.expectMessage {
assert.Contains(t, err.Error(), fragment)
}
})
}
}