From 0a280e57863c70495a2dfb65787615a68a0e7b03 Mon Sep 17 00:00:00 2001 From: Daniel Jiang Date: Mon, 8 Apr 2024 01:32:37 +0800 Subject: [PATCH] Track and persist restore volume info Signed-off-by: Daniel Jiang --- changelogs/unreleased/7630-reasonerjt | 1 + internal/volume/utils.go | 42 ++ internal/volume/utils_test.go | 57 ++ internal/volume/volumes_information.go | 498 ++++++++++++++---- internal/volume/volumes_information_test.go | 376 +++++++++++-- pkg/apis/velero/v1/download_request_types.go | 1 + pkg/backup/backup.go | 6 +- pkg/backup/backup_test.go | 16 +- pkg/backup/request.go | 6 +- pkg/builder/volume_snapshot_builder.go | 6 + pkg/cmd/server/server.go | 1 + pkg/cmd/util/output/backup_describer.go | 30 +- pkg/cmd/util/output/backup_describer_test.go | 22 +- .../output/backup_structured_describer.go | 18 +- .../backup_structured_describer_test.go | 22 +- pkg/controller/restore_controller.go | 48 +- pkg/controller/restore_controller_test.go | 55 +- .../restore_finalizer_controller.go | 25 +- .../restore_finalizer_controller_test.go | 52 +- pkg/label/label.go | 6 + pkg/persistence/mocks/backup_store.go | 23 +- pkg/persistence/object_store.go | 13 +- pkg/persistence/object_store_layout.go | 4 + pkg/persistence/object_store_test.go | 16 +- pkg/podvolume/mocks/restorer.go | 34 +- pkg/podvolume/restorer.go | 7 +- pkg/podvolume/restorer_test.go | 5 +- pkg/restore/pv_restorer.go | 6 + pkg/restore/pv_restorer_test.go | 14 +- pkg/restore/request.go | 23 +- pkg/restore/restore.go | 21 +- pkg/restore/restore_test.go | 45 +- test/util/providers/common.go | 4 +- 33 files changed, 1116 insertions(+), 387 deletions(-) create mode 100644 changelogs/unreleased/7630-reasonerjt create mode 100644 internal/volume/utils.go create mode 100644 internal/volume/utils_test.go diff --git a/changelogs/unreleased/7630-reasonerjt b/changelogs/unreleased/7630-reasonerjt new file mode 100644 index 0000000000..d793e90203 --- /dev/null +++ b/changelogs/unreleased/7630-reasonerjt @@ -0,0 +1 @@ +Track and persist restore volume info \ No newline at end of file diff --git a/internal/volume/utils.go b/internal/volume/utils.go new file mode 100644 index 0000000000..fbb3ec67ba --- /dev/null +++ b/internal/volume/utils.go @@ -0,0 +1,42 @@ +/* +Copyright The Velero Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package volume + +import ( + "regexp" +) + +// it has to have the same value as "github.com/vmware-tanzu/velero/pkg/restore".ItemRestoreResultCreated +const itemRestoreResultCreated = "created" + +// RestoredPVCFromRestoredResourceList returns a set of PVCs that were restored from the given restoredResourceList. +func RestoredPVCFromRestoredResourceList(restoredResourceList map[string][]string) map[string]struct{} { + pvcKey := "v1/PersistentVolumeClaim" + pvcList := make(map[string]struct{}) + + for _, pvc := range restoredResourceList[pvcKey] { + // the format of pvc string in restoredResourceList is like: "namespace/pvcName(status)" + // extract the substring before "(created)" if the status in rightmost Parenthesis is "created" + r := regexp.MustCompile(`\(([^)]+)\)`) + matches := r.FindAllStringSubmatch(pvc, -1) + if len(matches) > 0 && matches[len(matches)-1][1] == itemRestoreResultCreated { + pvcList[pvc[:len(pvc)-len("(created)")]] = struct{}{} + } + } + + return pvcList +} diff --git a/internal/volume/utils_test.go b/internal/volume/utils_test.go new file mode 100644 index 0000000000..f4b4b0b3c2 --- /dev/null +++ b/internal/volume/utils_test.go @@ -0,0 +1,57 @@ +/* +Copyright The Velero Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package volume + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetRestoredPVCFromRestoredResourceList(t *testing.T) { + // test empty list + restoredResourceList := map[string][]string{} + actual := RestoredPVCFromRestoredResourceList(restoredResourceList) + assert.Empty(t, actual) + + // test no match + restoredResourceList = map[string][]string{ + "v1/PersistentVolumeClaim": { + "namespace1/pvc1(updated)", + }, + "v1/PersistentVolume": { + "namespace1/pv(created)", + }, + } + actual = RestoredPVCFromRestoredResourceList(restoredResourceList) + assert.Empty(t, actual) + + // test matches + restoredResourceList = map[string][]string{ + "v1/PersistentVolumeClaim": { + "namespace1/pvc1(created)", + "namespace2/pvc2(updated)", + "namespace3/pvc(3)(created)", + }, + } + expected := map[string]struct{}{ + "namespace1/pvc1": {}, + "namespace3/pvc(3)": {}, + } + actual = RestoredPVCFromRestoredResourceList(restoredResourceList) + assert.Equal(t, expected, actual) +} diff --git a/internal/volume/volumes_information.go b/internal/volume/volumes_information.go index e901280e52..51267c931b 100644 --- a/internal/volume/volumes_information.go +++ b/internal/volume/volumes_information.go @@ -19,13 +19,18 @@ package volume import ( "context" "strconv" + "strings" + "sync" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1" + "github.com/pkg/errors" "github.com/sirupsen/logrus" corev1api "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kbclient "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/vmware-tanzu/velero/pkg/label" + 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/features" @@ -34,19 +39,27 @@ import ( "github.com/vmware-tanzu/velero/pkg/plugin/velero" ) -type VolumeBackupMethod string +type Method string const ( - NativeSnapshot VolumeBackupMethod = "NativeSnapshot" - PodVolumeBackup VolumeBackupMethod = "PodVolumeBackup" - CSISnapshot VolumeBackupMethod = "CSISnapshot" + NativeSnapshot Method = "NativeSnapshot" + PodVolumeBackup Method = "PodVolumeBackup" + CSISnapshot Method = "CSISnapshot" + PodVolumeRestore Method = "PodVolumeRestore" ) const ( FieldValueIsUnknown string = "unknown" + kopia string = "kopia" + veleroDatamover string = "velero" + + //TODO reuse these constants from csi-plugin-for-velero after it's merged into the same repo + + CSIDriverNameAnnotation = "velero.io/csi-driver-name" + VolumeSnapshotHandleAnnotation = "velero.io/csi-volumesnapshot-handle" ) -type VolumeInfo struct { +type BackupVolumeInfo struct { // The PVC's name. PVCName string `json:"pvcName,omitempty"` @@ -57,7 +70,7 @@ type VolumeInfo struct { PVName string `json:"pvName,omitempty"` // The way the volume data is backed up. The valid value includes `VeleroNativeSnapshot`, `PodVolumeBackup` and `CSISnapshot`. - BackupMethod VolumeBackupMethod `json:"backupMethod,omitempty"` + BackupMethod Method `json:"backupMethod,omitempty"` // Whether the volume's snapshot data is moved to specified storage. SnapshotDataMoved bool `json:"snapshotDataMoved"` @@ -82,10 +95,34 @@ type VolumeInfo struct { CSISnapshotInfo *CSISnapshotInfo `json:"csiSnapshotInfo,omitempty"` SnapshotDataMovementInfo *SnapshotDataMovementInfo `json:"snapshotDataMovementInfo,omitempty"` NativeSnapshotInfo *NativeSnapshotInfo `json:"nativeSnapshotInfo,omitempty"` - PVBInfo *PodVolumeBackupInfo `json:"pvbInfo,omitempty"` + PVBInfo *PodVolumeInfo `json:"pvbInfo,omitempty"` PVInfo *PVInfo `json:"pvInfo,omitempty"` } +type RestoreVolumeInfo struct { + // The name of the restored PVC + PVCName string `json:"pvcName,omitempty"` + + // The namespace of the restored PVC + PVCNamespace string `json:"pvcNamespace,omitempty"` + + // The name of the restored PV, it is possible that in one item there is only PVC or PV info. + // But if both PVC and PV exist in one item of volume info, they should matched, and if the PV is bound to a PVC, + // they should coexist in one item. + PVName string `json:"pvName,omitempty"` + + // The way the volume data is restored. + RestoreMethod Method `json:"restoreMethod,omitempty"` + + // Whether the volume's data are restored via data movement + SnapshotDataMoved bool `json:"snapshotDataMoved"` + + CSISnapshotInfo *CSISnapshotInfo `json:"csiSnapshotInfo,omitempty"` + SnapshotDataMovementInfo *SnapshotDataMovementInfo `json:"snapshotDataMovementInfo,omitempty"` + NativeSnapshotInfo *NativeSnapshotInfo `json:"nativeSnapshotInfo,omitempty"` + PVRInfo *PodVolumeInfo `json:"pvrInfo,omitempty"` +} + // CSISnapshotInfo is used for displaying the CSI snapshot status type CSISnapshotInfo struct { // It's the storage provider's snapshot ID for CSI. @@ -101,7 +138,7 @@ type CSISnapshotInfo struct { VSCName string `json:"vscName"` // The Async Operation's ID. - OperationID string `json:"operationID"` + OperationID string `json:"operationID,omitempty"` } // SnapshotDataMovementInfo is used for displaying the snapshot data mover status. @@ -115,7 +152,7 @@ type SnapshotDataMovementInfo struct { // The name or ID of the snapshot associated object(SAO). // SAO is used to support local snapshots for the snapshot data mover, // e.g. it could be a VolumeSnapshot for CSI snapshot data movement. - RetainedSnapshot string `json:"retainedSnapshot"` + RetainedSnapshot string `json:"retainedSnapshot,omitempty"` // It's the filesystem repository's snapshot ID. SnapshotHandle string `json:"snapshotHandle"` @@ -145,13 +182,26 @@ type NativeSnapshotInfo struct { IOPS string `json:"iops"` } -// PodVolumeBackupInfo is used for displaying the PodVolumeBackup snapshot status. -type PodVolumeBackupInfo struct { - // It's the file-system uploader's snapshot ID for PodVolumeBackup. - SnapshotHandle string `json:"snapshotHandle"` +func newNativeSnapshotInfo(s *Snapshot) *NativeSnapshotInfo { + var iops int64 + if s.Spec.VolumeIOPS != nil { + iops = *s.Spec.VolumeIOPS + } + return &NativeSnapshotInfo{ + SnapshotHandle: s.Status.ProviderSnapshotID, + VolumeType: s.Spec.VolumeType, + VolumeAZ: s.Spec.VolumeAZ, + IOPS: strconv.FormatInt(iops, 10), + } +} + +// PodVolumeInfo is used for displaying the PodVolumeBackup/PodVolumeRestore snapshot status. +type PodVolumeInfo struct { + // It's the file-system uploader's snapshot ID for PodVolumeBackup/PodVolumeRestore. + SnapshotHandle string `json:"snapshotHandle,omitempty"` // The snapshot corresponding volume size. - Size int64 `json:"size"` + Size int64 `json:"size,omitempty"` // The type of the uploader that uploads the data. The valid values are `kopia` and `restic`. UploaderType string `json:"uploaderType"` @@ -167,7 +217,31 @@ type PodVolumeBackupInfo struct { PodNamespace string `json:"podNamespace"` // The PVB-taken k8s node's name. - NodeName string `json:"nodeName"` + // This field will be empty when the struct is used to represent a podvolumerestore. + NodeName string `json:"nodeName,omitempty"` +} + +func newPodVolumeInfoFromPVB(pvb *velerov1api.PodVolumeBackup) *PodVolumeInfo { + return &PodVolumeInfo{ + 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, + } +} + +func newPodVolumeInfoFromPVR(pvr *velerov1api.PodVolumeRestore) *PodVolumeInfo { + return &PodVolumeInfo{ + SnapshotHandle: pvr.Spec.SnapshotID, + Size: pvr.Status.Progress.TotalBytes, + UploaderType: pvr.Spec.UploaderType, + VolumeName: pvr.Spec.Volume, + PodName: pvr.Spec.Pod.Name, + PodNamespace: pvr.Spec.Pod.Namespace, + } } // PVInfo is used to store some PV information modified after creation. @@ -180,12 +254,12 @@ type PVInfo struct { Labels map[string]string `json:"labels"` } -// VolumesInformation contains the information needs by generating -// the backup VolumeInfo array. -type VolumesInformation struct { +// BackupVolumesInformation contains the information needs by generating +// the backup BackupVolumeInfo array. +type BackupVolumesInformation struct { // A map contains the backup-included PV detail content. The key is PV name. - pvMap map[string]pvcPvInfo - volumeInfos []*VolumeInfo + pvMap *pvcPvMap + volumeInfos []*BackupVolumeInfo logger logrus.FieldLogger crClient kbclient.Client @@ -205,30 +279,27 @@ type pvcPvInfo struct { PV corev1api.PersistentVolume } -func (v *VolumesInformation) Init() { - v.pvMap = make(map[string]pvcPvInfo) - v.volumeInfos = make([]*VolumeInfo, 0) +func (v *BackupVolumesInformation) Init() { + v.pvMap = &pvcPvMap{ + data: make(map[string]pvcPvInfo), + } + v.volumeInfos = make([]*BackupVolumeInfo, 0) } -func (v *VolumesInformation) InsertPVMap(pv corev1api.PersistentVolume, pvcName, pvcNamespace string) { +func (v *BackupVolumesInformation) InsertPVMap(pv corev1api.PersistentVolume, pvcName, pvcNamespace string) { if v.pvMap == nil { v.Init() } - - v.pvMap[pv.Name] = pvcPvInfo{ - PVCName: pvcName, - PVCNamespace: pvcNamespace, - PV: pv, - } + v.pvMap.insert(pv, pvcName, pvcNamespace) } -func (v *VolumesInformation) Result( +func (v *BackupVolumesInformation) Result( csiVolumeSnapshots []snapshotv1api.VolumeSnapshot, csiVolumeSnapshotContents []snapshotv1api.VolumeSnapshotContent, csiVolumesnapshotClasses []snapshotv1api.VolumeSnapshotClass, crClient kbclient.Client, logger logrus.FieldLogger, -) []*VolumeInfo { +) []*BackupVolumeInfo { v.logger = logger v.crClient = crClient v.volumeSnapshots = csiVolumeSnapshots @@ -245,12 +316,12 @@ func (v *VolumesInformation) Result( } // generateVolumeInfoForSkippedPV generate VolumeInfos for SkippedPV. -func (v *VolumesInformation) generateVolumeInfoForSkippedPV() { - tmpVolumeInfos := make([]*VolumeInfo, 0) +func (v *BackupVolumesInformation) generateVolumeInfoForSkippedPV() { + tmpVolumeInfos := make([]*BackupVolumeInfo, 0) for pvName, skippedReason := range v.SkippedPVs { - if pvcPVInfo := v.retrievePvcPvInfo(pvName, "", ""); pvcPVInfo != nil { - volumeInfo := &VolumeInfo{ + if pvcPVInfo := v.pvMap.retrieve(pvName, "", ""); pvcPVInfo != nil { + volumeInfo := &BackupVolumeInfo{ PVCName: pvcPVInfo.PVCName, PVCNamespace: pvcPVInfo.PVCNamespace, PVName: pvName, @@ -273,35 +344,24 @@ func (v *VolumesInformation) generateVolumeInfoForSkippedPV() { } // generateVolumeInfoForVeleroNativeSnapshot generate VolumeInfos for Velero native snapshot -func (v *VolumesInformation) generateVolumeInfoForVeleroNativeSnapshot() { - tmpVolumeInfos := make([]*VolumeInfo, 0) +func (v *BackupVolumesInformation) generateVolumeInfoForVeleroNativeSnapshot() { + tmpVolumeInfos := make([]*BackupVolumeInfo, 0) for _, nativeSnapshot := range v.NativeSnapshots { - var iops int64 - if nativeSnapshot.Spec.VolumeIOPS != nil { - iops = *nativeSnapshot.Spec.VolumeIOPS - } - - if pvcPVInfo := v.retrievePvcPvInfo(nativeSnapshot.Spec.PersistentVolumeName, "", ""); pvcPVInfo != nil { - volumeInfo := &VolumeInfo{ - BackupMethod: NativeSnapshot, - PVCName: pvcPVInfo.PVCName, - PVCNamespace: pvcPVInfo.PVCNamespace, - PVName: pvcPVInfo.PV.Name, - SnapshotDataMoved: false, - Skipped: false, - NativeSnapshotInfo: &NativeSnapshotInfo{ - SnapshotHandle: nativeSnapshot.Status.ProviderSnapshotID, - VolumeType: nativeSnapshot.Spec.VolumeType, - VolumeAZ: nativeSnapshot.Spec.VolumeAZ, - IOPS: strconv.FormatInt(iops, 10), - }, + if pvcPVInfo := v.pvMap.retrieve(nativeSnapshot.Spec.PersistentVolumeName, "", ""); pvcPVInfo != nil { + volumeInfo := &BackupVolumeInfo{ + BackupMethod: NativeSnapshot, + PVCName: pvcPVInfo.PVCName, + PVCNamespace: pvcPVInfo.PVCNamespace, + PVName: pvcPVInfo.PV.Name, + SnapshotDataMoved: false, + Skipped: false, + NativeSnapshotInfo: newNativeSnapshotInfo(nativeSnapshot), PVInfo: &PVInfo{ ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy), Labels: pvcPVInfo.PV.Labels, }, } - tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) } else { v.logger.Warnf("cannot find info for PV %s", nativeSnapshot.Spec.PersistentVolumeName) @@ -313,8 +373,8 @@ func (v *VolumesInformation) generateVolumeInfoForVeleroNativeSnapshot() { } // generateVolumeInfoForCSIVolumeSnapshot generate VolumeInfos for CSI VolumeSnapshot -func (v *VolumesInformation) generateVolumeInfoForCSIVolumeSnapshot() { - tmpVolumeInfos := make([]*VolumeInfo, 0) +func (v *BackupVolumesInformation) generateVolumeInfoForCSIVolumeSnapshot() { + tmpVolumeInfos := make([]*BackupVolumeInfo, 0) for _, volumeSnapshot := range v.volumeSnapshots { var volumeSnapshotClass *snapshotv1api.VolumeSnapshotClass @@ -376,8 +436,8 @@ func (v *VolumesInformation) generateVolumeInfoForCSIVolumeSnapshot() { if volumeSnapshotContent.Status.SnapshotHandle != nil { snapshotHandle = *volumeSnapshotContent.Status.SnapshotHandle } - if pvcPVInfo := v.retrievePvcPvInfo("", *volumeSnapshot.Spec.Source.PersistentVolumeClaimName, volumeSnapshot.Namespace); pvcPVInfo != nil { - volumeInfo := &VolumeInfo{ + if pvcPVInfo := v.pvMap.retrieve("", *volumeSnapshot.Spec.Source.PersistentVolumeClaimName, volumeSnapshot.Namespace); pvcPVInfo != nil { + volumeInfo := &BackupVolumeInfo{ BackupMethod: CSISnapshot, PVCName: pvcPVInfo.PVCName, PVCNamespace: pvcPVInfo.PVCNamespace, @@ -412,49 +472,25 @@ func (v *VolumesInformation) generateVolumeInfoForCSIVolumeSnapshot() { v.volumeInfos = append(v.volumeInfos, tmpVolumeInfos...) } -// generateVolumeInfoFromPVB generate VolumeInfo for PVB. -func (v *VolumesInformation) generateVolumeInfoFromPVB() { - tmpVolumeInfos := make([]*VolumeInfo, 0) - +// generateVolumeInfoFromPVB generate BackupVolumeInfo for PVB. +func (v *BackupVolumesInformation) generateVolumeInfoFromPVB() { + tmpVolumeInfos := make([]*BackupVolumeInfo, 0) for _, pvb := range v.PodVolumeBackups { - volumeInfo := &VolumeInfo{ - BackupMethod: PodVolumeBackup, - SnapshotDataMoved: false, - Skipped: false, - PVBInfo: &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, - }, - } - - if pvb.Status.StartTimestamp != nil { - volumeInfo.StartTimestamp = pvb.Status.StartTimestamp - } - - if pvb.Status.CompletionTimestamp != nil { - volumeInfo.CompletionTimestamp = pvb.Status.CompletionTimestamp + volumeInfo := &BackupVolumeInfo{ + BackupMethod: PodVolumeBackup, + SnapshotDataMoved: false, + Skipped: false, + StartTimestamp: pvb.Status.StartTimestamp, + CompletionTimestamp: pvb.Status.CompletionTimestamp, + PVBInfo: newPodVolumeInfoFromPVB(pvb), } - - pod := new(corev1api.Pod) - pvcName := "" - err := v.crClient.Get(context.TODO(), kbclient.ObjectKey{Namespace: pvb.Spec.Pod.Namespace, Name: pvb.Spec.Pod.Name}, pod) + pvcName, err := pvcByPodvolume(context.TODO(), v.crClient, pvb.Spec.Pod.Name, pvb.Spec.Pod.Namespace, pvb.Spec.Volume) if err != nil { - v.logger.WithError(err).Warn("Fail to get pod for PodVolumeBackup: ", pvb.Name) + v.logger.WithError(err).Warn("Fail to get PVC from 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 := v.retrievePvcPvInfo("", pvcName, pod.Namespace); pvcPVInfo != nil { + if pvcPVInfo := v.pvMap.retrieve("", pvcName, pvb.Spec.Pod.Namespace); pvcPVInfo != nil { volumeInfo.PVCName = pvcPVInfo.PVCName volumeInfo.PVCNamespace = pvcPVInfo.PVCNamespace volumeInfo.PVName = pvcPVInfo.PV.Name @@ -463,27 +499,25 @@ func (v *VolumesInformation) generateVolumeInfoFromPVB() { Labels: pvcPVInfo.PV.Labels, } } else { - v.logger.Warnf("Cannot find info for PVC %s/%s", pod.Namespace, pvcName) + v.logger.Warnf("Cannot find info for PVC %s/%s", pvb.Spec.Pod.Namespace, pvcName) continue } } else { v.logger.Debug("The PVB %s doesn't have a corresponding PVC", pvb.Name) } - tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) } - v.volumeInfos = append(v.volumeInfos, tmpVolumeInfos...) } -// generateVolumeInfoFromDataUpload generate VolumeInfo for DataUpload. -func (v *VolumesInformation) generateVolumeInfoFromDataUpload() { +// generateVolumeInfoFromDataUpload generate BackupVolumeInfo for DataUpload. +func (v *BackupVolumesInformation) generateVolumeInfoFromDataUpload() { if !features.IsEnabled(velerov1api.CSIFeatureFlag) { - v.logger.Debug("Skip generating VolumeInfo when the CSI feature is disabled.") + v.logger.Debug("Skip generating BackupVolumeInfo when the CSI feature is disabled.") return } - tmpVolumeInfos := make([]*VolumeInfo, 0) + tmpVolumeInfos := make([]*BackupVolumeInfo, 0) vsClassList := new(snapshotv1api.VolumeSnapshotClassList) if err := v.crClient.List(context.TODO(), vsClassList); err != nil { v.logger.WithError(err).Errorf("cannot list VolumeSnapshotClass %s", err.Error()) @@ -525,13 +559,13 @@ func (v *VolumesInformation) generateVolumeInfoFromDataUpload() { } } - if pvcPVInfo := v.retrievePvcPvInfo("", operation.Spec.ResourceIdentifier.Name, operation.Spec.ResourceIdentifier.Namespace); pvcPVInfo != nil { - dataMover := "velero" + if pvcPVInfo := v.pvMap.retrieve("", operation.Spec.ResourceIdentifier.Name, operation.Spec.ResourceIdentifier.Namespace); pvcPVInfo != nil { + dataMover := veleroDatamover if dataUpload.Spec.DataMover != "" { dataMover = dataUpload.Spec.DataMover } - volumeInfo := &VolumeInfo{ + volumeInfo := &BackupVolumeInfo{ BackupMethod: CSISnapshot, PVCName: pvcPVInfo.PVCName, PVCNamespace: pvcPVInfo.PVCNamespace, @@ -546,7 +580,7 @@ func (v *VolumesInformation) generateVolumeInfoFromDataUpload() { }, SnapshotDataMovementInfo: &SnapshotDataMovementInfo{ DataMover: dataMover, - UploaderType: "kopia", + UploaderType: kopia, OperationID: operation.Spec.OperationID, }, PVInfo: &PVInfo{ @@ -570,12 +604,21 @@ func (v *VolumesInformation) generateVolumeInfoFromDataUpload() { v.volumeInfos = append(v.volumeInfos, 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 { +type pvcPvMap struct { + data map[string]pvcPvInfo +} + +func (m *pvcPvMap) insert(pv corev1api.PersistentVolume, pvcName, pvcNamespace string) { + m.data[pv.Name] = pvcPvInfo{ + PVCName: pvcName, + PVCNamespace: pvcNamespace, + PV: pv, + } +} + +func (m *pvcPvMap) retrieve(pvName, pvcName, pvcNS string) *pvcPvInfo { if pvName != "" { - if info, ok := v.pvMap[pvName]; ok { + if info, ok := m.data[pvName]; ok { return &info } return nil @@ -585,7 +628,7 @@ func (v *VolumesInformation) retrievePvcPvInfo(pvName, pvcName, pvcNS string) *p return nil } - for _, info := range v.pvMap { + for _, info := range m.data { if pvcNS == info.PVCNamespace && pvcName == info.PVCName { return &info } @@ -593,3 +636,226 @@ func (v *VolumesInformation) retrievePvcPvInfo(pvName, pvcName, pvcNS string) *p return nil } + +func pvcByPodvolume(ctx context.Context, crClient kbclient.Client, podName, podNamespace, volumeName string) (string, error) { + pod := new(corev1api.Pod) + err := crClient.Get(ctx, kbclient.ObjectKey{Namespace: podNamespace, Name: podName}, pod) + if err != nil { + return "", errors.Wrap(err, "failed to get pod") + } + for _, volume := range pod.Spec.Volumes { + if volume.Name == volumeName && volume.PersistentVolumeClaim != nil { + return volume.PersistentVolumeClaim.ClaimName, nil + } + } + return "", nil +} + +// RestoreVolumeInfoTracker is used to track the volume information during restore. +// It is used to generate the RestoreVolumeInfo array. +type RestoreVolumeInfoTracker struct { + *sync.Mutex + restore *velerov1api.Restore + log logrus.FieldLogger + client kbclient.Client + pvPvc *pvcPvMap + + // map of PV name to the NativeSnapshotInfo from which the PV is restored + pvNativeSnapshotMap map[string]*NativeSnapshotInfo + // map of PV name to the CSISnapshot object from which the PV is restored + pvCSISnapshotMap map[string]snapshotv1api.VolumeSnapshot + datadownloadList *velerov2alpha1.DataDownloadList + pvrs []*velerov1api.PodVolumeRestore +} + +// Populate data objects in the tracker, which will be used to generate the RestoreVolumeInfo array in Result() +// The input param resourceList should be the final result of the restore. +func (t *RestoreVolumeInfoTracker) Populate(ctx context.Context, restoredResourceList map[string][]string) { + pvcs := RestoredPVCFromRestoredResourceList(restoredResourceList) + + t.Lock() + defer t.Unlock() + for item := range pvcs { + n := strings.Split(item, "/") + pvcNS, pvcName := n[0], n[1] + log := t.log.WithField("namespace", pvcNS).WithField("name", pvcName) + pvc := &corev1api.PersistentVolumeClaim{} + if err := t.client.Get(ctx, kbclient.ObjectKey{Namespace: pvcNS, Name: pvcName}, pvc); err != nil { + log.WithError(err).Error("Failed to get PVC") + continue + } + if pvc.Status.Phase != corev1api.ClaimBound || pvc.Spec.VolumeName == "" { + log.Info("PVC is not bound or has no volume name") + continue + } + pv := &corev1api.PersistentVolume{} + if err := t.client.Get(ctx, kbclient.ObjectKey{Name: pvc.Spec.VolumeName}, pv); err != nil { + log.WithError(err).Error("Failed to get PV") + } else { + t.pvPvc.insert(*pv, pvcName, pvcNS) + } + // Collect the CSI VolumeSnapshot objects referenced by the restored PVCs, + if pvc.Spec.DataSource != nil && pvc.Spec.DataSource.Kind == "VolumeSnapshot" { + vs := &snapshotv1api.VolumeSnapshot{} + if err := t.client.Get(ctx, kbclient.ObjectKey{Namespace: pvcNS, Name: pvc.Spec.DataSource.Name}, vs); err != nil { + log.WithError(err).Error("Failed to get VolumeSnapshot") + } else { + t.pvCSISnapshotMap[pv.Name] = *vs + } + } + } + if err := t.client.List(ctx, t.datadownloadList, &kbclient.ListOptions{ + Namespace: t.restore.Namespace, + LabelSelector: label.NewSelectorForRestore(t.restore.Name), + }); err != nil { + t.log.WithError(err).Error("Failed to List DataDownloads") + } +} + +// Result generates the RestoreVolumeInfo array, the data should come from the Tracker itself and it should not connect tokkkk API +// server again. +func (t *RestoreVolumeInfoTracker) Result() []*RestoreVolumeInfo { + volumeInfos := make([]*RestoreVolumeInfo, 0) + + // Generate RestoreVolumeInfo for PVRs + for _, pvr := range t.pvrs { + volumeInfo := &RestoreVolumeInfo{ + SnapshotDataMoved: false, + PVRInfo: newPodVolumeInfoFromPVR(pvr), + RestoreMethod: PodVolumeRestore, + } + pvcName, err := pvcByPodvolume(context.TODO(), t.client, pvr.Spec.Pod.Name, pvr.Spec.Pod.Namespace, pvr.Spec.Volume) + if err != nil { + t.log.WithError(err).Warn("Fail to get PVC from PodVolumeRestore: ", pvr.Name) + continue + } + if pvcName != "" { + volumeInfo.PVCName = pvcName + volumeInfo.PVCNamespace = pvr.Spec.Pod.Namespace + if pvcPVInfo := t.pvPvc.retrieve("", pvcName, pvr.Spec.Pod.Namespace); pvcPVInfo != nil { + volumeInfo.PVName = pvcPVInfo.PV.Name + } + } else { + // In this case, the volume is not bound to a PVC and + // the PVR will not be able to populate into the volume, so we'll skip it + t.log.Warnf("unable to get PVC for PodVolumeRestore %s/%s, pod: %s/%s, volume: %s", + pvr.Namespace, pvr.Name, pvr.Spec.Pod.Namespace, pvr.Spec.Pod.Name, pvr.Spec.Volume) + continue + } + volumeInfos = append(volumeInfos, volumeInfo) + } + + // Generate RestoreVolumeInfo for PVs restored from NativeSnapshots + for pvName, snapshotInfo := range t.pvNativeSnapshotMap { + volumeInfo := &RestoreVolumeInfo{ + PVName: pvName, + SnapshotDataMoved: false, + NativeSnapshotInfo: snapshotInfo, + RestoreMethod: NativeSnapshot, + } + if pvcPVInfo := t.pvPvc.retrieve(pvName, "", ""); pvcPVInfo != nil { + volumeInfo.PVCName = pvcPVInfo.PVCName + volumeInfo.PVCNamespace = pvcPVInfo.PVCNamespace + } + volumeInfos = append(volumeInfos, volumeInfo) + } + + // Generate RestoreVolumeInfo for PVs restored from CSISnapshots + for pvName, csiSnapshot := range t.pvCSISnapshotMap { + volumeInfo := &RestoreVolumeInfo{ + PVName: pvName, + SnapshotDataMoved: false, + RestoreMethod: CSISnapshot, + CSISnapshotInfo: &CSISnapshotInfo{ + SnapshotHandle: csiSnapshot.Annotations[VolumeSnapshotHandleAnnotation], + Size: csiSnapshot.Status.RestoreSize.Value(), + Driver: csiSnapshot.Annotations[CSIDriverNameAnnotation], + VSCName: *csiSnapshot.Spec.Source.VolumeSnapshotContentName, + }, + } + if pvcPVInfo := t.pvPvc.retrieve(pvName, "", ""); pvcPVInfo != nil { + volumeInfo.PVCName = pvcPVInfo.PVCName + volumeInfo.PVCNamespace = pvcPVInfo.PVCNamespace + } + volumeInfos = append(volumeInfos, volumeInfo) + } + + for _, dd := range t.datadownloadList.Items { + var pvcName, pvcNS, pvName string + if pvcPVInfo := t.pvPvc.retrieve(dd.Spec.TargetVolume.PV, dd.Spec.TargetVolume.PVC, dd.Spec.TargetVolume.Namespace); pvcPVInfo != nil { + pvcName = pvcPVInfo.PVCName + pvcNS = pvcPVInfo.PVCNamespace + pvName = pvcPVInfo.PV.Name + } else { + pvcName = dd.Spec.TargetVolume.PVC + pvName = dd.Spec.TargetVolume.PV + pvcNS = dd.Spec.TargetVolume.Namespace + } + operationID := dd.Labels[velerov1api.AsyncOperationIDLabel] + dataMover := veleroDatamover + if dd.Spec.DataMover != "" { + dataMover = dd.Spec.DataMover + } + volumeInfo := &RestoreVolumeInfo{ + PVName: pvName, + PVCNamespace: pvcNS, + PVCName: pvcName, + SnapshotDataMoved: true, + // The method will be CSI always no CSI related CRs are created during restore, because + // the datadownload was initiated in CSI plugin + // For the same reason, no CSI snapshot info will be populated into volumeInfo + RestoreMethod: CSISnapshot, + SnapshotDataMovementInfo: &SnapshotDataMovementInfo{ + DataMover: dataMover, + UploaderType: kopia, + SnapshotHandle: dd.Spec.SnapshotID, + OperationID: operationID, + }, + } + + volumeInfos = append(volumeInfos, volumeInfo) + } + + return volumeInfos +} + +func NewRestoreVolInfoTracker(restore *velerov1api.Restore, logger logrus.FieldLogger, client kbclient.Client) *RestoreVolumeInfoTracker { + return &RestoreVolumeInfoTracker{ + Mutex: &sync.Mutex{}, + client: client, + log: logger, + restore: restore, + pvPvc: &pvcPvMap{ + data: make(map[string]pvcPvInfo), + }, + pvNativeSnapshotMap: make(map[string]*NativeSnapshotInfo), + pvCSISnapshotMap: make(map[string]snapshotv1api.VolumeSnapshot), + datadownloadList: &velerov2alpha1.DataDownloadList{}, + } +} + +func (t *RestoreVolumeInfoTracker) TrackNativeSnapshot(pvName string, snapshotHandle, volumeType, volumeAZ string, iops int64) { + t.Lock() + defer t.Unlock() + t.pvNativeSnapshotMap[pvName] = &NativeSnapshotInfo{ + SnapshotHandle: snapshotHandle, + VolumeType: volumeType, + VolumeAZ: volumeAZ, + IOPS: strconv.FormatInt(iops, 10), + } +} + +func (t *RestoreVolumeInfoTracker) RenamePVForNativeSnapshot(oldName, newName string) { + t.Lock() + defer t.Unlock() + if snapshotInfo, ok := t.pvNativeSnapshotMap[oldName]; ok { + t.pvNativeSnapshotMap[newName] = snapshotInfo + delete(t.pvNativeSnapshotMap, oldName) + } +} + +func (t *RestoreVolumeInfoTracker) TrackPodVolume(pvr *velerov1api.PodVolumeRestore) { + t.Lock() + defer t.Unlock() + t.pvrs = append(t.pvrs, pvr) +} diff --git a/internal/volume/volumes_information_test.go b/internal/volume/volumes_information_test.go index bf8ee133e4..171f192152 100644 --- a/internal/volume/volumes_information_test.go +++ b/internal/volume/volumes_information_test.go @@ -18,8 +18,11 @@ package volume import ( "context" + "sync" "testing" + "github.com/stretchr/testify/assert" + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" @@ -43,7 +46,7 @@ func TestGenerateVolumeInfoForSkippedPV(t *testing.T) { name string skippedPVName string pvMap map[string]pvcPvInfo - expectedVolumeInfos []*VolumeInfo + expectedVolumeInfos []*BackupVolumeInfo }{ { name: "Cannot find info for PV", @@ -63,7 +66,7 @@ func TestGenerateVolumeInfoForSkippedPV(t *testing.T) { }, }, }, - expectedVolumeInfos: []*VolumeInfo{}, + expectedVolumeInfos: []*BackupVolumeInfo{}, }, { name: "Normal Skipped PV info", @@ -96,7 +99,7 @@ func TestGenerateVolumeInfoForSkippedPV(t *testing.T) { }, }, }, - expectedVolumeInfos: []*VolumeInfo{ + expectedVolumeInfos: []*BackupVolumeInfo{ { PVCName: "testPVC", PVCNamespace: "velero", @@ -116,7 +119,7 @@ func TestGenerateVolumeInfoForSkippedPV(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - volumesInfo := VolumesInformation{} + volumesInfo := BackupVolumesInformation{} volumesInfo.Init() if tc.skippedPVName != "" { @@ -127,7 +130,9 @@ func TestGenerateVolumeInfoForSkippedPV(t *testing.T) { if tc.pvMap != nil { for k, v := range tc.pvMap { - volumesInfo.pvMap[k] = v + if k == v.PV.Name { + volumesInfo.pvMap.insert(v.PV, v.PVCName, v.PVCNamespace) + } } } volumesInfo.logger = logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) @@ -143,7 +148,7 @@ func TestGenerateVolumeInfoForVeleroNativeSnapshot(t *testing.T) { name string nativeSnapshot Snapshot pvMap map[string]pvcPvInfo - expectedVolumeInfos []*VolumeInfo + expectedVolumeInfos []*BackupVolumeInfo }{ { name: "Native snapshot's IPOS pointer is nil", @@ -153,7 +158,7 @@ func TestGenerateVolumeInfoForVeleroNativeSnapshot(t *testing.T) { VolumeIOPS: nil, }, }, - expectedVolumeInfos: []*VolumeInfo{}, + expectedVolumeInfos: []*BackupVolumeInfo{}, }, { name: "Cannot find info for the PV", @@ -163,7 +168,7 @@ func TestGenerateVolumeInfoForVeleroNativeSnapshot(t *testing.T) { VolumeIOPS: int64Ptr(100), }, }, - expectedVolumeInfos: []*VolumeInfo{}, + expectedVolumeInfos: []*BackupVolumeInfo{}, }, { name: "Cannot find PV info in pvMap", @@ -193,7 +198,7 @@ func TestGenerateVolumeInfoForVeleroNativeSnapshot(t *testing.T) { ProviderSnapshotID: "pvc-b31e3386-4bbb-4937-95d-7934cd62-b0a1-494b-95d7-0687440e8d0c", }, }, - expectedVolumeInfos: []*VolumeInfo{}, + expectedVolumeInfos: []*BackupVolumeInfo{}, }, { name: "Normal native snapshot", @@ -223,7 +228,7 @@ func TestGenerateVolumeInfoForVeleroNativeSnapshot(t *testing.T) { ProviderSnapshotID: "pvc-b31e3386-4bbb-4937-95d-7934cd62-b0a1-494b-95d7-0687440e8d0c", }, }, - expectedVolumeInfos: []*VolumeInfo{ + expectedVolumeInfos: []*BackupVolumeInfo{ { PVCName: "testPVC", PVCNamespace: "velero", @@ -248,12 +253,14 @@ func TestGenerateVolumeInfoForVeleroNativeSnapshot(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - volumesInfo := VolumesInformation{} + volumesInfo := BackupVolumesInformation{} volumesInfo.Init() volumesInfo.NativeSnapshots = append(volumesInfo.NativeSnapshots, &tc.nativeSnapshot) if tc.pvMap != nil { for k, v := range tc.pvMap { - volumesInfo.pvMap[k] = v + if k == v.PV.Name { + volumesInfo.pvMap.insert(v.PV, v.PVCName, v.PVCNamespace) + } } } volumesInfo.logger = logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) @@ -274,7 +281,7 @@ func TestGenerateVolumeInfoForCSIVolumeSnapshot(t *testing.T) { volumeSnapshotClass snapshotv1api.VolumeSnapshotClass pvMap map[string]pvcPvInfo operation *itemoperation.BackupOperation - expectedVolumeInfos []*VolumeInfo + expectedVolumeInfos []*BackupVolumeInfo }{ { name: "VS doesn't have VolumeSnapshotClass name", @@ -285,7 +292,7 @@ func TestGenerateVolumeInfoForCSIVolumeSnapshot(t *testing.T) { }, Spec: snapshotv1api.VolumeSnapshotSpec{}, }, - expectedVolumeInfos: []*VolumeInfo{}, + expectedVolumeInfos: []*BackupVolumeInfo{}, }, { name: "VS doesn't have status", @@ -298,7 +305,7 @@ func TestGenerateVolumeInfoForCSIVolumeSnapshot(t *testing.T) { VolumeSnapshotClassName: stringPtr("testClass"), }, }, - expectedVolumeInfos: []*VolumeInfo{}, + expectedVolumeInfos: []*BackupVolumeInfo{}, }, { name: "VS doesn't have PVC", @@ -314,7 +321,7 @@ func TestGenerateVolumeInfoForCSIVolumeSnapshot(t *testing.T) { BoundVolumeSnapshotContentName: stringPtr("testContent"), }, }, - expectedVolumeInfos: []*VolumeInfo{}, + expectedVolumeInfos: []*BackupVolumeInfo{}, }, { name: "Cannot find VSC for VS", @@ -333,10 +340,10 @@ func TestGenerateVolumeInfoForCSIVolumeSnapshot(t *testing.T) { BoundVolumeSnapshotContentName: stringPtr("testContent"), }, }, - expectedVolumeInfos: []*VolumeInfo{}, + expectedVolumeInfos: []*BackupVolumeInfo{}, }, { - name: "Cannot find VolumeInfo for PVC", + name: "Cannot find BackupVolumeInfo for PVC", volumeSnapshot: snapshotv1api.VolumeSnapshot{ ObjectMeta: metav1.ObjectMeta{ Name: "testVS", @@ -354,7 +361,7 @@ func TestGenerateVolumeInfoForCSIVolumeSnapshot(t *testing.T) { }, volumeSnapshotClass: *builder.ForVolumeSnapshotClass("testClass").Driver("pd.csi.storage.gke.io").Result(), volumeSnapshotContent: *builder.ForVolumeSnapshotContent("testContent").Status(&snapshotv1api.VolumeSnapshotContentStatus{SnapshotHandle: stringPtr("testSnapshotHandle")}).Result(), - expectedVolumeInfos: []*VolumeInfo{}, + expectedVolumeInfos: []*BackupVolumeInfo{}, }, { name: "Normal VolumeSnapshot case", @@ -406,7 +413,7 @@ func TestGenerateVolumeInfoForCSIVolumeSnapshot(t *testing.T) { }, }, }, - expectedVolumeInfos: []*VolumeInfo{ + expectedVolumeInfos: []*BackupVolumeInfo{ { PVCName: "testPVC", PVCNamespace: "velero", @@ -434,12 +441,14 @@ func TestGenerateVolumeInfoForCSIVolumeSnapshot(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - volumesInfo := VolumesInformation{} + volumesInfo := BackupVolumesInformation{} volumesInfo.Init() if tc.pvMap != nil { for k, v := range tc.pvMap { - volumesInfo.pvMap[k] = v + if k == v.PV.Name { + volumesInfo.pvMap.insert(v.PV, v.PVCName, v.PVCNamespace) + } } } @@ -465,12 +474,12 @@ func TestGenerateVolumeInfoFromPVB(t *testing.T) { pvb *velerov1api.PodVolumeBackup pod *corev1api.Pod pvMap map[string]pvcPvInfo - expectedVolumeInfos []*VolumeInfo + expectedVolumeInfos []*BackupVolumeInfo }{ { name: "cannot find PVB's pod, should fail", pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").Result(), - expectedVolumeInfos: []*VolumeInfo{}, + expectedVolumeInfos: []*BackupVolumeInfo{}, }, { name: "PVB doesn't have a related PVC", @@ -491,13 +500,13 @@ func TestGenerateVolumeInfoFromPVB(t *testing.T) { }, }, ).Result(), - expectedVolumeInfos: []*VolumeInfo{ + expectedVolumeInfos: []*BackupVolumeInfo{ { PVCName: "", PVCNamespace: "", PVName: "", BackupMethod: PodVolumeBackup, - PVBInfo: &PodVolumeBackupInfo{ + PVBInfo: &PodVolumeInfo{ PodName: "testPod", PodNamespace: "velero", }, @@ -525,7 +534,7 @@ func TestGenerateVolumeInfoFromPVB(t *testing.T) { }, }, ).Result(), - expectedVolumeInfos: []*VolumeInfo{}, + expectedVolumeInfos: []*BackupVolumeInfo{}, }, { name: "PVB's volume has a PVC", @@ -563,7 +572,7 @@ func TestGenerateVolumeInfoFromPVB(t *testing.T) { }, }, ).Result(), - expectedVolumeInfos: []*VolumeInfo{ + expectedVolumeInfos: []*BackupVolumeInfo{ { PVCName: "testPVC", PVCNamespace: "velero", @@ -571,7 +580,7 @@ func TestGenerateVolumeInfoFromPVB(t *testing.T) { BackupMethod: PodVolumeBackup, StartTimestamp: &now, CompletionTimestamp: &now, - PVBInfo: &PodVolumeBackupInfo{ + PVBInfo: &PodVolumeInfo{ PodName: "testPod", PodNamespace: "velero", }, @@ -586,7 +595,7 @@ func TestGenerateVolumeInfoFromPVB(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - volumesInfo := VolumesInformation{} + volumesInfo := BackupVolumesInformation{} volumesInfo.Init() volumesInfo.crClient = velerotest.NewFakeControllerRuntimeClient(t) @@ -594,7 +603,9 @@ func TestGenerateVolumeInfoFromPVB(t *testing.T) { if tc.pvMap != nil { for k, v := range tc.pvMap { - volumesInfo.pvMap[k] = v + if k == v.PV.Name { + volumesInfo.pvMap.insert(v.PV, v.PVCName, v.PVCNamespace) + } } } if tc.pod != nil { @@ -621,7 +632,7 @@ func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { dataUpload *velerov2alpha1.DataUpload operation *itemoperation.BackupOperation pvMap map[string]pvcPvInfo - expectedVolumeInfos []*VolumeInfo + expectedVolumeInfos []*BackupVolumeInfo }{ { name: "Operation is not for PVC", @@ -635,7 +646,7 @@ func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { }, }, }, - expectedVolumeInfos: []*VolumeInfo{}, + expectedVolumeInfos: []*BackupVolumeInfo{}, }, { name: "Operation doesn't have DataUpload PostItemOperation", @@ -659,7 +670,7 @@ func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { }, }, }, - expectedVolumeInfos: []*VolumeInfo{}, + expectedVolumeInfos: []*BackupVolumeInfo{}, }, { name: "DataUpload cannot be found for operation", @@ -686,7 +697,7 @@ func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { }, }, }, - expectedVolumeInfos: []*VolumeInfo{}, + expectedVolumeInfos: []*BackupVolumeInfo{}, }, { name: "VolumeSnapshotClass cannot be found for operation", @@ -731,7 +742,7 @@ func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { }, }, }, - expectedVolumeInfos: []*VolumeInfo{ + expectedVolumeInfos: []*BackupVolumeInfo{ { PVCName: "testPVC", PVCNamespace: "velero", @@ -802,7 +813,7 @@ func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { }, }, }, - expectedVolumeInfos: []*VolumeInfo{ + expectedVolumeInfos: []*BackupVolumeInfo{ { PVCName: "testPVC", PVCNamespace: "velero", @@ -833,7 +844,7 @@ func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - volumesInfo := VolumesInformation{} + volumesInfo := BackupVolumesInformation{} volumesInfo.Init() if tc.operation != nil { @@ -842,7 +853,9 @@ func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { if tc.pvMap != nil { for k, v := range tc.pvMap { - volumesInfo.pvMap[k] = v + if k == v.PV.Name { + volumesInfo.pvMap.insert(v.PV, v.PVCName, v.PVCNamespace) + } } } @@ -868,6 +881,291 @@ func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { } } +func TestRestoreVolumeInfoTrackNativeSnapshot(t *testing.T) { + fakeCilent := velerotest.NewFakeControllerRuntimeClient(t) + + restore := builder.ForRestore("velero", "testRestore").Result() + tracker := NewRestoreVolInfoTracker(restore, logrus.New(), fakeCilent) + tracker.TrackNativeSnapshot("testPV", "snap-001", "ebs", "us-west-1", 10000) + assert.Equal(t, *tracker.pvNativeSnapshotMap["testPV"], NativeSnapshotInfo{ + SnapshotHandle: "snap-001", + VolumeType: "ebs", + VolumeAZ: "us-west-1", + IOPS: "10000", + }) + tracker.TrackNativeSnapshot("testPV", "snap-002", "ebs", "us-west-2", 15000) + assert.Equal(t, *tracker.pvNativeSnapshotMap["testPV"], NativeSnapshotInfo{ + SnapshotHandle: "snap-002", + VolumeType: "ebs", + VolumeAZ: "us-west-2", + IOPS: "15000", + }) + tracker.RenamePVForNativeSnapshot("testPV", "newPV") + _, ok := tracker.pvNativeSnapshotMap["testPV"] + assert.False(t, ok) + assert.Equal(t, *tracker.pvNativeSnapshotMap["newPV"], NativeSnapshotInfo{ + SnapshotHandle: "snap-002", + VolumeType: "ebs", + VolumeAZ: "us-west-2", + IOPS: "15000", + }) +} + +func TestRestoreVolumeInfoResult(t *testing.T) { + fakeClient := velerotest.NewFakeControllerRuntimeClient(t, + builder.ForPod("testNS", "testPod"). + Volumes(builder.ForVolume("data-volume-1").PersistentVolumeClaimSource("testPVC2").Result()). + Result()) + testRestore := builder.ForRestore("velero", "testRestore").Result() + tests := []struct { + name string + tracker *RestoreVolumeInfoTracker + expectResultValues []RestoreVolumeInfo + }{ + { + name: "empty", + tracker: &RestoreVolumeInfoTracker{ + Mutex: &sync.Mutex{}, + client: fakeClient, + log: logrus.New(), + restore: testRestore, + pvPvc: &pvcPvMap{ + data: make(map[string]pvcPvInfo), + }, + pvNativeSnapshotMap: map[string]*NativeSnapshotInfo{}, + pvCSISnapshotMap: map[string]snapshotv1api.VolumeSnapshot{}, + datadownloadList: &velerov2alpha1.DataDownloadList{}, + pvrs: []*velerov1api.PodVolumeRestore{}, + }, + expectResultValues: []RestoreVolumeInfo{}, + }, + { + name: "native snapshot and podvolumes", + tracker: &RestoreVolumeInfoTracker{ + Mutex: &sync.Mutex{}, + client: fakeClient, + log: logrus.New(), + restore: testRestore, + pvPvc: &pvcPvMap{ + data: map[string]pvcPvInfo{ + "testPV": { + PVCName: "testPVC", + PVCNamespace: "testNS", + PV: *builder.ForPersistentVolume("testPV").Result(), + }, + "testPV2": { + PVCName: "testPVC2", + PVCNamespace: "testNS", + PV: *builder.ForPersistentVolume("testPV2").Result(), + }, + }, + }, + pvNativeSnapshotMap: map[string]*NativeSnapshotInfo{ + "testPV": { + SnapshotHandle: "snap-001", + VolumeType: "ebs", + VolumeAZ: "us-west-1", + IOPS: "10000", + }, + }, + pvCSISnapshotMap: map[string]snapshotv1api.VolumeSnapshot{}, + datadownloadList: &velerov2alpha1.DataDownloadList{}, + pvrs: []*velerov1api.PodVolumeRestore{ + builder.ForPodVolumeRestore("velero", "testRestore-1234"). + PodNamespace("testNS"). + PodName("testPod"). + Volume("data-volume-1"). + UploaderType("kopia"). + SnapshotID("pvr-snap-001").Result(), + }, + }, + expectResultValues: []RestoreVolumeInfo{ + { + PVCName: "testPVC2", + PVCNamespace: "testNS", + PVName: "testPV2", + RestoreMethod: PodVolumeRestore, + SnapshotDataMoved: false, + PVRInfo: &PodVolumeInfo{ + SnapshotHandle: "pvr-snap-001", + PodName: "testPod", + PodNamespace: "testNS", + UploaderType: "kopia", + VolumeName: "data-volume-1", + }, + }, + { + PVCName: "testPVC", + PVCNamespace: "testNS", + PVName: "testPV", + RestoreMethod: NativeSnapshot, + SnapshotDataMoved: false, + NativeSnapshotInfo: &NativeSnapshotInfo{ + SnapshotHandle: "snap-001", + VolumeType: "ebs", + VolumeAZ: "us-west-1", + IOPS: "10000", + }, + }, + }, + }, + { + name: "CSI snapshot without datamovement and podvolumes", + tracker: &RestoreVolumeInfoTracker{ + Mutex: &sync.Mutex{}, + client: fakeClient, + log: logrus.New(), + restore: testRestore, + pvPvc: &pvcPvMap{ + data: map[string]pvcPvInfo{ + "testPV": { + PVCName: "testPVC", + PVCNamespace: "testNS", + PV: *builder.ForPersistentVolume("testPV").Result(), + }, + "testPV2": { + PVCName: "testPVC2", + PVCNamespace: "testNS", + PV: *builder.ForPersistentVolume("testPV2").Result(), + }, + }, + }, + pvNativeSnapshotMap: map[string]*NativeSnapshotInfo{}, + pvCSISnapshotMap: map[string]snapshotv1api.VolumeSnapshot{ + "testPV": *builder.ForVolumeSnapshot("sourceNS", "testCSISnapshot"). + ObjectMeta( + builder.WithAnnotations(VolumeSnapshotHandleAnnotation, "csi-snap-001", + CSIDriverNameAnnotation, "test-csi-driver"), + ).SourceVolumeSnapshotContentName("test-vsc-001"). + Status().RestoreSize("1Gi").Result(), + }, + datadownloadList: &velerov2alpha1.DataDownloadList{}, + pvrs: []*velerov1api.PodVolumeRestore{ + builder.ForPodVolumeRestore("velero", "testRestore-1234"). + PodNamespace("testNS"). + PodName("testPod"). + Volume("data-volume-1"). + UploaderType("kopia"). + SnapshotID("pvr-snap-001").Result(), + }, + }, + expectResultValues: []RestoreVolumeInfo{ + { + PVCName: "testPVC2", + PVCNamespace: "testNS", + PVName: "testPV2", + RestoreMethod: PodVolumeRestore, + SnapshotDataMoved: false, + PVRInfo: &PodVolumeInfo{ + SnapshotHandle: "pvr-snap-001", + PodName: "testPod", + PodNamespace: "testNS", + UploaderType: "kopia", + VolumeName: "data-volume-1", + }, + }, + { + PVCName: "testPVC", + PVCNamespace: "testNS", + PVName: "testPV", + RestoreMethod: CSISnapshot, + SnapshotDataMoved: false, + CSISnapshotInfo: &CSISnapshotInfo{ + SnapshotHandle: "csi-snap-001", + VSCName: "test-vsc-001", + Size: 1073741824, + Driver: "test-csi-driver", + }, + }, + }, + }, + { + name: "CSI snapshot with datamovement", + tracker: &RestoreVolumeInfoTracker{ + Mutex: &sync.Mutex{}, + client: fakeClient, + log: logrus.New(), + restore: testRestore, + pvPvc: &pvcPvMap{ + data: map[string]pvcPvInfo{ + "testPV": { + PVCName: "testPVC", + PVCNamespace: "testNS", + PV: *builder.ForPersistentVolume("testPV").Result(), + }, + "testPV2": { + PVCName: "testPVC2", + PVCNamespace: "testNS", + PV: *builder.ForPersistentVolume("testPV2").Result(), + }, + }, + }, + pvNativeSnapshotMap: map[string]*NativeSnapshotInfo{}, + pvCSISnapshotMap: map[string]snapshotv1api.VolumeSnapshot{}, + datadownloadList: &velerov2alpha1.DataDownloadList{ + Items: []velerov2alpha1.DataDownload{ + *builder.ForDataDownload("velero", "testDataDownload-1"). + ObjectMeta(builder.WithLabels(velerov1api.AsyncOperationIDLabel, "dd-operation-001")). + SnapshotID("dd-snap-001"). + TargetVolume(velerov2alpha1.TargetVolumeSpec{ + PVC: "testPVC", + Namespace: "testNS", + }). + Result(), + *builder.ForDataDownload("velero", "testDataDownload-2"). + ObjectMeta(builder.WithLabels(velerov1api.AsyncOperationIDLabel, "dd-operation-002")). + SnapshotID("dd-snap-002"). + TargetVolume(velerov2alpha1.TargetVolumeSpec{ + PVC: "testPVC2", + Namespace: "testNS", + }). + Result(), + }, + }, + pvrs: []*velerov1api.PodVolumeRestore{}, + }, + expectResultValues: []RestoreVolumeInfo{ + { + PVCName: "testPVC", + PVCNamespace: "testNS", + PVName: "testPV", + RestoreMethod: CSISnapshot, + SnapshotDataMoved: true, + SnapshotDataMovementInfo: &SnapshotDataMovementInfo{ + DataMover: "velero", + UploaderType: kopia, + SnapshotHandle: "dd-snap-001", + OperationID: "dd-operation-001", + }, + }, + { + PVCName: "testPVC2", + PVCNamespace: "testNS", + PVName: "testPV2", + RestoreMethod: CSISnapshot, + SnapshotDataMoved: true, + SnapshotDataMovementInfo: &SnapshotDataMovementInfo{ + DataMover: "velero", + UploaderType: kopia, + SnapshotHandle: "dd-snap-002", + OperationID: "dd-operation-002", + }, + }, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := tc.tracker.Result() + valuesList := []RestoreVolumeInfo{} + for _, item := range result { + valuesList = append(valuesList, *item) + } + assert.Equal(t, tc.expectResultValues, valuesList) + }) + } +} + func stringPtr(str string) *string { return &str } diff --git a/pkg/apis/velero/v1/download_request_types.go b/pkg/apis/velero/v1/download_request_types.go index 07f44b38d5..a2e0404025 100644 --- a/pkg/apis/velero/v1/download_request_types.go +++ b/pkg/apis/velero/v1/download_request_types.go @@ -42,6 +42,7 @@ const ( DownloadTargetKindCSIBackupVolumeSnapshots DownloadTargetKind = "CSIBackupVolumeSnapshots" DownloadTargetKindCSIBackupVolumeSnapshotContents DownloadTargetKind = "CSIBackupVolumeSnapshotContents" DownloadTargetKindBackupVolumeInfos DownloadTargetKind = "BackupVolumeInfos" + DownloadTargetKindRestoreVolumeInfo DownloadTargetKind = "RestoreVolumeInfo" ) // DownloadTarget is the specification for what kind of file to download, and the name of the diff --git a/pkg/backup/backup.go b/pkg/backup/backup.go index 55f2cd072a..cc2aefe900 100644 --- a/pkg/backup/backup.go +++ b/pkg/backup/backup.go @@ -798,7 +798,7 @@ type tarWriter interface { func (kb *kubernetesBackupper) getVolumeInfos( backup velerov1api.Backup, log logrus.FieldLogger, -) (persistence.BackupStore, []*volume.VolumeInfo, error) { +) (persistence.BackupStore, []*volume.BackupVolumeInfo, error) { location := &velerov1api.BackupStorageLocation{} if err := kb.kbClient.Get(context.Background(), kbclient.ObjectKey{ Namespace: backup.Namespace, @@ -825,7 +825,7 @@ func (kb *kubernetesBackupper) getVolumeInfos( // updateVolumeInfos update the VolumeInfos according to the AsyncOperations func updateVolumeInfos( - volumeInfos []*volume.VolumeInfo, + volumeInfos []*volume.BackupVolumeInfo, unstructuredItems []unstructured.Unstructured, operations []*itemoperation.BackupOperation, log logrus.FieldLogger, @@ -874,7 +874,7 @@ func updateVolumeInfos( func putVolumeInfos( backupName string, - volumeInfos []*volume.VolumeInfo, + volumeInfos []*volume.BackupVolumeInfo, backupStore persistence.BackupStore, ) error { backupVolumeInfoBuf := new(bytes.Buffer) diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index f083665eee..72ff3caeae 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -4454,7 +4454,7 @@ func TestGetVolumeInfos(t *testing.T) { backupStore := new(persistencemocks.BackupStore) h.backupper.pluginManager = func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager } h.backupper.backupStoreGetter = NewFakeSingleObjectBackupStoreGetter(backupStore) - backupStore.On("GetBackupVolumeInfos", "backup-01").Return([]*volume.VolumeInfo{}, nil) + backupStore.On("GetBackupVolumeInfos", "backup-01").Return([]*volume.BackupVolumeInfo{}, nil) pluginManager.On("CleanupClients").Return() backup := builder.ForBackup("velero", "backup-01").StorageLocation("default").Result() @@ -4474,8 +4474,8 @@ func TestUpdateVolumeInfos(t *testing.T) { name string operations []*itemoperation.BackupOperation dataUpload *velerov2alpha1.DataUpload - volumeInfos []*volume.VolumeInfo - expectedVolumeInfos []*volume.VolumeInfo + volumeInfos []*volume.BackupVolumeInfo + expectedVolumeInfos []*volume.BackupVolumeInfo }{ { name: "CSISnapshot VolumeInfo update", @@ -4489,7 +4489,7 @@ func TestUpdateVolumeInfos(t *testing.T) { }, }, }, - volumeInfos: []*volume.VolumeInfo{ + volumeInfos: []*volume.BackupVolumeInfo{ { BackupMethod: volume.CSISnapshot, CompletionTimestamp: &metav1.Time{}, @@ -4498,7 +4498,7 @@ func TestUpdateVolumeInfos(t *testing.T) { }, }, }, - expectedVolumeInfos: []*volume.VolumeInfo{ + expectedVolumeInfos: []*volume.BackupVolumeInfo{ { BackupMethod: volume.CSISnapshot, CompletionTimestamp: &now, @@ -4519,7 +4519,7 @@ func TestUpdateVolumeInfos(t *testing.T) { SourceNamespace("ns-1"). SourcePVC("pvc-1"). Result(), - volumeInfos: []*volume.VolumeInfo{ + volumeInfos: []*volume.BackupVolumeInfo{ { PVCName: "pvc-1", PVCNamespace: "ns-1", @@ -4529,7 +4529,7 @@ func TestUpdateVolumeInfos(t *testing.T) { }, }, }, - expectedVolumeInfos: []*volume.VolumeInfo{ + expectedVolumeInfos: []*volume.BackupVolumeInfo{ { PVCName: "pvc-1", PVCNamespace: "ns-1", @@ -4572,7 +4572,7 @@ func TestPutVolumeInfos(t *testing.T) { backupStore.On("PutBackupVolumeInfos", mock.Anything, mock.Anything).Return(nil) - require.NoError(t, putVolumeInfos(backupName, []*volume.VolumeInfo{}, backupStore)) + require.NoError(t, putVolumeInfos(backupName, []*volume.BackupVolumeInfo{}, backupStore)) } type fakeSingleObjectBackupStoreGetter struct { diff --git a/pkg/backup/request.go b/pkg/backup/request.go index b78200af6c..ab146d13e5 100644 --- a/pkg/backup/request.go +++ b/pkg/backup/request.go @@ -52,11 +52,11 @@ type Request struct { itemOperationsList *[]*itemoperation.BackupOperation ResPolicies *resourcepolicies.Policies SkippedPVTracker *skipPVTracker - VolumesInformation volume.VolumesInformation + VolumesInformation volume.BackupVolumesInformation } -// VolumesInformation contains the information needs by generating -// the backup VolumeInfo array. +// BackupVolumesInformation contains the information needs by generating +// the backup BackupVolumeInfo array. // GetItemOperationsList returns ItemOperationsList, initializing it if necessary func (r *Request) GetItemOperationsList() *[]*itemoperation.BackupOperation { diff --git a/pkg/builder/volume_snapshot_builder.go b/pkg/builder/volume_snapshot_builder.go index 91ea7b5c6e..36a2ba1739 100644 --- a/pkg/builder/volume_snapshot_builder.go +++ b/pkg/builder/volume_snapshot_builder.go @@ -75,6 +75,12 @@ func (v *VolumeSnapshotBuilder) SourcePVC(name string) *VolumeSnapshotBuilder { return v } +// SourceVolumeSnapshotContentName set the built VolumeSnapshot's spec.Source.VolumeSnapshotContentName +func (v *VolumeSnapshotBuilder) SourceVolumeSnapshotContentName(name string) *VolumeSnapshotBuilder { + v.object.Spec.Source.VolumeSnapshotContentName = &name + return v +} + // RestoreSize set the built VolumeSnapshot's status.RestoreSize. func (v *VolumeSnapshotBuilder) RestoreSize(size string) *VolumeSnapshotBuilder { resourceSize := resource.MustParse(size) diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index 13c939d6b8..fac74b1ddd 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -991,6 +991,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string s.config.formatFlag.Parse(), s.config.defaultItemOperationTimeout, s.config.disableInformerCache, + s.crClient, ) if err = r.SetupWithManager(s.mgr); err != nil { diff --git a/pkg/cmd/util/output/backup_describer.go b/pkg/cmd/util/output/backup_describer.go index d198c80cbe..d783f0c11a 100644 --- a/pkg/cmd/util/output/backup_describer.go +++ b/pkg/cmd/util/output/backup_describer.go @@ -439,8 +439,8 @@ func describeBackupVolumes(ctx context.Context, kbClient kbclient.Client, d *Des d.Println("Backup Volumes:") - nativeSnapshots := []*volume.VolumeInfo{} - csiSnapshots := []*volume.VolumeInfo{} + nativeSnapshots := []*volume.BackupVolumeInfo{} + csiSnapshots := []*volume.BackupVolumeInfo{} legacyInfoSource := false buf := new(bytes.Buffer) @@ -463,7 +463,7 @@ func describeBackupVolumes(ctx context.Context, kbClient kbclient.Client, d *Des d.Printf("\t\n", err) return } else { - var volumeInfos []volume.VolumeInfo + var volumeInfos []volume.BackupVolumeInfo if err := json.NewDecoder(buf).Decode(&volumeInfos); err != nil { d.Printf("\t\n", err) return @@ -488,9 +488,9 @@ func describeBackupVolumes(ctx context.Context, kbClient kbclient.Client, d *Des describePodVolumeBackups(d, details, podVolumeBackupCRs) } -func retrieveNativeSnapshotLegacy(ctx context.Context, kbClient kbclient.Client, backup *velerov1api.Backup, insecureSkipTLSVerify bool, caCertPath string) ([]*volume.VolumeInfo, error) { +func retrieveNativeSnapshotLegacy(ctx context.Context, kbClient kbclient.Client, backup *velerov1api.Backup, insecureSkipTLSVerify bool, caCertPath string) ([]*volume.BackupVolumeInfo, error) { status := backup.Status - nativeSnapshots := []*volume.VolumeInfo{} + nativeSnapshots := []*volume.BackupVolumeInfo{} if status.VolumeSnapshotsAttempted == 0 { return nativeSnapshots, nil @@ -507,7 +507,7 @@ func retrieveNativeSnapshotLegacy(ctx context.Context, kbClient kbclient.Client, } for _, snap := range snapshots { - volumeInfo := volume.VolumeInfo{ + volumeInfo := volume.BackupVolumeInfo{ PVName: snap.Spec.PersistentVolumeName, NativeSnapshotInfo: &volume.NativeSnapshotInfo{ SnapshotHandle: snap.Status.ProviderSnapshotID, @@ -526,9 +526,9 @@ func retrieveNativeSnapshotLegacy(ctx context.Context, kbClient kbclient.Client, return nativeSnapshots, nil } -func retrieveCSISnapshotLegacy(ctx context.Context, kbClient kbclient.Client, backup *velerov1api.Backup, insecureSkipTLSVerify bool, caCertPath string) ([]*volume.VolumeInfo, error) { +func retrieveCSISnapshotLegacy(ctx context.Context, kbClient kbclient.Client, backup *velerov1api.Backup, insecureSkipTLSVerify bool, caCertPath string) ([]*volume.BackupVolumeInfo, error) { status := backup.Status - csiSnapshots := []*volume.VolumeInfo{} + csiSnapshots := []*volume.BackupVolumeInfo{} if status.CSIVolumeSnapshotsAttempted == 0 { return csiSnapshots, nil @@ -557,7 +557,7 @@ func retrieveCSISnapshotLegacy(ctx context.Context, kbClient kbclient.Client, ba } for _, vsc := range vscList { - volInfo := volume.VolumeInfo{ + volInfo := volume.BackupVolumeInfo{ PreserveLocalSnapshot: true, CSISnapshotInfo: &volume.CSISnapshotInfo{ VSCName: vsc.Name, @@ -598,7 +598,7 @@ func retrieveCSISnapshotLegacy(ctx context.Context, kbClient kbclient.Client, ba return csiSnapshots, nil } -func describeNativeSnapshots(d *Describer, details bool, infos []*volume.VolumeInfo) { +func describeNativeSnapshots(d *Describer, details bool, infos []*volume.BackupVolumeInfo) { if len(infos) == 0 { d.Printf("\tVelero-Native Snapshots: \n") return @@ -610,7 +610,7 @@ func describeNativeSnapshots(d *Describer, details bool, infos []*volume.VolumeI } } -func describNativeSnapshot(d *Describer, details bool, info *volume.VolumeInfo) { +func describNativeSnapshot(d *Describer, details bool, info *volume.BackupVolumeInfo) { if details { d.Printf("\t\t%s:\n", info.PVName) d.Printf("\t\t\tSnapshot ID:\t%s\n", info.NativeSnapshotInfo.SnapshotHandle) @@ -622,7 +622,7 @@ func describNativeSnapshot(d *Describer, details bool, info *volume.VolumeInfo) } } -func describeCSISnapshots(d *Describer, details bool, infos []*volume.VolumeInfo, legacyInfoSource bool) { +func describeCSISnapshots(d *Describer, details bool, infos []*volume.BackupVolumeInfo, legacyInfoSource bool) { if len(infos) == 0 { if legacyInfoSource { d.Printf("\tCSI Snapshots: \n") @@ -638,14 +638,14 @@ func describeCSISnapshots(d *Describer, details bool, infos []*volume.VolumeInfo } } -func describeCSISnapshot(d *Describer, details bool, info *volume.VolumeInfo) { +func describeCSISnapshot(d *Describer, details bool, info *volume.BackupVolumeInfo) { d.Printf("\t\t%s:\n", fmt.Sprintf("%s/%s", info.PVCNamespace, info.PVCName)) describeLocalSnapshot(d, details, info) describeDataMovement(d, details, info) } -func describeLocalSnapshot(d *Describer, details bool, info *volume.VolumeInfo) { +func describeLocalSnapshot(d *Describer, details bool, info *volume.BackupVolumeInfo) { if !info.PreserveLocalSnapshot { return } @@ -665,7 +665,7 @@ func describeLocalSnapshot(d *Describer, details bool, info *volume.VolumeInfo) } } -func describeDataMovement(d *Describer, details bool, info *volume.VolumeInfo) { +func describeDataMovement(d *Describer, details bool, info *volume.BackupVolumeInfo) { if !info.SnapshotDataMoved { return } diff --git a/pkg/cmd/util/output/backup_describer_test.go b/pkg/cmd/util/output/backup_describer_test.go index 7789b02607..783cf12c45 100644 --- a/pkg/cmd/util/output/backup_describer_test.go +++ b/pkg/cmd/util/output/backup_describer_test.go @@ -325,13 +325,13 @@ OrderedResources: func TestDescribeNativeSnapshots(t *testing.T) { testcases := []struct { name string - volumeInfo []*volume.VolumeInfo + volumeInfo []*volume.BackupVolumeInfo inputDetails bool expect string }{ { name: "no details", - volumeInfo: []*volume.VolumeInfo{ + volumeInfo: []*volume.BackupVolumeInfo{ { BackupMethod: volume.NativeSnapshot, PVName: "pv-1", @@ -349,7 +349,7 @@ func TestDescribeNativeSnapshots(t *testing.T) { }, { name: "details", - volumeInfo: []*volume.VolumeInfo{ + volumeInfo: []*volume.BackupVolumeInfo{ { BackupMethod: volume.NativeSnapshot, PVName: "pv-1", @@ -390,27 +390,27 @@ func TestDescribeNativeSnapshots(t *testing.T) { func TestCSISnapshots(t *testing.T) { testcases := []struct { name string - volumeInfo []*volume.VolumeInfo + volumeInfo []*volume.BackupVolumeInfo inputDetails bool expect string legacyInfoSource bool }{ { name: "empty info, not legacy", - volumeInfo: []*volume.VolumeInfo{}, + volumeInfo: []*volume.BackupVolumeInfo{}, expect: ` CSI Snapshots: `, }, { name: "empty info, legacy", - volumeInfo: []*volume.VolumeInfo{}, + volumeInfo: []*volume.BackupVolumeInfo{}, legacyInfoSource: true, expect: ` CSI Snapshots: `, }, { name: "no details, local snapshot", - volumeInfo: []*volume.VolumeInfo{ + volumeInfo: []*volume.BackupVolumeInfo{ { BackupMethod: volume.CSISnapshot, PVCNamespace: "pvc-ns-1", @@ -432,7 +432,7 @@ func TestCSISnapshots(t *testing.T) { }, { name: "details, local snapshot", - volumeInfo: []*volume.VolumeInfo{ + volumeInfo: []*volume.BackupVolumeInfo{ { BackupMethod: volume.CSISnapshot, PVCNamespace: "pvc-ns-2", @@ -460,7 +460,7 @@ func TestCSISnapshots(t *testing.T) { }, { name: "no details, data movement", - volumeInfo: []*volume.VolumeInfo{ + volumeInfo: []*volume.BackupVolumeInfo{ { BackupMethod: volume.CSISnapshot, PVCNamespace: "pvc-ns-3", @@ -481,7 +481,7 @@ func TestCSISnapshots(t *testing.T) { }, { name: "details, data movement", - volumeInfo: []*volume.VolumeInfo{ + volumeInfo: []*volume.BackupVolumeInfo{ { BackupMethod: volume.CSISnapshot, PVCNamespace: "pvc-ns-4", @@ -506,7 +506,7 @@ func TestCSISnapshots(t *testing.T) { }, { name: "details, data movement, data mover is empty", - volumeInfo: []*volume.VolumeInfo{ + volumeInfo: []*volume.BackupVolumeInfo{ { BackupMethod: volume.CSISnapshot, PVCNamespace: "pvc-ns-5", diff --git a/pkg/cmd/util/output/backup_structured_describer.go b/pkg/cmd/util/output/backup_structured_describer.go index 20d6371c04..38b728d411 100644 --- a/pkg/cmd/util/output/backup_structured_describer.go +++ b/pkg/cmd/util/output/backup_structured_describer.go @@ -308,8 +308,8 @@ func describeBackupVolumesInSF(ctx context.Context, kbClient kbclient.Client, ba backupVolumes := make(map[string]interface{}) - nativeSnapshots := []*volume.VolumeInfo{} - csiSnapshots := []*volume.VolumeInfo{} + nativeSnapshots := []*volume.BackupVolumeInfo{} + csiSnapshots := []*volume.BackupVolumeInfo{} legacyInfoSource := false buf := new(bytes.Buffer) @@ -332,7 +332,7 @@ func describeBackupVolumesInSF(ctx context.Context, kbClient kbclient.Client, ba backupVolumes["errorGetBackupVolumeInfo"] = fmt.Sprintf("error getting backup volume info: %v", err) return } else { - var volumeInfos []volume.VolumeInfo + var volumeInfos []volume.BackupVolumeInfo if err := json.NewDecoder(buf).Decode(&volumeInfos); err != nil { backupVolumes["errorReadBackupVolumeInfo"] = fmt.Sprintf("error reading backup volume info: %v", err) return @@ -357,7 +357,7 @@ func describeBackupVolumesInSF(ctx context.Context, kbClient kbclient.Client, ba backupStatusInfo["backupVolumes"] = backupVolumes } -func describeNativeSnapshotsInSF(details bool, infos []*volume.VolumeInfo, backupVolumes map[string]interface{}) { +func describeNativeSnapshotsInSF(details bool, infos []*volume.BackupVolumeInfo, backupVolumes map[string]interface{}) { if len(infos) == 0 { backupVolumes["nativeSnapshots"] = "" return @@ -370,7 +370,7 @@ func describeNativeSnapshotsInSF(details bool, infos []*volume.VolumeInfo, backu backupVolumes["nativeSnapshots"] = snapshotDetails } -func describNativeSnapshotInSF(details bool, info *volume.VolumeInfo, snapshotDetails map[string]interface{}) { +func describNativeSnapshotInSF(details bool, info *volume.BackupVolumeInfo, snapshotDetails map[string]interface{}) { if details { snapshotInfo := make(map[string]string) snapshotInfo["snapshotID"] = info.NativeSnapshotInfo.SnapshotHandle @@ -384,7 +384,7 @@ func describNativeSnapshotInSF(details bool, info *volume.VolumeInfo, snapshotDe } } -func describeCSISnapshotsInSF(details bool, infos []*volume.VolumeInfo, backupVolumes map[string]interface{}, legacyInfoSource bool) { +func describeCSISnapshotsInSF(details bool, infos []*volume.BackupVolumeInfo, backupVolumes map[string]interface{}, legacyInfoSource bool) { if len(infos) == 0 { if legacyInfoSource { backupVolumes["csiSnapshots"] = "" @@ -401,7 +401,7 @@ func describeCSISnapshotsInSF(details bool, infos []*volume.VolumeInfo, backupVo backupVolumes["csiSnapshots"] = snapshotDetails } -func describeCSISnapshotInSF(details bool, info *volume.VolumeInfo, snapshotDetails map[string]interface{}) { +func describeCSISnapshotInSF(details bool, info *volume.BackupVolumeInfo, snapshotDetails map[string]interface{}) { snapshotDetail := make(map[string]interface{}) describeLocalSnapshotInSF(details, info, snapshotDetail) @@ -411,7 +411,7 @@ func describeCSISnapshotInSF(details bool, info *volume.VolumeInfo, snapshotDeta } // describeLocalSnapshotInSF describes CSI volume snapshot contents in structured format. -func describeLocalSnapshotInSF(details bool, info *volume.VolumeInfo, snapshotDetail map[string]interface{}) { +func describeLocalSnapshotInSF(details bool, info *volume.BackupVolumeInfo, snapshotDetail map[string]interface{}) { if !info.PreserveLocalSnapshot { return } @@ -434,7 +434,7 @@ func describeLocalSnapshotInSF(details bool, info *volume.VolumeInfo, snapshotDe } } -func describeDataMovementInSF(details bool, info *volume.VolumeInfo, snapshotDetail map[string]interface{}) { +func describeDataMovementInSF(details bool, info *volume.BackupVolumeInfo, snapshotDetail map[string]interface{}) { if !info.SnapshotDataMoved { return } diff --git a/pkg/cmd/util/output/backup_structured_describer_test.go b/pkg/cmd/util/output/backup_structured_describer_test.go index 58a141cd9c..29fe83cbe5 100644 --- a/pkg/cmd/util/output/backup_structured_describer_test.go +++ b/pkg/cmd/util/output/backup_structured_describer_test.go @@ -284,13 +284,13 @@ func TestDescribePodVolumeBackupsInSF(t *testing.T) { func TestDescribeNativeSnapshotsInSF(t *testing.T) { testcases := []struct { name string - volumeInfo []*volume.VolumeInfo + volumeInfo []*volume.BackupVolumeInfo inputDetails bool expect map[string]interface{} }{ { name: "no details", - volumeInfo: []*volume.VolumeInfo{ + volumeInfo: []*volume.BackupVolumeInfo{ { BackupMethod: volume.NativeSnapshot, PVName: "pv-1", @@ -310,7 +310,7 @@ func TestDescribeNativeSnapshotsInSF(t *testing.T) { }, { name: "details", - volumeInfo: []*volume.VolumeInfo{ + volumeInfo: []*volume.BackupVolumeInfo{ { BackupMethod: volume.NativeSnapshot, PVName: "pv-1", @@ -348,21 +348,21 @@ func TestDescribeNativeSnapshotsInSF(t *testing.T) { func TestDescribeCSISnapshotsInSF(t *testing.T) { testcases := []struct { name string - volumeInfo []*volume.VolumeInfo + volumeInfo []*volume.BackupVolumeInfo inputDetails bool expect map[string]interface{} legacyInfoSource bool }{ { name: "empty info, not legacy", - volumeInfo: []*volume.VolumeInfo{}, + volumeInfo: []*volume.BackupVolumeInfo{}, expect: map[string]interface{}{ "csiSnapshots": "", }, }, { name: "empty info, legacy", - volumeInfo: []*volume.VolumeInfo{}, + volumeInfo: []*volume.BackupVolumeInfo{}, legacyInfoSource: true, expect: map[string]interface{}{ "csiSnapshots": "", @@ -370,7 +370,7 @@ func TestDescribeCSISnapshotsInSF(t *testing.T) { }, { name: "no details, local snapshot", - volumeInfo: []*volume.VolumeInfo{ + volumeInfo: []*volume.BackupVolumeInfo{ { BackupMethod: volume.CSISnapshot, PVCNamespace: "pvc-ns-1", @@ -395,7 +395,7 @@ func TestDescribeCSISnapshotsInSF(t *testing.T) { }, { name: "details, local snapshot", - volumeInfo: []*volume.VolumeInfo{ + volumeInfo: []*volume.BackupVolumeInfo{ { BackupMethod: volume.CSISnapshot, PVCNamespace: "pvc-ns-2", @@ -427,7 +427,7 @@ func TestDescribeCSISnapshotsInSF(t *testing.T) { }, { name: "no details, data movement", - volumeInfo: []*volume.VolumeInfo{ + volumeInfo: []*volume.BackupVolumeInfo{ { BackupMethod: volume.CSISnapshot, PVCNamespace: "pvc-ns-3", @@ -451,7 +451,7 @@ func TestDescribeCSISnapshotsInSF(t *testing.T) { }, { name: "details, data movement", - volumeInfo: []*volume.VolumeInfo{ + volumeInfo: []*volume.BackupVolumeInfo{ { BackupMethod: volume.CSISnapshot, PVCNamespace: "pvc-ns-4", @@ -480,7 +480,7 @@ func TestDescribeCSISnapshotsInSF(t *testing.T) { }, { name: "details, data movement, data mover is empty", - volumeInfo: []*volume.VolumeInfo{ + volumeInfo: []*volume.BackupVolumeInfo{ { BackupMethod: volume.CSISnapshot, PVCNamespace: "pvc-ns-4", diff --git a/pkg/controller/restore_controller.go b/pkg/controller/restore_controller.go index 4b282d21e5..86372b453a 100644 --- a/pkg/controller/restore_controller.go +++ b/pkg/controller/restore_controller.go @@ -107,6 +107,7 @@ type restoreReconciler struct { newPluginManager func(logger logrus.FieldLogger) clientmgmt.Manager backupStoreGetter persistence.ObjectBackupStoreGetter + globalCrClient client.Client } type backupInfo struct { @@ -127,6 +128,7 @@ func NewRestoreReconciler( logFormat logging.Format, defaultItemOperationTimeout time.Duration, disableInformerCache bool, + globalCrClient client.Client, ) *restoreReconciler { r := &restoreReconciler{ ctx: ctx, @@ -145,6 +147,8 @@ func NewRestoreReconciler( // replaced with fakes for testing. newPluginManager: newPluginManager, backupStoreGetter: backupStoreGetter, + + globalCrClient: globalCrClient, } // Move the periodical backup and restore metrics computing logic from controllers to here. @@ -521,7 +525,7 @@ 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) + backupVolumeInfoMap := make(map[string]volume.BackupVolumeInfo) volumeInfos, err := backupStore.GetBackupVolumeInfos(restore.Spec.BackupName) if err != nil { restoreLog.WithError(err).Errorf("fail to get VolumeInfos metadata file for backup %s", restore.Spec.BackupName) @@ -540,16 +544,17 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu } restoreReq := &pkgrestore.Request{ - Log: restoreLog, - Restore: restore, - Backup: info.backup, - PodVolumeBackups: podVolumeBackups, - VolumeSnapshots: volumeSnapshots, - BackupReader: backupFile, - ResourceModifiers: resourceModifiers, - DisableInformerCache: r.disableInformerCache, - CSIVolumeSnapshots: csiVolumeSnapshots, - VolumeInfoMap: backupVolumeInfoMap, + Log: restoreLog, + Restore: restore, + Backup: info.backup, + PodVolumeBackups: podVolumeBackups, + VolumeSnapshots: volumeSnapshots, + BackupReader: backupFile, + ResourceModifiers: resourceModifiers, + DisableInformerCache: r.disableInformerCache, + CSIVolumeSnapshots: csiVolumeSnapshots, + BackupVolumeInfoMap: backupVolumeInfoMap, + RestoreVolumeInfoTracker: volume.NewRestoreVolInfoTracker(restore, restoreLog, r.globalCrClient), } restoreWarnings, restoreErrors := r.restorer.RestoreWithResolvers(restoreReq, actionsResolver, pluginManager) @@ -640,6 +645,11 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu r.logger.WithError(err).Error("Error uploading restore item action operation resource list to backup storage") } + restoreReq.RestoreVolumeInfoTracker.Populate(context.TODO(), restoreReq.RestoredResourceList()) + if err := putRestoreVolumeInfoList(restore, restoreReq.RestoreVolumeInfoTracker.Result(), backupStore); err != nil { + r.logger.WithError(err).Error("Error uploading restored volume info to backup storage") + } + if restore.Status.Errors > 0 { if inProgressOperations { r.logger.Debug("Restore WaitingForPluginOperationsPartiallyFailed") @@ -776,6 +786,22 @@ func putOperationsForRestore(restore *api.Restore, operations []*itemoperation.R return nil } +func putRestoreVolumeInfoList(restore *api.Restore, volInfoList []*volume.RestoreVolumeInfo, store persistence.BackupStore) error { + buf := new(bytes.Buffer) + gzw := gzip.NewWriter(buf) + defer gzw.Close() + + if err := json.NewEncoder(gzw).Encode(volInfoList); err != nil { + return errors.Wrap(err, "error encoding restore volume info list to JSON") + } + + if err := gzw.Close(); err != nil { + return errors.Wrap(err, "error closing gzip writer") + } + + return store.PutRestoreVolumeInfo(restore.Name, buf) +} + func downloadToTempFile(backupName string, backupStore persistence.BackupStore, logger logrus.FieldLogger) (*os.File, error) { readCloser, err := backupStore.GetBackupContents(backupName) if err != nil { diff --git a/pkg/controller/restore_controller_test.go b/pkg/controller/restore_controller_test.go index 95628c3cf2..1b60f95326 100644 --- a/pkg/controller/restore_controller_test.go +++ b/pkg/controller/restore_controller_test.go @@ -91,11 +91,12 @@ func TestFetchBackupInfo(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { var ( - fakeClient = velerotest.NewFakeControllerRuntimeClient(t) - restorer = &fakeRestorer{kbClient: fakeClient} - logger = velerotest.NewLogger() - pluginManager = &pluginmocks.Manager{} - backupStore = &persistencemocks.BackupStore{} + fakeClient = velerotest.NewFakeControllerRuntimeClient(t) + fakeGlobalClient = velerotest.NewFakeControllerRuntimeClient(t) + restorer = &fakeRestorer{kbClient: fakeClient} + logger = velerotest.NewLogger() + pluginManager = &pluginmocks.Manager{} + backupStore = &persistencemocks.BackupStore{} ) defer restorer.AssertExpectations(t) @@ -114,6 +115,7 @@ func TestFetchBackupInfo(t *testing.T) { formatFlag, 60*time.Minute, false, + fakeGlobalClient, ) if test.backupStoreError == nil { @@ -170,9 +172,10 @@ func TestProcessQueueItemSkips(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { var ( - fakeClient = velerotest.NewFakeControllerRuntimeClient(t) - restorer = &fakeRestorer{kbClient: fakeClient} - logger = velerotest.NewLogger() + fakeClient = velerotest.NewFakeControllerRuntimeClient(t) + fakeGlobalClient = velerotest.NewFakeControllerRuntimeClient(t) + restorer = &fakeRestorer{kbClient: fakeClient} + logger = velerotest.NewLogger() ) if test.restore != nil { @@ -192,6 +195,7 @@ func TestProcessQueueItemSkips(t *testing.T) { formatFlag, 60*time.Minute, false, + fakeGlobalClient, ) _, err := r.Reconcile(context.Background(), ctrl.Request{NamespacedName: types.NamespacedName{ @@ -432,11 +436,12 @@ func TestRestoreReconcile(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { var ( - fakeClient = velerotest.NewFakeControllerRuntimeClientBuilder(t).Build() - restorer = &fakeRestorer{kbClient: fakeClient} - logger = velerotest.NewLogger() - pluginManager = &pluginmocks.Manager{} - backupStore = &persistencemocks.BackupStore{} + fakeClient = velerotest.NewFakeControllerRuntimeClientBuilder(t).Build() + fakeGlobalClient = velerotest.NewFakeControllerRuntimeClient(t) + restorer = &fakeRestorer{kbClient: fakeClient} + logger = velerotest.NewLogger() + pluginManager = &pluginmocks.Manager{} + backupStore = &persistencemocks.BackupStore{} ) defer restorer.AssertExpectations(t) @@ -459,6 +464,7 @@ func TestRestoreReconcile(t *testing.T) { formatFlag, 60*time.Minute, false, + fakeGlobalClient, ) r.clock = clocktesting.NewFakeClock(now) @@ -500,10 +506,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) + backupStore.On("PutRestoreVolumeInfo", test.restore.Name, 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.VolumeInfo{}, nil) + backupStore.On("GetBackupVolumeInfos", test.backup.Name).Return([]*volume.BackupVolumeInfo{}, nil) } volumeSnapshots := []*volume.Snapshot{ @@ -626,10 +633,11 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) { formatFlag := logging.FormatText var ( - logger = velerotest.NewLogger() - pluginManager = &pluginmocks.Manager{} - fakeClient = velerotest.NewFakeControllerRuntimeClient(t) - backupStore = &persistencemocks.BackupStore{} + logger = velerotest.NewLogger() + pluginManager = &pluginmocks.Manager{} + fakeClient = velerotest.NewFakeControllerRuntimeClient(t) + fakeGlobalClient = velerotest.NewFakeControllerRuntimeClient(t) + backupStore = &persistencemocks.BackupStore{} ) r := NewRestoreReconciler( @@ -645,6 +653,7 @@ func TestValidateAndCompleteWhenScheduleNameSpecified(t *testing.T) { formatFlag, 60*time.Minute, false, + fakeGlobalClient, ) restore := &velerov1api.Restore{ @@ -719,10 +728,11 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) { formatFlag := logging.FormatText var ( - logger = velerotest.NewLogger() - pluginManager = &pluginmocks.Manager{} - fakeClient = velerotest.NewFakeControllerRuntimeClient(t) - backupStore = &persistencemocks.BackupStore{} + logger = velerotest.NewLogger() + pluginManager = &pluginmocks.Manager{} + fakeClient = velerotest.NewFakeControllerRuntimeClient(t) + fakeGlobalClient = velerotest.NewFakeControllerRuntimeClient(t) + backupStore = &persistencemocks.BackupStore{} ) r := NewRestoreReconciler( @@ -738,6 +748,7 @@ func TestValidateAndCompleteWithResourceModifierSpecified(t *testing.T) { formatFlag, 60*time.Minute, false, + fakeGlobalClient, ) restore := &velerov1api.Restore{ diff --git a/pkg/controller/restore_finalizer_controller.go b/pkg/controller/restore_finalizer_controller.go index 45b03159b8..899fb0b36a 100644 --- a/pkg/controller/restore_finalizer_controller.go +++ b/pkg/controller/restore_finalizer_controller.go @@ -19,7 +19,6 @@ package controller import ( "context" "fmt" - "regexp" "sync" "time" @@ -39,7 +38,6 @@ import ( "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/restore" kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" "github.com/vmware-tanzu/velero/pkg/util/results" ) @@ -150,7 +148,7 @@ func (r *restoreFinalizerReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, errors.Wrap(err, "error getting restoredResourceList") } - restoredPVCList := getRestoredPVCFromRestoredResourceList(restoredResourceList) + restoredPVCList := volume.RestoredPVCFromRestoredResourceList(restoredResourceList) finalizerCtx := &finalizerContext{ logger: log, @@ -238,7 +236,7 @@ type finalizerContext struct { logger logrus.FieldLogger restore *velerov1api.Restore crClient client.Client - volumeInfo []*volume.VolumeInfo + volumeInfo []*volume.BackupVolumeInfo restoredPVCList map[string]struct{} } @@ -277,7 +275,7 @@ func (ctx *finalizerContext) patchDynamicPVWithVolumeInfo() (errs results.Result } pvWaitGroup.Add(1) - go func(volInfo volume.VolumeInfo, restoredNamespace string) { + go func(volInfo volume.BackupVolumeInfo, restoredNamespace string) { defer pvWaitGroup.Done() semaphore <- struct{}{} @@ -358,23 +356,6 @@ func (ctx *finalizerContext) patchDynamicPVWithVolumeInfo() (errs results.Result return errs } -func getRestoredPVCFromRestoredResourceList(restoredResourceList map[string][]string) map[string]struct{} { - pvcKey := "v1/PersistentVolumeClaim" - pvcList := make(map[string]struct{}) - - for _, pvc := range restoredResourceList[pvcKey] { - // the format of pvc string in restoredResourceList is like: "namespace/pvcName(status)" - // extract the substring before "(created)" if the status in rightmost Parenthesis is "created" - r := regexp.MustCompile(`\(([^)]+)\)`) - matches := r.FindAllStringSubmatch(pvc, -1) - if len(matches) > 0 && matches[len(matches)-1][1] == restore.ItemRestoreResultCreated { - pvcList[pvc[:len(pvc)-len("(created)")]] = struct{}{} - } - } - - return pvcList -} - func needPatch(newPV *v1.PersistentVolume, pvInfo *volume.PVInfo) bool { if newPV.Spec.PersistentVolumeReclaimPolicy != v1.PersistentVolumeReclaimPolicy(pvInfo.ReclaimPolicy) { return true diff --git a/pkg/controller/restore_finalizer_controller_test.go b/pkg/controller/restore_finalizer_controller_test.go index 76f4d295f5..c72ab49c43 100644 --- a/pkg/controller/restore_finalizer_controller_test.go +++ b/pkg/controller/restore_finalizer_controller_test.go @@ -210,7 +210,7 @@ func TestUpdateResult(t *testing.T) { func TestPatchDynamicPVWithVolumeInfo(t *testing.T) { tests := []struct { name string - volumeInfo []*volume.VolumeInfo + volumeInfo []*volume.BackupVolumeInfo restoredPVCNames map[string]struct{} restore *velerov1api.Restore restoredPVC []*corev1api.PersistentVolumeClaim @@ -220,21 +220,21 @@ func TestPatchDynamicPVWithVolumeInfo(t *testing.T) { }{ { name: "no applicable volumeInfo", - volumeInfo: []*volume.VolumeInfo{{BackupMethod: "VeleroNativeSnapshot", PVCName: "pvc1"}}, + volumeInfo: []*volume.BackupVolumeInfo{{BackupMethod: "VeleroNativeSnapshot", PVCName: "pvc1"}}, restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore").Result(), expectedPatch: nil, expectedErrNum: 0, }, { name: "no restored PVC", - volumeInfo: []*volume.VolumeInfo{{BackupMethod: "PodVolumeBackup", PVCName: "pvc1"}}, + volumeInfo: []*volume.BackupVolumeInfo{{BackupMethod: "PodVolumeBackup", PVCName: "pvc1"}}, restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore").Result(), expectedPatch: nil, expectedErrNum: 0, }, { name: "no applicable pv patch", - volumeInfo: []*volume.VolumeInfo{{ + volumeInfo: []*volume.BackupVolumeInfo{{ BackupMethod: "PodVolumeBackup", PVCName: "pvc1", PVName: "pv1", @@ -256,7 +256,7 @@ func TestPatchDynamicPVWithVolumeInfo(t *testing.T) { }, { name: "an applicable pv patch", - volumeInfo: []*volume.VolumeInfo{{ + volumeInfo: []*volume.BackupVolumeInfo{{ BackupMethod: "PodVolumeBackup", PVCName: "pvc1", PVName: "pv1", @@ -281,7 +281,7 @@ func TestPatchDynamicPVWithVolumeInfo(t *testing.T) { }, { name: "a mapped namespace restore", - volumeInfo: []*volume.VolumeInfo{{ + volumeInfo: []*volume.BackupVolumeInfo{{ BackupMethod: "PodVolumeBackup", PVCName: "pvc1", PVName: "pv1", @@ -306,7 +306,7 @@ func TestPatchDynamicPVWithVolumeInfo(t *testing.T) { }, { name: "two applicable pv patches", - volumeInfo: []*volume.VolumeInfo{{ + volumeInfo: []*volume.BackupVolumeInfo{{ BackupMethod: "PodVolumeBackup", PVCName: "pvc1", PVName: "pv1", @@ -354,7 +354,7 @@ func TestPatchDynamicPVWithVolumeInfo(t *testing.T) { }, { name: "an applicable pv patch with bound error", - volumeInfo: []*volume.VolumeInfo{{ + volumeInfo: []*volume.BackupVolumeInfo{{ BackupMethod: "PodVolumeBackup", PVCName: "pvc1", PVName: "pv1", @@ -375,7 +375,7 @@ func TestPatchDynamicPVWithVolumeInfo(t *testing.T) { }, { name: "two applicable pv patches with an error", - volumeInfo: []*volume.VolumeInfo{{ + volumeInfo: []*volume.BackupVolumeInfo{{ BackupMethod: "PodVolumeBackup", PVCName: "pvc1", PVName: "pv1", @@ -454,37 +454,3 @@ func TestPatchDynamicPVWithVolumeInfo(t *testing.T) { } } } - -func TestGetRestoredPVCFromRestoredResourceList(t *testing.T) { - // test empty list - restoredResourceList := map[string][]string{} - actual := getRestoredPVCFromRestoredResourceList(restoredResourceList) - assert.Empty(t, actual) - - // test no match - restoredResourceList = map[string][]string{ - "v1/PersistentVolumeClaim": { - "namespace1/pvc1(updated)", - }, - "v1/PersistentVolume": { - "namespace1/pv(created)", - }, - } - actual = getRestoredPVCFromRestoredResourceList(restoredResourceList) - assert.Empty(t, actual) - - // test matches - restoredResourceList = map[string][]string{ - "v1/PersistentVolumeClaim": { - "namespace1/pvc1(created)", - "namespace2/pvc2(updated)", - "namespace3/pvc(3)(created)", - }, - } - expected := map[string]struct{}{ - "namespace1/pvc1": {}, - "namespace3/pvc(3)": {}, - } - actual = getRestoredPVCFromRestoredResourceList(restoredResourceList) - assert.Equal(t, expected, actual) -} diff --git a/pkg/label/label.go b/pkg/label/label.go index 445e4af469..411de2cda9 100644 --- a/pkg/label/label.go +++ b/pkg/label/label.go @@ -61,3 +61,9 @@ func NewListOptionsForBackup(name string) metav1.ListOptions { LabelSelector: fmt.Sprintf("%s=%s", velerov1api.BackupNameLabel, GetValidName(name)), } } + +// NewSelectorForRestore returns a Selector based on the restore name. +// This is useful for interacting with Listers that need a Selector. +func NewSelectorForRestore(name string) labels.Selector { + return labels.SelectorFromSet(map[string]string{velerov1api.RestoreNameLabel: GetValidName(name)}) +} diff --git a/pkg/persistence/mocks/backup_store.go b/pkg/persistence/mocks/backup_store.go index 1c2cf7d05e..8abdd35bb3 100644 --- a/pkg/persistence/mocks/backup_store.go +++ b/pkg/persistence/mocks/backup_store.go @@ -314,16 +314,16 @@ func (_m *BackupStore) GetRestoreItemOperations(name string) ([]*itemoperation.R return r0, r1 } -// GetBackupVolumeInfos provides a mock function with given fields: name -func (_m *BackupStore) GetBackupVolumeInfos(name string) ([]*volume.VolumeInfo, error) { +// GetRestoreItemOperations provides a mock function with given fields: name +func (_m *BackupStore) GetBackupVolumeInfos(name string) ([]*volume.BackupVolumeInfo, error) { ret := _m.Called(name) - var r0 []*volume.VolumeInfo - if rf, ok := ret.Get(0).(func(string) []*volume.VolumeInfo); ok { + var r0 []*volume.BackupVolumeInfo + if rf, ok := ret.Get(0).(func(string) []*volume.BackupVolumeInfo); ok { r0 = rf(name) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*volume.VolumeInfo) + r0 = ret.Get(0).([]*volume.BackupVolumeInfo) } } @@ -546,6 +546,19 @@ func (_m *BackupStore) PutRestoredResourceList(restore string, results io.Reader return r0 } +// PutRestoreVolumeInfo provides a mock function with given fields: restore, results +func (_m *BackupStore) PutRestoreVolumeInfo(restore string, results io.Reader) error { + ret := _m.Called(restore, results) + + var r0 error + if rf, ok := ret.Get(0).(func(string, io.Reader) error); ok { + r0 = rf(restore, results) + } else { + r0 = ret.Error(0) + } + + return r0 +} type mockConstructorTestingTNewBackupStore interface { mock.TestingT Cleanup(func()) diff --git a/pkg/persistence/object_store.go b/pkg/persistence/object_store.go index 1036f88eb0..5cdb36b6f0 100644 --- a/pkg/persistence/object_store.go +++ b/pkg/persistence/object_store.go @@ -74,8 +74,8 @@ 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.VolumeInfo, error) PutBackupVolumeInfos(name string, volumeInfo io.Reader) error + GetBackupVolumeInfos(name string) ([]*volume.BackupVolumeInfo, error) GetRestoreResults(name string) (map[string]results.Result, error) // BackupExists checks if the backup metadata file exists in object storage. @@ -88,6 +88,7 @@ type BackupStore interface { PutRestoredResourceList(restore string, results io.Reader) error PutRestoreItemOperations(restore string, restoreItemOperations io.Reader) error GetRestoreItemOperations(name string) ([]*itemoperation.RestoreOperation, error) + PutRestoreVolumeInfo(restore string, volumeInfo io.Reader) error DeleteRestore(name string) error GetRestoredResourceList(name string) (map[string][]string, error) @@ -498,8 +499,8 @@ func (s *objectBackupStore) GetPodVolumeBackups(name string) ([]*velerov1api.Pod return podVolumeBackups, nil } -func (s *objectBackupStore) GetBackupVolumeInfos(name string) ([]*volume.VolumeInfo, error) { - volumeInfos := make([]*volume.VolumeInfo, 0) +func (s *objectBackupStore) GetBackupVolumeInfos(name string) ([]*volume.BackupVolumeInfo, error) { + volumeInfos := make([]*volume.BackupVolumeInfo, 0) res, err := tryGet(s.objectStore, s.bucket, s.layout.getBackupVolumeInfoKey(name)) if err != nil { @@ -602,6 +603,10 @@ func (s *objectBackupStore) PutRestoreItemOperations(restore string, restoreItem return seekAndPutObject(s.objectStore, s.bucket, s.layout.getRestoreItemOperationsKey(restore), restoreItemOperations) } +func (s *objectBackupStore) PutRestoreVolumeInfo(restore string, volumeInfo io.Reader) error { + return seekAndPutObject(s.objectStore, s.bucket, s.layout.getRestoreVolumeInfoKey(restore), volumeInfo) +} + func (s *objectBackupStore) PutBackupItemOperations(backup string, backupItemOperations io.Reader) error { return seekAndPutObject(s.objectStore, s.bucket, s.layout.getBackupItemOperationsKey(backup), backupItemOperations) } @@ -638,6 +643,8 @@ func (s *objectBackupStore) GetDownloadURL(target velerov1api.DownloadTarget) (s return s.objectStore.CreateSignedURL(s.bucket, s.layout.getBackupResultsKey(target.Name), DownloadURLTTL) case velerov1api.DownloadTargetKindBackupVolumeInfos: return s.objectStore.CreateSignedURL(s.bucket, s.layout.getBackupVolumeInfoKey(target.Name), DownloadURLTTL) + case velerov1api.DownloadTargetKindRestoreVolumeInfo: + return s.objectStore.CreateSignedURL(s.bucket, s.layout.getRestoreVolumeInfoKey(target.Name), DownloadURLTTL) default: return "", errors.Errorf("unsupported download target kind %q", target.Kind) } diff --git a/pkg/persistence/object_store_layout.go b/pkg/persistence/object_store_layout.go index dae68d3324..45e229e6c1 100644 --- a/pkg/persistence/object_store_layout.go +++ b/pkg/persistence/object_store_layout.go @@ -132,3 +132,7 @@ func (l *ObjectStoreLayout) getBackupResultsKey(backup string) string { func (l *ObjectStoreLayout) getBackupVolumeInfoKey(backup string) string { return path.Join(l.subdirs["backups"], backup, fmt.Sprintf("%s-volumeinfo.json.gz", backup)) } + +func (l *ObjectStoreLayout) getRestoreVolumeInfoKey(restore string) string { + return path.Join(l.subdirs["restores"], restore, fmt.Sprintf("%s-volumeinfo.json.gz", restore)) +} diff --git a/pkg/persistence/object_store_test.go b/pkg/persistence/object_store_test.go index a23906eefa..58c10d1a8c 100644 --- a/pkg/persistence/object_store_test.go +++ b/pkg/persistence/object_store_test.go @@ -1068,17 +1068,17 @@ func TestNewObjectBackupStoreGetterConfig(t *testing.T) { func TestGetBackupVolumeInfos(t *testing.T) { tests := []struct { name string - volumeInfo []*volume.VolumeInfo + volumeInfo []*volume.BackupVolumeInfo volumeInfoStr string expectedErr string - expectedResult []*volume.VolumeInfo + expectedResult []*volume.BackupVolumeInfo }{ { name: "No VolumeInfos, expect no error.", }, { - name: "Valid VolumeInfo, should pass.", - volumeInfo: []*volume.VolumeInfo{ + name: "Valid BackupVolumeInfo, should pass.", + volumeInfo: []*volume.BackupVolumeInfo{ { PVCName: "pvcName", PVName: "pvName", @@ -1086,7 +1086,7 @@ func TestGetBackupVolumeInfos(t *testing.T) { SnapshotDataMoved: false, }, }, - expectedResult: []*volume.VolumeInfo{ + expectedResult: []*volume.BackupVolumeInfo{ { PVCName: "pvcName", PVName: "pvName", @@ -1096,9 +1096,9 @@ func TestGetBackupVolumeInfos(t *testing.T) { }, }, { - name: "Invalid VolumeInfo string, should also pass.", + name: "Invalid BackupVolumeInfo string, should also pass.", volumeInfoStr: `[{"abc": "123", "def": "456", "pvcName": "pvcName"}]`, - expectedResult: []*volume.VolumeInfo{ + expectedResult: []*volume.BackupVolumeInfo{ { PVCName: "pvcName", }, @@ -1223,7 +1223,7 @@ func TestPutBackupVolumeInfos(t *testing.T) { t.Run(tc.name, func(t *testing.T) { harness := newObjectBackupStoreTestHarness("foo", tc.prefix) - volumeInfos := []*volume.VolumeInfo{ + volumeInfos := []*volume.BackupVolumeInfo{ { PVCName: "test", }, diff --git a/pkg/podvolume/mocks/restorer.go b/pkg/podvolume/mocks/restorer.go index fd210aa00f..56de97f446 100644 --- a/pkg/podvolume/mocks/restorer.go +++ b/pkg/podvolume/mocks/restorer.go @@ -1,10 +1,12 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. +// Code generated by mockery v2.42.2. DO NOT EDIT. package mocks import ( mock "github.com/stretchr/testify/mock" - "github.com/vmware-tanzu/velero/pkg/podvolume" + podvolume "github.com/vmware-tanzu/velero/pkg/podvolume" + + volume "github.com/vmware-tanzu/velero/internal/volume" ) // Restorer is an autogenerated mock type for the Restorer type @@ -12,13 +14,17 @@ type Restorer struct { mock.Mock } -// RestorePodVolumes provides a mock function with given fields: _a0 -func (_m *Restorer) RestorePodVolumes(_a0 podvolume.RestoreData) []error { - ret := _m.Called(_a0) +// RestorePodVolumes provides a mock function with given fields: _a0, _a1 +func (_m *Restorer) RestorePodVolumes(_a0 podvolume.RestoreData, _a1 *volume.RestoreVolumeInfoTracker) []error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for RestorePodVolumes") + } var r0 []error - if rf, ok := ret.Get(0).(func(podvolume.RestoreData) []error); ok { - r0 = rf(_a0) + if rf, ok := ret.Get(0).(func(podvolume.RestoreData, *volume.RestoreVolumeInfoTracker) []error); ok { + r0 = rf(_a0, _a1) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]error) @@ -27,3 +33,17 @@ func (_m *Restorer) RestorePodVolumes(_a0 podvolume.RestoreData) []error { return r0 } + +// NewRestorer creates a new instance of Restorer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRestorer(t interface { + mock.TestingT + Cleanup(func()) +}) *Restorer { + mock := &Restorer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/podvolume/restorer.go b/pkg/podvolume/restorer.go index 568ad8c197..4b3e4354dd 100644 --- a/pkg/podvolume/restorer.go +++ b/pkg/podvolume/restorer.go @@ -21,6 +21,8 @@ import ( "sync" "time" + "github.com/vmware-tanzu/velero/internal/volume" + "github.com/pkg/errors" "github.com/sirupsen/logrus" corev1api "k8s.io/api/core/v1" @@ -51,7 +53,7 @@ type RestoreData struct { // Restorer can execute pod volume restores of volumes in a pod. type Restorer interface { // RestorePodVolumes restores all annotated volumes in a pod. - RestorePodVolumes(RestoreData) []error + RestorePodVolumes(RestoreData, *volume.RestoreVolumeInfoTracker) []error } type restorer struct { @@ -114,7 +116,7 @@ func newRestorer( return r } -func (r *restorer) RestorePodVolumes(data RestoreData) []error { +func (r *restorer) RestorePodVolumes(data RestoreData, tracker *volume.RestoreVolumeInfoTracker) []error { volumesToRestore := getVolumeBackupInfoForPod(data.PodVolumeBackups, data.Pod, data.SourceNamespace) if len(volumesToRestore) == 0 { return nil @@ -229,6 +231,7 @@ ForEachVolume: if res.Status.Phase == velerov1api.PodVolumeRestorePhaseFailed { errs = append(errs, errors.Errorf("pod volume restore failed: %s", res.Status.Message)) } + tracker.TrackPodVolume(res) case err := <-r.nodeAgentCheck: errs = append(errs, err) break ForEachVolume diff --git a/pkg/podvolume/restorer_test.go b/pkg/podvolume/restorer_test.go index efccc3ebc1..dc8755f72b 100644 --- a/pkg/podvolume/restorer_test.go +++ b/pkg/podvolume/restorer_test.go @@ -22,6 +22,8 @@ import ( "testing" "time" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appv1 "k8s.io/api/apps/v1" @@ -33,6 +35,7 @@ import ( "k8s.io/client-go/tools/cache" ctrlfake "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/vmware-tanzu/velero/internal/volume" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/repository" @@ -418,7 +421,7 @@ func TestRestorePodVolumes(t *testing.T) { PodVolumeBackups: test.pvbs, SourceNamespace: test.sourceNamespace, BackupLocation: test.bsl, - }) + }, volume.NewRestoreVolInfoTracker(restoreObj, logrus.New(), fakeCRClient)) if errs == nil { assert.Nil(t, test.errs) diff --git a/pkg/restore/pv_restorer.go b/pkg/restore/pv_restorer.go index aa13627501..22cb8f09a0 100644 --- a/pkg/restore/pv_restorer.go +++ b/pkg/restore/pv_restorer.go @@ -44,6 +44,7 @@ type pvRestorer struct { volumeSnapshotterGetter VolumeSnapshotterGetter kbclient client.Client credentialFileStore credentials.FileStore + volInfoTracker *volume.RestoreVolumeInfoTracker } func (r *pvRestorer) executePVAction(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { @@ -97,6 +98,11 @@ func (r *pvRestorer) executePVAction(obj *unstructured.Unstructured) (*unstructu if !ok { return nil, errors.Errorf("unexpected type %T", updated1) } + var iops int64 = 0 + if snapshotInfo.volumeIOPS != nil { + iops = *snapshotInfo.volumeIOPS + } + r.volInfoTracker.TrackNativeSnapshot(updated2.GetName(), snapshotInfo.providerSnapshotID, snapshotInfo.volumeType, snapshotInfo.volumeAZ, iops) return updated2, nil } diff --git a/pkg/restore/pv_restorer_test.go b/pkg/restore/pv_restorer_test.go index 5609c7e924..98cadd88f2 100644 --- a/pkg/restore/pv_restorer_test.go +++ b/pkg/restore/pv_restorer_test.go @@ -20,6 +20,8 @@ import ( "context" "testing" + "github.com/sirupsen/logrus" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -39,6 +41,7 @@ func defaultBackup() *builder.BackupBuilder { } func TestExecutePVAction_NoSnapshotRestores(t *testing.T) { + fakeClient := velerotest.NewFakeControllerRuntimeClient(t) tests := []struct { name string obj *unstructured.Unstructured @@ -115,9 +118,10 @@ func TestExecutePVAction_NoSnapshotRestores(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { r := &pvRestorer{ - logger: velerotest.NewLogger(), - restorePVs: tc.restore.Spec.RestorePVs, - kbclient: velerotest.NewFakeControllerRuntimeClient(t), + logger: velerotest.NewLogger(), + restorePVs: tc.restore.Spec.RestorePVs, + kbclient: velerotest.NewFakeControllerRuntimeClient(t), + volInfoTracker: volume.NewRestoreVolInfoTracker(tc.restore, logrus.New(), fakeClient), } if tc.backup != nil { r.backup = tc.backup @@ -180,6 +184,7 @@ func TestExecutePVAction_SnapshotRestores(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var ( + logger = velerotest.NewLogger() volumeSnapshotter = new(providermocks.VolumeSnapshotter) volumeSnapshotterGetter = providerToVolumeSnapshotterMap(map[string]vsv1.VolumeSnapshotter{ tc.expectedProvider: volumeSnapshotter, @@ -192,11 +197,12 @@ func TestExecutePVAction_SnapshotRestores(t *testing.T) { } r := &pvRestorer{ - logger: velerotest.NewLogger(), + logger: logger, backup: tc.backup, volumeSnapshots: tc.volumeSnapshots, kbclient: fakeClient, volumeSnapshotterGetter: volumeSnapshotterGetter, + volInfoTracker: volume.NewRestoreVolInfoTracker(tc.restore, logger, fakeClient), } volumeSnapshotter.On("Init", mock.Anything).Return(nil) diff --git a/pkg/restore/request.go b/pkg/restore/request.go index ff27c6ba78..d6e341c4ff 100644 --- a/pkg/restore/request.go +++ b/pkg/restore/request.go @@ -52,17 +52,18 @@ func resourceKey(obj runtime.Object) string { type Request struct { *velerov1api.Restore - Log logrus.FieldLogger - Backup *velerov1api.Backup - PodVolumeBackups []*velerov1api.PodVolumeBackup - VolumeSnapshots []*volume.Snapshot - BackupReader io.Reader - RestoredItems map[itemKey]restoredItemStatus - itemOperationsList *[]*itemoperation.RestoreOperation - ResourceModifiers *resourcemodifiers.ResourceModifiers - DisableInformerCache bool - CSIVolumeSnapshots []*snapshotv1api.VolumeSnapshot - VolumeInfoMap map[string]volume.VolumeInfo + Log logrus.FieldLogger + Backup *velerov1api.Backup + PodVolumeBackups []*velerov1api.PodVolumeBackup + VolumeSnapshots []*volume.Snapshot + BackupReader io.Reader + RestoredItems map[itemKey]restoredItemStatus + itemOperationsList *[]*itemoperation.RestoreOperation + ResourceModifiers *resourcemodifiers.ResourceModifiers + DisableInformerCache bool + CSIVolumeSnapshots []*snapshotv1api.VolumeSnapshot + BackupVolumeInfoMap map[string]volume.BackupVolumeInfo + RestoreVolumeInfoTracker *volume.RestoreVolumeInfoTracker } type restoredItemStatus struct { diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 956f92479d..33a8bab173 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -276,6 +276,7 @@ func (kr *kubernetesRestorer) RestoreWithResolvers( volumeSnapshotterGetter: volumeSnapshotterGetter, kbclient: kr.kbClient, credentialFileStore: kr.credentialFileStore, + volInfoTracker: req.RestoreVolumeInfoTracker, } req.RestoredItems = make(map[itemKey]restoredItemStatus) @@ -323,7 +324,8 @@ func (kr *kubernetesRestorer) RestoreWithResolvers( disableInformerCache: req.DisableInformerCache, featureVerifier: kr.featureVerifier, hookTracker: hook.NewHookTracker(), - volumeInfoMap: req.VolumeInfoMap, + backupVolumeInfoMap: req.BackupVolumeInfoMap, + restoreVolumeInfoTracker: req.RestoreVolumeInfoTracker, } return restoreCtx.execute() @@ -376,7 +378,8 @@ type restoreContext struct { disableInformerCache bool featureVerifier features.Verifier hookTracker *hook.HookTracker - volumeInfoMap map[string]volume.VolumeInfo + backupVolumeInfoMap map[string]volume.BackupVolumeInfo + restoreVolumeInfoTracker *volume.RestoreVolumeInfoTracker } type resourceClientKey struct { @@ -1224,8 +1227,8 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso return warnings, errs, itemExists } - if volumeInfo, ok := ctx.volumeInfoMap[obj.GetName()]; ok { - ctx.log.Infof("Find VolumeInfo for PV %s.", obj.GetName()) + if volumeInfo, ok := ctx.backupVolumeInfoMap[obj.GetName()]; ok { + ctx.log.Infof("Find BackupVolumeInfo for PV %s.", obj.GetName()) switch volumeInfo.BackupMethod { case volume.NativeSnapshot: @@ -1258,7 +1261,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso // want to dynamically re-provision it. return warnings, errs, itemExists - // When the PV data is skipped from backup, it's VolumeInfo BackupMethod + // When the PV data is skipped from backup, it's BackupVolumeInfo BackupMethod // is not set, and it will fall into the default case. default: if hasDeleteReclaimPolicy(obj.Object) { @@ -1277,9 +1280,9 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso } } } else { - // TODO: VolumeInfo is adopted and old logic is deprecated in v1.13. + // TODO: BackupVolumeInfo 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()) + ctx.log.Infof("Cannot find BackupVolumeInfo for PV %s.", obj.GetName()) switch { case hasSnapshot(name, ctx.volumeSnapshots): @@ -1899,7 +1902,7 @@ func restorePodVolumeBackups(ctx *restoreContext, createdObj *unstructured.Unstr SourceNamespace: originalNamespace, BackupLocation: ctx.backup.Spec.StorageLocation, } - if errs := ctx.podVolumeRestorer.RestorePodVolumes(data); errs != nil { + if errs := ctx.podVolumeRestorer.RestorePodVolumes(data, ctx.restoreVolumeInfoTracker); errs != nil { ctx.log.WithError(kubeerrs.NewAggregate(errs)).Error("unable to successfully complete pod volume restores of pod's volumes") for _, err := range errs { @@ -2498,7 +2501,7 @@ func (ctx *restoreContext) handlePVHasNativeSnapshot(obj *unstructured.Unstructu ctx.renamedPVs[oldName] = pvName retObj.SetName(pvName) - + ctx.restoreVolumeInfoTracker.RenamePVForNativeSnapshot(oldName, pvName) // Add the original PV name as an annotation. annotations := retObj.GetAnnotations() if annotations == nil { diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index f098537e2d..57294013bc 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -70,7 +70,7 @@ func TestRestorePVWithVolumeInfo(t *testing.T) { apiResources []*test.APIResource tarball io.Reader want map[*test.APIResource][]string - volumeInfoMap map[string]volume.VolumeInfo + volumeInfoMap map[string]volume.BackupVolumeInfo }{ { name: "Restore PV with native snapshot", @@ -83,7 +83,7 @@ func TestRestorePVWithVolumeInfo(t *testing.T) { apiResources: []*test.APIResource{ test.PVs(), }, - volumeInfoMap: map[string]volume.VolumeInfo{ + volumeInfoMap: map[string]volume.BackupVolumeInfo{ "pv-1": { BackupMethod: volume.NativeSnapshot, PVName: "pv-1", @@ -107,11 +107,11 @@ func TestRestorePVWithVolumeInfo(t *testing.T) { apiResources: []*test.APIResource{ test.PVs(), }, - volumeInfoMap: map[string]volume.VolumeInfo{ + volumeInfoMap: map[string]volume.BackupVolumeInfo{ "pv-1": { BackupMethod: volume.PodVolumeBackup, PVName: "pv-1", - PVBInfo: &volume.PodVolumeBackupInfo{ + PVBInfo: &volume.PodVolumeInfo{ SnapshotHandle: "testSnapshotHandle", Size: 100, NodeName: "testNode", @@ -133,7 +133,7 @@ func TestRestorePVWithVolumeInfo(t *testing.T) { apiResources: []*test.APIResource{ test.PVs(), }, - volumeInfoMap: map[string]volume.VolumeInfo{ + volumeInfoMap: map[string]volume.BackupVolumeInfo{ "pv-1": { BackupMethod: volume.CSISnapshot, SnapshotDataMoved: false, @@ -158,7 +158,7 @@ func TestRestorePVWithVolumeInfo(t *testing.T) { apiResources: []*test.APIResource{ test.PVs(), }, - volumeInfoMap: map[string]volume.VolumeInfo{ + volumeInfoMap: map[string]volume.BackupVolumeInfo{ "pv-1": { BackupMethod: volume.CSISnapshot, SnapshotDataMoved: true, @@ -186,7 +186,7 @@ func TestRestorePVWithVolumeInfo(t *testing.T) { apiResources: []*test.APIResource{ test.PVs(), }, - volumeInfoMap: map[string]volume.VolumeInfo{ + volumeInfoMap: map[string]volume.BackupVolumeInfo{ "pv-1": { PVName: "pv-1", Skipped: true, @@ -207,7 +207,7 @@ func TestRestorePVWithVolumeInfo(t *testing.T) { apiResources: []*test.APIResource{ test.PVs(), }, - volumeInfoMap: map[string]volume.VolumeInfo{ + volumeInfoMap: map[string]volume.BackupVolumeInfo{ "pv-1": { PVName: "pv-1", Skipped: true, @@ -235,13 +235,13 @@ func TestRestorePVWithVolumeInfo(t *testing.T) { 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, + Log: h.log, + Restore: tc.restore, + Backup: tc.backup, + PodVolumeBackups: nil, + VolumeSnapshots: nil, + BackupReader: tc.tarball, + BackupVolumeInfoMap: tc.volumeInfoMap, } warnings, errs := h.restorer.Restore( data, @@ -3311,12 +3311,13 @@ func TestRestorePersistentVolumes(t *testing.T) { } data := &Request{ - Log: h.log, - Restore: tc.restore, - Backup: tc.backup, - VolumeSnapshots: tc.volumeSnapshots, - BackupReader: tc.tarball, - CSIVolumeSnapshots: tc.csiVolumeSnapshots, + Log: h.log, + Restore: tc.restore, + Backup: tc.backup, + VolumeSnapshots: tc.volumeSnapshots, + BackupReader: tc.tarball, + CSIVolumeSnapshots: tc.csiVolumeSnapshots, + RestoreVolumeInfoTracker: volume.NewRestoreVolInfoTracker(tc.restore, h.log, test.NewFakeControllerRuntimeClient(t)), } warnings, errs := h.restorer.Restore( data, @@ -3441,7 +3442,7 @@ func TestRestoreWithPodVolume(t *testing.T) { BackupLocation: "", } restorer. - On("RestorePodVolumes", expectedArgs). + On("RestorePodVolumes", expectedArgs, mock.Anything). Return(nil) } diff --git a/test/util/providers/common.go b/test/util/providers/common.go index dcb599f253..1b39f22712 100644 --- a/test/util/providers/common.go +++ b/test/util/providers/common.go @@ -235,7 +235,7 @@ func GetVolumeInfo( bslConfig, backupName, subPrefix string, -) ([]*volume.VolumeInfo, error) { +) ([]*volume.BackupVolumeInfo, error) { readCloser, err := GetVolumeInfoMetadataContent(objectStoreProvider, cloudCredentialsFile, bslBucket, @@ -254,7 +254,7 @@ func GetVolumeInfo( } defer gzr.Close() - volumeInfos := make([]*volume.VolumeInfo, 0) + volumeInfos := make([]*volume.BackupVolumeInfo, 0) if err := json.NewDecoder(gzr).Decode(&volumeInfos); err != nil { return nil, errors.Wrap(err, "error decoding object data")