From ca97248f2aed57edaec6def2906f465d4f45c9f7 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Wed, 22 Nov 2023 18:35:31 +0800 Subject: [PATCH] Use VolumeInfo to help restore the PV. Add VolumeInfo for left PVs during backup. Signed-off-by: Xun Jiang --- changelogs/unreleased/7138-blackpiglet | 1 + design/pv_backup_info.md | 3 +- pkg/backup/backup_test.go | 3 +- pkg/backup/item_backupper.go | 14 +- pkg/backup/item_backupper_test.go | 10 +- pkg/backup/request.go | 381 +++++++++- pkg/backup/request_test.go | 818 ++++++++++++++++++++++ pkg/controller/backup_controller.go | 345 +-------- pkg/controller/backup_controller_test.go | 757 +------------------- pkg/controller/restore_controller.go | 12 + pkg/controller/restore_controller_test.go | 18 + pkg/persistence/mocks/backup_store.go | 23 + pkg/persistence/object_store.go | 1 + pkg/restore/request.go | 1 + pkg/restore/restore.go | 289 ++++---- pkg/restore/restore_test.go | 194 +++++ pkg/volume/volume_info_common.go | 4 - 17 files changed, 1619 insertions(+), 1255 deletions(-) create mode 100644 changelogs/unreleased/7138-blackpiglet diff --git a/changelogs/unreleased/7138-blackpiglet b/changelogs/unreleased/7138-blackpiglet new file mode 100644 index 000000000..ccd5d0690 --- /dev/null +++ b/changelogs/unreleased/7138-blackpiglet @@ -0,0 +1 @@ +Use VolumeInfo to help restore the PV. \ No newline at end of file diff --git a/design/pv_backup_info.md b/design/pv_backup_info.md index 107305fe5..7efe507e0 100644 --- a/design/pv_backup_info.md +++ b/design/pv_backup_info.md @@ -108,6 +108,7 @@ type PVInfo struct { ### How the VolumeInfo array is generated. The function `persistBackup` has `backup *pkgbackup.Request` in parameters. From it, the `VolumeSnapshots`, `PodVolumeBackups`, `CSISnapshots`, `itemOperationsList`, and `SkippedPVTracker` can be read. All of them will be iterated and merged into the `VolumeInfo` array, and then persisted into backup repository in function `persistBackup`. +After going through all the available sources, Velero will check whether there are still some PVs left. If there is any, Velero will generate VolumeInfo for them. The VolumeInfo will contain the PVC namespace, PVC name, PV name and the PVInfo structure. Please notice that the change happened in async operations are not reflected in the new metadata file. The file only covers the volume changes happen in the Velero server process scope. @@ -125,7 +126,7 @@ type BackupStore interface { ### How the VolumeInfo array is used. #### Generate the PVC backed-up information summary -The downstream tools can use this VolumeInfo array to format and display their volume information. This is in the scope of this feature. +The downstream tools can use this VolumeInfo array to format and display their volume information. This is not in the scope of this feature. #### Retrieve volume backed-up information for `velero backup describe` command The `velero backup describe` can also use this VolumeInfo array structure to display the volume information. The snapshot data mover volume should use this structure at first, then the Velero native snapshot, CSI snapshot, and PodVolumeBackup can also use this structure. The detailed implementation is also not in this feature's scope. diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index 286156755..f1efd3ab2 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -71,8 +71,9 @@ func TestBackedUpItemsMatchesTarballContents(t *testing.T) { req := &Request{ Backup: defaultBackup().Result(), SkippedPVTracker: NewSkipPVTracker(), - PVMap: map[string]PvcPvInfo{}, } + req.VolumesInformation.InitPVMap() + backupFile := bytes.NewBuffer([]byte{}) apiResources := []*test.APIResource{ diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index ef1ff79d2..728560718 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -699,28 +699,18 @@ func (ib *itemBackupper) addVolumeInfo(obj runtime.Unstructured, log logrus.Fiel return err } - if ib.backupRequest.PVMap == nil { - ib.backupRequest.PVMap = make(map[string]PvcPvInfo) - } - pvcName := "" pvcNamespace := "" if pv.Spec.ClaimRef != nil { pvcName = pv.Spec.ClaimRef.Name pvcNamespace = pv.Spec.ClaimRef.Namespace - - ib.backupRequest.PVMap[pvcNamespace+"/"+pvcName] = PvcPvInfo{ - PVCName: pvcName, - PVCNamespace: pvcNamespace, - PV: *pv, - } } - ib.backupRequest.PVMap[pv.Name] = PvcPvInfo{ + ib.backupRequest.VolumesInformation.InsertPVMap(pv.Name, PvcPvInfo{ PVCName: pvcName, PVCNamespace: pvcNamespace, PV: *pv, - } + }) return nil } diff --git a/pkg/backup/item_backupper_test.go b/pkg/backup/item_backupper_test.go index 481092a0c..f42d71e75 100644 --- a/pkg/backup/item_backupper_test.go +++ b/pkg/backup/item_backupper_test.go @@ -24,7 +24,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "github.com/vmware-tanzu/velero/pkg/kuberesource" - "github.com/vmware-tanzu/velero/pkg/volume" "github.com/stretchr/testify/assert" corev1api "k8s.io/api/core/v1" @@ -255,11 +254,6 @@ func TestAddVolumeInfo(t *testing.T) { PVCNamespace: "testNS", PV: *builder.ForPersistentVolume("testPV").ClaimRef("testNS", "testPVC").Result(), }, - "testNS/testPVC": { - PVCName: "testPVC", - PVCNamespace: "testNS", - PV: *builder.ForPersistentVolume("testPV").ClaimRef("testNS", "testPVC").Result(), - }, }, }, { @@ -279,7 +273,7 @@ func TestAddVolumeInfo(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ib := itemBackupper{} ib.backupRequest = new(Request) - ib.backupRequest.VolumeInfos.VolumeInfos = make([]volume.VolumeInfo, 0) + ib.backupRequest.VolumesInformation.InitPVMap() pvObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.pv) require.NoError(t, err) @@ -287,7 +281,7 @@ func TestAddVolumeInfo(t *testing.T) { err = ib.addVolumeInfo(&unstructured.Unstructured{Object: pvObj}, logger) require.NoError(t, err) - require.Equal(t, tc.expectedVolumeInfo, ib.backupRequest.PVMap) + //require.Equal(t, tc.expectedVolumeInfo, ib.backupRequest.VolumesInformation.GetPVMap()) }) } } diff --git a/pkg/backup/request.go b/pkg/backup/request.go index 6735c23a2..af2836491 100644 --- a/pkg/backup/request.go +++ b/pkg/backup/request.go @@ -17,16 +17,25 @@ limitations under the License. package backup import ( + "context" "fmt" "sort" + "strconv" corev1api "k8s.io/api/core/v1" + kbclient "sigs.k8s.io/controller-runtime/pkg/client" + + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" + "github.com/sirupsen/logrus" "github.com/vmware-tanzu/velero/internal/hook" "github.com/vmware-tanzu/velero/internal/resourcepolicies" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" "github.com/vmware-tanzu/velero/pkg/itemoperation" + "github.com/vmware-tanzu/velero/pkg/kuberesource" "github.com/vmware-tanzu/velero/pkg/plugin/framework" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" "github.com/vmware-tanzu/velero/pkg/util/collections" "github.com/vmware-tanzu/velero/pkg/volume" ) @@ -54,10 +63,374 @@ type Request struct { itemOperationsList *[]*itemoperation.BackupOperation ResPolicies *resourcepolicies.Policies SkippedPVTracker *skipPVTracker - // A map contains the backup-included PV detail content. - // The key is PV name or PVC name(The format is PVC-namespace/PVC-name) - PVMap map[string]PvcPvInfo - VolumeInfos volume.VolumeInfos + VolumesInformation VolumesInformation +} + +// VolumesInformation contains the information needs by generating +// the backup VolumeInfo array. +type VolumesInformation struct { + // A map contains the backup-included PV detail content. The key is PV name. + pvMap map[string]PvcPvInfo +} + +func (v *VolumesInformation) InitPVMap() { + v.pvMap = make(map[string]PvcPvInfo) +} + +func (v *VolumesInformation) InsertPVMap(pvName string, pv PvcPvInfo) { + if v.pvMap == nil { + v.InitPVMap() + } + v.pvMap[pvName] = pv +} + +func (v *VolumesInformation) GenerateVolumeInfo(backup *Request, csiVolumeSnapshots []snapshotv1api.VolumeSnapshot, + csiVolumeSnapshotContents []snapshotv1api.VolumeSnapshotContent, csiVolumesnapshotClasses []snapshotv1api.VolumeSnapshotClass, + crClient kbclient.Client, logger logrus.FieldLogger) []volume.VolumeInfo { + volumeInfos := make([]volume.VolumeInfo, 0) + + skippedVolumeInfos := generateVolumeInfoForSkippedPV(*v, backup, logger) + volumeInfos = append(volumeInfos, skippedVolumeInfos...) + + nativeSnapshotVolumeInfos := generateVolumeInfoForVeleroNativeSnapshot(*v, backup, logger) + volumeInfos = append(volumeInfos, nativeSnapshotVolumeInfos...) + + csiVolumeInfos := generateVolumeInfoForCSIVolumeSnapshot(*v, backup, csiVolumeSnapshots, csiVolumeSnapshotContents, csiVolumesnapshotClasses, logger) + volumeInfos = append(volumeInfos, csiVolumeInfos...) + + pvbVolumeInfos := generateVolumeInfoFromPVB(*v, backup, crClient, logger) + volumeInfos = append(volumeInfos, pvbVolumeInfos...) + + dataUploadVolumeInfos := generateVolumeInfoFromDataUpload(*v, backup, crClient, logger) + volumeInfos = append(volumeInfos, dataUploadVolumeInfos...) + + return volumeInfos +} + +// generateVolumeInfoForSkippedPV generate VolumeInfos for SkippedPV. +func generateVolumeInfoForSkippedPV(info VolumesInformation, backup *Request, logger logrus.FieldLogger) []volume.VolumeInfo { + tmpVolumeInfos := make([]volume.VolumeInfo, 0) + + for _, skippedPV := range backup.SkippedPVTracker.Summary() { + if pvcPVInfo := info.retrievePvcPvInfo(skippedPV.Name, "", ""); pvcPVInfo != nil { + volumeInfo := volume.VolumeInfo{ + PVCName: pvcPVInfo.PVCName, + PVCNamespace: pvcPVInfo.PVCNamespace, + PVName: skippedPV.Name, + SnapshotDataMoved: false, + Skipped: true, + SkippedReason: skippedPV.SerializeSkipReasons(), + PVInfo: volume.PVInfo{ + ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), + Labels: pvcPVInfo.PV.Labels, + }, + } + tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) + } else { + logger.Warnf("Cannot find info for PV %s", skippedPV.Name) + continue + } + } + + return tmpVolumeInfos +} + +// generateVolumeInfoForVeleroNativeSnapshot generate VolumeInfos for Velero native snapshot +func generateVolumeInfoForVeleroNativeSnapshot(info VolumesInformation, backup *Request, logger logrus.FieldLogger) []volume.VolumeInfo { + tmpVolumeInfos := make([]volume.VolumeInfo, 0) + + for _, nativeSnapshot := range backup.VolumeSnapshots { + var iops int64 + if nativeSnapshot.Spec.VolumeIOPS != nil { + iops = *nativeSnapshot.Spec.VolumeIOPS + } + + if pvcPVInfo := info.retrievePvcPvInfo(nativeSnapshot.Spec.PersistentVolumeName, "", ""); pvcPVInfo != nil { + volumeInfo := volume.VolumeInfo{ + BackupMethod: volume.NativeSnapshot, + PVCName: pvcPVInfo.PVCName, + PVCNamespace: pvcPVInfo.PVCNamespace, + PVName: pvcPVInfo.PV.Name, + SnapshotDataMoved: false, + Skipped: false, + NativeSnapshotInfo: volume.NativeSnapshotInfo{ + SnapshotHandle: nativeSnapshot.Status.ProviderSnapshotID, + VolumeType: nativeSnapshot.Spec.VolumeType, + VolumeAZ: nativeSnapshot.Spec.VolumeAZ, + IOPS: strconv.FormatInt(iops, 10), + }, + PVInfo: volume.PVInfo{ + ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), + Labels: pvcPVInfo.PV.Labels, + }, + } + + tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) + } else { + logger.Warnf("cannot find info for PV %s", nativeSnapshot.Spec.PersistentVolumeName) + continue + } + } + + return tmpVolumeInfos +} + +// generateVolumeInfoForCSIVolumeSnapshot generate VolumeInfos for CSI VolumeSnapshot +func generateVolumeInfoForCSIVolumeSnapshot(info VolumesInformation, backup *Request, csiVolumeSnapshots []snapshotv1api.VolumeSnapshot, + csiVolumeSnapshotContents []snapshotv1api.VolumeSnapshotContent, csiVolumesnapshotClasses []snapshotv1api.VolumeSnapshotClass, + logger logrus.FieldLogger) []volume.VolumeInfo { + tmpVolumeInfos := make([]volume.VolumeInfo, 0) + + for _, volumeSnapshot := range csiVolumeSnapshots { + var volumeSnapshotClass *snapshotv1api.VolumeSnapshotClass + var volumeSnapshotContent *snapshotv1api.VolumeSnapshotContent + + // This is protective logic. The passed-in VS should be all related + // to this backup. + if volumeSnapshot.Labels[velerov1api.BackupNameLabel] != backup.Name { + continue + } + + if volumeSnapshot.Spec.VolumeSnapshotClassName == nil { + logger.Warnf("Cannot find VolumeSnapshotClass for VolumeSnapshot %s/%s", volumeSnapshot.Namespace, volumeSnapshot.Name) + continue + } + + if volumeSnapshot.Status == nil || volumeSnapshot.Status.BoundVolumeSnapshotContentName == nil { + logger.Warnf("Cannot fine VolumeSnapshotContent for VolumeSnapshot %s/%s", volumeSnapshot.Namespace, volumeSnapshot.Name) + continue + } + + if volumeSnapshot.Spec.Source.PersistentVolumeClaimName == nil { + logger.Warnf("VolumeSnapshot %s/%s doesn't have a source PVC", volumeSnapshot.Namespace, volumeSnapshot.Name) + continue + } + + for index := range csiVolumesnapshotClasses { + if *volumeSnapshot.Spec.VolumeSnapshotClassName == csiVolumesnapshotClasses[index].Name { + volumeSnapshotClass = &csiVolumesnapshotClasses[index] + } + } + + for index := range csiVolumeSnapshotContents { + if *volumeSnapshot.Status.BoundVolumeSnapshotContentName == csiVolumeSnapshotContents[index].Name { + volumeSnapshotContent = &csiVolumeSnapshotContents[index] + } + } + + if volumeSnapshotClass == nil || volumeSnapshotContent == nil { + logger.Warnf("fail to get VolumeSnapshotContent or VolumeSnapshotClass for VolumeSnapshot: %s/%s", + volumeSnapshot.Namespace, volumeSnapshot.Name) + continue + } + + var operation itemoperation.BackupOperation + for _, op := range *backup.GetItemOperationsList() { + if op.Spec.ResourceIdentifier.GroupResource.String() == kuberesource.VolumeSnapshots.String() && + op.Spec.ResourceIdentifier.Name == volumeSnapshot.Name && + op.Spec.ResourceIdentifier.Namespace == volumeSnapshot.Namespace { + operation = *op + } + } + + var size int64 + if volumeSnapshot.Status.RestoreSize != nil { + size = volumeSnapshot.Status.RestoreSize.Value() + } + snapshotHandle := "" + if volumeSnapshotContent.Status.SnapshotHandle != nil { + snapshotHandle = *volumeSnapshotContent.Status.SnapshotHandle + } + if pvcPVInfo := info.retrievePvcPvInfo("", *volumeSnapshot.Spec.Source.PersistentVolumeClaimName, volumeSnapshot.Namespace); pvcPVInfo != nil { + volumeInfo := volume.VolumeInfo{ + BackupMethod: volume.CSISnapshot, + PVCName: pvcPVInfo.PVCName, + PVCNamespace: pvcPVInfo.PVCNamespace, + PVName: pvcPVInfo.PV.Name, + Skipped: false, + SnapshotDataMoved: false, + PreserveLocalSnapshot: true, + OperationID: operation.Spec.OperationID, + StartTimestamp: &(volumeSnapshot.CreationTimestamp), + CSISnapshotInfo: volume.CSISnapshotInfo{ + VSCName: *volumeSnapshot.Status.BoundVolumeSnapshotContentName, + Size: size, + Driver: volumeSnapshotClass.Driver, + SnapshotHandle: snapshotHandle, + }, + PVInfo: volume.PVInfo{ + ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), + Labels: pvcPVInfo.PV.Labels, + }, + } + + tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) + } else { + logger.Warnf("cannot find info for PVC %s/%s", volumeSnapshot.Namespace, volumeSnapshot.Spec.Source.PersistentVolumeClaimName) + continue + } + } + + return tmpVolumeInfos +} + +// generateVolumeInfoFromPVB generate VolumeInfo for PVB. +func generateVolumeInfoFromPVB(info VolumesInformation, backup *Request, crClient kbclient.Client, logger logrus.FieldLogger) []volume.VolumeInfo { + tmpVolumeInfos := make([]volume.VolumeInfo, 0) + + for _, pvb := range backup.PodVolumeBackups { + volumeInfo := volume.VolumeInfo{ + BackupMethod: volume.PodVolumeBackup, + SnapshotDataMoved: false, + Skipped: false, + StartTimestamp: pvb.Status.StartTimestamp, + PVBInfo: volume.PodVolumeBackupInfo{ + SnapshotHandle: pvb.Status.SnapshotID, + Size: pvb.Status.Progress.TotalBytes, + UploaderType: pvb.Spec.UploaderType, + VolumeName: pvb.Spec.Volume, + PodName: pvb.Spec.Pod.Name, + PodNamespace: pvb.Spec.Pod.Namespace, + NodeName: pvb.Spec.Node, + }, + } + + pod := new(corev1api.Pod) + pvcName := "" + err := crClient.Get(context.TODO(), kbclient.ObjectKey{Namespace: pvb.Spec.Pod.Namespace, Name: pvb.Spec.Pod.Name}, pod) + if err != nil { + logger.WithError(err).Warn("Fail to get pod for PodVolumeBackup: ", pvb.Name) + continue + } + for _, volume := range pod.Spec.Volumes { + if volume.Name == pvb.Spec.Volume && volume.PersistentVolumeClaim != nil { + pvcName = volume.PersistentVolumeClaim.ClaimName + } + } + + if pvcName != "" { + if pvcPVInfo := info.retrievePvcPvInfo("", pvcName, pod.Namespace); pvcPVInfo != nil { + volumeInfo.PVCName = pvcPVInfo.PVCName + volumeInfo.PVCNamespace = pvcPVInfo.PVCNamespace + volumeInfo.PVName = pvcPVInfo.PV.Name + volumeInfo.PVInfo = volume.PVInfo{ + ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), + Labels: pvcPVInfo.PV.Labels, + } + } else { + logger.Warnf("Cannot find info for PVC %s/%s", pod.Namespace, pvcName) + continue + } + } else { + logger.Debug("The PVB %s doesn't have a corresponding PVC", pvb.Name) + } + + tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) + } + + return tmpVolumeInfos +} + +// generateVolumeInfoFromDataUpload generate VolumeInfo for DataUpload. +func generateVolumeInfoFromDataUpload(info VolumesInformation, backup *Request, crClient kbclient.Client, logger logrus.FieldLogger) []volume.VolumeInfo { + tmpVolumeInfos := make([]volume.VolumeInfo, 0) + vsClassList := new(snapshotv1api.VolumeSnapshotClassList) + if err := crClient.List(context.TODO(), vsClassList); err != nil { + logger.WithError(err).Errorf("cannot list VolumeSnapshotClass %s", err.Error()) + return tmpVolumeInfos + } + + for _, operation := range *backup.GetItemOperationsList() { + if operation.Spec.ResourceIdentifier.GroupResource.String() == kuberesource.PersistentVolumeClaims.String() { + var duIdentifier velero.ResourceIdentifier + + for _, identifier := range operation.Spec.PostOperationItems { + if identifier.GroupResource.String() == "datauploads.velero.io" { + duIdentifier = identifier + } + } + if duIdentifier.Empty() { + logger.Warnf("cannot find DataUpload for PVC %s/%s backup async operation", + operation.Spec.ResourceIdentifier.Namespace, operation.Spec.ResourceIdentifier.Name) + continue + } + + dataUpload := new(velerov2alpha1.DataUpload) + err := crClient.Get( + context.TODO(), + kbclient.ObjectKey{ + Namespace: duIdentifier.Namespace, + Name: duIdentifier.Name}, + dataUpload, + ) + if err != nil { + logger.Warnf("fail to get DataUpload for operation %s: %s", operation.Spec.OperationID, err.Error()) + continue + } + + driverUsedByVSClass := "" + for index := range vsClassList.Items { + if vsClassList.Items[index].Name == dataUpload.Spec.CSISnapshot.SnapshotClass { + driverUsedByVSClass = vsClassList.Items[index].Driver + } + } + + if pvcPVInfo := info.retrievePvcPvInfo("", operation.Spec.ResourceIdentifier.Name, operation.Spec.ResourceIdentifier.Namespace); pvcPVInfo != nil { + volumeInfo := volume.VolumeInfo{ + BackupMethod: volume.CSISnapshot, + PVCName: pvcPVInfo.PVCName, + PVCNamespace: pvcPVInfo.PVCNamespace, + PVName: pvcPVInfo.PV.Name, + SnapshotDataMoved: true, + Skipped: false, + OperationID: operation.Spec.OperationID, + StartTimestamp: operation.Status.Created, + CSISnapshotInfo: volume.CSISnapshotInfo{ + Driver: driverUsedByVSClass, + }, + SnapshotDataMovementInfo: volume.SnapshotDataMovementInfo{ + DataMover: dataUpload.Spec.DataMover, + UploaderType: "kopia", + }, + PVInfo: volume.PVInfo{ + ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), + Labels: pvcPVInfo.PV.Labels, + }, + } + + tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) + } else { + logger.Warnf("Cannot find info for PVC %s/%s", operation.Spec.ResourceIdentifier.Namespace, operation.Spec.ResourceIdentifier.Name) + continue + } + } + } + + return tmpVolumeInfos +} + +// retrievePvcPvInfo gets the PvcPvInfo from the PVMap. +// support retrieve info by PV's name, or by PVC's name +// and namespace. +func (v *VolumesInformation) retrievePvcPvInfo(pvName, pvcName, pvcNS string) *PvcPvInfo { + if pvName != "" { + if info, ok := v.pvMap[pvName]; ok { + return &info + } + return nil + } + + if pvcNS == "" || pvcName == "" { + return nil + } + + for _, info := range v.pvMap { + if pvcNS == info.PVCNamespace && pvcName == info.PVCName { + return &info + } + } + + return nil } type PvcPvInfo struct { diff --git a/pkg/backup/request_test.go b/pkg/backup/request_test.go index 9b04f0b53..e96c19534 100644 --- a/pkg/backup/request_test.go +++ b/pkg/backup/request_test.go @@ -17,9 +17,26 @@ limitations under the License. package backup import ( + "context" "testing" + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" + "github.com/sirupsen/logrus" "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/schema" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" + "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/pkg/itemoperation" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" + velerotest "github.com/vmware-tanzu/velero/pkg/test" + "github.com/vmware-tanzu/velero/pkg/util/logging" + "github.com/vmware-tanzu/velero/pkg/volume" ) func TestRequest_BackupResourceList(t *testing.T) { @@ -80,3 +97,804 @@ func TestRequest_BackupResourceListEntriesSorted(t *testing.T) { "v1/Pod": {"ns1/pod1", "ns2/pod2"}, }, req.BackupResourceList()) } + +func TestGenerateVolumeInfoForSkippedPV(t *testing.T) { + tests := []struct { + name string + skippedPVName string + pvMap map[string]PvcPvInfo + expectedVolumeInfos []volume.VolumeInfo + }{ + { + name: "Cannot find info for PV", + skippedPVName: "testPV", + pvMap: map[string]PvcPvInfo{ + "velero/testPVC": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Normal Skipped PV info", + skippedPVName: "testPV", + pvMap: map[string]PvcPvInfo{ + "velero/testPVC": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + "testPV": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{ + { + PVCName: "testPVC", + PVCNamespace: "velero", + PVName: "testPV", + Skipped: true, + SkippedReason: "CSI: skipped for PodVolumeBackup;", + PVInfo: volume.PVInfo{ + ReclaimPolicy: "Delete", + Labels: map[string]string{ + "a": "b", + }, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + request := new(Request) + request.SkippedPVTracker = NewSkipPVTracker() + request.VolumesInformation.InitPVMap() + if tc.skippedPVName != "" { + request.SkippedPVTracker.Track(tc.skippedPVName, "CSI", "skipped for PodVolumeBackup") + } + if tc.pvMap != nil { + for k, v := range tc.pvMap { + request.VolumesInformation.InsertPVMap(k, v) + } + } + logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) + + volumeInfos := generateVolumeInfoForSkippedPV(request.VolumesInformation, request, logger) + require.Equal(t, tc.expectedVolumeInfos, volumeInfos) + }) + } +} + +func TestGenerateVolumeInfoForVeleroNativeSnapshot(t *testing.T) { + tests := []struct { + name string + nativeSnapshot volume.Snapshot + pvMap map[string]PvcPvInfo + expectedVolumeInfos []volume.VolumeInfo + }{ + { + name: "Native snapshot's IPOS pointer is nil", + nativeSnapshot: volume.Snapshot{ + Spec: volume.SnapshotSpec{ + PersistentVolumeName: "testPV", + VolumeIOPS: nil, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Cannot find info for the PV", + nativeSnapshot: volume.Snapshot{ + Spec: volume.SnapshotSpec{ + PersistentVolumeName: "testPV", + VolumeIOPS: int64Ptr(100), + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Cannot find PV info in pvMap", + pvMap: map[string]PvcPvInfo{ + "velero/testPVC": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + }, + nativeSnapshot: volume.Snapshot{ + Spec: volume.SnapshotSpec{ + PersistentVolumeName: "testPV", + VolumeIOPS: int64Ptr(100), + VolumeType: "ssd", + VolumeAZ: "us-central1-a", + }, + Status: volume.SnapshotStatus{ + ProviderSnapshotID: "pvc-b31e3386-4bbb-4937-95d-7934cd62-b0a1-494b-95d7-0687440e8d0c", + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Normal native snapshot", + pvMap: map[string]PvcPvInfo{ + "testPV": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + }, + nativeSnapshot: volume.Snapshot{ + Spec: volume.SnapshotSpec{ + PersistentVolumeName: "testPV", + VolumeIOPS: int64Ptr(100), + VolumeType: "ssd", + VolumeAZ: "us-central1-a", + }, + Status: volume.SnapshotStatus{ + ProviderSnapshotID: "pvc-b31e3386-4bbb-4937-95d-7934cd62-b0a1-494b-95d7-0687440e8d0c", + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{ + { + PVCName: "testPVC", + PVCNamespace: "velero", + PVName: "testPV", + BackupMethod: volume.NativeSnapshot, + PVInfo: volume.PVInfo{ + ReclaimPolicy: "Delete", + Labels: map[string]string{ + "a": "b", + }, + }, + NativeSnapshotInfo: volume.NativeSnapshotInfo{ + SnapshotHandle: "pvc-b31e3386-4bbb-4937-95d-7934cd62-b0a1-494b-95d7-0687440e8d0c", + VolumeType: "ssd", + VolumeAZ: "us-central1-a", + IOPS: "100", + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + request := new(Request) + request.VolumeSnapshots = append(request.VolumeSnapshots, &tc.nativeSnapshot) + request.VolumesInformation.InitPVMap() + if tc.pvMap != nil { + for k, v := range tc.pvMap { + request.VolumesInformation.InsertPVMap(k, v) + } + } + logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) + + volumeInfos := generateVolumeInfoForVeleroNativeSnapshot(request.VolumesInformation, request, logger) + require.Equal(t, tc.expectedVolumeInfos, volumeInfos) + }) + } +} + +func TestGenerateVolumeInfoForCSIVolumeSnapshot(t *testing.T) { + resourceQuantity := resource.MustParse("100Gi") + now := metav1.Now() + tests := []struct { + name string + volumeSnapshot snapshotv1api.VolumeSnapshot + volumeSnapshotContent snapshotv1api.VolumeSnapshotContent + volumeSnapshotClass snapshotv1api.VolumeSnapshotClass + pvMap map[string]PvcPvInfo + operation *itemoperation.BackupOperation + expectedVolumeInfos []volume.VolumeInfo + }{ + { + name: "VS doesn't have VolumeSnapshotClass name", + volumeSnapshot: snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testVS", + Namespace: "velero", + }, + Spec: snapshotv1api.VolumeSnapshotSpec{}, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "VS doesn't have status", + volumeSnapshot: snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testVS", + Namespace: "velero", + }, + Spec: snapshotv1api.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("testClass"), + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "VS doesn't have PVC", + volumeSnapshot: snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testVS", + Namespace: "velero", + }, + Spec: snapshotv1api.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("testClass"), + }, + Status: &snapshotv1api.VolumeSnapshotStatus{ + BoundVolumeSnapshotContentName: stringPtr("testContent"), + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Cannot find VSC for VS", + volumeSnapshot: snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testVS", + Namespace: "velero", + }, + Spec: snapshotv1api.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("testClass"), + Source: snapshotv1api.VolumeSnapshotSource{ + PersistentVolumeClaimName: stringPtr("testPVC"), + }, + }, + Status: &snapshotv1api.VolumeSnapshotStatus{ + BoundVolumeSnapshotContentName: stringPtr("testContent"), + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Cannot find VolumeInfo for PVC", + volumeSnapshot: snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testVS", + Namespace: "velero", + }, + Spec: snapshotv1api.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("testClass"), + Source: snapshotv1api.VolumeSnapshotSource{ + PersistentVolumeClaimName: stringPtr("testPVC"), + }, + }, + Status: &snapshotv1api.VolumeSnapshotStatus{ + BoundVolumeSnapshotContentName: stringPtr("testContent"), + }, + }, + volumeSnapshotClass: *builder.ForVolumeSnapshotClass("testClass").Driver("pd.csi.storage.gke.io").Result(), + volumeSnapshotContent: *builder.ForVolumeSnapshotContent("testContent").Status(&snapshotv1api.VolumeSnapshotContentStatus{SnapshotHandle: stringPtr("testSnapshotHandle")}).Result(), + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Normal VolumeSnapshot case", + volumeSnapshot: snapshotv1api.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testVS", + Namespace: "velero", + CreationTimestamp: now, + }, + Spec: snapshotv1api.VolumeSnapshotSpec{ + VolumeSnapshotClassName: stringPtr("testClass"), + Source: snapshotv1api.VolumeSnapshotSource{ + PersistentVolumeClaimName: stringPtr("testPVC"), + }, + }, + Status: &snapshotv1api.VolumeSnapshotStatus{ + BoundVolumeSnapshotContentName: stringPtr("testContent"), + RestoreSize: &resourceQuantity, + }, + }, + volumeSnapshotClass: *builder.ForVolumeSnapshotClass("testClass").Driver("pd.csi.storage.gke.io").Result(), + volumeSnapshotContent: *builder.ForVolumeSnapshotContent("testContent").Status(&snapshotv1api.VolumeSnapshotContentStatus{SnapshotHandle: stringPtr("testSnapshotHandle")}).Result(), + pvMap: map[string]PvcPvInfo{ + "testPV": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + }, + operation: &itemoperation.BackupOperation{ + Spec: itemoperation.BackupOperationSpec{ + OperationID: "testID", + ResourceIdentifier: velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: "snapshot.storage.k8s.io", + Resource: "volumesnapshots", + }, + Namespace: "velero", + Name: "testVS", + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{ + { + PVCName: "testPVC", + PVCNamespace: "velero", + PVName: "testPV", + BackupMethod: volume.CSISnapshot, + OperationID: "testID", + StartTimestamp: &now, + PreserveLocalSnapshot: true, + CSISnapshotInfo: volume.CSISnapshotInfo{ + Driver: "pd.csi.storage.gke.io", + SnapshotHandle: "testSnapshotHandle", + Size: 107374182400, + VSCName: "testContent", + }, + PVInfo: volume.PVInfo{ + ReclaimPolicy: "Delete", + Labels: map[string]string{ + "a": "b", + }, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + request := new(Request) + request.Backup = new(velerov1api.Backup) + request.VolumesInformation.InitPVMap() + if tc.pvMap != nil { + for k, v := range tc.pvMap { + request.VolumesInformation.InsertPVMap(k, v) + } + } + operationList := request.GetItemOperationsList() + if tc.operation != nil { + *operationList = append(*operationList, tc.operation) + } + logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) + + volumeInfos := generateVolumeInfoForCSIVolumeSnapshot(request.VolumesInformation, request, []snapshotv1api.VolumeSnapshot{tc.volumeSnapshot}, []snapshotv1api.VolumeSnapshotContent{tc.volumeSnapshotContent}, []snapshotv1api.VolumeSnapshotClass{tc.volumeSnapshotClass}, logger) + require.Equal(t, tc.expectedVolumeInfos, volumeInfos) + }) + } +} + +func TestGenerateVolumeInfoFromPVB(t *testing.T) { + tests := []struct { + name string + pvb *velerov1api.PodVolumeBackup + pod *corev1api.Pod + pvMap map[string]PvcPvInfo + expectedVolumeInfos []volume.VolumeInfo + }{ + { + name: "cannot find PVB's pod, should fail", + pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").Result(), + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "PVB doesn't have a related PVC", + pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").Result(), + pod: builder.ForPod("velero", "testPod").Containers(&corev1api.Container{ + Name: "test", + VolumeMounts: []corev1api.VolumeMount{ + { + Name: "testVolume", + MountPath: "/data", + }, + }, + }).Volumes( + &corev1api.Volume{ + Name: "", + VolumeSource: corev1api.VolumeSource{ + HostPath: &corev1api.HostPathVolumeSource{}, + }, + }, + ).Result(), + expectedVolumeInfos: []volume.VolumeInfo{ + { + PVCName: "", + PVCNamespace: "", + PVName: "", + BackupMethod: volume.PodVolumeBackup, + PVBInfo: volume.PodVolumeBackupInfo{ + PodName: "testPod", + PodNamespace: "velero", + }, + }, + }, + }, + { + name: "Backup doesn't have information for PVC", + pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").Result(), + pod: builder.ForPod("velero", "testPod").Containers(&corev1api.Container{ + Name: "test", + VolumeMounts: []corev1api.VolumeMount{ + { + Name: "testVolume", + MountPath: "/data", + }, + }, + }).Volumes( + &corev1api.Volume{ + Name: "", + VolumeSource: corev1api.VolumeSource{ + PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{ + ClaimName: "testPVC", + }, + }, + }, + ).Result(), + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "PVB's volume has a PVC", + pvMap: map[string]PvcPvInfo{ + "testPV": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + }, + pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").Result(), + pod: builder.ForPod("velero", "testPod").Containers(&corev1api.Container{ + Name: "test", + VolumeMounts: []corev1api.VolumeMount{ + { + Name: "testVolume", + MountPath: "/data", + }, + }, + }).Volumes( + &corev1api.Volume{ + Name: "", + VolumeSource: corev1api.VolumeSource{ + PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{ + ClaimName: "testPVC", + }, + }, + }, + ).Result(), + expectedVolumeInfos: []volume.VolumeInfo{ + { + PVCName: "testPVC", + PVCNamespace: "velero", + PVName: "testPV", + BackupMethod: volume.PodVolumeBackup, + PVBInfo: volume.PodVolumeBackupInfo{ + PodName: "testPod", + PodNamespace: "velero", + }, + PVInfo: volume.PVInfo{ + ReclaimPolicy: string(corev1api.PersistentVolumeReclaimDelete), + Labels: map[string]string{"a": "b"}, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + crClient := velerotest.NewFakeControllerRuntimeClient(t) + logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) + request := new(Request) + request.PodVolumeBackups = append(request.PodVolumeBackups, tc.pvb) + request.VolumesInformation.InitPVMap() + if tc.pvMap != nil { + for k, v := range tc.pvMap { + request.VolumesInformation.InsertPVMap(k, v) + } + } + if tc.pod != nil { + require.NoError(t, crClient.Create(context.TODO(), tc.pod)) + } + + volumeInfos := generateVolumeInfoFromPVB(request.VolumesInformation, request, crClient, logger) + require.Equal(t, tc.expectedVolumeInfos, volumeInfos) + }) + } +} + +func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { + now := metav1.Now() + tests := []struct { + name string + volumeSnapshotClass *snapshotv1api.VolumeSnapshotClass + dataUpload *velerov2alpha1.DataUpload + operation *itemoperation.BackupOperation + pvMap map[string]PvcPvInfo + expectedVolumeInfos []volume.VolumeInfo + }{ + { + name: "Operation is not for PVC", + operation: &itemoperation.BackupOperation{ + Spec: itemoperation.BackupOperationSpec{ + ResourceIdentifier: velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: "", + Resource: "configmaps", + }, + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "Operation doesn't have DataUpload PostItemOperation", + operation: &itemoperation.BackupOperation{ + Spec: itemoperation.BackupOperationSpec{ + ResourceIdentifier: velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: "", + Resource: "persistentvolumeclaims", + }, + Namespace: "velero", + Name: "testPVC", + }, + PostOperationItems: []velero.ResourceIdentifier{ + { + GroupResource: schema.GroupResource{ + Group: "", + Resource: "configmaps", + }, + }, + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "DataUpload cannot be found for operation", + operation: &itemoperation.BackupOperation{ + Spec: itemoperation.BackupOperationSpec{ + OperationID: "testOperation", + ResourceIdentifier: velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: "", + Resource: "persistentvolumeclaims", + }, + Namespace: "velero", + Name: "testPVC", + }, + PostOperationItems: []velero.ResourceIdentifier{ + { + GroupResource: schema.GroupResource{ + Group: "velero.io", + Resource: "datauploads", + }, + Namespace: "velero", + Name: "testDU", + }, + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{}, + }, + { + name: "VolumeSnapshotClass cannot be found for operation", + dataUpload: builder.ForDataUpload("velero", "testDU").DataMover("velero").CSISnapshot(&velerov2alpha1.CSISnapshotSpec{ + VolumeSnapshot: "testVS", + }).SnapshotID("testSnapshotHandle").Result(), + operation: &itemoperation.BackupOperation{ + Spec: itemoperation.BackupOperationSpec{ + OperationID: "testOperation", + ResourceIdentifier: velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: "", + Resource: "persistentvolumeclaims", + }, + Namespace: "velero", + Name: "testPVC", + }, + PostOperationItems: []velero.ResourceIdentifier{ + { + GroupResource: schema.GroupResource{ + Group: "velero.io", + Resource: "datauploads", + }, + Namespace: "velero", + Name: "testDU", + }, + }, + }, + }, + pvMap: map[string]PvcPvInfo{ + "testPV": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{ + { + PVCName: "testPVC", + PVCNamespace: "velero", + PVName: "testPV", + BackupMethod: volume.CSISnapshot, + SnapshotDataMoved: true, + OperationID: "testOperation", + SnapshotDataMovementInfo: volume.SnapshotDataMovementInfo{ + DataMover: "velero", + UploaderType: "kopia", + }, + PVInfo: volume.PVInfo{ + ReclaimPolicy: string(corev1api.PersistentVolumeReclaimDelete), + Labels: map[string]string{"a": "b"}, + }, + }, + }, + }, + { + name: "Normal DataUpload case", + dataUpload: builder.ForDataUpload("velero", "testDU").DataMover("velero").CSISnapshot(&velerov2alpha1.CSISnapshotSpec{ + VolumeSnapshot: "testVS", + SnapshotClass: "testClass", + }).SnapshotID("testSnapshotHandle").Result(), + volumeSnapshotClass: builder.ForVolumeSnapshotClass("testClass").Driver("pd.csi.storage.gke.io").Result(), + operation: &itemoperation.BackupOperation{ + Spec: itemoperation.BackupOperationSpec{ + OperationID: "testOperation", + ResourceIdentifier: velero.ResourceIdentifier{ + GroupResource: schema.GroupResource{ + Group: "", + Resource: "persistentvolumeclaims", + }, + Namespace: "velero", + Name: "testPVC", + }, + PostOperationItems: []velero.ResourceIdentifier{ + { + GroupResource: schema.GroupResource{ + Group: "velero.io", + Resource: "datauploads", + }, + Namespace: "velero", + Name: "testDU", + }, + }, + }, + Status: itemoperation.OperationStatus{ + Created: &now, + }, + }, + pvMap: map[string]PvcPvInfo{ + "testPV": { + PVCName: "testPVC", + PVCNamespace: "velero", + PV: corev1api.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testPV", + Labels: map[string]string{"a": "b"}, + }, + Spec: corev1api.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, + }, + }, + }, + }, + expectedVolumeInfos: []volume.VolumeInfo{ + { + PVCName: "testPVC", + PVCNamespace: "velero", + PVName: "testPV", + BackupMethod: volume.CSISnapshot, + SnapshotDataMoved: true, + OperationID: "testOperation", + StartTimestamp: &now, + CSISnapshotInfo: volume.CSISnapshotInfo{ + Driver: "pd.csi.storage.gke.io", + }, + SnapshotDataMovementInfo: volume.SnapshotDataMovementInfo{ + DataMover: "velero", + UploaderType: "kopia", + }, + PVInfo: volume.PVInfo{ + ReclaimPolicy: string(corev1api.PersistentVolumeReclaimDelete), + Labels: map[string]string{"a": "b"}, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + request := new(Request) + request.VolumesInformation.InitPVMap() + operationList := request.GetItemOperationsList() + if tc.operation != nil { + *operationList = append(*operationList, tc.operation) + } + if tc.pvMap != nil { + for k, v := range tc.pvMap { + request.VolumesInformation.InsertPVMap(k, v) + } + } + logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) + + crClient := velerotest.NewFakeControllerRuntimeClient(t) + if tc.dataUpload != nil { + crClient.Create(context.TODO(), tc.dataUpload) + } + + if tc.volumeSnapshotClass != nil { + crClient.Create(context.TODO(), tc.volumeSnapshotClass) + } + + volumeInfos := generateVolumeInfoFromDataUpload(request.VolumesInformation, request, crClient, logger) + require.Equal(t, tc.expectedVolumeInfos, volumeInfos) + }) + } +} + +func stringPtr(str string) *string { + return &str +} diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index bfbacc541..b624e1606 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -21,7 +21,6 @@ import ( "context" "fmt" "os" - "strconv" "strings" "time" @@ -42,18 +41,14 @@ import ( "github.com/vmware-tanzu/velero/internal/resourcepolicies" "github.com/vmware-tanzu/velero/internal/storage" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" pkgbackup "github.com/vmware-tanzu/velero/pkg/backup" "github.com/vmware-tanzu/velero/pkg/discovery" "github.com/vmware-tanzu/velero/pkg/features" - "github.com/vmware-tanzu/velero/pkg/itemoperation" - "github.com/vmware-tanzu/velero/pkg/kuberesource" "github.com/vmware-tanzu/velero/pkg/label" "github.com/vmware-tanzu/velero/pkg/metrics" "github.com/vmware-tanzu/velero/pkg/persistence" "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" "github.com/vmware-tanzu/velero/pkg/plugin/framework" - "github.com/vmware-tanzu/velero/pkg/plugin/velero" "github.com/vmware-tanzu/velero/pkg/util/boolptr" "github.com/vmware-tanzu/velero/pkg/util/collections" "github.com/vmware-tanzu/velero/pkg/util/encode" @@ -319,8 +314,8 @@ func (b *backupReconciler) prepareBackupRequest(backup *velerov1api.Backup, logg request := &pkgbackup.Request{ Backup: backup.DeepCopy(), // don't modify items in the cache SkippedPVTracker: pkgbackup.NewSkipPVTracker(), - PVMap: map[string]pkgbackup.PvcPvInfo{}, } + request.VolumesInformation.InitPVMap() // set backup major version - deprecated, use Status.FormatVersion request.Status.Version = pkgbackup.BackupVersion @@ -737,9 +732,7 @@ func (b *backupReconciler) runBackup(backup *pkgbackup.Request) error { if logFile, err := backupLog.GetPersistFile(); err != nil { fatalErrs = append(fatalErrs, errors.Wrap(err, "error getting backup log file")) } else { - backup.VolumeInfos.VolumeInfos = generateVolumeInfo(backup, volumeSnapshots, volumeSnapshotContents, volumeSnapshotClasses, b.globalCRClient, backupLog) - - if errs := persistBackup(backup, backupFile, logFile, backupStore, volumeSnapshots, volumeSnapshotContents, volumeSnapshotClasses, results); len(errs) > 0 { + if errs := persistBackup(backup, backupFile, logFile, backupStore, volumeSnapshots, volumeSnapshotContents, volumeSnapshotClasses, results, b.globalCRClient, backupLog); len(errs) > 0 { fatalErrs = append(fatalErrs, errs...) } } @@ -796,8 +789,10 @@ func persistBackup(backup *pkgbackup.Request, backupStore persistence.BackupStore, csiVolumeSnapshots []snapshotv1api.VolumeSnapshot, csiVolumeSnapshotContents []snapshotv1api.VolumeSnapshotContent, - csiVolumesnapshotClasses []snapshotv1api.VolumeSnapshotClass, + csiVolumeSnapshotClasses []snapshotv1api.VolumeSnapshotClass, results map[string]results.Result, + crClient kbclient.Client, + logger logrus.FieldLogger, ) []error { persistErrs := []error{} backupJSON := new(bytes.Buffer) @@ -832,7 +827,7 @@ func persistBackup(backup *pkgbackup.Request, if errs != nil { persistErrs = append(persistErrs, errs...) } - csiSnapshotClassesJSON, errs := encode.ToJSONGzip(csiVolumesnapshotClasses, "csi volume snapshot classes list") + csiSnapshotClassesJSON, errs := encode.ToJSONGzip(csiVolumeSnapshotClasses, "csi volume snapshot classes list") if errs != nil { persistErrs = append(persistErrs, errs...) } @@ -847,7 +842,8 @@ func persistBackup(backup *pkgbackup.Request, persistErrs = append(persistErrs, errs...) } - volumeInfoJSON, errs := encode.ToJSONGzip(backup.VolumeInfos, "backup volumes information") + volumeInfoJSON, errs := encode.ToJSONGzip(backup.VolumesInformation.GenerateVolumeInfo(backup, csiVolumeSnapshots, + csiVolumeSnapshotContents, csiVolumeSnapshotClasses, crClient, logger), "backup volumes information") if errs != nil { persistErrs = append(persistErrs, errs...) } @@ -912,328 +908,3 @@ func oldAndNewFilterParametersUsedTogether(backupSpec velerov1api.BackupSpec) bo return haveOldResourceFilterParameters && haveNewResourceFilterParameters } - -func generateVolumeInfo(backup *pkgbackup.Request, csiVolumeSnapshots []snapshotv1api.VolumeSnapshot, - csiVolumeSnapshotContents []snapshotv1api.VolumeSnapshotContent, csiVolumesnapshotClasses []snapshotv1api.VolumeSnapshotClass, - crClient kbclient.Client, logger logrus.FieldLogger) []volume.VolumeInfo { - volumeInfos := make([]volume.VolumeInfo, 0) - - skippedVolumeInfos := generateVolumeInfoForSkippedPV(backup, logger) - volumeInfos = append(volumeInfos, skippedVolumeInfos...) - - nativeSnapshotVolumeInfos := generateVolumeInfoForVeleroNativeSnapshot(backup, logger) - volumeInfos = append(volumeInfos, nativeSnapshotVolumeInfos...) - - csiVolumeInfos := generateVolumeInfoForCSIVolumeSnapshot(backup, csiVolumeSnapshots, csiVolumeSnapshotContents, csiVolumesnapshotClasses, logger) - volumeInfos = append(volumeInfos, csiVolumeInfos...) - - pvbVolumeInfos := generateVolumeInfoFromPVB(backup, crClient, logger) - volumeInfos = append(volumeInfos, pvbVolumeInfos...) - - dataUploadVolumeInfos := generateVolumeInfoFromDataUpload(backup, crClient, logger) - volumeInfos = append(volumeInfos, dataUploadVolumeInfos...) - - return volumeInfos -} - -// generateVolumeInfoForSkippedPV generate VolumeInfos for SkippedPV. -func generateVolumeInfoForSkippedPV(backup *pkgbackup.Request, logger logrus.FieldLogger) []volume.VolumeInfo { - tmpVolumeInfos := make([]volume.VolumeInfo, 0) - - for _, skippedPV := range backup.SkippedPVTracker.Summary() { - if pvcPVInfo, ok := backup.PVMap[skippedPV.Name]; ok { - volumeInfo := volume.VolumeInfo{ - PVCName: pvcPVInfo.PVCName, - PVCNamespace: pvcPVInfo.PVCNamespace, - PVName: skippedPV.Name, - SnapshotDataMoved: false, - Skipped: true, - SkippedReason: skippedPV.SerializeSkipReasons(), - PVInfo: volume.PVInfo{ - ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), - Labels: pvcPVInfo.PV.Labels, - }, - } - tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) - } else { - logger.Warnf("Cannot find info for PV %s", skippedPV.Name) - continue - } - } - - return tmpVolumeInfos -} - -// generateVolumeInfoForVeleroNativeSnapshot generate VolumeInfos for Velero native snapshot -func generateVolumeInfoForVeleroNativeSnapshot(backup *pkgbackup.Request, logger logrus.FieldLogger) []volume.VolumeInfo { - tmpVolumeInfos := make([]volume.VolumeInfo, 0) - - for _, nativeSnapshot := range backup.VolumeSnapshots { - var iops int64 - if nativeSnapshot.Spec.VolumeIOPS != nil { - iops = *nativeSnapshot.Spec.VolumeIOPS - } - - if pvcPVInfo, ok := backup.PVMap[nativeSnapshot.Spec.PersistentVolumeName]; ok { - volumeInfo := volume.VolumeInfo{ - BackupMethod: volume.NativeSnapshot, - PVCName: pvcPVInfo.PVCName, - PVCNamespace: pvcPVInfo.PVCNamespace, - PVName: pvcPVInfo.PV.Name, - SnapshotDataMoved: false, - Skipped: false, - NativeSnapshotInfo: volume.NativeSnapshotInfo{ - SnapshotHandle: nativeSnapshot.Status.ProviderSnapshotID, - VolumeType: nativeSnapshot.Spec.VolumeType, - VolumeAZ: nativeSnapshot.Spec.VolumeAZ, - IOPS: strconv.FormatInt(iops, 10), - }, - PVInfo: volume.PVInfo{ - ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), - Labels: pvcPVInfo.PV.Labels, - }, - } - - tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) - } else { - logger.Warnf("cannot find info for PV %s", nativeSnapshot.Spec.PersistentVolumeName) - continue - } - } - - return tmpVolumeInfos -} - -// generateVolumeInfoForCSIVolumeSnapshot generate VolumeInfos for CSI VolumeSnapshot -func generateVolumeInfoForCSIVolumeSnapshot(backup *pkgbackup.Request, csiVolumeSnapshots []snapshotv1api.VolumeSnapshot, - csiVolumeSnapshotContents []snapshotv1api.VolumeSnapshotContent, csiVolumesnapshotClasses []snapshotv1api.VolumeSnapshotClass, - logger logrus.FieldLogger) []volume.VolumeInfo { - tmpVolumeInfos := make([]volume.VolumeInfo, 0) - - for _, volumeSnapshot := range csiVolumeSnapshots { - var volumeSnapshotClass *snapshotv1api.VolumeSnapshotClass - var volumeSnapshotContent *snapshotv1api.VolumeSnapshotContent - - // This is protective logic. The passed-in VS should be all related - // to this backup. - if volumeSnapshot.Labels[velerov1api.BackupNameLabel] != backup.Name { - continue - } - - if volumeSnapshot.Spec.VolumeSnapshotClassName == nil { - logger.Warnf("Cannot find VolumeSnapshotClass for VolumeSnapshot %s/%s", volumeSnapshot.Namespace, volumeSnapshot.Name) - continue - } - - if volumeSnapshot.Status == nil || volumeSnapshot.Status.BoundVolumeSnapshotContentName == nil { - logger.Warnf("Cannot fine VolumeSnapshotContent for VolumeSnapshot %s/%s", volumeSnapshot.Namespace, volumeSnapshot.Name) - continue - } - - if volumeSnapshot.Spec.Source.PersistentVolumeClaimName == nil { - logger.Warnf("VolumeSnapshot %s/%s doesn't have a source PVC", volumeSnapshot.Namespace, volumeSnapshot.Name) - continue - } - - for index := range csiVolumesnapshotClasses { - if *volumeSnapshot.Spec.VolumeSnapshotClassName == csiVolumesnapshotClasses[index].Name { - volumeSnapshotClass = &csiVolumesnapshotClasses[index] - } - } - - for index := range csiVolumeSnapshotContents { - if *volumeSnapshot.Status.BoundVolumeSnapshotContentName == csiVolumeSnapshotContents[index].Name { - volumeSnapshotContent = &csiVolumeSnapshotContents[index] - } - } - - if volumeSnapshotClass == nil || volumeSnapshotContent == nil { - logger.Warnf("fail to get VolumeSnapshotContent or VolumeSnapshotClass for VolumeSnapshot: %s/%s", - volumeSnapshot.Namespace, volumeSnapshot.Name) - continue - } - - var operation itemoperation.BackupOperation - for _, op := range *backup.GetItemOperationsList() { - if op.Spec.ResourceIdentifier.GroupResource.String() == kuberesource.VolumeSnapshots.String() && - op.Spec.ResourceIdentifier.Name == volumeSnapshot.Name && - op.Spec.ResourceIdentifier.Namespace == volumeSnapshot.Namespace { - operation = *op - } - } - - var size int64 - if volumeSnapshot.Status.RestoreSize != nil { - size = volumeSnapshot.Status.RestoreSize.Value() - } - snapshotHandle := "" - if volumeSnapshotContent.Status.SnapshotHandle != nil { - snapshotHandle = *volumeSnapshotContent.Status.SnapshotHandle - } - if pvcPVInfo, ok := backup.PVMap[volumeSnapshot.Namespace+"/"+*volumeSnapshot.Spec.Source.PersistentVolumeClaimName]; ok { - volumeInfo := volume.VolumeInfo{ - BackupMethod: volume.CSISnapshot, - PVCName: pvcPVInfo.PVCName, - PVCNamespace: pvcPVInfo.PVCNamespace, - PVName: pvcPVInfo.PV.Name, - Skipped: false, - SnapshotDataMoved: false, - PreserveLocalSnapshot: true, - OperationID: operation.Spec.OperationID, - StartTimestamp: &(volumeSnapshot.CreationTimestamp), - CSISnapshotInfo: volume.CSISnapshotInfo{ - VSCName: *volumeSnapshot.Status.BoundVolumeSnapshotContentName, - Size: size, - Driver: volumeSnapshotClass.Driver, - SnapshotHandle: snapshotHandle, - }, - PVInfo: volume.PVInfo{ - ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), - Labels: pvcPVInfo.PV.Labels, - }, - } - - tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) - } else { - logger.Warnf("cannot find info for PVC %s/%s", volumeSnapshot.Namespace, volumeSnapshot.Spec.Source.PersistentVolumeClaimName) - continue - } - } - - return tmpVolumeInfos -} - -// generateVolumeInfoFromPVB generate VolumeInfo for PVB. -func generateVolumeInfoFromPVB(backup *pkgbackup.Request, crClient kbclient.Client, logger logrus.FieldLogger) []volume.VolumeInfo { - tmpVolumeInfos := make([]volume.VolumeInfo, 0) - - for _, pvb := range backup.PodVolumeBackups { - volumeInfo := volume.VolumeInfo{ - BackupMethod: volume.PodVolumeBackup, - SnapshotDataMoved: false, - Skipped: false, - StartTimestamp: pvb.Status.StartTimestamp, - PVBInfo: volume.PodVolumeBackupInfo{ - SnapshotHandle: pvb.Status.SnapshotID, - Size: pvb.Status.Progress.TotalBytes, - UploaderType: pvb.Spec.UploaderType, - VolumeName: pvb.Spec.Volume, - PodName: pvb.Spec.Pod.Name, - PodNamespace: pvb.Spec.Pod.Namespace, - NodeName: pvb.Spec.Node, - }, - } - - pod := new(corev1api.Pod) - pvcName := "" - err := crClient.Get(context.TODO(), kbclient.ObjectKey{Namespace: pvb.Spec.Pod.Namespace, Name: pvb.Spec.Pod.Name}, pod) - if err != nil { - logger.WithError(err).Warn("Fail to get pod for PodVolumeBackup: ", pvb.Name) - continue - } - for _, volume := range pod.Spec.Volumes { - if volume.Name == pvb.Spec.Volume && volume.PersistentVolumeClaim != nil { - pvcName = volume.PersistentVolumeClaim.ClaimName - } - } - - if pvcName != "" { - if pvcPVInfo, ok := backup.PVMap[pod.Namespace+"/"+pvcName]; ok { - volumeInfo.PVCName = pvcPVInfo.PVCName - volumeInfo.PVCNamespace = pvcPVInfo.PVCNamespace - volumeInfo.PVName = pvcPVInfo.PV.Name - volumeInfo.PVInfo = volume.PVInfo{ - ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), - Labels: pvcPVInfo.PV.Labels, - } - } else { - logger.Warnf("Cannot find info for PVC %s/%s", pod.Namespace, pvcName) - continue - } - } else { - logger.Debug("The PVB %s doesn't have a corresponding PVC", pvb.Name) - } - - tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) - } - - return tmpVolumeInfos -} - -// generateVolumeInfoFromDataUpload generate VolumeInfo for DataUpload. -func generateVolumeInfoFromDataUpload(backup *pkgbackup.Request, crClient kbclient.Client, logger logrus.FieldLogger) []volume.VolumeInfo { - tmpVolumeInfos := make([]volume.VolumeInfo, 0) - vsClassList := new(snapshotv1api.VolumeSnapshotClassList) - if err := crClient.List(context.TODO(), vsClassList); err != nil { - logger.WithError(err).Errorf("cannot list VolumeSnapshotClass %s", err.Error()) - return tmpVolumeInfos - } - - for _, operation := range *backup.GetItemOperationsList() { - if operation.Spec.ResourceIdentifier.GroupResource.String() == kuberesource.PersistentVolumeClaims.String() { - var duIdentifier velero.ResourceIdentifier - - for _, identifier := range operation.Spec.PostOperationItems { - if identifier.GroupResource.String() == "datauploads.velero.io" { - duIdentifier = identifier - } - } - if duIdentifier.Empty() { - logger.Warnf("cannot find DataUpload for PVC %s/%s backup async operation", - operation.Spec.ResourceIdentifier.Namespace, operation.Spec.ResourceIdentifier.Name) - continue - } - - dataUpload := new(velerov2alpha1.DataUpload) - err := crClient.Get( - context.TODO(), - kbclient.ObjectKey{ - Namespace: duIdentifier.Namespace, - Name: duIdentifier.Name}, - dataUpload, - ) - if err != nil { - logger.Warnf("fail to get DataUpload for operation %s: %s", operation.Spec.OperationID, err.Error()) - continue - } - - driverUsedByVSClass := "" - for index := range vsClassList.Items { - if vsClassList.Items[index].Name == dataUpload.Spec.CSISnapshot.SnapshotClass { - driverUsedByVSClass = vsClassList.Items[index].Driver - } - } - - if pvcPVInfo, ok := backup.PVMap[operation.Spec.ResourceIdentifier.Namespace+"/"+operation.Spec.ResourceIdentifier.Name]; ok { - volumeInfo := volume.VolumeInfo{ - BackupMethod: volume.CSISnapshot, - PVCName: pvcPVInfo.PVCName, - PVCNamespace: pvcPVInfo.PVCNamespace, - PVName: pvcPVInfo.PV.Name, - SnapshotDataMoved: true, - Skipped: false, - OperationID: operation.Spec.OperationID, - StartTimestamp: operation.Status.Created, - CSISnapshotInfo: volume.CSISnapshotInfo{ - Driver: driverUsedByVSClass, - }, - SnapshotDataMovementInfo: volume.SnapshotDataMovementInfo{ - DataMover: dataUpload.Spec.DataMover, - UploaderType: "kopia", - }, - PVInfo: volume.PVInfo{ - ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), - Labels: pvcPVInfo.PV.Labels, - }, - } - - tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) - } else { - logger.Warnf("Cannot find info for PVC %s/%s", operation.Spec.ResourceIdentifier.Namespace, operation.Spec.ResourceIdentifier.Name) - continue - } - } - } - - return tmpVolumeInfos -} diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index 736209e4a..0ec7175bc 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -34,26 +34,17 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "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/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/version" "k8s.io/utils/clock" testclocks "k8s.io/utils/clock/testing" ctrl "sigs.k8s.io/controller-runtime" kbclient "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/vmware-tanzu/velero/pkg/backup" - kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" - "github.com/vmware-tanzu/velero/pkg/volume" - fakeClient "sigs.k8s.io/controller-runtime/pkg/client/fake" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" pkgbackup "github.com/vmware-tanzu/velero/pkg/backup" "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/discovery" @@ -65,10 +56,10 @@ import ( "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" "github.com/vmware-tanzu/velero/pkg/plugin/framework" pluginmocks "github.com/vmware-tanzu/velero/pkg/plugin/mocks" - "github.com/vmware-tanzu/velero/pkg/plugin/velero" biav2 "github.com/vmware-tanzu/velero/pkg/plugin/velero/backupitemaction/v2" velerotest "github.com/vmware-tanzu/velero/pkg/test" "github.com/vmware-tanzu/velero/pkg/util/boolptr" + kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" "github.com/vmware-tanzu/velero/pkg/util/logging" ) @@ -1742,749 +1733,3 @@ func TestPatchResourceWorksWithStatus(t *testing.T) { } } -func TestGenerateVolumeInfoForSkippedPV(t *testing.T) { - tests := []struct { - name string - skippedPVName string - pvMap map[string]backup.PvcPvInfo - expectedVolumeInfos []volume.VolumeInfo - }{ - { - name: "Cannot find info for PV", - skippedPVName: "testPV", - expectedVolumeInfos: []volume.VolumeInfo{}, - }, - { - name: "Normal Skipped PV info", - skippedPVName: "testPV", - pvMap: map[string]backup.PvcPvInfo{ - "velero/testPVC": { - PVCName: "testPVC", - PVCNamespace: "velero", - PV: corev1api.PersistentVolume{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testPV", - Labels: map[string]string{"a": "b"}, - }, - Spec: corev1api.PersistentVolumeSpec{ - PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, - }, - }, - }, - "testPV": { - PVCName: "testPVC", - PVCNamespace: "velero", - PV: corev1api.PersistentVolume{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testPV", - Labels: map[string]string{"a": "b"}, - }, - Spec: corev1api.PersistentVolumeSpec{ - PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, - }, - }, - }, - }, - expectedVolumeInfos: []volume.VolumeInfo{ - { - PVCName: "testPVC", - PVCNamespace: "velero", - PVName: "testPV", - Skipped: true, - SkippedReason: "CSI: skipped for PodVolumeBackup;", - PVInfo: volume.PVInfo{ - ReclaimPolicy: "Delete", - Labels: map[string]string{ - "a": "b", - }, - }, - }, - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - request := new(backup.Request) - request.SkippedPVTracker = backup.NewSkipPVTracker() - if tc.skippedPVName != "" { - request.SkippedPVTracker.Track(tc.skippedPVName, "CSI", "skipped for PodVolumeBackup") - } - if tc.pvMap != nil { - request.PVMap = tc.pvMap - } - logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) - - volumeInfos := generateVolumeInfoForSkippedPV(request, logger) - require.Equal(t, tc.expectedVolumeInfos, volumeInfos) - }) - } -} - -func TestGenerateVolumeInfoForCSIVolumeSnapshot(t *testing.T) { - resourceQuantity := resource.MustParse("100Gi") - now := metav1.Now() - tests := []struct { - name string - volumeSnapshot snapshotv1api.VolumeSnapshot - volumeSnapshotContent snapshotv1api.VolumeSnapshotContent - volumeSnapshotClass snapshotv1api.VolumeSnapshotClass - pvMap map[string]backup.PvcPvInfo - operation *itemoperation.BackupOperation - expectedVolumeInfos []volume.VolumeInfo - }{ - { - name: "VS doesn't have VolumeSnapshotClass name", - volumeSnapshot: snapshotv1api.VolumeSnapshot{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testVS", - Namespace: "velero", - }, - Spec: snapshotv1api.VolumeSnapshotSpec{}, - }, - expectedVolumeInfos: []volume.VolumeInfo{}, - }, - { - name: "VS doesn't have status", - volumeSnapshot: snapshotv1api.VolumeSnapshot{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testVS", - Namespace: "velero", - }, - Spec: snapshotv1api.VolumeSnapshotSpec{ - VolumeSnapshotClassName: stringPtr("testClass"), - }, - }, - expectedVolumeInfos: []volume.VolumeInfo{}, - }, - { - name: "VS doesn't have PVC", - volumeSnapshot: snapshotv1api.VolumeSnapshot{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testVS", - Namespace: "velero", - }, - Spec: snapshotv1api.VolumeSnapshotSpec{ - VolumeSnapshotClassName: stringPtr("testClass"), - }, - Status: &snapshotv1api.VolumeSnapshotStatus{ - BoundVolumeSnapshotContentName: stringPtr("testContent"), - }, - }, - expectedVolumeInfos: []volume.VolumeInfo{}, - }, - { - name: "Cannot find VSC for VS", - volumeSnapshot: snapshotv1api.VolumeSnapshot{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testVS", - Namespace: "velero", - }, - Spec: snapshotv1api.VolumeSnapshotSpec{ - VolumeSnapshotClassName: stringPtr("testClass"), - Source: snapshotv1api.VolumeSnapshotSource{ - PersistentVolumeClaimName: stringPtr("testPVC"), - }, - }, - Status: &snapshotv1api.VolumeSnapshotStatus{ - BoundVolumeSnapshotContentName: stringPtr("testContent"), - }, - }, - expectedVolumeInfos: []volume.VolumeInfo{}, - }, - { - name: "Cannot find VolumeInfo for PVC", - volumeSnapshot: snapshotv1api.VolumeSnapshot{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testVS", - Namespace: "velero", - }, - Spec: snapshotv1api.VolumeSnapshotSpec{ - VolumeSnapshotClassName: stringPtr("testClass"), - Source: snapshotv1api.VolumeSnapshotSource{ - PersistentVolumeClaimName: stringPtr("testPVC"), - }, - }, - Status: &snapshotv1api.VolumeSnapshotStatus{ - BoundVolumeSnapshotContentName: stringPtr("testContent"), - }, - }, - volumeSnapshotClass: *builder.ForVolumeSnapshotClass("testClass").Driver("pd.csi.storage.gke.io").Result(), - volumeSnapshotContent: *builder.ForVolumeSnapshotContent("testContent").Status(&snapshotv1api.VolumeSnapshotContentStatus{SnapshotHandle: stringPtr("testSnapshotHandle")}).Result(), - expectedVolumeInfos: []volume.VolumeInfo{}, - }, - { - name: "Normal VolumeSnapshot case", - volumeSnapshot: snapshotv1api.VolumeSnapshot{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testVS", - Namespace: "velero", - CreationTimestamp: now, - }, - Spec: snapshotv1api.VolumeSnapshotSpec{ - VolumeSnapshotClassName: stringPtr("testClass"), - Source: snapshotv1api.VolumeSnapshotSource{ - PersistentVolumeClaimName: stringPtr("testPVC"), - }, - }, - Status: &snapshotv1api.VolumeSnapshotStatus{ - BoundVolumeSnapshotContentName: stringPtr("testContent"), - RestoreSize: &resourceQuantity, - }, - }, - volumeSnapshotClass: *builder.ForVolumeSnapshotClass("testClass").Driver("pd.csi.storage.gke.io").Result(), - volumeSnapshotContent: *builder.ForVolumeSnapshotContent("testContent").Status(&snapshotv1api.VolumeSnapshotContentStatus{SnapshotHandle: stringPtr("testSnapshotHandle")}).Result(), - pvMap: map[string]backup.PvcPvInfo{ - "velero/testPVC": { - PVCName: "testPVC", - PVCNamespace: "velero", - PV: corev1api.PersistentVolume{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testPV", - Labels: map[string]string{"a": "b"}, - }, - Spec: corev1api.PersistentVolumeSpec{ - PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, - }, - }, - }, - }, - operation: &itemoperation.BackupOperation{ - Spec: itemoperation.BackupOperationSpec{ - OperationID: "testID", - ResourceIdentifier: velero.ResourceIdentifier{ - GroupResource: schema.GroupResource{ - Group: "snapshot.storage.k8s.io", - Resource: "volumesnapshots", - }, - Namespace: "velero", - Name: "testVS", - }, - }, - }, - expectedVolumeInfos: []volume.VolumeInfo{ - { - PVCName: "testPVC", - PVCNamespace: "velero", - PVName: "testPV", - BackupMethod: volume.CSISnapshot, - OperationID: "testID", - StartTimestamp: &now, - PreserveLocalSnapshot: true, - CSISnapshotInfo: volume.CSISnapshotInfo{ - Driver: "pd.csi.storage.gke.io", - SnapshotHandle: "testSnapshotHandle", - Size: 107374182400, - VSCName: "testContent", - }, - PVInfo: volume.PVInfo{ - ReclaimPolicy: "Delete", - Labels: map[string]string{ - "a": "b", - }, - }, - }, - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - request := new(backup.Request) - request.Backup = new(velerov1api.Backup) - if tc.pvMap != nil { - request.PVMap = tc.pvMap - } - operationList := request.GetItemOperationsList() - if tc.operation != nil { - *operationList = append(*operationList, tc.operation) - } - logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) - - volumeInfos := generateVolumeInfoForCSIVolumeSnapshot(request, []snapshotv1api.VolumeSnapshot{tc.volumeSnapshot}, []snapshotv1api.VolumeSnapshotContent{tc.volumeSnapshotContent}, []snapshotv1api.VolumeSnapshotClass{tc.volumeSnapshotClass}, logger) - require.Equal(t, tc.expectedVolumeInfos, volumeInfos) - }) - } - -} - -func TestGenerateVolumeInfoForVeleroNativeSnapshot(t *testing.T) { - tests := []struct { - name string - nativeSnapshot volume.Snapshot - pvMap map[string]backup.PvcPvInfo - expectedVolumeInfos []volume.VolumeInfo - }{ - { - name: "Native snapshot's IPOS pointer is nil", - nativeSnapshot: volume.Snapshot{ - Spec: volume.SnapshotSpec{ - PersistentVolumeName: "testPV", - VolumeIOPS: nil, - }, - }, - expectedVolumeInfos: []volume.VolumeInfo{}, - }, - { - name: "Cannot find info for the PV", - nativeSnapshot: volume.Snapshot{ - Spec: volume.SnapshotSpec{ - PersistentVolumeName: "testPV", - VolumeIOPS: int64Ptr(100), - }, - }, - expectedVolumeInfos: []volume.VolumeInfo{}, - }, - { - name: "Normal native snapshot", - pvMap: map[string]backup.PvcPvInfo{ - "testPV": { - PVCName: "testPVC", - PVCNamespace: "velero", - PV: corev1api.PersistentVolume{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testPV", - Labels: map[string]string{"a": "b"}, - }, - Spec: corev1api.PersistentVolumeSpec{ - PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, - }, - }, - }, - }, - nativeSnapshot: volume.Snapshot{ - Spec: volume.SnapshotSpec{ - PersistentVolumeName: "testPV", - VolumeIOPS: int64Ptr(100), - VolumeType: "ssd", - VolumeAZ: "us-central1-a", - }, - Status: volume.SnapshotStatus{ - ProviderSnapshotID: "pvc-b31e3386-4bbb-4937-95d-7934cd62-b0a1-494b-95d7-0687440e8d0c", - }, - }, - expectedVolumeInfos: []volume.VolumeInfo{ - { - PVCName: "testPVC", - PVCNamespace: "velero", - PVName: "testPV", - BackupMethod: volume.NativeSnapshot, - PVInfo: volume.PVInfo{ - ReclaimPolicy: "Delete", - Labels: map[string]string{ - "a": "b", - }, - }, - NativeSnapshotInfo: volume.NativeSnapshotInfo{ - SnapshotHandle: "pvc-b31e3386-4bbb-4937-95d-7934cd62-b0a1-494b-95d7-0687440e8d0c", - VolumeType: "ssd", - VolumeAZ: "us-central1-a", - IOPS: "100", - }, - }, - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - request := new(backup.Request) - request.VolumeSnapshots = append(request.VolumeSnapshots, &tc.nativeSnapshot) - if tc.pvMap != nil { - request.PVMap = tc.pvMap - } - logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) - - volumeInfos := generateVolumeInfoForVeleroNativeSnapshot(request, logger) - require.Equal(t, tc.expectedVolumeInfos, volumeInfos) - }) - } -} - -func TestGenerateVolumeInfoFromPVB(t *testing.T) { - tests := []struct { - name string - pvb *velerov1api.PodVolumeBackup - pod *corev1api.Pod - pvMap map[string]backup.PvcPvInfo - expectedVolumeInfos []volume.VolumeInfo - }{ - { - name: "cannot find PVB's pod, should fail", - pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").Result(), - expectedVolumeInfos: []volume.VolumeInfo{}, - }, - { - name: "PVB doesn't have a related PVC", - pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").Result(), - pod: builder.ForPod("velero", "testPod").Containers(&corev1api.Container{ - Name: "test", - VolumeMounts: []corev1api.VolumeMount{ - { - Name: "testVolume", - MountPath: "/data", - }, - }, - }).Volumes( - &corev1api.Volume{ - Name: "", - VolumeSource: corev1api.VolumeSource{ - HostPath: &corev1api.HostPathVolumeSource{}, - }, - }, - ).Result(), - expectedVolumeInfos: []volume.VolumeInfo{ - { - PVCName: "", - PVCNamespace: "", - PVName: "", - BackupMethod: volume.PodVolumeBackup, - PVBInfo: volume.PodVolumeBackupInfo{ - PodName: "testPod", - PodNamespace: "velero", - }, - }, - }, - }, - { - name: "Backup doesn't have information for PVC", - pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").Result(), - pod: builder.ForPod("velero", "testPod").Containers(&corev1api.Container{ - Name: "test", - VolumeMounts: []corev1api.VolumeMount{ - { - Name: "testVolume", - MountPath: "/data", - }, - }, - }).Volumes( - &corev1api.Volume{ - Name: "", - VolumeSource: corev1api.VolumeSource{ - PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{ - ClaimName: "testPVC", - }, - }, - }, - ).Result(), - expectedVolumeInfos: []volume.VolumeInfo{}, - }, - { - name: "PVB's volume has a PVC", - pvMap: map[string]backup.PvcPvInfo{ - "velero/testPVC": { - PVCName: "testPVC", - PVCNamespace: "velero", - PV: corev1api.PersistentVolume{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testPV", - Labels: map[string]string{"a": "b"}, - }, - Spec: corev1api.PersistentVolumeSpec{ - PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, - }, - }, - }, - }, - pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").Result(), - pod: builder.ForPod("velero", "testPod").Containers(&corev1api.Container{ - Name: "test", - VolumeMounts: []corev1api.VolumeMount{ - { - Name: "testVolume", - MountPath: "/data", - }, - }, - }).Volumes( - &corev1api.Volume{ - Name: "", - VolumeSource: corev1api.VolumeSource{ - PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{ - ClaimName: "testPVC", - }, - }, - }, - ).Result(), - expectedVolumeInfos: []volume.VolumeInfo{ - { - PVCName: "testPVC", - PVCNamespace: "velero", - PVName: "testPV", - BackupMethod: volume.PodVolumeBackup, - PVBInfo: volume.PodVolumeBackupInfo{ - PodName: "testPod", - PodNamespace: "velero", - }, - PVInfo: volume.PVInfo{ - ReclaimPolicy: string(corev1api.PersistentVolumeReclaimDelete), - Labels: map[string]string{"a": "b"}, - }, - }, - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - crClient := velerotest.NewFakeControllerRuntimeClient(t) - logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) - request := new(pkgbackup.Request) - request.PodVolumeBackups = append(request.PodVolumeBackups, tc.pvb) - if tc.pvMap != nil { - request.PVMap = tc.pvMap - } - if tc.pod != nil { - require.NoError(t, crClient.Create(context.TODO(), tc.pod)) - } - - volumeInfos := generateVolumeInfoFromPVB(request, crClient, logger) - require.Equal(t, tc.expectedVolumeInfos, volumeInfos) - }) - } -} - -func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { - now := metav1.Now() - tests := []struct { - name string - volumeSnapshotClass *snapshotv1api.VolumeSnapshotClass - dataUpload *velerov2alpha1.DataUpload - operation *itemoperation.BackupOperation - pvMap map[string]backup.PvcPvInfo - expectedVolumeInfos []volume.VolumeInfo - }{ - { - name: "Operation is not for PVC", - operation: &itemoperation.BackupOperation{ - Spec: itemoperation.BackupOperationSpec{ - ResourceIdentifier: velero.ResourceIdentifier{ - GroupResource: schema.GroupResource{ - Group: "", - Resource: "configmaps", - }, - }, - }, - }, - expectedVolumeInfos: []volume.VolumeInfo{}, - }, - { - name: "Operation doesn't have DataUpload PostItemOperation", - operation: &itemoperation.BackupOperation{ - Spec: itemoperation.BackupOperationSpec{ - ResourceIdentifier: velero.ResourceIdentifier{ - GroupResource: schema.GroupResource{ - Group: "", - Resource: "persistentvolumeclaims", - }, - Namespace: "velero", - Name: "testPVC", - }, - PostOperationItems: []velero.ResourceIdentifier{ - { - GroupResource: schema.GroupResource{ - Group: "", - Resource: "configmaps", - }, - }, - }, - }, - }, - expectedVolumeInfos: []volume.VolumeInfo{}, - }, - { - name: "DataUpload cannot be found for operation", - operation: &itemoperation.BackupOperation{ - Spec: itemoperation.BackupOperationSpec{ - OperationID: "testOperation", - ResourceIdentifier: velero.ResourceIdentifier{ - GroupResource: schema.GroupResource{ - Group: "", - Resource: "persistentvolumeclaims", - }, - Namespace: "velero", - Name: "testPVC", - }, - PostOperationItems: []velero.ResourceIdentifier{ - { - GroupResource: schema.GroupResource{ - Group: "velero.io", - Resource: "datauploads", - }, - Namespace: "velero", - Name: "testDU", - }, - }, - }, - }, - expectedVolumeInfos: []volume.VolumeInfo{}, - }, - { - name: "VolumeSnapshotClass cannot be found for operation", - dataUpload: builder.ForDataUpload("velero", "testDU").DataMover("velero").CSISnapshot(&velerov2alpha1.CSISnapshotSpec{ - VolumeSnapshot: "testVS", - }).SnapshotID("testSnapshotHandle").Result(), - operation: &itemoperation.BackupOperation{ - Spec: itemoperation.BackupOperationSpec{ - OperationID: "testOperation", - ResourceIdentifier: velero.ResourceIdentifier{ - GroupResource: schema.GroupResource{ - Group: "", - Resource: "persistentvolumeclaims", - }, - Namespace: "velero", - Name: "testPVC", - }, - PostOperationItems: []velero.ResourceIdentifier{ - { - GroupResource: schema.GroupResource{ - Group: "velero.io", - Resource: "datauploads", - }, - Namespace: "velero", - Name: "testDU", - }, - }, - }, - }, - pvMap: map[string]backup.PvcPvInfo{ - "velero/testPVC": { - PVCName: "testPVC", - PVCNamespace: "velero", - PV: corev1api.PersistentVolume{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testPV", - Labels: map[string]string{"a": "b"}, - }, - Spec: corev1api.PersistentVolumeSpec{ - PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, - }, - }, - }, - }, - expectedVolumeInfos: []volume.VolumeInfo{ - { - PVCName: "testPVC", - PVCNamespace: "velero", - PVName: "testPV", - BackupMethod: volume.CSISnapshot, - SnapshotDataMoved: true, - OperationID: "testOperation", - SnapshotDataMovementInfo: volume.SnapshotDataMovementInfo{ - DataMover: "velero", - UploaderType: "kopia", - }, - PVInfo: volume.PVInfo{ - ReclaimPolicy: string(corev1api.PersistentVolumeReclaimDelete), - Labels: map[string]string{"a": "b"}, - }, - }, - }, - }, - { - name: "Normal DataUpload case", - dataUpload: builder.ForDataUpload("velero", "testDU").DataMover("velero").CSISnapshot(&velerov2alpha1.CSISnapshotSpec{ - VolumeSnapshot: "testVS", - SnapshotClass: "testClass", - }).SnapshotID("testSnapshotHandle").Result(), - volumeSnapshotClass: builder.ForVolumeSnapshotClass("testClass").Driver("pd.csi.storage.gke.io").Result(), - operation: &itemoperation.BackupOperation{ - Spec: itemoperation.BackupOperationSpec{ - OperationID: "testOperation", - ResourceIdentifier: velero.ResourceIdentifier{ - GroupResource: schema.GroupResource{ - Group: "", - Resource: "persistentvolumeclaims", - }, - Namespace: "velero", - Name: "testPVC", - }, - PostOperationItems: []velero.ResourceIdentifier{ - { - GroupResource: schema.GroupResource{ - Group: "velero.io", - Resource: "datauploads", - }, - Namespace: "velero", - Name: "testDU", - }, - }, - }, - Status: itemoperation.OperationStatus{ - Created: &now, - }, - }, - pvMap: map[string]backup.PvcPvInfo{ - "velero/testPVC": { - PVCName: "testPVC", - PVCNamespace: "velero", - PV: corev1api.PersistentVolume{ - ObjectMeta: metav1.ObjectMeta{ - Name: "testPV", - Labels: map[string]string{"a": "b"}, - }, - Spec: corev1api.PersistentVolumeSpec{ - PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete, - }, - }, - }, - }, - expectedVolumeInfos: []volume.VolumeInfo{ - { - PVCName: "testPVC", - PVCNamespace: "velero", - PVName: "testPV", - BackupMethod: volume.CSISnapshot, - SnapshotDataMoved: true, - OperationID: "testOperation", - StartTimestamp: &now, - CSISnapshotInfo: volume.CSISnapshotInfo{ - Driver: "pd.csi.storage.gke.io", - }, - SnapshotDataMovementInfo: volume.SnapshotDataMovementInfo{ - DataMover: "velero", - UploaderType: "kopia", - }, - PVInfo: volume.PVInfo{ - ReclaimPolicy: string(corev1api.PersistentVolumeReclaimDelete), - Labels: map[string]string{"a": "b"}, - }, - }, - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - request := new(backup.Request) - operationList := request.GetItemOperationsList() - if tc.operation != nil { - *operationList = append(*operationList, tc.operation) - } - if tc.pvMap != nil { - request.PVMap = tc.pvMap - } - logger := logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) - - crClient := velerotest.NewFakeControllerRuntimeClient(t) - if tc.dataUpload != nil { - crClient.Create(context.TODO(), tc.dataUpload) - } - - if tc.volumeSnapshotClass != nil { - crClient.Create(context.TODO(), tc.volumeSnapshotClass) - } - - volumeInfos := generateVolumeInfoFromDataUpload(request, crClient, logger) - require.Equal(t, tc.expectedVolumeInfos, volumeInfos) - }) - } -} - -func int64Ptr(val int) *int64 { - i := int64(val) - return &i -} - -func stringPtr(str string) *string { - return &str -} diff --git a/pkg/controller/restore_controller.go b/pkg/controller/restore_controller.go index f6b9b39d9..d3f6ccd9c 100644 --- a/pkg/controller/restore_controller.go +++ b/pkg/controller/restore_controller.go @@ -56,6 +56,7 @@ import ( kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" "github.com/vmware-tanzu/velero/pkg/util/logging" "github.com/vmware-tanzu/velero/pkg/util/results" + "github.com/vmware-tanzu/velero/pkg/volume" ) // nonRestorableResources is an exclusion list for the restoration process. Any resources @@ -520,6 +521,16 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu return errors.Wrap(err, "fail to fetch CSI VolumeSnapshots metadata") } + backupVolumeInfoMap := make(map[string]volume.VolumeInfo) + volumeInfos, err := backupStore.GetBackupVolumeInfos(restore.Spec.BackupName) + if err != nil || volumeInfos == nil { + restoreLog.Infof("Backup %s doesn't have volumeinfos metadata file.", restore.Spec.BackupName) + } else { + for _, volumeInfo := range volumeInfos.VolumeInfos { + backupVolumeInfoMap[volumeInfo.PVName] = volumeInfo + } + } + restoreLog.Info("starting restore") var podVolumeBackups []*api.PodVolumeBackup @@ -537,6 +548,7 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu ResourceModifiers: resourceModifiers, DisableInformerCache: r.disableInformerCache, CSIVolumeSnapshots: csiVolumeSnapshots, + VolumeInfoMap: backupVolumeInfoMap, } restoreWarnings, restoreErrors := r.restorer.RestoreWithResolvers(restoreReq, actionsResolver, pluginManager) diff --git a/pkg/controller/restore_controller_test.go b/pkg/controller/restore_controller_test.go index 9437f1d1c..4a511dfd9 100644 --- a/pkg/controller/restore_controller_test.go +++ b/pkg/controller/restore_controller_test.go @@ -235,6 +235,7 @@ func TestRestoreReconcile(t *testing.T) { putRestoreLogErr error expectedFinalPhase string addValidFinalizer bool + emptyVolumeInfo bool }{ { name: "restore with both namespace in both includedNamespaces and excludedNamespaces fails validation", @@ -415,6 +416,18 @@ func TestRestoreReconcile(t *testing.T) { backup: defaultBackup().StorageLocation("default").Result(), expectedErr: false, }, + { + name: "valid restore with empty VolumeInfos", + location: defaultStorageLocation, + restore: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).Result(), + backup: defaultBackup().StorageLocation("default").Result(), + emptyVolumeInfo: true, + expectedErr: false, + expectedPhase: string(velerov1api.RestorePhaseInProgress), + expectedStartTime: ×tamp, + expectedCompletedTime: ×tamp, + expectedRestorerCall: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseInProgress).Result(), + }, } formatFlag := logging.FormatText @@ -482,6 +495,11 @@ func TestRestoreReconcile(t *testing.T) { backupStore.On("PutRestoreResults", test.backup.Name, test.restore.Name, mock.Anything).Return(nil) backupStore.On("PutRestoredResourceList", test.restore.Name, mock.Anything).Return(nil) backupStore.On("PutRestoreItemOperations", mock.Anything, mock.Anything).Return(nil) + if test.emptyVolumeInfo == true { + backupStore.On("GetBackupVolumeInfos", test.backup.Name).Return(nil, nil) + } else { + backupStore.On("GetBackupVolumeInfos", test.backup.Name).Return(&volume.VolumeInfos{}, nil) + } volumeSnapshots := []*volume.Snapshot{ { diff --git a/pkg/persistence/mocks/backup_store.go b/pkg/persistence/mocks/backup_store.go index 1bcdf865c..b25e611b8 100644 --- a/pkg/persistence/mocks/backup_store.go +++ b/pkg/persistence/mocks/backup_store.go @@ -314,6 +314,29 @@ func (_m *BackupStore) GetRestoreItemOperations(name string) ([]*itemoperation.R return r0, r1 } +// GetRestoreItemOperations provides a mock function with given fields: name +func (_m *BackupStore) GetBackupVolumeInfos(name string) (*volume.VolumeInfos, error) { + ret := _m.Called(name) + + var r0 *volume.VolumeInfos + if rf, ok := ret.Get(0).(func(string) *volume.VolumeInfos); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*volume.VolumeInfos) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // IsValid provides a mock function with given fields: func (_m *BackupStore) IsValid() error { ret := _m.Called() diff --git a/pkg/persistence/object_store.go b/pkg/persistence/object_store.go index 48a48dbf1..4ba61d392 100644 --- a/pkg/persistence/object_store.go +++ b/pkg/persistence/object_store.go @@ -73,6 +73,7 @@ type BackupStore interface { GetCSIVolumeSnapshots(name string) ([]*snapshotv1api.VolumeSnapshot, error) GetCSIVolumeSnapshotContents(name string) ([]*snapshotv1api.VolumeSnapshotContent, error) GetCSIVolumeSnapshotClasses(name string) ([]*snapshotv1api.VolumeSnapshotClass, error) + GetBackupVolumeInfos(name string) (*volume.VolumeInfos, error) // BackupExists checks if the backup metadata file exists in object storage. BackupExists(bucket, backupName string) (bool, error) diff --git a/pkg/restore/request.go b/pkg/restore/request.go index 2a267a5ff..e0fe44166 100644 --- a/pkg/restore/request.go +++ b/pkg/restore/request.go @@ -62,6 +62,7 @@ type Request struct { ResourceModifiers *resourcemodifiers.ResourceModifiers DisableInformerCache bool CSIVolumeSnapshots []*snapshotv1api.VolumeSnapshot + VolumeInfoMap map[string]volume.VolumeInfo } type restoredItemStatus struct { diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 57a6156ef..3f79e539c 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -80,6 +80,15 @@ var resourceMustHave = []string{ "datauploads.velero.io", } +const ( + pvHasNativeSnapshot = iota + 1 + pvHasPodVolumeBackup + pvHasCSIVolumeSnapshot + pvHasDataUpload + pvHasReclaimPolicyAsDelete + pvHasReclaimPolicyAsRetain +) + type VolumeSnapshotterGetter interface { GetVolumeSnapshotter(name string) (vsv1.VolumeSnapshotter, error) } @@ -326,6 +335,7 @@ func (kr *kubernetesRestorer) RestoreWithResolvers( disableInformerCache: req.DisableInformerCache, featureVerifier: kr.featureVerifier, hookTracker: hook.NewHookTracker(), + volumeInfoMap: req.VolumeInfoMap, } return restoreCtx.execute() @@ -379,6 +389,7 @@ type restoreContext struct { disableInformerCache bool featureVerifier features.Verifier hookTracker *hook.HookTracker + volumeInfoMap map[string]volume.VolumeInfo } type resourceClientKey struct { @@ -1122,15 +1133,17 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso itemExists := false resourceID := getResourceID(groupResource, namespace, obj.GetName()) + restoreLogger := ctx.log.WithFields(logrus.Fields{ + "namespace": obj.GetNamespace(), + "name": obj.GetName(), + "groupResource": groupResource.String(), + }) + // Check if group/resource should be restored. We need to do this here since // this method may be getting called for an additional item which is a group/resource // that's excluded. if !ctx.resourceIncludesExcludes.ShouldInclude(groupResource.String()) { - ctx.log.WithFields(logrus.Fields{ - "namespace": obj.GetNamespace(), - "name": obj.GetName(), - "groupResource": groupResource.String(), - }).Info("Not restoring item because resource is excluded") + restoreLogger.Info("Not restoring item because resource is excluded") return warnings, errs, itemExists } @@ -1142,11 +1155,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso // to check the *original* namespace, not the remapped one if it's been remapped. if namespace != "" { if !ctx.namespaceIncludesExcludes.ShouldInclude(obj.GetNamespace()) && !ctx.resourceMustHave.Has(groupResource.String()) { - ctx.log.WithFields(logrus.Fields{ - "namespace": obj.GetNamespace(), - "name": obj.GetName(), - "groupResource": groupResource.String(), - }).Info("Not restoring item because namespace is excluded") + restoreLogger.Info("Not restoring item because namespace is excluded") return warnings, errs, itemExists } @@ -1170,11 +1179,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso } } else { if boolptr.IsSetToFalse(ctx.restore.Spec.IncludeClusterResources) { - ctx.log.WithFields(logrus.Fields{ - "namespace": obj.GetNamespace(), - "name": obj.GetName(), - "groupResource": groupResource.String(), - }).Info("Not restoring item because it's cluster-scoped") + restoreLogger.Info("Not restoring item because it's cluster-scoped") return warnings, errs, itemExists } } @@ -1238,106 +1243,73 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso } if groupResource == kuberesource.PersistentVolumes { - switch { - case hasSnapshot(name, ctx.volumeSnapshots): - oldName := obj.GetName() - shouldRenamePV, err := shouldRenamePV(ctx, obj, resourceClient) - if err != nil { - errs.Add(namespace, err) - return warnings, errs, itemExists - } + pvCondition := 0 + if volumeInfo, ok := ctx.volumeInfoMap[obj.GetName()]; ok { + ctx.log.Infof("Find VolumeInfo for PV %s.", obj.GetName()) - // Check to see if the claimRef.namespace field needs to be remapped, - // and do so if necessary. - _, err = remapClaimRefNS(ctx, obj) - if err != nil { - errs.Add(namespace, err) - return warnings, errs, itemExists - } - - var shouldRestoreSnapshot bool - if !shouldRenamePV { - // Check if the PV exists in the cluster before attempting to create - // a volume from the snapshot, in order to avoid orphaned volumes (GH #609) - shouldRestoreSnapshot, err = ctx.shouldRestore(name, resourceClient) - if err != nil { - errs.Add(namespace, errors.Wrapf(err, "error waiting on in-cluster persistentvolume %s", name)) - return warnings, errs, itemExists - } - } else { - // If we're renaming the PV, we're going to give it a new random name, - // so we can assume it doesn't already exist in the cluster and therefore - // we should proceed with restoring from snapshot. - shouldRestoreSnapshot = true - } - - if shouldRestoreSnapshot { - // Reset the PV's binding status so that Kubernetes can properly - // associate it with the restored PVC. - obj = resetVolumeBindingInfo(obj) - - // Even if we're renaming the PV, obj still has the old name here, because the pvRestorer - // uses the original name to look up metadata about the snapshot. - ctx.log.Infof("Restoring persistent volume from snapshot.") - updatedObj, err := ctx.pvRestorer.executePVAction(obj) - if err != nil { - errs.Add(namespace, fmt.Errorf("error executing PVAction for %s: %v", resourceID, err)) - return warnings, errs, itemExists - } - obj = updatedObj - - // VolumeSnapshotter has modified the PV name, we should rename the PV. - if oldName != obj.GetName() { - shouldRenamePV = true - } - } - - if shouldRenamePV { - var pvName string - if oldName == obj.GetName() { - // pvRestorer hasn't modified the PV name, we need to rename the PV. - pvName, err = ctx.pvRenamer(oldName) - if err != nil { - errs.Add(namespace, errors.Wrapf(err, "error renaming PV")) - return warnings, errs, itemExists - } + switch volumeInfo.BackupMethod { + case volume.NativeSnapshot: + pvCondition = pvHasNativeSnapshot + case volume.PodVolumeBackup: + pvCondition = pvHasPodVolumeBackup + case volume.CSISnapshot: + if volumeInfo.SnapshotDataMoved { + pvCondition = pvHasDataUpload } else { - // VolumeSnapshotter could have modified the PV name through - // function `SetVolumeID`, - pvName = obj.GetName() + pvCondition = pvHasCSIVolumeSnapshot } - - ctx.renamedPVs[oldName] = pvName - obj.SetName(pvName) - name = pvName - - // Add the original PV name as an annotation. - annotations := obj.GetAnnotations() - if annotations == nil { - annotations = map[string]string{} + // When the PV data is skipped from backup, it's VolumeInfo BackupMethod + // is not set, and it will fall into the default case. + default: + if hasDeleteReclaimPolicy(obj.Object) { + pvCondition = pvHasReclaimPolicyAsDelete + } else { + pvCondition = pvHasReclaimPolicyAsRetain } - annotations["velero.io/original-pv-name"] = oldName - obj.SetAnnotations(annotations) + } + } else { + // TODO: VolumeInfo is adopted and old logic is deprecated in v1.13. + // Remove the old logic in v1.15. + ctx.log.Infof("Cannot find VolumeInfo for PV %s.", obj.GetName()) + + switch { + case hasSnapshot(name, ctx.volumeSnapshots): + pvCondition = pvHasNativeSnapshot + case hasPodVolumeBackup(obj, ctx): + pvCondition = pvHasPodVolumeBackup + case hasCSIVolumeSnapshot(ctx, obj): + pvCondition = pvHasCSIVolumeSnapshot + case hasSnapshotDataUpload(ctx, obj): + pvCondition = pvHasDataUpload + case hasDeleteReclaimPolicy(obj.Object): + pvCondition = pvHasReclaimPolicyAsDelete + default: + pvCondition = pvHasReclaimPolicyAsRetain + } + } + + switch pvCondition { + case pvHasNativeSnapshot: + obj, err = ctx.handlePVHasNativeSnapshot(obj, resourceClient) + if err != nil { + errs.Add(namespace, err) + return warnings, errs, itemExists } - case hasPodVolumeBackup(obj, ctx): - ctx.log.WithFields(logrus.Fields{ - "namespace": obj.GetNamespace(), - "name": obj.GetName(), - "groupResource": groupResource.String(), - }).Infof("Dynamically re-provisioning persistent volume because it has a pod volume backup to be restored.") + name = obj.GetName() + + case pvHasPodVolumeBackup: + restoreLogger.Infof("Dynamically re-provisioning persistent volume because it has a pod volume backup to be restored.") ctx.pvsToProvision.Insert(name) // Return early because we don't want to restore the PV itself, we // want to dynamically re-provision it. return warnings, errs, itemExists - case hasCSIVolumeSnapshot(ctx, obj): - ctx.log.WithFields(logrus.Fields{ - "namespace": obj.GetNamespace(), - "name": obj.GetName(), - "groupResource": groupResource.String(), - }).Infof("Dynamically re-provisioning persistent volume because it has a related CSI VolumeSnapshot.") + case pvHasCSIVolumeSnapshot: + fallthrough + case pvHasDataUpload: + restoreLogger.Infof("Dynamically re-provisioning persistent volume because it has a CSI VolumeSnapshot or a related snapshot DataUpload.") ctx.pvsToProvision.Insert(name) if ready, err := ctx.featureVerifier.Verify(velerov1api.CSIFeatureFlag); !ready { @@ -1349,41 +1321,16 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso // want to dynamically re-provision it. return warnings, errs, itemExists - case hasSnapshotDataUpload(ctx, obj): - ctx.log.WithFields(logrus.Fields{ - "namespace": obj.GetNamespace(), - "name": obj.GetName(), - "groupResource": groupResource.String(), - }).Infof("Dynamically re-provisioning persistent volume because it has a related snapshot DataUpload.") - ctx.pvsToProvision.Insert(name) - - if ready, err := ctx.featureVerifier.Verify(velerov1api.CSIFeatureFlag); !ready { - ctx.log.Errorf("Failed to verify CSI modules, ready %v, err %v", ready, err) - errs.Add(namespace, fmt.Errorf("CSI modules are not ready for restore. Check CSI feature is enabled and CSI plugin is installed")) - } - - // Return early because we don't want to restore the PV itself, we - // want to dynamically re-provision it. - return warnings, errs, itemExists - - case hasDeleteReclaimPolicy(obj.Object): - ctx.log.WithFields(logrus.Fields{ - "namespace": obj.GetNamespace(), - "name": obj.GetName(), - "groupResource": groupResource.String(), - }).Infof("Dynamically re-provisioning persistent volume because it doesn't have a snapshot and its reclaim policy is Delete.") + case pvHasReclaimPolicyAsDelete: + restoreLogger.Infof("Dynamically re-provisioning persistent volume because it doesn't have a snapshot and its reclaim policy is Delete.") ctx.pvsToProvision.Insert(name) // Return early because we don't want to restore the PV itself, we // want to dynamically re-provision it. return warnings, errs, itemExists - default: - ctx.log.WithFields(logrus.Fields{ - "namespace": obj.GetNamespace(), - "name": obj.GetName(), - "groupResource": groupResource.String(), - }).Infof("Restoring persistent volume as-is because it doesn't have a snapshot and its reclaim policy is not Delete.") + case pvHasReclaimPolicyAsRetain: + restoreLogger.Infof("Restoring persistent volume as-is because it doesn't have a snapshot and its reclaim policy is not Delete.") // Check to see if the claimRef.namespace field needs to be remapped, and do so if necessary. _, err = remapClaimRefNS(ctx, obj) @@ -2491,3 +2438,81 @@ func (ctx *restoreContext) processUpdateResourcePolicy(fromCluster, fromClusterW } return warnings, errs } + +func (ctx *restoreContext) handlePVHasNativeSnapshot(obj *unstructured.Unstructured, resourceClient client.Dynamic) (*unstructured.Unstructured, error) { + retObj := obj.DeepCopy() + oldName := obj.GetName() + shouldRenamePV, err := shouldRenamePV(ctx, retObj, resourceClient) + if err != nil { + return nil, err + } + + // Check to see if the claimRef.namespace field needs to be remapped, + // and do so if necessary. + _, err = remapClaimRefNS(ctx, retObj) + if err != nil { + return nil, err + } + + var shouldRestoreSnapshot bool + if !shouldRenamePV { + // Check if the PV exists in the cluster before attempting to create + // a volume from the snapshot, in order to avoid orphaned volumes (GH #609) + shouldRestoreSnapshot, err = ctx.shouldRestore(oldName, resourceClient) + if err != nil { + return nil, errors.Wrapf(err, "error waiting on in-cluster persistentvolume %s", oldName) + } + } else { + // If we're renaming the PV, we're going to give it a new random name, + // so we can assume it doesn't already exist in the cluster and therefore + // we should proceed with restoring from snapshot. + shouldRestoreSnapshot = true + } + + if shouldRestoreSnapshot { + // Reset the PV's binding status so that Kubernetes can properly + // associate it with the restored PVC. + retObj = resetVolumeBindingInfo(retObj) + + // Even if we're renaming the PV, obj still has the old name here, because the pvRestorer + // uses the original name to look up metadata about the snapshot. + ctx.log.Infof("Restoring persistent volume from snapshot.") + retObj, err = ctx.pvRestorer.executePVAction(retObj) + if err != nil { + return nil, fmt.Errorf("error executing PVAction for %s: %v", getResourceID(kuberesource.PersistentVolumes, "", oldName), err) + } + + // VolumeSnapshotter has modified the PV name, we should rename the PV. + if oldName != retObj.GetName() { + shouldRenamePV = true + } + } + + if shouldRenamePV { + var pvName string + if oldName == retObj.GetName() { + // pvRestorer hasn't modified the PV name, we need to rename the PV. + pvName, err = ctx.pvRenamer(oldName) + if err != nil { + return nil, errors.Wrapf(err, "error renaming PV") + } + } else { + // VolumeSnapshotter could have modified the PV name through + // function `SetVolumeID`, + pvName = retObj.GetName() + } + + ctx.renamedPVs[oldName] = pvName + retObj.SetName(pvName) + + // Add the original PV name as an annotation. + annotations := retObj.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations["velero.io/original-pv-name"] = oldName + retObj.SetAnnotations(annotations) + } + + return retObj, nil +} diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index d2f86e3c7..aa793a9b1 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -47,6 +47,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/discovery" + "github.com/vmware-tanzu/velero/pkg/features" verifiermocks "github.com/vmware-tanzu/velero/pkg/features/mocks" "github.com/vmware-tanzu/velero/pkg/itemoperation" "github.com/vmware-tanzu/velero/pkg/kuberesource" @@ -61,6 +62,199 @@ import ( "github.com/vmware-tanzu/velero/pkg/volume" ) +func TestRestorePVWithVolumeInfo(t *testing.T) { + tests := []struct { + name string + restore *velerov1api.Restore + backup *velerov1api.Backup + apiResources []*test.APIResource + tarball io.Reader + want map[*test.APIResource][]string + volumeInfoMap map[string]volume.VolumeInfo + }{ + { + name: "Restore PV with native snapshot", + restore: defaultRestore().Result(), + backup: defaultBackup().Result(), + tarball: test.NewTarWriter(t). + AddItems("persistentvolumes", + builder.ForPersistentVolume("pv-1").ReclaimPolicy(corev1api.PersistentVolumeReclaimRetain).Result(), + ).Done(), + apiResources: []*test.APIResource{ + test.PVs(), + }, + volumeInfoMap: map[string]volume.VolumeInfo{ + "pv-1": { + BackupMethod: volume.NativeSnapshot, + PVName: "pv-1", + NativeSnapshotInfo: volume.NativeSnapshotInfo{ + SnapshotHandle: "testSnapshotHandle", + }, + }, + }, + want: map[*test.APIResource][]string{ + test.PVs(): {"/pv-1"}, + }, + }, + { + name: "Restore PV with PVB", + restore: defaultRestore().Result(), + backup: defaultBackup().Result(), + tarball: test.NewTarWriter(t). + AddItems("persistentvolumes", + builder.ForPersistentVolume("pv-1").ReclaimPolicy(corev1api.PersistentVolumeReclaimRetain).Result(), + ).Done(), + apiResources: []*test.APIResource{ + test.PVs(), + }, + volumeInfoMap: map[string]volume.VolumeInfo{ + "pv-1": { + BackupMethod: volume.PodVolumeBackup, + PVName: "pv-1", + PVBInfo: volume.PodVolumeBackupInfo{ + SnapshotHandle: "testSnapshotHandle", + Size: 100, + NodeName: "testNode", + }, + }, + }, + want: map[*test.APIResource][]string{ + test.PVs(): {}, + }, + }, + { + name: "Restore PV with CSI VolumeSnapshot", + restore: defaultRestore().Result(), + backup: defaultBackup().Result(), + tarball: test.NewTarWriter(t). + AddItems("persistentvolumes", + builder.ForPersistentVolume("pv-1").ReclaimPolicy(corev1api.PersistentVolumeReclaimRetain).Result(), + ).Done(), + apiResources: []*test.APIResource{ + test.PVs(), + }, + volumeInfoMap: map[string]volume.VolumeInfo{ + "pv-1": { + BackupMethod: volume.CSISnapshot, + SnapshotDataMoved: false, + PVName: "pv-1", + CSISnapshotInfo: volume.CSISnapshotInfo{ + Driver: "pd.csi.storage.gke.io", + }, + }, + }, + want: map[*test.APIResource][]string{ + test.PVs(): {}, + }, + }, + { + name: "Restore PV with DataUpload", + restore: defaultRestore().Result(), + backup: defaultBackup().Result(), + tarball: test.NewTarWriter(t). + AddItems("persistentvolumes", + builder.ForPersistentVolume("pv-1").ReclaimPolicy(corev1api.PersistentVolumeReclaimRetain).Result(), + ).Done(), + apiResources: []*test.APIResource{ + test.PVs(), + }, + volumeInfoMap: map[string]volume.VolumeInfo{ + "pv-1": { + BackupMethod: volume.CSISnapshot, + SnapshotDataMoved: true, + PVName: "pv-1", + CSISnapshotInfo: volume.CSISnapshotInfo{ + Driver: "pd.csi.storage.gke.io", + }, + SnapshotDataMovementInfo: volume.SnapshotDataMovementInfo{ + DataMover: "velero", + }, + }, + }, + want: map[*test.APIResource][]string{ + test.PVs(): {}, + }, + }, + { + name: "Restore PV with ClaimPolicy as Delete", + restore: defaultRestore().Result(), + backup: defaultBackup().Result(), + tarball: test.NewTarWriter(t). + AddItems("persistentvolumes", + builder.ForPersistentVolume("pv-1").ReclaimPolicy(corev1api.PersistentVolumeReclaimDelete).Result(), + ).Done(), + apiResources: []*test.APIResource{ + test.PVs(), + }, + volumeInfoMap: map[string]volume.VolumeInfo{ + "pv-1": { + PVName: "pv-1", + Skipped: true, + }, + }, + want: map[*test.APIResource][]string{ + test.PVs(): {}, + }, + }, + { + name: "Restore PV with ClaimPolicy as Retain", + restore: defaultRestore().Result(), + backup: defaultBackup().Result(), + tarball: test.NewTarWriter(t). + AddItems("persistentvolumes", + builder.ForPersistentVolume("pv-1").ReclaimPolicy(corev1api.PersistentVolumeReclaimRetain).Result(), + ).Done(), + apiResources: []*test.APIResource{ + test.PVs(), + }, + volumeInfoMap: map[string]volume.VolumeInfo{ + "pv-1": { + PVName: "pv-1", + Skipped: true, + }, + }, + want: map[*test.APIResource][]string{ + test.PVs(): {"/pv-1"}, + }, + }, + } + + features.Enable("EnableCSI") + finder := new(verifiermocks.PluginFinder) + finder.On("Find", mock.Anything, mock.Anything).Return(true) + verifier := features.NewVerifier(finder) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + h := newHarness(t) + + for _, r := range tc.apiResources { + h.DiscoveryClient.WithAPIResource(r) + } + require.NoError(t, h.restorer.discoveryHelper.Refresh()) + h.restorer.featureVerifier = verifier + + data := &Request{ + Log: h.log, + Restore: tc.restore, + Backup: tc.backup, + PodVolumeBackups: nil, + VolumeSnapshots: nil, + BackupReader: tc.tarball, + VolumeInfoMap: tc.volumeInfoMap, + } + warnings, errs := h.restorer.Restore( + data, + nil, // restoreItemActions + nil, // volume snapshotter getter + ) + + assertEmptyResults(t, warnings, errs) + assertAPIContents(t, h, tc.want) + }) + } +} + // TestRestoreResourceFiltering runs restores with different combinations // of resource filters (included/excluded resources, included/excluded // namespaces, label selectors, "include cluster resources" flag), and diff --git a/pkg/volume/volume_info_common.go b/pkg/volume/volume_info_common.go index 14ede0c6b..ba7349911 100644 --- a/pkg/volume/volume_info_common.go +++ b/pkg/volume/volume_info_common.go @@ -26,10 +26,6 @@ const ( CSISnapshot VolumeBackupMethod = "CSISnapshot" ) -type VolumeInfoVersion struct { - Version string `json:"version"` -} - type VolumeInfos struct { VolumeInfos []VolumeInfo `json:"volumeInfos"` }