mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-28 02:44:19 +00:00
The pre-flight check that verifies the existing PVC is still bound to the backed-up volume compared PV names. A block data mover restore of a file system volume recreates the PV under a new name (the volumeMode field is immutable), so a second in-place restore of the same workload failed the check even though the PVC was bound to the very same volume. Record the CSI volume handle in the backup volume info (PVInfo) and compare handles when both the backup and the bound PV record one; the PV name remains the fallback for non-CSI volumes and for backups taken before the handle was recorded. The handle reaches the PVC CSI RIA through the same carrier annotation mechanism as the source size. Signed-off-by: chlins <chlins.zhang@gmail.com>
411 lines
13 KiB
Go
411 lines
13 KiB
Go
/*
|
|
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"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
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
|
|
pvc *corev1api.PersistentVolumeClaim
|
|
restoreUID types.UID
|
|
expectPass bool
|
|
expectMessage []string
|
|
expectError string
|
|
}{
|
|
{
|
|
name: "nil PVC returns error",
|
|
expectError: "pvc cannot be nil",
|
|
},
|
|
{
|
|
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 := tc.pvc
|
|
if pvc == nil && tc.name != "nil PVC returns error" {
|
|
pvc = &corev1api.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-1", Namespace: "default"},
|
|
}
|
|
}
|
|
|
|
err := CheckPVCNotInUse(t.Context(), cli, pvc, tc.restoreUID)
|
|
if tc.expectError != "" {
|
|
require.Error(t, err)
|
|
assert.EqualError(t, err, tc.expectError)
|
|
return
|
|
}
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckPVCBoundToBackedUpVolume(t *testing.T) {
|
|
pvc := func(namespace, pvName string, phase corev1api.PersistentVolumeClaimPhase) *corev1api.PersistentVolumeClaim {
|
|
return &corev1api.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-1", Namespace: namespace},
|
|
Spec: corev1api.PersistentVolumeClaimSpec{VolumeName: pvName},
|
|
Status: corev1api.PersistentVolumeClaimStatus{Phase: phase},
|
|
}
|
|
}
|
|
csiPV := func(name, handle string) *corev1api.PersistentVolume {
|
|
return &corev1api.PersistentVolume{
|
|
ObjectMeta: metav1.ObjectMeta{Name: name},
|
|
Spec: corev1api.PersistentVolumeSpec{PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
|
CSI: &corev1api.CSIPersistentVolumeSource{VolumeHandle: handle},
|
|
}},
|
|
}
|
|
}
|
|
localPV := func(name string) *corev1api.PersistentVolume {
|
|
return &corev1api.PersistentVolume{ObjectMeta: metav1.ObjectMeta{Name: name}}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
existingPVC *corev1api.PersistentVolumeClaim
|
|
existingPV *corev1api.PersistentVolume
|
|
backedUpPVName string
|
|
backedUpHandle string
|
|
expectError string
|
|
}{
|
|
{
|
|
name: "nil existing PVC returns error",
|
|
backedUpPVName: "pv-1",
|
|
expectError: "existing PVC cannot be nil",
|
|
},
|
|
{
|
|
name: "same volume handle under the same PV name, check passes",
|
|
existingPVC: pvc("default", "pv-1", corev1api.ClaimBound),
|
|
existingPV: csiPV("pv-1", "vol-1"),
|
|
backedUpPVName: "pv-1",
|
|
backedUpHandle: "vol-1",
|
|
},
|
|
{
|
|
name: "same volume handle under a recreated PV name, check passes",
|
|
existingPVC: pvc("default", "pv-recreated", corev1api.ClaimBound),
|
|
existingPV: csiPV("pv-recreated", "vol-1"),
|
|
backedUpPVName: "pv-1",
|
|
backedUpHandle: "vol-1",
|
|
},
|
|
{
|
|
name: "different volume handle under the same PV name, check fails",
|
|
existingPVC: pvc("default", "pv-1", corev1api.ClaimBound),
|
|
existingPV: csiPV("pv-1", "vol-other"),
|
|
backedUpPVName: "pv-1",
|
|
backedUpHandle: "vol-1",
|
|
expectError: "is bound to volume vol-other (PV pv-1), but was bound to volume vol-1 (PV pv-1) at backup time",
|
|
},
|
|
{
|
|
name: "bound PV not found, check fails",
|
|
existingPVC: pvc("default", "pv-gone", corev1api.ClaimBound),
|
|
backedUpPVName: "pv-1",
|
|
backedUpHandle: "vol-1",
|
|
expectError: "failed to get PV pv-gone bound to PVC default/pvc-1",
|
|
},
|
|
{
|
|
name: "non-CSI volume with the same PV name, check passes",
|
|
existingPVC: pvc("default", "pv-1", corev1api.ClaimBound),
|
|
existingPV: localPV("pv-1"),
|
|
backedUpPVName: "pv-1",
|
|
},
|
|
{
|
|
name: "non-CSI volume with a different PV name, check fails",
|
|
existingPVC: pvc("default", "pv-other", corev1api.ClaimBound),
|
|
existingPV: localPV("pv-other"),
|
|
backedUpPVName: "pv-1",
|
|
expectError: "is bound to PV pv-other, but was bound to PV pv-1 at backup time",
|
|
},
|
|
{
|
|
name: "backup without volume handle falls back to the PV name",
|
|
existingPVC: pvc("default", "pv-other", corev1api.ClaimBound),
|
|
existingPV: csiPV("pv-other", "vol-1"),
|
|
backedUpPVName: "pv-1",
|
|
expectError: "is bound to PV pv-other, but was bound to PV pv-1 at backup time",
|
|
},
|
|
{
|
|
name: "existing PV without volume handle falls back to the PV name",
|
|
existingPVC: pvc("default", "pv-other", corev1api.ClaimBound),
|
|
existingPV: localPV("pv-other"),
|
|
backedUpPVName: "pv-1",
|
|
backedUpHandle: "vol-1",
|
|
expectError: "is bound to PV pv-other, but was bound to PV pv-1 at backup time",
|
|
},
|
|
{
|
|
name: "PVC not bound, check fails",
|
|
existingPVC: pvc("default", "", corev1api.ClaimPending),
|
|
backedUpPVName: "pv-1",
|
|
backedUpHandle: "vol-1",
|
|
expectError: "is not bound (phase Pending)",
|
|
},
|
|
{
|
|
name: "different volume in a different namespace, check passes",
|
|
existingPVC: pvc("mapped-ns", "pv-other", corev1api.ClaimBound),
|
|
existingPV: csiPV("pv-other", "vol-other"),
|
|
backedUpPVName: "pv-1",
|
|
backedUpHandle: "vol-1",
|
|
},
|
|
{
|
|
name: "backed-up volume unknown, check passes",
|
|
existingPVC: pvc("default", "pv-other", corev1api.ClaimBound),
|
|
existingPV: csiPV("pv-other", "vol-other"),
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
objs := []runtime.Object{}
|
|
if tc.existingPV != nil {
|
|
objs = append(objs, tc.existingPV)
|
|
}
|
|
cli := velerotest.NewFakeControllerRuntimeClient(t, objs...)
|
|
err := CheckPVCBoundToBackedUpVolume(t.Context(), cli, tc.existingPVC, tc.backedUpPVName, tc.backedUpHandle, "default")
|
|
if tc.expectError == "" {
|
|
require.NoError(t, err)
|
|
return
|
|
}
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tc.expectError)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckPVCCapacity(t *testing.T) {
|
|
pvc := func(capacity string) *corev1api.PersistentVolumeClaim {
|
|
p := &corev1api.PersistentVolumeClaim{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "pvc-1", Namespace: "default"},
|
|
}
|
|
if capacity != "" {
|
|
p.Status.Capacity = corev1api.ResourceList{corev1api.ResourceStorage: resource.MustParse(capacity)}
|
|
}
|
|
return p
|
|
}
|
|
const mi = int64(1 << 20)
|
|
|
|
tests := []struct {
|
|
name string
|
|
existingPVC *corev1api.PersistentVolumeClaim
|
|
sourceSize int64
|
|
expectError string
|
|
}{
|
|
{
|
|
name: "capacity larger than source volume, check passes",
|
|
existingPVC: pvc("100Mi"),
|
|
sourceSize: 50 * mi,
|
|
},
|
|
{
|
|
name: "capacity equal to source volume, check passes",
|
|
existingPVC: pvc("100Mi"),
|
|
sourceSize: 100 * mi,
|
|
},
|
|
{
|
|
name: "capacity smaller than source volume, check fails",
|
|
existingPVC: pvc("50Mi"),
|
|
sourceSize: 100 * mi,
|
|
expectError: "capacity 50Mi is smaller than the backed-up volume size 104857600 bytes",
|
|
},
|
|
{
|
|
name: "unknown source size is skipped",
|
|
existingPVC: pvc("50Mi"),
|
|
sourceSize: 0,
|
|
},
|
|
{
|
|
name: "missing capacity is skipped",
|
|
existingPVC: pvc(""),
|
|
sourceSize: 100 * mi,
|
|
},
|
|
{
|
|
name: "capacity in decimal units compares by value",
|
|
existingPVC: pvc("104857600"),
|
|
sourceSize: 100 * mi,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := CheckPVCCapacity(tc.existingPVC, tc.sourceSize)
|
|
if tc.expectError == "" {
|
|
require.NoError(t, err)
|
|
return
|
|
}
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tc.expectError)
|
|
})
|
|
}
|
|
}
|