From b06d7a467f39926b8c0c42845e65c993ecd97cb6 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Fri, 22 Mar 2024 16:15:06 +0800 Subject: [PATCH] Support update the backup VolumeInfos by the Async ops result. 1. Add PutBackupVolumeInfos method. 2. Add CompletionTimestamp in VolumeInfo. 3. Add Size in SnapshotDataMovementInfo. 4. Update CompletionTimpstmap, SnapshotHandle, RetainedSnapshot and Size in VolumeInfo on DataUpload Operation completes. Signed-off-by: Xun Jiang --- changelogs/unreleased/7554-blackpiglet | 1 + internal/volume/volumes_information.go | 25 ++++- internal/volume/volumes_information_test.go | 34 ++++--- pkg/backup/backup.go | 91 ++++++++++++++++--- pkg/backup/backup_test.go | 58 ++++++++++++ pkg/builder/data_upload_builder.go | 10 +- pkg/builder/pod_volume_backup_builder.go | 10 ++ pkg/controller/backup_controller_test.go | 11 ++- pkg/controller/backup_finalizer_controller.go | 47 +++++++++- .../backup_finalizer_controller_test.go | 2 + pkg/kuberesource/kuberesource.go | 1 + pkg/persistence/mocks/backup_store.go | 16 +++- pkg/persistence/object_store.go | 5 + pkg/persistence/object_store_test.go | 45 +++++++++ 14 files changed, 319 insertions(+), 37 deletions(-) create mode 100644 changelogs/unreleased/7554-blackpiglet diff --git a/changelogs/unreleased/7554-blackpiglet b/changelogs/unreleased/7554-blackpiglet new file mode 100644 index 000000000..1cbdd597e --- /dev/null +++ b/changelogs/unreleased/7554-blackpiglet @@ -0,0 +1 @@ +Support update the backup VolumeInfos by the Async ops result. \ No newline at end of file diff --git a/internal/volume/volumes_information.go b/internal/volume/volumes_information.go index 7e3acd555..e901280e5 100644 --- a/internal/volume/volumes_information.go +++ b/internal/volume/volumes_information.go @@ -76,6 +76,9 @@ type VolumeInfo struct { // Snapshot starts timestamp. StartTimestamp *metav1.Time `json:"startTimestamp,omitempty"` + // Snapshot completes timestamp. + CompletionTimestamp *metav1.Time `json:"completionTimestamp,omitempty"` + CSISnapshotInfo *CSISnapshotInfo `json:"csiSnapshotInfo,omitempty"` SnapshotDataMovementInfo *SnapshotDataMovementInfo `json:"snapshotDataMovementInfo,omitempty"` NativeSnapshotInfo *NativeSnapshotInfo `json:"nativeSnapshotInfo,omitempty"` @@ -119,6 +122,9 @@ type SnapshotDataMovementInfo struct { // The Async Operation's ID. OperationID string `json:"operationID"` + + // Moved snapshot data size. + Size int64 `json:"size"` } // NativeSnapshotInfo is used for displaying the Velero native snapshot status. @@ -379,7 +385,6 @@ func (v *VolumesInformation) generateVolumeInfoForCSIVolumeSnapshot() { Skipped: false, SnapshotDataMoved: false, PreserveLocalSnapshot: true, - StartTimestamp: &(volumeSnapshot.CreationTimestamp), CSISnapshotInfo: &CSISnapshotInfo{ VSCName: *volumeSnapshot.Status.BoundVolumeSnapshotContentName, Size: size, @@ -393,6 +398,10 @@ func (v *VolumesInformation) generateVolumeInfoForCSIVolumeSnapshot() { }, } + if volumeSnapshot.Status.CreationTime != nil { + volumeInfo.StartTimestamp = volumeSnapshot.Status.CreationTime + } + tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) } else { v.logger.Warnf("cannot find info for PVC %s/%s", volumeSnapshot.Namespace, volumeSnapshot.Spec.Source.PersistentVolumeClaimName) @@ -412,7 +421,6 @@ func (v *VolumesInformation) generateVolumeInfoFromPVB() { BackupMethod: PodVolumeBackup, SnapshotDataMoved: false, Skipped: false, - StartTimestamp: pvb.Status.StartTimestamp, PVBInfo: &PodVolumeBackupInfo{ SnapshotHandle: pvb.Status.SnapshotID, Size: pvb.Status.Progress.TotalBytes, @@ -424,6 +432,14 @@ func (v *VolumesInformation) generateVolumeInfoFromPVB() { }, } + if pvb.Status.StartTimestamp != nil { + volumeInfo.StartTimestamp = pvb.Status.StartTimestamp + } + + if pvb.Status.CompletionTimestamp != nil { + volumeInfo.CompletionTimestamp = pvb.Status.CompletionTimestamp + } + pod := new(corev1api.Pod) pvcName := "" err := v.crClient.Get(context.TODO(), kbclient.ObjectKey{Namespace: pvb.Spec.Pod.Namespace, Name: pvb.Spec.Pod.Name}, pod) @@ -522,7 +538,6 @@ func (v *VolumesInformation) generateVolumeInfoFromDataUpload() { PVName: pvcPVInfo.PV.Name, SnapshotDataMoved: true, Skipped: false, - StartTimestamp: operation.Status.Created, CSISnapshotInfo: &CSISnapshotInfo{ SnapshotHandle: FieldValueIsUnknown, VSCName: FieldValueIsUnknown, @@ -540,6 +555,10 @@ func (v *VolumesInformation) generateVolumeInfoFromDataUpload() { }, } + if dataUpload.Status.StartTimestamp != nil { + volumeInfo.StartTimestamp = dataUpload.Status.StartTimestamp + } + tmpVolumeInfos = append(tmpVolumeInfos, volumeInfo) } else { v.logger.Warnf("Cannot find info for PVC %s/%s", operation.Spec.ResourceIdentifier.Namespace, operation.Spec.ResourceIdentifier.Name) diff --git a/internal/volume/volumes_information_test.go b/internal/volume/volumes_information_test.go index d91f3004a..bf8ee133e 100644 --- a/internal/volume/volumes_information_test.go +++ b/internal/volume/volumes_information_test.go @@ -372,6 +372,7 @@ func TestGenerateVolumeInfoForCSIVolumeSnapshot(t *testing.T) { }, Status: &snapshotv1api.VolumeSnapshotStatus{ BoundVolumeSnapshotContentName: stringPtr("testContent"), + CreationTime: &now, RestoreSize: &resourceQuantity, }, }, @@ -458,6 +459,7 @@ func TestGenerateVolumeInfoForCSIVolumeSnapshot(t *testing.T) { } func TestGenerateVolumeInfoFromPVB(t *testing.T) { + now := metav1.Now() tests := []struct { name string pvb *velerov1api.PodVolumeBackup @@ -542,7 +544,7 @@ func TestGenerateVolumeInfoFromPVB(t *testing.T) { }, }, }, - pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").Result(), + pvb: builder.ForPodVolumeBackup("velero", "testPVB").PodName("testPod").PodNamespace("velero").StartTimestamp(&now).CompletionTimestamp(&now).Result(), pod: builder.ForPod("velero", "testPod").Containers(&corev1api.Container{ Name: "test", VolumeMounts: []corev1api.VolumeMount{ @@ -563,10 +565,12 @@ func TestGenerateVolumeInfoFromPVB(t *testing.T) { ).Result(), expectedVolumeInfos: []*VolumeInfo{ { - PVCName: "testPVC", - PVCNamespace: "velero", - PVName: "testPV", - BackupMethod: PodVolumeBackup, + PVCName: "testPVC", + PVCNamespace: "velero", + PVName: "testPV", + BackupMethod: PodVolumeBackup, + StartTimestamp: &now, + CompletionTimestamp: &now, PVBInfo: &PodVolumeBackupInfo{ PodName: "testPod", PodNamespace: "velero", @@ -605,9 +609,12 @@ func TestGenerateVolumeInfoFromPVB(t *testing.T) { } func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { + // The unstructured conversion will loose the time precision to second + // level. To make test pass. Set the now precision at second at the + // beginning. + now := metav1.Now().Rfc3339Copy() features.Enable(velerov1api.CSIFeatureFlag) defer features.Disable(velerov1api.CSIFeatureFlag) - now := metav1.Now() tests := []struct { name string volumeSnapshotClass *snapshotv1api.VolumeSnapshotClass @@ -685,7 +692,7 @@ func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { name: "VolumeSnapshotClass cannot be found for operation", dataUpload: builder.ForDataUpload("velero", "testDU").DataMover("velero").CSISnapshot(&velerov2alpha1.CSISnapshotSpec{ VolumeSnapshot: "testVS", - }).SnapshotID("testSnapshotHandle").Result(), + }).SnapshotID("testSnapshotHandle").StartTimestamp(&now).Result(), operation: &itemoperation.BackupOperation{ Spec: itemoperation.BackupOperationSpec{ OperationID: "testOperation", @@ -731,6 +738,7 @@ func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { PVName: "testPV", BackupMethod: CSISnapshot, SnapshotDataMoved: true, + StartTimestamp: &now, CSISnapshotInfo: &CSISnapshotInfo{ SnapshotHandle: FieldValueIsUnknown, VSCName: FieldValueIsUnknown, @@ -754,7 +762,7 @@ func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { dataUpload: builder.ForDataUpload("velero", "testDU").DataMover("velero").CSISnapshot(&velerov2alpha1.CSISnapshotSpec{ VolumeSnapshot: "testVS", SnapshotClass: "testClass", - }).SnapshotID("testSnapshotHandle").Result(), + }).SnapshotID("testSnapshotHandle").StartTimestamp(&now).Result(), volumeSnapshotClass: builder.ForVolumeSnapshotClass("testClass").Driver("pd.csi.storage.gke.io").Result(), operation: &itemoperation.BackupOperation{ Spec: itemoperation.BackupOperationSpec{ @@ -778,9 +786,6 @@ func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { }, }, }, - Status: itemoperation.OperationStatus{ - Created: &now, - }, }, pvMap: map[string]pvcPvInfo{ "testPV": { @@ -853,7 +858,12 @@ func TestGenerateVolumeInfoFromDataUpload(t *testing.T) { volumesInfo.logger = logging.DefaultLogger(logrus.DebugLevel, logging.FormatJSON) volumesInfo.generateVolumeInfoFromDataUpload() - require.Equal(t, tc.expectedVolumeInfos, volumesInfo.volumeInfos) + + if len(tc.expectedVolumeInfos) > 0 { + require.Equal(t, tc.expectedVolumeInfos[0].PVInfo, volumesInfo.volumeInfos[0].PVInfo) + require.Equal(t, tc.expectedVolumeInfos[0].SnapshotDataMovementInfo, volumesInfo.volumeInfos[0].SnapshotDataMovementInfo) + require.Equal(t, tc.expectedVolumeInfos[0].CSISnapshotInfo, volumesInfo.volumeInfos[0].CSISnapshotInfo) + } }) } } diff --git a/pkg/backup/backup.go b/pkg/backup/backup.go index 67e20acb0..8dcc8a12e 100644 --- a/pkg/backup/backup.go +++ b/pkg/backup/backup.go @@ -32,15 +32,16 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" kubeerrs "k8s.io/apimachinery/pkg/util/errors" - - "github.com/vmware-tanzu/velero/internal/hook" - velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - "github.com/vmware-tanzu/velero/pkg/client" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/vmware-tanzu/velero/internal/hook" + "github.com/vmware-tanzu/velero/internal/volume" + 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/client" "github.com/vmware-tanzu/velero/pkg/discovery" "github.com/vmware-tanzu/velero/pkg/itemoperation" "github.com/vmware-tanzu/velero/pkg/kuberesource" @@ -66,11 +67,31 @@ const BackupFormatVersion = "1.1.0" type Backupper interface { // Backup takes a backup using the specification in the velerov1api.Backup and writes backup and log data // to the given writers. - Backup(logger logrus.FieldLogger, backup *Request, backupFile io.Writer, actions []biav2.BackupItemAction, volumeSnapshotterGetter VolumeSnapshotterGetter) error - BackupWithResolvers(log logrus.FieldLogger, backupRequest *Request, backupFile io.Writer, backupItemActionResolver framework.BackupItemActionResolverV2, volumeSnapshotterGetter VolumeSnapshotterGetter) error - FinalizeBackup(log logrus.FieldLogger, backupRequest *Request, inBackupFile io.Reader, outBackupFile io.Writer, + Backup( + logger logrus.FieldLogger, + backup *Request, + backupFile io.Writer, + actions []biav2.BackupItemAction, + volumeSnapshotterGetter VolumeSnapshotterGetter, + ) error + + BackupWithResolvers( + log logrus.FieldLogger, + backupRequest *Request, + backupFile io.Writer, backupItemActionResolver framework.BackupItemActionResolverV2, - asyncBIAOperations []*itemoperation.BackupOperation) error + volumeSnapshotterGetter VolumeSnapshotterGetter, + ) error + + FinalizeBackup( + log logrus.FieldLogger, + backupRequest *Request, + inBackupFile io.Reader, + outBackupFile io.Writer, + backupItemActionResolver framework.BackupItemActionResolverV2, + asyncBIAOperations []*itemoperation.BackupOperation, + volumeInfos []*volume.VolumeInfo, + ) error } // kubernetesBackupper implements Backupper. @@ -183,11 +204,13 @@ func (kb *kubernetesBackupper) Backup(log logrus.FieldLogger, backupRequest *Req return kb.BackupWithResolvers(log, backupRequest, backupFile, backupItemActions, volumeSnapshotterGetter) } -func (kb *kubernetesBackupper) BackupWithResolvers(log logrus.FieldLogger, +func (kb *kubernetesBackupper) BackupWithResolvers( + log logrus.FieldLogger, backupRequest *Request, backupFile io.Writer, backupItemActionResolver framework.BackupItemActionResolverV2, - volumeSnapshotterGetter VolumeSnapshotterGetter) error { + volumeSnapshotterGetter VolumeSnapshotterGetter, +) error { gzippedData := gzip.NewWriter(backupFile) defer gzippedData.Close() @@ -470,7 +493,13 @@ func (kb *kubernetesBackupper) backupItem(log logrus.FieldLogger, gr schema.Grou return backedUpItem } -func (kb *kubernetesBackupper) finalizeItem(log logrus.FieldLogger, gr schema.GroupResource, itemBackupper *itemBackupper, unstructured *unstructured.Unstructured, preferredGVR schema.GroupVersionResource) (bool, []FileForArchive) { +func (kb *kubernetesBackupper) finalizeItem( + log logrus.FieldLogger, + gr schema.GroupResource, + itemBackupper *itemBackupper, + unstructured *unstructured.Unstructured, + preferredGVR schema.GroupVersionResource, +) (bool, []FileForArchive) { backedUpItem, updateFiles, err := itemBackupper.backupItem(log, unstructured, gr, preferredGVR, true, true) if aggregate, ok := err.(kubeerrs.Aggregate); ok { log.WithField("name", unstructured.GetName()).Infof("%d errors encountered backup up item", len(aggregate.Errors())) @@ -548,12 +577,15 @@ func (kb *kubernetesBackupper) writeBackupVersion(tw *tar.Writer) error { return nil } -func (kb *kubernetesBackupper) FinalizeBackup(log logrus.FieldLogger, +func (kb *kubernetesBackupper) FinalizeBackup( + log logrus.FieldLogger, backupRequest *Request, inBackupFile io.Reader, outBackupFile io.Writer, backupItemActionResolver framework.BackupItemActionResolverV2, - asyncBIAOperations []*itemoperation.BackupOperation) error { + asyncBIAOperations []*itemoperation.BackupOperation, + volumeInfos []*volume.VolumeInfo, +) error { gzw := gzip.NewWriter(outBackupFile) defer gzw.Close() tw := tar.NewWriter(gzw) @@ -642,6 +674,8 @@ func (kb *kubernetesBackupper) FinalizeBackup(log logrus.FieldLogger, return } + updateVolumeInfos(volumeInfos, unstructured, item.groupResource, log) + backedUp, itemFiles := kb.finalizeItem(log, item.groupResource, itemBackupper, &unstructured, item.preferredGVR) if backedUp { backedUpGroupResources[item.groupResource] = true @@ -730,3 +764,32 @@ type tarWriter interface { Write([]byte) (int, error) WriteHeader(*tar.Header) error } + +func updateVolumeInfos( + volumeInfos []*volume.VolumeInfo, + unstructured unstructured.Unstructured, + groupResource schema.GroupResource, + log logrus.FieldLogger, +) { + switch groupResource.String() { + case kuberesource.DataUploads.String(): + var dataUpload velerov2alpha1.DataUpload + err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructured.UnstructuredContent(), &dataUpload) + if err != nil { + log.WithError(err).Errorf("fail to convert DataUpload: %s/%s", + unstructured.GetNamespace(), unstructured.GetName()) + } + + for index := range volumeInfos { + if volumeInfos[index].PVCName == dataUpload.Spec.SourcePVC && + volumeInfos[index].PVCNamespace == dataUpload.Spec.SourceNamespace { + if dataUpload.Status.CompletionTimestamp != nil { + volumeInfos[index].CompletionTimestamp = dataUpload.Status.CompletionTimestamp + } + volumeInfos[index].SnapshotDataMovementInfo.SnapshotHandle = dataUpload.Status.SnapshotID + volumeInfos[index].SnapshotDataMovementInfo.RetainedSnapshot = dataUpload.Spec.CSISnapshot.VolumeSnapshot + volumeInfos[index].SnapshotDataMovementInfo.Size = dataUpload.Status.Progress.TotalBytes + } + } + } +} diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index 0ceb0572a..6f23e18e8 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -43,7 +43,9 @@ import ( "github.com/vmware-tanzu/velero/internal/resourcepolicies" "github.com/vmware-tanzu/velero/internal/volume" + "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" "github.com/vmware-tanzu/velero/pkg/builder" "github.com/vmware-tanzu/velero/pkg/client" "github.com/vmware-tanzu/velero/pkg/discovery" @@ -4433,3 +4435,59 @@ func TestBackupNamespaces(t *testing.T) { }) } } + +func TestUpdateVolumeInfos(t *testing.T) { + logger := logrus.StandardLogger() + // The unstructured conversion will loose the time precision to second + // level. To make test pass. Set the now precision at second at the + // beginning. + now := metav1.Now().Rfc3339Copy() + volumeInfos := []*volume.VolumeInfo{ + { + PVCName: "pvc1", + PVCNamespace: "ns1", + SnapshotDataMovementInfo: &volume.SnapshotDataMovementInfo{}, + }, + } + dataUpload := velerov2alpha1.DataUpload{ + ObjectMeta: metav1.ObjectMeta{ + Name: "du1", + Namespace: "velero", + }, + Spec: velerov2alpha1.DataUploadSpec{ + SourcePVC: "pvc1", + SourceNamespace: "ns1", + CSISnapshot: &velerov2alpha1.CSISnapshotSpec{ + VolumeSnapshot: "vs1", + }, + }, + Status: velerov2alpha1.DataUploadStatus{ + CompletionTimestamp: &now, + SnapshotID: "snapshot1", + Progress: shared.DataMoveOperationProgress{ + TotalBytes: 10000, + }, + }, + } + duMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&dataUpload) + require.NoError(t, err) + + expectedVolumeInfos := []*volume.VolumeInfo{ + { + PVCName: "pvc1", + PVCNamespace: "ns1", + CompletionTimestamp: &now, + SnapshotDataMovementInfo: &volume.SnapshotDataMovementInfo{ + SnapshotHandle: "snapshot1", + Size: 10000, + RetainedSnapshot: "vs1", + }, + }, + } + + updateVolumeInfos(volumeInfos, unstructured.Unstructured{Object: duMap}, kuberesource.DataUploads, logger) + + if len(expectedVolumeInfos) > 0 { + require.Equal(t, expectedVolumeInfos[0].SnapshotDataMovementInfo, volumeInfos[0].SnapshotDataMovementInfo) + } +} diff --git a/pkg/builder/data_upload_builder.go b/pkg/builder/data_upload_builder.go index 29e7a0191..4623dac6e 100644 --- a/pkg/builder/data_upload_builder.go +++ b/pkg/builder/data_upload_builder.go @@ -115,8 +115,14 @@ func (d *DataUploadBuilder) CSISnapshot(cSISnapshot *velerov2alpha1api.CSISnapsh } // StartTimestamp sets the DataUpload's StartTimestamp. -func (d *DataUploadBuilder) StartTimestamp(startTime *metav1.Time) *DataUploadBuilder { - d.object.Status.StartTimestamp = startTime +func (d *DataUploadBuilder) StartTimestamp(startTimestamp *metav1.Time) *DataUploadBuilder { + d.object.Status.StartTimestamp = startTimestamp + return d +} + +// CompletionTimestamp sets the DataUpload's StartTimestamp. +func (d *DataUploadBuilder) CompletionTimestamp(completionTimestamp *metav1.Time) *DataUploadBuilder { + d.object.Status.CompletionTimestamp = completionTimestamp return d } diff --git a/pkg/builder/pod_volume_backup_builder.go b/pkg/builder/pod_volume_backup_builder.go index 14e57a063..80cb55042 100644 --- a/pkg/builder/pod_volume_backup_builder.go +++ b/pkg/builder/pod_volume_backup_builder.go @@ -80,6 +80,16 @@ func (b *PodVolumeBackupBuilder) SnapshotID(snapshotID string) *PodVolumeBackupB return b } +func (b *PodVolumeBackupBuilder) StartTimestamp(startTimestamp *metav1.Time) *PodVolumeBackupBuilder { + b.object.Status.StartTimestamp = startTimestamp + return b +} + +func (b *PodVolumeBackupBuilder) CompletionTimestamp(completionTimestamp *metav1.Time) *PodVolumeBackupBuilder { + b.object.Status.CompletionTimestamp = completionTimestamp + return b +} + // PodName sets the name of the pod associated with this PodVolumeBackup. func (b *PodVolumeBackupBuilder) PodName(name string) *PodVolumeBackupBuilder { b.object.Spec.Pod.Name = name diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index 37caf839e..627f17e46 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -44,6 +44,7 @@ import ( kbclient "sigs.k8s.io/controller-runtime/pkg/client" fakeClient "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" pkgbackup "github.com/vmware-tanzu/velero/pkg/backup" "github.com/vmware-tanzu/velero/pkg/builder" @@ -78,9 +79,15 @@ func (b *fakeBackupper) BackupWithResolvers(logger logrus.FieldLogger, backup *p return args.Error(0) } -func (b *fakeBackupper) FinalizeBackup(logger logrus.FieldLogger, backup *pkgbackup.Request, inBackupFile io.Reader, outBackupFile io.Writer, +func (b *fakeBackupper) FinalizeBackup( + logger logrus.FieldLogger, + backup *pkgbackup.Request, + inBackupFile io.Reader, + outBackupFile io.Writer, backupItemActionResolver framework.BackupItemActionResolverV2, - asyncBIAOperations []*itemoperation.BackupOperation) error { + asyncBIAOperations []*itemoperation.BackupOperation, + volumeInfos []*volume.VolumeInfo, +) error { args := b.Called(logger, backup, inBackupFile, outBackupFile, backupItemActionResolver, asyncBIAOperations) return args.Error(0) } diff --git a/pkg/controller/backup_finalizer_controller.go b/pkg/controller/backup_finalizer_controller.go index c8fc93358..d10935fd7 100644 --- a/pkg/controller/backup_finalizer_controller.go +++ b/pkg/controller/backup_finalizer_controller.go @@ -18,7 +18,9 @@ package controller import ( "bytes" + "compress/gzip" "context" + "encoding/json" "os" "github.com/pkg/errors" @@ -29,6 +31,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" kbclient "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/vmware-tanzu/velero/internal/volume" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" pkgbackup "github.com/vmware-tanzu/velero/pkg/backup" "github.com/vmware-tanzu/velero/pkg/itemoperation" @@ -111,7 +114,11 @@ func (r *backupFinalizerReconciler) Reconcile(ctx context.Context, req ctrl.Requ original := backup.DeepCopy() defer func() { switch backup.Status.Phase { - case velerov1api.BackupPhaseCompleted, velerov1api.BackupPhasePartiallyFailed, velerov1api.BackupPhaseFailed, velerov1api.BackupPhaseFailedValidation: + case + velerov1api.BackupPhaseCompleted, + velerov1api.BackupPhasePartiallyFailed, + velerov1api.BackupPhaseFailed, + velerov1api.BackupPhaseFailedValidation: r.backupTracker.Delete(backup.Namespace, backup.Name) } // Always attempt to Patch the backup object and status after each reconciliation. @@ -150,8 +157,14 @@ func (r *backupFinalizerReconciler) Reconcile(ctx context.Context, req ctrl.Requ SkippedPVTracker: pkgbackup.NewSkipPVTracker(), } var outBackupFile *os.File + var volumeInfos []*volume.VolumeInfo if len(operations) > 0 { - // Call itemBackupper.BackupItem for the list of items updated by async operations + volumeInfos, err = backupStore.GetBackupVolumeInfos(backup.Name) + if err != nil { + log.WithError(err).Error("error getting backup VolumeInfos") + return ctrl.Result{}, errors.WithStack(err) + } + log.Info("Setting up finalized backup temp file") inBackupFile, err := downloadToTempFile(backup.Name, backupStore, log) if err != nil { @@ -172,7 +185,17 @@ func (r *backupFinalizerReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, errors.WithStack(err) } backupItemActionsResolver := framework.NewBackupItemActionResolverV2(actions) - err = r.backupper.FinalizeBackup(log, backupRequest, inBackupFile, outBackupFile, backupItemActionsResolver, operations) + + // Call itemBackupper.BackupItem for the list of items updated by async operations + err = r.backupper.FinalizeBackup( + log, + backupRequest, + inBackupFile, + outBackupFile, + backupItemActionsResolver, + operations, + volumeInfos, + ) if err != nil { log.WithError(err).Error("error finalizing Backup") return ctrl.Result{}, errors.WithStack(err) @@ -209,6 +232,24 @@ func (r *backupFinalizerReconciler) Reconcile(ctx context.Context, req ctrl.Requ if err != nil { return ctrl.Result{}, errors.Wrap(err, "error uploading backup final contents") } + + // Update the backup's VolumeInfos + backupVolumeInfoBuf := new(bytes.Buffer) + gzw := gzip.NewWriter(backupVolumeInfoBuf) + defer gzw.Close() + + if err := json.NewEncoder(gzw).Encode(volumeInfos); err != nil { + return ctrl.Result{}, errors.Wrap(err, "error encoding restore results to JSON") + } + + if err := gzw.Close(); err != nil { + return ctrl.Result{}, errors.Wrap(err, "error closing gzip writer") + } + + err = backupStore.PutBackupVolumeInfos(backup.Name, backupVolumeInfoBuf) + if err != nil { + return ctrl.Result{}, errors.Wrap(err, "fail to upload backup VolumeInfos") + } } return ctrl.Result{}, nil } diff --git a/pkg/controller/backup_finalizer_controller_test.go b/pkg/controller/backup_finalizer_controller_test.go index e2f07b3e3..8c1a5e361 100644 --- a/pkg/controller/backup_finalizer_controller_test.go +++ b/pkg/controller/backup_finalizer_controller_test.go @@ -222,6 +222,8 @@ func TestBackupFinalizerReconcile(t *testing.T) { backupStore.On("GetBackupContents", mock.Anything).Return(io.NopCloser(bytes.NewReader([]byte("hello world"))), nil) backupStore.On("PutBackupContents", mock.Anything, mock.Anything).Return(nil) backupStore.On("PutBackupMetadata", mock.Anything, mock.Anything).Return(nil) + backupStore.On("GetBackupVolumeInfos", mock.Anything).Return(nil, nil) + backupStore.On("PutBackupVolumeInfos", mock.Anything, mock.Anything).Return(nil) pluginManager.On("GetBackupItemActionsV2").Return(nil, nil) backupper.On("FinalizeBackup", mock.Anything, mock.Anything, mock.Anything, mock.Anything, framework.BackupItemActionResolverV2{}, mock.Anything).Return(nil) _, err := reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: types.NamespacedName{Namespace: test.backup.Namespace, Name: test.backup.Name}}) diff --git a/pkg/kuberesource/kuberesource.go b/pkg/kuberesource/kuberesource.go index c2c2d84ee..c39e769db 100644 --- a/pkg/kuberesource/kuberesource.go +++ b/pkg/kuberesource/kuberesource.go @@ -35,4 +35,5 @@ var ( VolumeSnapshots = schema.GroupResource{Group: "snapshot.storage.k8s.io", Resource: "volumesnapshots"} VolumeSnapshotContents = schema.GroupResource{Group: "snapshot.storage.k8s.io", Resource: "volumesnapshotcontents"} PriorityClasses = schema.GroupResource{Group: "scheduling.k8s.io", Resource: "priorityclasses"} + DataUploads = schema.GroupResource{Group: "velero.io", Resource: "datauploads"} ) diff --git a/pkg/persistence/mocks/backup_store.go b/pkg/persistence/mocks/backup_store.go index b45928e77..1c2cf7d05 100644 --- a/pkg/persistence/mocks/backup_store.go +++ b/pkg/persistence/mocks/backup_store.go @@ -314,7 +314,7 @@ func (_m *BackupStore) GetRestoreItemOperations(name string) ([]*itemoperation.R return r0, r1 } -// GetRestoreItemOperations provides a mock function with given fields: name +// GetBackupVolumeInfos provides a mock function with given fields: name func (_m *BackupStore) GetBackupVolumeInfos(name string) ([]*volume.VolumeInfo, error) { ret := _m.Called(name) @@ -337,6 +337,20 @@ func (_m *BackupStore) GetBackupVolumeInfos(name string) ([]*volume.VolumeInfo, return r0, r1 } +// PutBackupVolumeInfos provides a mock function with given fields: name, volumeInfo +func (_m *BackupStore) PutBackupVolumeInfos(name string, volumeInfo io.Reader) error { + ret := _m.Called(name, volumeInfo) + + var r0 error + if rf, ok := ret.Get(0).(func(string, io.Reader) error); ok { + r0 = rf(name, volumeInfo) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // GetRestoreResults provides a mock function with given fields: name func (_m *BackupStore) GetRestoreResults(name string) (map[string]results.Result, error) { ret := _m.Called(name) diff --git a/pkg/persistence/object_store.go b/pkg/persistence/object_store.go index 962c36de7..1036f88eb 100644 --- a/pkg/persistence/object_store.go +++ b/pkg/persistence/object_store.go @@ -75,6 +75,7 @@ type BackupStore interface { 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 GetRestoreResults(name string) (map[string]results.Result, error) // BackupExists checks if the backup metadata file exists in object storage. @@ -516,6 +517,10 @@ func (s *objectBackupStore) GetBackupVolumeInfos(name string) ([]*volume.VolumeI return volumeInfos, nil } +func (s *objectBackupStore) PutBackupVolumeInfos(name string, volumeInfo io.Reader) error { + return s.objectStore.PutObject(s.bucket, s.layout.getBackupVolumeInfoKey(name), volumeInfo) +} + func (s *objectBackupStore) GetRestoreResults(name string) (map[string]results.Result, error) { results := make(map[string]results.Result) diff --git a/pkg/persistence/object_store_test.go b/pkg/persistence/object_store_test.go index a63d31825..a23906eef 100644 --- a/pkg/persistence/object_store_test.go +++ b/pkg/persistence/object_store_test.go @@ -1203,6 +1203,51 @@ func TestGetRestoredResourceList(t *testing.T) { assert.EqualValues(t, list["pod"], res["pod"]) } +func TestPutBackupVolumeInfos(t *testing.T) { + tests := []struct { + name string + prefix string + expectedErr string + expectedKeys []string + }{ + { + name: "normal case", + expectedErr: "", + expectedKeys: []string{ + "backups/backup-1/backup-1-volumeinfo.json.gz", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + harness := newObjectBackupStoreTestHarness("foo", tc.prefix) + + volumeInfos := []*volume.VolumeInfo{ + { + PVCName: "test", + }, + } + + buf := new(bytes.Buffer) + gzw := gzip.NewWriter(buf) + defer gzw.Close() + + require.NoError(t, json.NewEncoder(gzw).Encode(volumeInfos)) + bufferContent := buf.Bytes() + + err := harness.PutBackupVolumeInfos("backup-1", buf) + + velerotest.AssertErrorMatches(t, tc.expectedErr, err) + assert.Len(t, harness.objectStore.Data[harness.bucket], len(tc.expectedKeys)) + for _, key := range tc.expectedKeys { + assert.Contains(t, harness.objectStore.Data[harness.bucket], key) + assert.Equal(t, harness.objectStore.Data[harness.bucket][key], bufferContent) + } + }) + } +} + func encodeToBytes(obj runtime.Object) []byte { res, err := encode.Encode(obj, "json") if err != nil {