/* 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 controller import ( "bytes" "fmt" "io" "sort" "time" "context" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "strings" "testing" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" corev1api "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/vmware-tanzu/velero/pkg/plugin/velero" "github.com/vmware-tanzu/velero/pkg/plugin/velero/mocks" "github.com/vmware-tanzu/velero/pkg/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" "github.com/vmware-tanzu/velero/pkg/metrics" persistencemocks "github.com/vmware-tanzu/velero/pkg/persistence/mocks" "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" pluginmocks "github.com/vmware-tanzu/velero/pkg/plugin/mocks" "github.com/vmware-tanzu/velero/pkg/repository" velerotest "github.com/vmware-tanzu/velero/pkg/test" ) type backupDeletionControllerTestData struct { fakeClient client.Client volumeSnapshotter *velerotest.FakeVolumeSnapshotter backupStore *persistencemocks.BackupStore controller *backupDeletionReconciler req ctrl.Request } func defaultTestDbr() *velerov1api.DeleteBackupRequest { req := pkgbackup.NewDeleteBackupRequest("foo", "uid") req.Namespace = velerov1api.DefaultNamespace req.Name = "foo-abcde" return req } func setupBackupDeletionControllerTest(t *testing.T, req *velerov1api.DeleteBackupRequest, objects ...runtime.Object) *backupDeletionControllerTestData { var ( fakeClient = velerotest.NewFakeControllerRuntimeClient(t, append(objects, req)...) volumeSnapshotter = &velerotest.FakeVolumeSnapshotter{SnapshotsTaken: sets.NewString()} pluginManager = &pluginmocks.Manager{} backupStore = &persistencemocks.BackupStore{} ) data := &backupDeletionControllerTestData{ fakeClient: fakeClient, volumeSnapshotter: volumeSnapshotter, backupStore: backupStore, controller: NewBackupDeletionReconciler( velerotest.NewLogger(), fakeClient, NewBackupTracker(), nil, // repository manager metrics.NewServerMetrics(), nil, // discovery helper func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager }, NewFakeSingleObjectBackupStoreGetter(backupStore), velerotest.NewFakeCredentialsFileStore("", nil), ), req: ctrl.Request{NamespacedName: types.NamespacedName{Namespace: req.Namespace, Name: req.Name}}, } pluginManager.On("CleanupClients").Return(nil) return data } func TestBackupDeletionControllerReconcile(t *testing.T) { t.Run("failed to get backup store", func(t *testing.T) { backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").StorageLocation("default").Result() location := &velerov1api.BackupStorageLocation{ ObjectMeta: metav1.ObjectMeta{ Namespace: backup.Namespace, Name: backup.Spec.StorageLocation, }, Spec: velerov1api.BackupStorageLocationSpec{ Provider: "objStoreProvider", StorageType: velerov1api.StorageType{ ObjectStorage: &velerov1api.ObjectStorageLocation{ Bucket: "bucket", }, }, }, } td := setupBackupDeletionControllerTest(t, defaultTestDbr(), location, backup) td.controller.backupStoreGetter = &fakeErrorBackupStoreGetter{} _, err := td.controller.Reconcile(ctx, td.req) assert.NotNil(t, err) assert.True(t, strings.HasPrefix(err.Error(), "error getting the backup store")) }) t.Run("missing spec.backupName", func(t *testing.T) { dbr := defaultTestDbr() dbr.Spec.BackupName = "" td := setupBackupDeletionControllerTest(t, dbr) _, err := td.controller.Reconcile(ctx, td.req) require.NoError(t, err) res := &velerov1api.DeleteBackupRequest{} err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) require.NoError(t, err) assert.Equal(t, "Processed", string(res.Status.Phase)) assert.Equal(t, 1, len(res.Status.Errors)) assert.Equal(t, "spec.backupName is required", res.Status.Errors[0]) }) t.Run("existing deletion requests for the backup are deleted", func(t *testing.T) { input := defaultTestDbr() td := setupBackupDeletionControllerTest(t, input) // add the backup to the tracker so the execution of reconcile doesn't progress // past checking for an in-progress backup. this makes validation easier. td.controller.backupTracker.Add(td.req.Namespace, input.Spec.BackupName) existing := &velerov1api.DeleteBackupRequest{ ObjectMeta: metav1.ObjectMeta{ Namespace: td.req.Namespace, Name: "bar", Labels: map[string]string{ velerov1api.BackupNameLabel: input.Spec.BackupName, }, }, Spec: velerov1api.DeleteBackupRequestSpec{ BackupName: input.Spec.BackupName, }, } err := td.fakeClient.Create(context.TODO(), existing) require.NoError(t, err) existing2 := &velerov1api.DeleteBackupRequest{ ObjectMeta: metav1.ObjectMeta{ Namespace: td.req.Namespace, Name: "bar-2", Labels: map[string]string{ velerov1api.BackupNameLabel: "some-other-backup", }, }, Spec: velerov1api.DeleteBackupRequestSpec{ BackupName: "some-other-backup", }, } err = td.fakeClient.Create(context.TODO(), existing2) require.NoError(t, err) _, err = td.controller.Reconcile(context.TODO(), td.req) assert.NoError(t, err) // verify "existing" is deleted err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ Namespace: existing.Namespace, Name: existing.Name, }, &velerov1api.DeleteBackupRequest{}) assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) // verify "existing2" remains assert.NoError(t, td.fakeClient.Get(context.TODO(), types.NamespacedName{ Namespace: existing2.Namespace, Name: existing2.Name, }, &velerov1api.DeleteBackupRequest{})) }) t.Run("deleting an in progress backup isn't allowed", func(t *testing.T) { dbr := defaultTestDbr() td := setupBackupDeletionControllerTest(t, dbr) td.controller.backupTracker.Add(td.req.Namespace, dbr.Spec.BackupName) _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) res := &velerov1api.DeleteBackupRequest{} err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) require.NoError(t, err) assert.Equal(t, "Processed", string(res.Status.Phase)) assert.Equal(t, 1, len(res.Status.Errors)) assert.Equal(t, "backup is still in progress", res.Status.Errors[0]) }) t.Run("unable to find backup", func(t *testing.T) { td := setupBackupDeletionControllerTest(t, defaultTestDbr()) _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) res := &velerov1api.DeleteBackupRequest{} err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) require.NoError(t, err) assert.Equal(t, "Processed", string(res.Status.Phase)) assert.Equal(t, 1, len(res.Status.Errors)) assert.Equal(t, "backup not found", res.Status.Errors[0]) }) t.Run("unable to find backup storage location", func(t *testing.T) { backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").StorageLocation("default").Result() td := setupBackupDeletionControllerTest(t, defaultTestDbr(), backup) _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) res := &velerov1api.DeleteBackupRequest{} err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) require.NoError(t, err) assert.Equal(t, "Processed", string(res.Status.Phase)) assert.Equal(t, 1, len(res.Status.Errors)) assert.Equal(t, "backup storage location default not found", res.Status.Errors[0]) }) t.Run("backup storage location is in read-only mode", func(t *testing.T) { backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").StorageLocation("default").Result() location := builder.ForBackupStorageLocation("velero", "default").AccessMode(velerov1api.BackupStorageLocationAccessModeReadOnly).Result() td := setupBackupDeletionControllerTest(t, defaultTestDbr(), location, backup) _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) res := &velerov1api.DeleteBackupRequest{} err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) require.NoError(t, err) assert.Equal(t, "Processed", string(res.Status.Phase)) assert.Equal(t, 1, len(res.Status.Errors)) assert.Equal(t, "cannot delete backup because backup storage location default is currently in read-only mode", res.Status.Errors[0]) }) t.Run("full delete, no errors", func(t *testing.T) { input := defaultTestDbr() // Clear out resource labels to make sure the controller adds them and does not // panic when encountering a nil Labels map // (https://github.com/vmware-tanzu/velero/issues/1546) input.Labels = nil backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").Result() backup.UID = "uid" backup.Spec.StorageLocation = "primary" restore1 := builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Phase(velerov1api.RestorePhaseCompleted).Backup("foo").Result() restore2 := builder.ForRestore(velerov1api.DefaultNamespace, "restore-2").Phase(velerov1api.RestorePhaseCompleted).Backup("foo").Result() restore3 := builder.ForRestore(velerov1api.DefaultNamespace, "restore-3").Phase(velerov1api.RestorePhaseCompleted).Backup("some-other-backup").Result() location := &velerov1api.BackupStorageLocation{ ObjectMeta: metav1.ObjectMeta{ Namespace: backup.Namespace, Name: backup.Spec.StorageLocation, }, Spec: velerov1api.BackupStorageLocationSpec{ Provider: "objStoreProvider", StorageType: velerov1api.StorageType{ ObjectStorage: &velerov1api.ObjectStorageLocation{ Bucket: "bucket", }, }, }, } snapshotLocation := &velerov1api.VolumeSnapshotLocation{ ObjectMeta: metav1.ObjectMeta{ Namespace: backup.Namespace, Name: "vsl-1", }, Spec: velerov1api.VolumeSnapshotLocationSpec{ Provider: "provider-1", }, } td := setupBackupDeletionControllerTest(t, input, backup, restore1, restore2, restore3, location, snapshotLocation) td.volumeSnapshotter.SnapshotsTaken.Insert("snap-1") snapshots := []*volume.Snapshot{ { Spec: volume.SnapshotSpec{ Location: "vsl-1", }, Status: volume.SnapshotStatus{ ProviderSnapshotID: "snap-1", }, }, } pluginManager := &pluginmocks.Manager{} pluginManager.On("GetVolumeSnapshotter", "provider-1").Return(td.volumeSnapshotter, nil) pluginManager.On("GetDeleteItemActions").Return(nil, nil) pluginManager.On("CleanupClients") td.controller.newPluginManager = func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager } td.backupStore.On("GetBackupVolumeSnapshots", input.Spec.BackupName).Return(snapshots, nil) td.backupStore.On("GetBackupContents", input.Spec.BackupName).Return(io.NopCloser(bytes.NewReader([]byte("hello world"))), nil) td.backupStore.On("DeleteBackup", input.Spec.BackupName).Return(nil) td.backupStore.On("DeleteRestore", "restore-1").Return(nil) td.backupStore.On("DeleteRestore", "restore-2").Return(nil) _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) // the dbr should be deleted res := &velerov1api.DeleteBackupRequest{} err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) if err == nil { t.Logf("status of the dbr: %s, errors in dbr: %v", res.Status.Phase, res.Status.Errors) } // backup CR, restore CR restore-1 and restore-2 should be deleted err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ Namespace: velerov1api.DefaultNamespace, Name: backup.Name, }, &velerov1api.Backup{}) assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ Namespace: velerov1api.DefaultNamespace, Name: "restore-1", }, &velerov1api.Restore{}) assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ Namespace: velerov1api.DefaultNamespace, Name: "restore-2", }, &velerov1api.Restore{}) assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) // restore-3 should remain err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ Namespace: velerov1api.DefaultNamespace, Name: "restore-3", }, &velerov1api.Restore{}) assert.Nil(t, err) td.backupStore.AssertCalled(t, "DeleteBackup", input.Spec.BackupName) td.backupStore.AssertCalled(t, "DeleteRestore", "restore-1") td.backupStore.AssertCalled(t, "DeleteRestore", "restore-2") // Make sure snapshot was deleted assert.Equal(t, 0, td.volumeSnapshotter.SnapshotsTaken.Len()) }) t.Run("full delete, no errors, with backup name greater than 63 chars", func(t *testing.T) { backup := defaultBackup(). ObjectMeta( builder.WithName("the-really-long-backup-name-that-is-much-more-than-63-characters"), ). Result() backup.UID = "uid" backup.Spec.StorageLocation = "primary" restore1 := builder.ForRestore(backup.Namespace, "restore-1"). Phase(velerov1api.RestorePhaseCompleted). Backup(backup.Name). Result() restore2 := builder.ForRestore(backup.Namespace, "restore-2"). Phase(velerov1api.RestorePhaseCompleted). Backup(backup.Name). Result() restore3 := builder.ForRestore(backup.Namespace, "restore-3"). Phase(velerov1api.RestorePhaseCompleted). Backup("some-other-backup"). Result() dbr := pkgbackup.NewDeleteBackupRequest(backup.Name, "uid") dbr.Namespace = velerov1api.DefaultNamespace dbr.Name = "foo-abcde" // Clear out resource labels to make sure the controller adds them dbr.Labels = nil location := &velerov1api.BackupStorageLocation{ ObjectMeta: metav1.ObjectMeta{ Namespace: backup.Namespace, Name: backup.Spec.StorageLocation, }, Spec: velerov1api.BackupStorageLocationSpec{ Provider: "objStoreProvider", StorageType: velerov1api.StorageType{ ObjectStorage: &velerov1api.ObjectStorageLocation{ Bucket: "bucket", }, }, }, } snapshotLocation := &velerov1api.VolumeSnapshotLocation{ ObjectMeta: metav1.ObjectMeta{ Namespace: backup.Namespace, Name: "vsl-1", }, Spec: velerov1api.VolumeSnapshotLocationSpec{ Provider: "provider-1", }, } td := setupBackupDeletionControllerTest(t, dbr, backup, restore1, restore2, restore3, location, snapshotLocation) snapshots := []*volume.Snapshot{ { Spec: volume.SnapshotSpec{ Location: "vsl-1", }, Status: volume.SnapshotStatus{ ProviderSnapshotID: "snap-1", }, }, } pluginManager := &pluginmocks.Manager{} pluginManager.On("GetVolumeSnapshotter", "provider-1").Return(td.volumeSnapshotter, nil) pluginManager.On("GetDeleteItemActions").Return(nil, nil) pluginManager.On("CleanupClients") td.controller.newPluginManager = func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager } td.backupStore.On("GetBackupVolumeSnapshots", dbr.Spec.BackupName).Return(snapshots, nil) td.backupStore.On("GetBackupContents", dbr.Spec.BackupName).Return(io.NopCloser(bytes.NewReader([]byte("hello world"))), nil) td.backupStore.On("DeleteBackup", dbr.Spec.BackupName).Return(nil) td.backupStore.On("DeleteRestore", "restore-1").Return(nil) td.backupStore.On("DeleteRestore", "restore-2").Return(nil) td.volumeSnapshotter.SnapshotsTaken.Insert("snap-1") _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) // the dbr should be deleted res := &velerov1api.DeleteBackupRequest{} err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) if err == nil { t.Logf("status of the dbr: %s, errors in dbr: %v", res.Status.Phase, res.Status.Errors) } // backup CR, restore CR restore-1 and restore-2 should be deleted err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ Namespace: velerov1api.DefaultNamespace, Name: backup.Name, }, &velerov1api.Backup{}) assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ Namespace: velerov1api.DefaultNamespace, Name: "restore-1", }, &velerov1api.Restore{}) assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ Namespace: velerov1api.DefaultNamespace, Name: "restore-2", }, &velerov1api.Restore{}) assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) // restore-3 should remain err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ Namespace: velerov1api.DefaultNamespace, Name: "restore-3", }, &velerov1api.Restore{}) assert.Nil(t, err) // Make sure snapshot was deleted assert.Equal(t, 0, td.volumeSnapshotter.SnapshotsTaken.Len()) }) t.Run("backup is not downloaded when there are no DeleteItemAction plugins", func(t *testing.T) { backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").Result() backup.UID = "uid" backup.Spec.StorageLocation = "primary" input := defaultTestDbr() // Clear out resource labels to make sure the controller adds them and does not // panic when encountering a nil Labels map // (https://github.com/vmware-tanzu/velero/issues/1546) input.Labels = nil location := &velerov1api.BackupStorageLocation{ ObjectMeta: metav1.ObjectMeta{ Namespace: backup.Namespace, Name: backup.Spec.StorageLocation, }, Spec: velerov1api.BackupStorageLocationSpec{ Provider: "objStoreProvider", StorageType: velerov1api.StorageType{ ObjectStorage: &velerov1api.ObjectStorageLocation{ Bucket: "bucket", }, }, }, } snapshotLocation := &velerov1api.VolumeSnapshotLocation{ ObjectMeta: metav1.ObjectMeta{ Namespace: backup.Namespace, Name: "vsl-1", }, Spec: velerov1api.VolumeSnapshotLocationSpec{ Provider: "provider-1", }, } td := setupBackupDeletionControllerTest(t, defaultTestDbr(), backup, location, snapshotLocation) td.volumeSnapshotter.SnapshotsTaken.Insert("snap-1") snapshots := []*volume.Snapshot{ { Spec: volume.SnapshotSpec{ Location: "vsl-1", }, Status: volume.SnapshotStatus{ ProviderSnapshotID: "snap-1", }, }, } pluginManager := &pluginmocks.Manager{} pluginManager.On("GetVolumeSnapshotter", "provider-1").Return(td.volumeSnapshotter, nil) pluginManager.On("GetDeleteItemActions").Return([]velero.DeleteItemAction{}, nil) pluginManager.On("CleanupClients") td.controller.newPluginManager = func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager } td.backupStore.On("GetBackupVolumeSnapshots", input.Spec.BackupName).Return(snapshots, nil) td.backupStore.On("DeleteBackup", input.Spec.BackupName).Return(nil) _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) td.backupStore.AssertNotCalled(t, "GetBackupContents", mock.Anything) td.backupStore.AssertCalled(t, "DeleteBackup", input.Spec.BackupName) // the dbr should be deleted res := &velerov1api.DeleteBackupRequest{} err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) if err == nil { t.Logf("status of the dbr: %s, errors in dbr: %v", res.Status.Phase, res.Status.Errors) } // backup CR should be deleted err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ Namespace: velerov1api.DefaultNamespace, Name: backup.Name, }, &velerov1api.Backup{}) assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) // Make sure snapshot was deleted assert.Equal(t, 0, td.volumeSnapshotter.SnapshotsTaken.Len()) }) t.Run("backup is still deleted if downloading tarball fails for DeleteItemAction plugins", func(t *testing.T) { backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").Result() backup.UID = "uid" backup.Spec.StorageLocation = "primary" input := defaultTestDbr() // Clear out resource labels to make sure the controller adds them and does not // panic when encountering a nil Labels map // (https://github.com/vmware-tanzu/velero/issues/1546) input.Labels = nil location := &velerov1api.BackupStorageLocation{ ObjectMeta: metav1.ObjectMeta{ Namespace: backup.Namespace, Name: backup.Spec.StorageLocation, }, Spec: velerov1api.BackupStorageLocationSpec{ Provider: "objStoreProvider", StorageType: velerov1api.StorageType{ ObjectStorage: &velerov1api.ObjectStorageLocation{ Bucket: "bucket", }, }, }, } snapshotLocation := &velerov1api.VolumeSnapshotLocation{ ObjectMeta: metav1.ObjectMeta{ Namespace: backup.Namespace, Name: "vsl-1", }, Spec: velerov1api.VolumeSnapshotLocationSpec{ Provider: "provider-1", }, } td := setupBackupDeletionControllerTest(t, defaultTestDbr(), backup, location, snapshotLocation) td.volumeSnapshotter.SnapshotsTaken.Insert("snap-1") snapshots := []*volume.Snapshot{ { Spec: volume.SnapshotSpec{ Location: "vsl-1", }, Status: volume.SnapshotStatus{ ProviderSnapshotID: "snap-1", }, }, } pluginManager := &pluginmocks.Manager{} pluginManager.On("GetVolumeSnapshotter", "provider-1").Return(td.volumeSnapshotter, nil) pluginManager.On("GetDeleteItemActions").Return([]velero.DeleteItemAction{new(mocks.DeleteItemAction)}, nil) pluginManager.On("CleanupClients") td.controller.newPluginManager = func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager } td.backupStore.On("GetBackupVolumeSnapshots", input.Spec.BackupName).Return(snapshots, nil) td.backupStore.On("GetBackupContents", input.Spec.BackupName).Return(nil, fmt.Errorf("error downloading tarball")) td.backupStore.On("DeleteBackup", input.Spec.BackupName).Return(nil) _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) td.backupStore.AssertCalled(t, "GetBackupContents", input.Spec.BackupName) td.backupStore.AssertCalled(t, "DeleteBackup", input.Spec.BackupName) // the dbr should be deleted res := &velerov1api.DeleteBackupRequest{} err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) if err == nil { t.Logf("status of the dbr: %s, errors in dbr: %v", res.Status.Phase, res.Status.Errors) } // backup CR should be deleted err = td.fakeClient.Get(context.TODO(), types.NamespacedName{ Namespace: velerov1api.DefaultNamespace, Name: backup.Name, }, &velerov1api.Backup{}) assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) // Make sure snapshot was deleted assert.Equal(t, 0, td.volumeSnapshotter.SnapshotsTaken.Len()) }) t.Run("Expired request will be deleted if the status is processed", func(t *testing.T) { expired := time.Date(2018, 4, 3, 12, 0, 0, 0, time.UTC) input := defaultTestDbr() input.CreationTimestamp = metav1.Time{ Time: expired, } input.Status.Phase = velerov1api.DeleteBackupRequestPhaseProcessed td := setupBackupDeletionControllerTest(t, input) td.backupStore.On("DeleteBackup", mock.Anything).Return(nil) _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) res := &velerov1api.DeleteBackupRequest{} err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) td.backupStore.AssertNotCalled(t, "DeleteBackup", mock.Anything) }) t.Run("Expired request will not be deleted if the status is not processed", func(t *testing.T) { expired := time.Date(2018, 4, 3, 12, 0, 0, 0, time.UTC) input := defaultTestDbr() input.CreationTimestamp = metav1.Time{ Time: expired, } input.Status.Phase = velerov1api.DeleteBackupRequestPhaseNew td := setupBackupDeletionControllerTest(t, input) td.backupStore.On("DeleteBackup", mock.Anything).Return(nil) _, err := td.controller.Reconcile(context.TODO(), td.req) require.NoError(t, err) res := &velerov1api.DeleteBackupRequest{} err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) require.NoError(t, err) assert.Equal(t, "Processed", string(res.Status.Phase)) assert.Equal(t, 1, len(res.Status.Errors)) assert.Equal(t, "backup not found", res.Status.Errors[0]) }) } func TestGetSnapshotsInBackup(t *testing.T) { tests := []struct { name string podVolumeBackups []velerov1api.PodVolumeBackup expected []repository.SnapshotIdentifier longBackupNameEnabled bool }{ { name: "no pod volume backups", podVolumeBackups: nil, expected: nil, }, { name: "no pod volume backups with matching label", podVolumeBackups: []velerov1api.PodVolumeBackup{ { ObjectMeta: metav1.ObjectMeta{Name: "foo", Labels: map[string]string{velerov1api.BackupNameLabel: "non-matching-backup-1"}}, Spec: velerov1api.PodVolumeBackupSpec{ Pod: corev1api.ObjectReference{Name: "pod-1", Namespace: "ns-1"}, }, Status: velerov1api.PodVolumeBackupStatus{SnapshotID: "snap-1"}, }, { ObjectMeta: metav1.ObjectMeta{Name: "bar", Labels: map[string]string{velerov1api.BackupNameLabel: "non-matching-backup-2"}}, Spec: velerov1api.PodVolumeBackupSpec{ Pod: corev1api.ObjectReference{Name: "pod-2", Namespace: "ns-2"}, }, Status: velerov1api.PodVolumeBackupStatus{SnapshotID: "snap-2"}, }, }, expected: nil, }, { name: "some pod volume backups with matching label", podVolumeBackups: []velerov1api.PodVolumeBackup{ { ObjectMeta: metav1.ObjectMeta{Name: "foo", Labels: map[string]string{velerov1api.BackupNameLabel: "non-matching-backup-1"}}, Spec: velerov1api.PodVolumeBackupSpec{ Pod: corev1api.ObjectReference{Name: "pod-1", Namespace: "ns-1"}, }, Status: velerov1api.PodVolumeBackupStatus{SnapshotID: "snap-1"}, }, { ObjectMeta: metav1.ObjectMeta{Name: "bar", Labels: map[string]string{velerov1api.BackupNameLabel: "non-matching-backup-2"}}, Spec: velerov1api.PodVolumeBackupSpec{ Pod: corev1api.ObjectReference{Name: "pod-2", Namespace: "ns-2"}, }, Status: velerov1api.PodVolumeBackupStatus{SnapshotID: "snap-2"}, }, { ObjectMeta: metav1.ObjectMeta{Name: "completed-pvb", Labels: map[string]string{velerov1api.BackupNameLabel: "backup-1"}}, Spec: velerov1api.PodVolumeBackupSpec{ Pod: corev1api.ObjectReference{Name: "pod-1", Namespace: "ns-1"}, }, Status: velerov1api.PodVolumeBackupStatus{SnapshotID: "snap-3"}, }, { ObjectMeta: metav1.ObjectMeta{Name: "completed-pvb-2", Labels: map[string]string{velerov1api.BackupNameLabel: "backup-1"}}, Spec: velerov1api.PodVolumeBackupSpec{ Pod: corev1api.ObjectReference{Name: "pod-1", Namespace: "ns-1"}, }, Status: velerov1api.PodVolumeBackupStatus{SnapshotID: "snap-4"}, }, { ObjectMeta: metav1.ObjectMeta{Name: "incomplete-or-failed-pvb", Labels: map[string]string{velerov1api.BackupNameLabel: "backup-1"}}, Spec: velerov1api.PodVolumeBackupSpec{ Pod: corev1api.ObjectReference{Name: "pod-1", Namespace: "ns-2"}, }, Status: velerov1api.PodVolumeBackupStatus{SnapshotID: ""}, }, }, expected: []repository.SnapshotIdentifier{ { VolumeNamespace: "ns-1", SnapshotID: "snap-3", RepositoryType: "restic", }, { VolumeNamespace: "ns-1", SnapshotID: "snap-4", RepositoryType: "restic", }, }, }, { name: "some pod volume backups with matching label and backup name greater than 63 chars", longBackupNameEnabled: true, podVolumeBackups: []velerov1api.PodVolumeBackup{ { ObjectMeta: metav1.ObjectMeta{Name: "foo", Labels: map[string]string{velerov1api.BackupNameLabel: "non-matching-backup-1"}}, Spec: velerov1api.PodVolumeBackupSpec{ Pod: corev1api.ObjectReference{Name: "pod-1", Namespace: "ns-1"}, }, Status: velerov1api.PodVolumeBackupStatus{SnapshotID: "snap-1"}, }, { ObjectMeta: metav1.ObjectMeta{Name: "bar", Labels: map[string]string{velerov1api.BackupNameLabel: "non-matching-backup-2"}}, Spec: velerov1api.PodVolumeBackupSpec{ Pod: corev1api.ObjectReference{Name: "pod-2", Namespace: "ns-2"}, }, Status: velerov1api.PodVolumeBackupStatus{SnapshotID: "snap-2"}, }, { ObjectMeta: metav1.ObjectMeta{Name: "completed-pvb", Labels: map[string]string{velerov1api.BackupNameLabel: "the-really-long-backup-name-that-is-much-more-than-63-cha6ca4bc"}}, Spec: velerov1api.PodVolumeBackupSpec{ Pod: corev1api.ObjectReference{Name: "pod-1", Namespace: "ns-1"}, }, Status: velerov1api.PodVolumeBackupStatus{SnapshotID: "snap-3"}, }, { ObjectMeta: metav1.ObjectMeta{Name: "completed-pvb-2", Labels: map[string]string{velerov1api.BackupNameLabel: "backup-1"}}, Spec: velerov1api.PodVolumeBackupSpec{ Pod: corev1api.ObjectReference{Name: "pod-1", Namespace: "ns-1"}, }, Status: velerov1api.PodVolumeBackupStatus{SnapshotID: "snap-4"}, }, { ObjectMeta: metav1.ObjectMeta{Name: "incomplete-or-failed-pvb", Labels: map[string]string{velerov1api.BackupNameLabel: "backup-1"}}, Spec: velerov1api.PodVolumeBackupSpec{ Pod: corev1api.ObjectReference{Name: "pod-1", Namespace: "ns-2"}, }, Status: velerov1api.PodVolumeBackupStatus{SnapshotID: ""}, }, }, expected: []repository.SnapshotIdentifier{ { VolumeNamespace: "ns-1", SnapshotID: "snap-3", RepositoryType: "restic", }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var ( clientBuilder = velerotest.NewFakeControllerRuntimeClientBuilder(t) veleroBackup = &velerov1api.Backup{} ) veleroBackup.Name = "backup-1" if test.longBackupNameEnabled { veleroBackup.Name = "the-really-long-backup-name-that-is-much-more-than-63-characters" } clientBuilder.WithLists(&velerov1api.PodVolumeBackupList{ Items: test.podVolumeBackups, }) res, err := getSnapshotsInBackup(context.TODO(), veleroBackup, clientBuilder.Build()) assert.NoError(t, err) // sort to ensure good compare of slices less := func(snapshots []repository.SnapshotIdentifier) func(i, j int) bool { return func(i, j int) bool { if snapshots[i].VolumeNamespace == snapshots[j].VolumeNamespace { return snapshots[i].SnapshotID < snapshots[j].SnapshotID } return snapshots[i].VolumeNamespace < snapshots[j].VolumeNamespace } } sort.Slice(test.expected, less(test.expected)) sort.Slice(res, less(res)) assert.Equal(t, test.expected, res) }) } }