From 30995bcbd29f7a50de498ceb4112ecdd4999f9b4 Mon Sep 17 00:00:00 2001 From: Xun Jiang Date: Thu, 11 Apr 2024 22:26:54 +0800 Subject: [PATCH] Add more UT for the CSI plugins. Signed-off-by: Xun Jiang --- .../actions/csi/volumesnapshot_action_test.go | 151 +++++++++ .../csi/volumesnapshotcontent_action.go | 2 +- .../csi/volumesnapshotcontent_action_test.go | 142 ++++++++ pkg/apis/velero/v1/labels_annotations.go | 2 - pkg/backup/actions/csi/pvc_action_test.go | 36 ++ .../actions/csi/volumesnapshot_action.go | 11 +- .../actions/csi/volumesnapshot_action_test.go | 312 ++++++++++++++++++ .../actions/csi/volumesnapshotclass_action.go | 2 +- .../csi/volumesnapshotclass_action_test.go | 113 +++++++ .../csi/volumesnapshotcontent_action.go | 2 +- .../csi/volumesnapshotcontent_action_test.go | 125 +++++++ pkg/builder/volume_snapshot_builder.go | 12 + .../volume_snapshot_content_builder.go | 10 + pkg/restore/actions/csi/pvc_action_test.go | 36 ++ .../actions/csi/volumesnapshot_action.go | 19 +- .../actions/csi/volumesnapshot_action_test.go | 155 +++++++++ .../actions/csi/volumesnapshotclass_action.go | 2 +- .../csi/volumesnapshotclass_action_test.go | 122 +++++++ .../csi/volumesnapshotcontent_action.go | 6 +- .../csi/volumesnapshotcontent_action_test.go | 114 +++++++ pkg/util/csi/volume_snapshot.go | 8 - pkg/util/csi/volume_snapshot_test.go | 88 ----- 22 files changed, 1356 insertions(+), 114 deletions(-) create mode 100644 internal/delete/actions/csi/volumesnapshot_action_test.go create mode 100644 internal/delete/actions/csi/volumesnapshotcontent_action_test.go create mode 100644 pkg/backup/actions/csi/volumesnapshot_action_test.go create mode 100644 pkg/backup/actions/csi/volumesnapshotclass_action_test.go create mode 100644 pkg/backup/actions/csi/volumesnapshotcontent_action_test.go create mode 100644 pkg/restore/actions/csi/volumesnapshotclass_action_test.go create mode 100644 pkg/restore/actions/csi/volumesnapshotcontent_action_test.go diff --git a/internal/delete/actions/csi/volumesnapshot_action_test.go b/internal/delete/actions/csi/volumesnapshot_action_test.go new file mode 100644 index 000000000..9aa8bb1ae --- /dev/null +++ b/internal/delete/actions/csi/volumesnapshot_action_test.go @@ -0,0 +1,151 @@ +/* +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 csi + +import ( + "context" + "fmt" + "testing" + + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" + factorymocks "github.com/vmware-tanzu/velero/pkg/client/mocks" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" + velerotest "github.com/vmware-tanzu/velero/pkg/test" +) + +func TestVSExecute(t *testing.T) { + tests := []struct { + name string + item runtime.Unstructured + vs *snapshotv1api.VolumeSnapshot + backup *velerov1api.Backup + createVS bool + expectErr bool + }{ + { + name: "VolumeSnapshot doesn't have backup label", + item: velerotest.UnstructuredOrDie( + ` + { + "apiVersion": "snapshot.storage.k8s.io/v1", + "kind": "VolumeSnapshot", + "metadata": { + "namespace": "ns", + "name": "foo" + } + } + `, + ), + backup: builder.ForBackup("velero", "backup").Result(), + expectErr: false, + }, + { + name: "VolumeSnapshot doesn't exist in the cluster", + vs: builder.ForVolumeSnapshot("foo", "bar"). + ObjectMeta(builder.WithLabelsMap( + map[string]string{velerov1api.BackupNameLabel: "backup"}, + )).Status(). + BoundVolumeSnapshotContentName("vsc"). + Result(), + backup: builder.ForBackup("velero", "backup").Result(), + expectErr: true, + }, + { + name: "Normal case, VolumeSnapshot should be deleted", + vs: builder.ForVolumeSnapshot("foo", "bar"). + ObjectMeta(builder.WithLabelsMap( + map[string]string{velerov1api.BackupNameLabel: "backup"}, + )).Status(). + BoundVolumeSnapshotContentName("vsc"). + Result(), + backup: builder.ForBackup("velero", "backup").Result(), + expectErr: false, + createVS: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + crClient := velerotest.NewFakeControllerRuntimeClient(t) + logger := logrus.StandardLogger() + + p := volumeSnapshotDeleteItemAction{log: logger, crClient: crClient} + + if test.vs != nil { + vsMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(test.vs) + require.NoError(t, err) + test.item = &unstructured.Unstructured{Object: vsMap} + } + + if test.createVS { + require.NoError(t, crClient.Create(context.TODO(), test.vs)) + } + + err := p.Execute( + &velero.DeleteItemActionExecuteInput{ + Item: test.item, + Backup: test.backup, + }, + ) + + if test.expectErr == false { + require.NoError(t, err) + } + }) + } +} + +func TestVSAppliesTo(t *testing.T) { + p := volumeSnapshotDeleteItemAction{ + log: logrus.StandardLogger(), + } + selector, err := p.AppliesTo() + + require.NoError(t, err) + + require.Equal( + t, + velero.ResourceSelector{ + IncludedResources: []string{"volumesnapshots.snapshot.storage.k8s.io"}, + }, + selector, + ) +} + +func TestNewVolumeSnapshotDeleteItemAction(t *testing.T) { + logger := logrus.StandardLogger() + crClient := velerotest.NewFakeControllerRuntimeClient(t) + + f := &factorymocks.Factory{} + f.On("KubebuilderClient").Return(nil, fmt.Errorf("")) + plugin := NewVolumeSnapshotDeleteItemAction(f) + _, err := plugin(logger) + require.Error(t, err) + + f1 := &factorymocks.Factory{} + f1.On("KubebuilderClient").Return(crClient, nil) + plugin1 := NewVolumeSnapshotDeleteItemAction(f1) + _, err1 := plugin1(logger) + require.NoError(t, err1) +} diff --git a/internal/delete/actions/csi/volumesnapshotcontent_action.go b/internal/delete/actions/csi/volumesnapshotcontent_action.go index f3cfe41f7..0ff0bb68d 100644 --- a/internal/delete/actions/csi/volumesnapshotcontent_action.go +++ b/internal/delete/actions/csi/volumesnapshotcontent_action.go @@ -45,7 +45,7 @@ type volumeSnapshotContentDeleteItemAction struct { // while restoring VolumeSnapshotContent.snapshot.storage.k8s.io resources func (p *volumeSnapshotContentDeleteItemAction) AppliesTo() (velero.ResourceSelector, error) { return velero.ResourceSelector{ - IncludedResources: []string{"volumesnapshotcontent.snapshot.storage.k8s.io"}, + IncludedResources: []string{"volumesnapshotcontents.snapshot.storage.k8s.io"}, }, nil } diff --git a/internal/delete/actions/csi/volumesnapshotcontent_action_test.go b/internal/delete/actions/csi/volumesnapshotcontent_action_test.go new file mode 100644 index 000000000..5c1f6dcb9 --- /dev/null +++ b/internal/delete/actions/csi/volumesnapshotcontent_action_test.go @@ -0,0 +1,142 @@ +/* +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 csi + +import ( + "context" + "fmt" + "testing" + + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" + factorymocks "github.com/vmware-tanzu/velero/pkg/client/mocks" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" + velerotest "github.com/vmware-tanzu/velero/pkg/test" +) + +func TestVSCExecute(t *testing.T) { + snapshotHandleStr := "test" + tests := []struct { + name string + item runtime.Unstructured + vsc *snapshotv1api.VolumeSnapshotContent + backup *velerov1api.Backup + createVSC bool + expectErr bool + }{ + { + name: "VolumeSnapshotContent doesn't have backup label", + item: velerotest.UnstructuredOrDie( + ` + { + "apiVersion": "snapshot.storage.k8s.io/v1", + "kind": "VolumeSnapshotContent", + "metadata": { + "namespace": "ns", + "name": "foo" + } + } + `, + ), + backup: builder.ForBackup("velero", "backup").Result(), + expectErr: false, + }, + { + name: "VolumeSnapshotContent doesn't exist in the cluster, no error", + vsc: builder.ForVolumeSnapshotContent("bar").ObjectMeta(builder.WithLabelsMap(map[string]string{velerov1api.BackupNameLabel: "backup"})).Status(&snapshotv1api.VolumeSnapshotContentStatus{SnapshotHandle: &snapshotHandleStr}).Result(), + backup: builder.ForBackup("velero", "backup").Result(), + expectErr: false, + }, + { + name: "Normal case, VolumeSnapshot should be deleted", + vsc: builder.ForVolumeSnapshotContent("bar").ObjectMeta(builder.WithLabelsMap(map[string]string{velerov1api.BackupNameLabel: "backup"})).Status(&snapshotv1api.VolumeSnapshotContentStatus{SnapshotHandle: &snapshotHandleStr}).Result(), + backup: builder.ForBackup("velero", "backup").Result(), + expectErr: false, + createVSC: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + crClient := velerotest.NewFakeControllerRuntimeClient(t) + logger := logrus.StandardLogger() + + p := volumeSnapshotContentDeleteItemAction{log: logger, crClient: crClient} + + if test.vsc != nil { + vscMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(test.vsc) + require.NoError(t, err) + test.item = &unstructured.Unstructured{Object: vscMap} + } + + if test.createVSC { + require.NoError(t, crClient.Create(context.TODO(), test.vsc)) + } + + err := p.Execute( + &velero.DeleteItemActionExecuteInput{ + Item: test.item, + Backup: test.backup, + }, + ) + + if test.expectErr == false { + require.NoError(t, err) + } + }) + } +} + +func TestVSCAppliesTo(t *testing.T) { + p := volumeSnapshotContentDeleteItemAction{ + log: logrus.StandardLogger(), + } + selector, err := p.AppliesTo() + + require.NoError(t, err) + + require.Equal( + t, + velero.ResourceSelector{ + IncludedResources: []string{"volumesnapshotcontents.snapshot.storage.k8s.io"}, + }, + selector, + ) +} + +func TestNewVolumeSnapshotContentDeleteItemAction(t *testing.T) { + logger := logrus.StandardLogger() + crClient := velerotest.NewFakeControllerRuntimeClient(t) + + f := &factorymocks.Factory{} + f.On("KubebuilderClient").Return(nil, fmt.Errorf("")) + plugin := NewVolumeSnapshotContentDeleteItemAction(f) + _, err := plugin(logger) + require.Error(t, err) + + f1 := &factorymocks.Factory{} + f1.On("KubebuilderClient").Return(crClient, nil) + plugin1 := NewVolumeSnapshotContentDeleteItemAction(f1) + _, err1 := plugin1(logger) + require.NoError(t, err1) +} diff --git a/pkg/apis/velero/v1/labels_annotations.go b/pkg/apis/velero/v1/labels_annotations.go index 6a2b59d49..c86b4e91b 100644 --- a/pkg/apis/velero/v1/labels_annotations.go +++ b/pkg/apis/velero/v1/labels_annotations.go @@ -122,8 +122,6 @@ const ( VolumeSnapshotHandleAnnotation = "velero.io/csi-volumesnapshot-handle" VolumeSnapshotRestoreSize = "velero.io/csi-volumesnapshot-restore-size" DriverNameAnnotation = "velero.io/csi-driver-name" - DeleteSecretNameAnnotation = "velero.io/csi-deletesnapshotsecret-name" // #nosec G101 - DeleteSecretNamespaceAnnotation = "velero.io/csi-deletesnapshotsecret-namespace" // #nosec G101 VSCDeletionPolicyAnnotation = "velero.io/csi-vsc-deletion-policy" VolumeSnapshotClassSelectorLabel = "velero.io/csi-volumesnapshot-class" VolumeSnapshotClassDriverBackupAnnotationPrefix = "velero.io/csi-volumesnapshot-class" diff --git a/pkg/backup/actions/csi/pvc_action_test.go b/pkg/backup/actions/csi/pvc_action_test.go index 4d8a0e910..aeda894dc 100644 --- a/pkg/backup/actions/csi/pvc_action_test.go +++ b/pkg/backup/actions/csi/pvc_action_test.go @@ -18,6 +18,7 @@ package csi import ( "context" + "fmt" "testing" "time" @@ -40,6 +41,7 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" "github.com/vmware-tanzu/velero/pkg/builder" + factorymocks "github.com/vmware-tanzu/velero/pkg/client/mocks" "github.com/vmware-tanzu/velero/pkg/plugin/velero" velerotest "github.com/vmware-tanzu/velero/pkg/test" "github.com/vmware-tanzu/velero/pkg/util/boolptr" @@ -365,3 +367,37 @@ func TestCancel(t *testing.T) { }) } } + +func TestPVCAppliesTo(t *testing.T) { + p := pvcBackupItemAction{ + log: logrus.StandardLogger(), + } + selector, err := p.AppliesTo() + + require.NoError(t, err) + + require.Equal( + t, + velero.ResourceSelector{ + IncludedResources: []string{"persistentvolumeclaims"}, + }, + selector, + ) +} + +func TestNewPVCBackupItemAction(t *testing.T) { + logger := logrus.StandardLogger() + crClient := velerotest.NewFakeControllerRuntimeClient(t) + + f := &factorymocks.Factory{} + f.On("KubebuilderClient").Return(nil, fmt.Errorf("")) + plugin := NewPvcBackupItemAction(f) + _, err := plugin(logger) + require.Error(t, err) + + f1 := &factorymocks.Factory{} + f1.On("KubebuilderClient").Return(crClient, nil) + plugin1 := NewPvcBackupItemAction(f1) + _, err1 := plugin1(logger) + require.NoError(t, err1) +} diff --git a/pkg/backup/actions/csi/volumesnapshot_action.go b/pkg/backup/actions/csi/volumesnapshot_action.go index 9a07ec765..8be46ea40 100644 --- a/pkg/backup/actions/csi/volumesnapshot_action.go +++ b/pkg/backup/actions/csi/volumesnapshot_action.go @@ -84,10 +84,15 @@ func (p *volumeSnapshotBackupItemAction) Execute( return nil, nil, "", nil, errors.WithStack(err) } + volumeSnapshotClassName := "" + if vs.Spec.VolumeSnapshotClassName != nil { + volumeSnapshotClassName = *vs.Spec.VolumeSnapshotClassName + } + additionalItems := []velero.ResourceIdentifier{ { GroupResource: kuberesource.VolumeSnapshotClasses, - Name: *vs.Spec.VolumeSnapshotClassName, + Name: volumeSnapshotClassName, }, } @@ -128,6 +133,10 @@ func (p *volumeSnapshotBackupItemAction) Execute( WithField("Backup", fmt.Sprintf("%s/%s", backup.Namespace, backup.Name)). WithField("BackupPhase", backup.Status.Phase).Debugf("Clean VolumeSnapshots.") + if vsc == nil { + vsc = &snapshotv1api.VolumeSnapshotContent{} + } + csi.DeleteVolumeSnapshot(*vs, *vsc, backup, p.crClient, p.log) return item, nil, "", nil, nil } diff --git a/pkg/backup/actions/csi/volumesnapshot_action_test.go b/pkg/backup/actions/csi/volumesnapshot_action_test.go new file mode 100644 index 000000000..5abf7ac9e --- /dev/null +++ b/pkg/backup/actions/csi/volumesnapshot_action_test.go @@ -0,0 +1,312 @@ +/* +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 csi + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" + factorymocks "github.com/vmware-tanzu/velero/pkg/client/mocks" + "github.com/vmware-tanzu/velero/pkg/kuberesource" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" + velerotest "github.com/vmware-tanzu/velero/pkg/test" +) + +func TestVSExecute(t *testing.T) { + snapshotHandle := "handle" + + tests := []struct { + name string + backup *velerov1api.Backup + vs *snapshotv1api.VolumeSnapshot + vsc *snapshotv1api.VolumeSnapshotContent + expectedErr string + expectedAdditionalItems []velero.ResourceIdentifier + expectedItemToUpdate []velero.ResourceIdentifier + }{ + { + name: "VS not created by backup, has no status. Backup is finalizing", + backup: builder.ForBackup("velero", "backup"). + Phase(velerov1api.BackupPhaseFinalizing).Result(), + vs: builder.ForVolumeSnapshot("velero", "vs"). + VolumeSnapshotClass("class").Result(), + expectedErr: "", + }, + { + name: "VS is not created by the backup, associated VSC not exists", + backup: builder.ForBackup("velero", "backup"). + Phase(velerov1api.BackupPhaseInProgress).Result(), + vs: builder.ForVolumeSnapshot("velero", "vs"). + VolumeSnapshotClass("class").Status(). + BoundVolumeSnapshotContentName("vsc").Result(), + expectedErr: `error getting volume snapshot content from API: volumesnapshotcontents.snapshot.storage.k8s.io "vsc" not found`, + }, + { + name: "Normal case", + backup: builder.ForBackup("velero", "backup"). + Phase(velerov1api.BackupPhaseInProgress).Result(), + vs: builder.ForVolumeSnapshot("velero", "vs"). + ObjectMeta(builder.WithLabels( + velerov1api.BackupNameLabel, "backup")). + VolumeSnapshotClass("class").Status(). + BoundVolumeSnapshotContentName("vsc").Result(), + vsc: builder.ForVolumeSnapshotContent("vsc").Status( + &snapshotv1api.VolumeSnapshotContentStatus{ + SnapshotHandle: &snapshotHandle, + }, + ).Result(), + expectedErr: "", + expectedAdditionalItems: []velero.ResourceIdentifier{ + { + GroupResource: kuberesource.VolumeSnapshotClasses, + Name: "class", + }, + { + GroupResource: kuberesource.VolumeSnapshotContents, + Name: "vsc", + }, + }, + expectedItemToUpdate: []velero.ResourceIdentifier{ + { + GroupResource: kuberesource.VolumeSnapshots, + Namespace: "velero", + Name: "vs", + }, + { + GroupResource: kuberesource.VolumeSnapshotContents, + Name: "vsc", + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(*testing.T) { + vsBIA := volumeSnapshotBackupItemAction{ + log: logrus.New(), + crClient: velerotest.NewFakeControllerRuntimeClient(t, tc.vs), + } + + item, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.vs) + require.NoError(t, err) + + if tc.vsc != nil { + require.NoError(t, vsBIA.crClient.Create(context.TODO(), tc.vsc)) + } + + _, additionalItems, _, itemToUpdate, err := vsBIA.Execute(&unstructured.UnstructuredList{Object: item}, tc.backup) + if tc.expectedErr == "" { + require.NoError(t, err) + } else { + require.Equal(t, tc.expectedErr, err.Error()) + } + + require.ElementsMatch(t, tc.expectedAdditionalItems, additionalItems) + require.ElementsMatch(t, tc.expectedItemToUpdate, itemToUpdate) + }) + } +} + +func TestVSProgress(t *testing.T) { + errorStr := "error" + readyToUse := true + tests := []struct { + name string + backup *velerov1api.Backup + vs *snapshotv1api.VolumeSnapshot + vsc *snapshotv1api.VolumeSnapshotContent + operationID string + expectedErr bool + expectedProgress *velero.OperationProgress + }{ + { + name: "Empty OperationID", + operationID: "", + backup: builder.ForBackup("velero", "backup").Result(), + expectedErr: true, + }, + { + name: "OperationID doesn't have slash", + operationID: "invalid", + backup: builder.ForBackup("velero", "backup").Result(), + expectedErr: true, + }, + { + name: "OperationID doesn't have valid timestamp", + operationID: "ns/name/invalid", + backup: builder.ForBackup("velero", "backup").Result(), + expectedErr: true, + }, + { + name: "OperationID represents VS does not exist", + operationID: "ns/name/2024-04-11T18:49:00+08:00", + backup: builder.ForBackup("velero", "backup").Result(), + expectedErr: true, + }, + { + name: "VS status is nil", + operationID: "ns/name/2024-04-11T18:49:00+08:00", + vs: builder.ForVolumeSnapshot("ns", "name").Result(), + backup: builder.ForBackup("velero", "backup").Result(), + expectedErr: false, + }, + { + name: "VS status has error", + operationID: "ns/name/2024-04-11T18:49:00+08:00", + vs: builder.ForVolumeSnapshot("ns", "name").Status(). + StatusError(snapshotv1api.VolumeSnapshotError{ + Message: &errorStr, + }).Result(), + backup: builder.ForBackup("velero", "backup").Result(), + expectedErr: false, + }, + { + name: "Fail to get VSC", + operationID: "ns/name/2024-04-11T18:49:00+08:00", + vs: builder.ForVolumeSnapshot("ns", "name").Status(). + ReadyToUse(true).BoundVolumeSnapshotContentName("vsc").Result(), + backup: builder.ForBackup("velero", "backup").Result(), + expectedErr: true, + }, + { + name: "VSC status is nil", + operationID: "ns/name/2024-04-11T18:49:00+08:00", + vs: builder.ForVolumeSnapshot("ns", "name").Status(). + ReadyToUse(true).BoundVolumeSnapshotContentName("vsc").Result(), + vsc: builder.ForVolumeSnapshotContent("vsc").Result(), + backup: builder.ForBackup("velero", "backup").Result(), + expectedErr: false, + }, + { + name: "VSC is ReadyToUse", + operationID: "ns/name/2024-04-11T18:49:00+08:00", + vs: builder.ForVolumeSnapshot("ns", "name").Status(). + ReadyToUse(true).BoundVolumeSnapshotContentName("vsc").Result(), + vsc: builder.ForVolumeSnapshotContent("vsc"). + Status(&snapshotv1api.VolumeSnapshotContentStatus{ + ReadyToUse: &readyToUse, + }).Result(), + backup: builder.ForBackup("velero", "backup").Result(), + expectedErr: false, + expectedProgress: &velero.OperationProgress{Completed: true}, + }, + { + name: "VSC status has error", + operationID: "ns/name/2024-04-11T18:49:00+08:00", + vs: builder.ForVolumeSnapshot("ns", "name").Status(). + ReadyToUse(true).BoundVolumeSnapshotContentName("vsc").Result(), + vsc: builder.ForVolumeSnapshotContent("vsc"). + Status(&snapshotv1api.VolumeSnapshotContentStatus{ + Error: &snapshotv1api.VolumeSnapshotError{ + Message: &errorStr, + }, + }).Result(), + backup: builder.ForBackup("velero", "backup").Result(), + expectedErr: false, + expectedProgress: &velero.OperationProgress{ + Completed: true, + Err: "error", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(*testing.T) { + crClient := velerotest.NewFakeControllerRuntimeClient(t) + logger := logrus.New() + + vsBIA := volumeSnapshotBackupItemAction{ + log: logger, + crClient: crClient, + } + + if tc.vs != nil { + err := crClient.Create(context.Background(), tc.vs) + require.NoError(t, err) + } + + if tc.vsc != nil { + require.NoError(t, crClient.Create(context.TODO(), tc.vsc)) + } + + progress, err := vsBIA.Progress(tc.operationID, tc.backup) + if tc.expectedErr == false { + require.NoError(t, err) + } + + if tc.expectedProgress != nil { + require.True( + t, + cmp.Equal( + *tc.expectedProgress, + progress, + cmpopts.IgnoreFields( + velero.OperationProgress{}, + "Started", + "Updated", + ), + ), + ) + } + }) + } +} + +func TestVSAppliesTo(t *testing.T) { + p := volumeSnapshotBackupItemAction{ + log: logrus.StandardLogger(), + } + selector, err := p.AppliesTo() + + require.NoError(t, err) + + require.Equal( + t, + velero.ResourceSelector{ + IncludedResources: []string{"volumesnapshots.snapshot.storage.k8s.io"}, + }, + selector, + ) +} + +func TestNewVolumeSnapshotBackupItemAction(t *testing.T) { + logger := logrus.StandardLogger() + crClient := velerotest.NewFakeControllerRuntimeClient(t) + + f := &factorymocks.Factory{} + f.On("KubebuilderClient").Return(nil, fmt.Errorf("")) + plugin := NewVolumeSnapshotBackupItemAction(f) + _, err := plugin(logger) + require.Error(t, err) + + f1 := &factorymocks.Factory{} + f1.On("KubebuilderClient").Return(crClient, nil) + plugin1 := NewVolumeSnapshotBackupItemAction(f1) + _, err1 := plugin1(logger) + require.NoError(t, err1) +} diff --git a/pkg/backup/actions/csi/volumesnapshotclass_action.go b/pkg/backup/actions/csi/volumesnapshotclass_action.go index d6f9ca4e5..f67c5d718 100644 --- a/pkg/backup/actions/csi/volumesnapshotclass_action.go +++ b/pkg/backup/actions/csi/volumesnapshotclass_action.go @@ -45,7 +45,7 @@ func (p *volumeSnapshotClassBackupItemAction) AppliesTo() ( error, ) { return velero.ResourceSelector{ - IncludedResources: []string{"volumesnapshotclass.snapshot.storage.k8s.io"}, + IncludedResources: []string{"volumesnapshotclasses.snapshot.storage.k8s.io"}, }, nil } diff --git a/pkg/backup/actions/csi/volumesnapshotclass_action_test.go b/pkg/backup/actions/csi/volumesnapshotclass_action_test.go new file mode 100644 index 000000000..a3a6a7e25 --- /dev/null +++ b/pkg/backup/actions/csi/volumesnapshotclass_action_test.go @@ -0,0 +1,113 @@ +/* +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 csi + +import ( + "testing" + + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/pkg/kuberesource" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" +) + +func TestVSClassExecute(t *testing.T) { + tests := []struct { + name string + item runtime.Unstructured + vsClass *snapshotv1api.VolumeSnapshotClass + backup *velerov1api.Backup + expectErr bool + expectedItems []velero.ResourceIdentifier + }{ + { + name: "No Secret in the VS Class, no return additional items", + vsClass: builder.ForVolumeSnapshotClass("test").Result(), + backup: builder.ForBackup("velero", "backup").Result(), + expectErr: false, + }, + { + name: "Normal case, additional items should return", + vsClass: builder.ForVolumeSnapshotClass("test").ObjectMeta(builder.WithAnnotationsMap( + map[string]string{ + velerov1api.PrefixedListSecretNameAnnotation: "name", + velerov1api.PrefixedListSecretNamespaceAnnotation: "namespace", + }, + )).Result(), + backup: builder.ForBackup("velero", "backup").Result(), + expectErr: false, + expectedItems: []velero.ResourceIdentifier{ + { + GroupResource: kuberesource.Secrets, + Namespace: "namespace", + Name: "name", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p, err := NewVolumeSnapshotClassBackupItemAction(logrus.StandardLogger()) + require.NoError(t, err) + + action := p.(*volumeSnapshotClassBackupItemAction) + + if test.vsClass != nil { + vsMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(test.vsClass) + require.NoError(t, err) + test.item = &unstructured.Unstructured{Object: vsMap} + } + + _, additionalItems, _, _, err := action.Execute( + test.item, + test.backup, + ) + + if test.expectErr == false { + require.NoError(t, err) + } + + if len(test.expectedItems) > 0 { + require.Equal(t, test.expectedItems, additionalItems) + } + }) + } +} + +func TestVSClassAppliesTo(t *testing.T) { + p := volumeSnapshotClassBackupItemAction{ + log: logrus.StandardLogger(), + } + selector, err := p.AppliesTo() + + require.NoError(t, err) + + require.Equal( + t, + velero.ResourceSelector{ + IncludedResources: []string{"volumesnapshotclasses.snapshot.storage.k8s.io"}, + }, + selector, + ) +} diff --git a/pkg/backup/actions/csi/volumesnapshotcontent_action.go b/pkg/backup/actions/csi/volumesnapshotcontent_action.go index e8e17280d..f40f0787d 100644 --- a/pkg/backup/actions/csi/volumesnapshotcontent_action.go +++ b/pkg/backup/actions/csi/volumesnapshotcontent_action.go @@ -43,7 +43,7 @@ type volumeSnapshotContentBackupItemAction struct { // backup VolumeSnapshotContents. func (p *volumeSnapshotContentBackupItemAction) AppliesTo() (velero.ResourceSelector, error) { return velero.ResourceSelector{ - IncludedResources: []string{"volumesnapshotcontent.snapshot.storage.k8s.io"}, + IncludedResources: []string{"volumesnapshotcontents.snapshot.storage.k8s.io"}, }, nil } diff --git a/pkg/backup/actions/csi/volumesnapshotcontent_action_test.go b/pkg/backup/actions/csi/volumesnapshotcontent_action_test.go new file mode 100644 index 000000000..b2e75e34a --- /dev/null +++ b/pkg/backup/actions/csi/volumesnapshotcontent_action_test.go @@ -0,0 +1,125 @@ +/* +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 csi + +import ( + "testing" + + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/pkg/kuberesource" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" + velerotest "github.com/vmware-tanzu/velero/pkg/test" +) + +func TestVSCExecute(t *testing.T) { + tests := []struct { + name string + item runtime.Unstructured + vsc *snapshotv1api.VolumeSnapshotContent + backup *velerov1api.Backup + expectErr bool + expectedItems []velero.ResourceIdentifier + }{ + { + name: "Invalid VolumeSnapshotClass", + item: velerotest.UnstructuredOrDie( + ` + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "namespace": "ns", + "name": "foo" + } + } + `, + ), + backup: builder.ForBackup("velero", "backup").Result(), + expectErr: true, + }, + { + name: "Normal case, additional items should return", + vsc: builder.ForVolumeSnapshotContent("test").ObjectMeta(builder.WithAnnotationsMap( + map[string]string{ + velerov1api.PrefixedSecretNameAnnotation: "name", + velerov1api.PrefixedSecretNamespaceAnnotation: "namespace", + }, + )).Result(), + backup: builder.ForBackup("velero", "backup").Result(), + expectErr: false, + expectedItems: []velero.ResourceIdentifier{ + { + GroupResource: kuberesource.Secrets, + Namespace: "namespace", + Name: "name", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p, err := NewVolumeSnapshotContentBackupItemAction(logrus.StandardLogger()) + require.NoError(t, err) + + action := p.(*volumeSnapshotContentBackupItemAction) + + if test.vsc != nil { + vsMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(test.vsc) + require.NoError(t, err) + test.item = &unstructured.Unstructured{Object: vsMap} + } + + _, additionalItems, _, _, err := action.Execute( + test.item, + test.backup, + ) + + if test.expectErr == false { + require.NoError(t, err) + } + + if len(test.expectedItems) > 0 { + require.Equal(t, test.expectedItems, additionalItems) + } + }) + } +} + +func TestVSCAppliesTo(t *testing.T) { + p := volumeSnapshotContentBackupItemAction{ + log: logrus.StandardLogger(), + } + selector, err := p.AppliesTo() + + require.NoError(t, err) + + require.Equal( + t, + velero.ResourceSelector{ + IncludedResources: []string{"volumesnapshotcontents.snapshot.storage.k8s.io"}, + }, + selector, + ) +} diff --git a/pkg/builder/volume_snapshot_builder.go b/pkg/builder/volume_snapshot_builder.go index 36a2ba173..2f4ebe56a 100644 --- a/pkg/builder/volume_snapshot_builder.go +++ b/pkg/builder/volume_snapshot_builder.go @@ -93,3 +93,15 @@ func (v *VolumeSnapshotBuilder) VolumeSnapshotClass(name string) *VolumeSnapshot v.object.Spec.VolumeSnapshotClassName = &name return v } + +// StatusError set the built VolumeSnapshot's status.Error value. +func (v *VolumeSnapshotBuilder) StatusError(snapshotError snapshotv1api.VolumeSnapshotError) *VolumeSnapshotBuilder { + v.object.Status.Error = &snapshotError + return v +} + +// ReadyToUse set the built VolumeSnapshot's status.ReadyToUse value. +func (v *VolumeSnapshotBuilder) ReadyToUse(readyToUse bool) *VolumeSnapshotBuilder { + v.object.Status.ReadyToUse = &readyToUse + return v +} diff --git a/pkg/builder/volume_snapshot_content_builder.go b/pkg/builder/volume_snapshot_content_builder.go index c12cb64b5..3f8567e7e 100644 --- a/pkg/builder/volume_snapshot_content_builder.go +++ b/pkg/builder/volume_snapshot_content_builder.go @@ -84,3 +84,13 @@ func (v *VolumeSnapshotContentBuilder) ObjectMeta(opts ...ObjectMetaOpt) *Volume return v } + +func (v *VolumeSnapshotContentBuilder) Driver(driver string) *VolumeSnapshotContentBuilder { + v.object.Spec.Driver = driver + return v +} + +func (v *VolumeSnapshotContentBuilder) Source(source snapshotv1api.VolumeSnapshotContentSource) *VolumeSnapshotContentBuilder { + v.object.Spec.Source = source + return v +} diff --git a/pkg/restore/actions/csi/pvc_action_test.go b/pkg/restore/actions/csi/pvc_action_test.go index 4ba8ca83e..095bd8dbe 100644 --- a/pkg/restore/actions/csi/pvc_action_test.go +++ b/pkg/restore/actions/csi/pvc_action_test.go @@ -18,6 +18,7 @@ package csi import ( "context" + "fmt" "testing" "time" @@ -40,6 +41,7 @@ import ( velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" "github.com/vmware-tanzu/velero/pkg/builder" + factorymocks "github.com/vmware-tanzu/velero/pkg/client/mocks" "github.com/vmware-tanzu/velero/pkg/label" "github.com/vmware-tanzu/velero/pkg/plugin/velero" velerotest "github.com/vmware-tanzu/velero/pkg/test" @@ -711,3 +713,37 @@ func TestExecute(t *testing.T) { }) } } + +func TestPVCAppliesTo(t *testing.T) { + p := pvcRestoreItemAction{ + log: logrus.StandardLogger(), + } + selector, err := p.AppliesTo() + + require.NoError(t, err) + + require.Equal( + t, + velero.ResourceSelector{ + IncludedResources: []string{"persistentvolumeclaims"}, + }, + selector, + ) +} + +func TestNewPvcRestoreItemAction(t *testing.T) { + logger := logrus.StandardLogger() + crClient := velerotest.NewFakeControllerRuntimeClient(t) + + f := &factorymocks.Factory{} + f.On("KubebuilderClient").Return(nil, fmt.Errorf("")) + plugin := NewPvcRestoreItemAction(f) + _, err := plugin(logger) + require.Error(t, err) + + f1 := &factorymocks.Factory{} + f1.On("KubebuilderClient").Return(crClient, nil) + plugin1 := NewPvcRestoreItemAction(f1) + _, err1 := plugin1(logger) + require.NoError(t, err1) +} diff --git a/pkg/restore/actions/csi/volumesnapshot_action.go b/pkg/restore/actions/csi/volumesnapshot_action.go index 072fe1da8..61af12c67 100644 --- a/pkg/restore/actions/csi/volumesnapshot_action.go +++ b/pkg/restore/actions/csi/volumesnapshot_action.go @@ -76,13 +76,14 @@ func (p *volumeSnapshotRestoreItemAction) Execute( input *velero.RestoreItemActionExecuteInput, ) (*velero.RestoreItemActionExecuteOutput, error) { p.log.Info("Starting VolumeSnapshotRestoreItemAction") + if boolptr.IsSetToFalse(input.Restore.Spec.RestorePVs) { p.log.Infof("Restore %s/%s did not request for PVs to be restored.", input.Restore.Namespace, input.Restore.Name) return &velero.RestoreItemActionExecuteOutput{SkipRestore: true}, nil } - var vs snapshotv1api.VolumeSnapshot + var vs snapshotv1api.VolumeSnapshot if err := runtime.DefaultUnstructuredConverter.FromUnstructured( input.Item.UnstructuredContent(), &vs); err != nil { return &velero.RestoreItemActionExecuteOutput{}, @@ -101,7 +102,7 @@ func (p *volumeSnapshotRestoreItemAction) Execute( snapHandle, exists := vs.Annotations[velerov1api.VolumeSnapshotHandleAnnotation] if !exists { return nil, errors.Errorf( - "Volumesnapshot %s/%s does not have a %s annotation", + "VolumeSnapshot %s/%s does not have a %s annotation", vs.Namespace, vs.Name, velerov1api.VolumeSnapshotHandleAnnotation, @@ -111,7 +112,7 @@ func (p *volumeSnapshotRestoreItemAction) Execute( csiDriverName, exists := vs.Annotations[velerov1api.DriverNameAnnotation] if !exists { return nil, errors.Errorf( - "Volumesnapshot %s/%s does not have a %s annotation", + "VolumeSnapshot %s/%s does not have a %s annotation", vs.Namespace, vs.Name, velerov1api.DriverNameAnnotation) } @@ -129,9 +130,10 @@ func (p *volumeSnapshotRestoreItemAction) Execute( DeletionPolicy: snapshotv1api.VolumeSnapshotContentRetain, Driver: csiDriverName, VolumeSnapshotRef: core_v1.ObjectReference{ - Kind: "VolumeSnapshot", - Namespace: newNamespace, - Name: vs.Name, + APIVersion: "snapshot.storage.k8s.io/v1", + Kind: "VolumeSnapshot", + Namespace: newNamespace, + Name: vs.Name, }, Source: snapshotv1api.VolumeSnapshotContentSource{ SnapshotHandle: &snapHandle, @@ -149,7 +151,7 @@ func (p *volumeSnapshotRestoreItemAction) Execute( // See: https://github.com/kubernetes-csi/external-snapshotter/issues/274 if err := p.crClient.Create(context.TODO(), &vsc); err != nil { return nil, errors.Wrapf(err, - "failed to create volumesnapshotcontents %s", + "failed to create VolumeSnapshotContents %s", vsc.GenerateName) } p.log.Infof("Created VolumesnapshotContents %s with static binding to volumesnapshot %s/%s", @@ -169,7 +171,8 @@ func (p *volumeSnapshotRestoreItemAction) Execute( return nil, errors.WithStack(err) } - p.log.Infof("Returning from VolumeSnapshotRestoreItemAction with no additionalItems") + p.log.Infof(`Returning from VolumeSnapshotRestoreItemAction with + no additionalItems`) return &velero.RestoreItemActionExecuteOutput{ UpdatedItem: &unstructured.Unstructured{Object: vsMap}, diff --git a/pkg/restore/actions/csi/volumesnapshot_action_test.go b/pkg/restore/actions/csi/volumesnapshot_action_test.go index ed425a660..1cc3f0ba9 100644 --- a/pkg/restore/actions/csi/volumesnapshot_action_test.go +++ b/pkg/restore/actions/csi/volumesnapshot_action_test.go @@ -17,11 +17,25 @@ limitations under the License. package csi import ( + "context" + "fmt" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" + factorymocks "github.com/vmware-tanzu/velero/pkg/client/mocks" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" + velerotest "github.com/vmware-tanzu/velero/pkg/test" ) var ( @@ -87,3 +101,144 @@ func TestResetVolumeSnapshotSpecForRestore(t *testing.T) { }) } } + +func TestVSExecute(t *testing.T) { + snapshotHandle := "vsc" + tests := []struct { + name string + item runtime.Unstructured + vs *snapshotv1api.VolumeSnapshot + restore *velerov1api.Restore + expectErr bool + createVS bool + expectedVSC *snapshotv1api.VolumeSnapshotContent + }{ + { + name: "Restore's RestorePVs is false", + restore: builder.ForRestore("velero", "restore").RestorePVs(false).Result(), + expectErr: false, + }, + { + name: "Namespace remapping and VS already exists in cluster. Nothing change", + vs: builder.ForVolumeSnapshot("ns", "name").Result(), + restore: builder.ForRestore("velero", "restore").NamespaceMappings("ns", "newNS").Result(), + createVS: true, + expectErr: false, + }, + { + name: "VS doesn't have VolumeSnapshotHandleAnnotation annotation", + vs: builder.ForVolumeSnapshot("ns", "name").Result(), + restore: builder.ForRestore("velero", "restore").NamespaceMappings("ns", "newNS").Result(), + expectErr: true, + }, + { + name: "VS doesn't have DriverNameAnnotation annotation", + vs: builder.ForVolumeSnapshot("ns", "name").ObjectMeta( + builder.WithAnnotations(velerov1api.VolumeSnapshotHandleAnnotation, ""), + ).Result(), + restore: builder.ForRestore("velero", "restore").NamespaceMappings("ns", "newNS").Result(), + expectErr: true, + }, + { + name: "Normal case, VSC should be created", + vs: builder.ForVolumeSnapshot("ns", "test").ObjectMeta(builder.WithAnnotationsMap( + map[string]string{ + velerov1api.VolumeSnapshotHandleAnnotation: "vsc", + velerov1api.DriverNameAnnotation: "pd.csi.storage.gke.io", + }, + )).Result(), + restore: builder.ForRestore("velero", "restore").Result(), + expectErr: false, + expectedVSC: builder.ForVolumeSnapshotContent("vsc").ObjectMeta( + builder.WithLabels(velerov1api.RestoreNameLabel, "restore"), + ).VolumeSnapshotRef("ns", "test"). + DeletionPolicy(snapshotv1api.VolumeSnapshotContentRetain). + Driver("pd.csi.storage.gke.io"). + Source(snapshotv1api.VolumeSnapshotContentSource{ + SnapshotHandle: &snapshotHandle, + }). + Result(), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p := volumeSnapshotRestoreItemAction{ + log: logrus.StandardLogger(), + crClient: velerotest.NewFakeControllerRuntimeClient(t), + } + + if test.vs != nil { + vsMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(test.vs) + require.NoError(t, err) + test.item = &unstructured.Unstructured{Object: vsMap} + + if test.createVS == true { + if newNS, ok := test.restore.Spec.NamespaceMapping[test.vs.Namespace]; ok { + test.vs.SetNamespace(newNS) + } + require.NoError(t, p.crClient.Create(context.TODO(), test.vs)) + } + } + + _, err := p.Execute( + &velero.RestoreItemActionExecuteInput{ + Item: test.item, + Restore: test.restore, + }, + ) + + if test.expectErr == false { + require.NoError(t, err) + } + + if test.expectedVSC != nil { + vscList := new(snapshotv1api.VolumeSnapshotContentList) + require.NoError(t, p.crClient.List(context.TODO(), vscList)) + require.True(t, cmp.Equal( + *test.expectedVSC, + vscList.Items[0], + cmpopts.IgnoreFields( + snapshotv1api.VolumeSnapshotContent{}, + "Kind", "APIVersion", "GenerateName", "Name", + "ResourceVersion", + ), + )) + } + }) + } +} + +func TestVSAppliesTo(t *testing.T) { + p := volumeSnapshotRestoreItemAction{ + log: logrus.StandardLogger(), + } + selector, err := p.AppliesTo() + + require.NoError(t, err) + + require.Equal( + t, + velero.ResourceSelector{ + IncludedResources: []string{"volumesnapshots.snapshot.storage.k8s.io"}, + }, + selector, + ) +} + +func TestNewVolumeSnapshotRestoreItemAction(t *testing.T) { + logger := logrus.StandardLogger() + crClient := velerotest.NewFakeControllerRuntimeClient(t) + + f := &factorymocks.Factory{} + f.On("KubebuilderClient").Return(nil, fmt.Errorf("")) + plugin := NewVolumeSnapshotRestoreItemAction(f) + _, err := plugin(logger) + require.Error(t, err) + + f1 := &factorymocks.Factory{} + f1.On("KubebuilderClient").Return(crClient, nil) + plugin1 := NewVolumeSnapshotRestoreItemAction(f1) + _, err1 := plugin1(logger) + require.NoError(t, err1) +} diff --git a/pkg/restore/actions/csi/volumesnapshotclass_action.go b/pkg/restore/actions/csi/volumesnapshotclass_action.go index 5afba786b..577445a4b 100644 --- a/pkg/restore/actions/csi/volumesnapshotclass_action.go +++ b/pkg/restore/actions/csi/volumesnapshotclass_action.go @@ -39,7 +39,7 @@ type volumeSnapshotClassRestoreItemAction struct { // should be invoked while restoring volumesnapshotclass.snapshot.storage.k8s.io resources. func (p *volumeSnapshotClassRestoreItemAction) AppliesTo() (velero.ResourceSelector, error) { return velero.ResourceSelector{ - IncludedResources: []string{"volumesnapshotclass.snapshot.storage.k8s.io"}, + IncludedResources: []string{"volumesnapshotclasses.snapshot.storage.k8s.io"}, }, nil } diff --git a/pkg/restore/actions/csi/volumesnapshotclass_action_test.go b/pkg/restore/actions/csi/volumesnapshotclass_action_test.go new file mode 100644 index 000000000..fe222dbb3 --- /dev/null +++ b/pkg/restore/actions/csi/volumesnapshotclass_action_test.go @@ -0,0 +1,122 @@ +/* +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 csi + +import ( + "testing" + + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/pkg/kuberesource" + + //"github.com/vmware-tanzu/velero/pkg/kuberesource" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" +) + +func TestVSClassExecute(t *testing.T) { + tests := []struct { + name string + item runtime.Unstructured + vsClass *snapshotv1api.VolumeSnapshotClass + restore *velerov1api.Restore + expectErr bool + expectedItems []velero.ResourceIdentifier + }{ + { + name: "Restore's RestorePVs is false", + restore: builder.ForRestore("velero", "restore").RestorePVs(false).Result(), + expectErr: false, + }, + { + name: "No Secret in the VS Class, no return additional items", + vsClass: builder.ForVolumeSnapshotClass("test").Result(), + restore: builder.ForRestore("velero", "restore").Result(), + expectErr: false, + }, + { + name: "Normal case, additional items should return", + vsClass: builder.ForVolumeSnapshotClass("test").ObjectMeta(builder.WithAnnotationsMap( + map[string]string{ + velerov1api.PrefixedListSecretNameAnnotation: "name", + velerov1api.PrefixedListSecretNamespaceAnnotation: "namespace", + }, + )).Result(), + restore: builder.ForRestore("velero", "restore").Result(), + expectErr: false, + expectedItems: []velero.ResourceIdentifier{ + { + GroupResource: kuberesource.Secrets, + Namespace: "namespace", + Name: "name", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p, err := NewVolumeSnapshotClassRestoreItemAction(logrus.StandardLogger()) + require.NoError(t, err) + + action := p.(*volumeSnapshotClassRestoreItemAction) + + if test.vsClass != nil { + vsMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(test.vsClass) + require.NoError(t, err) + test.item = &unstructured.Unstructured{Object: vsMap} + } + + output, err := action.Execute( + &velero.RestoreItemActionExecuteInput{ + Item: test.item, + Restore: test.restore, + }, + ) + + if test.expectErr == false { + require.NoError(t, err) + } + + if len(test.expectedItems) > 0 { + require.Equal(t, test.expectedItems, output.AdditionalItems) + } + }) + } +} + +func TestVSClassAppliesTo(t *testing.T) { + p := volumeSnapshotClassRestoreItemAction{ + log: logrus.StandardLogger(), + } + selector, err := p.AppliesTo() + + require.NoError(t, err) + + require.Equal( + t, + velero.ResourceSelector{ + IncludedResources: []string{"volumesnapshotclasses.snapshot.storage.k8s.io"}, + }, + selector, + ) +} diff --git a/pkg/restore/actions/csi/volumesnapshotcontent_action.go b/pkg/restore/actions/csi/volumesnapshotcontent_action.go index c927d4dd5..c00919a27 100644 --- a/pkg/restore/actions/csi/volumesnapshotcontent_action.go +++ b/pkg/restore/actions/csi/volumesnapshotcontent_action.go @@ -42,7 +42,7 @@ func (p *volumeSnapshotContentRestoreItemAction) AppliesTo() ( velero.ResourceSelector, error, ) { return velero.ResourceSelector{ - IncludedResources: []string{"volumesnapshotcontent.snapshot.storage.k8s.io"}, + IncludedResources: []string{"volumesnapshotcontents.snapshot.storage.k8s.io"}, }, nil } @@ -69,8 +69,8 @@ func (p *volumeSnapshotContentRestoreItemAction) Execute( additionalItems = append(additionalItems, velero.ResourceIdentifier{ GroupResource: schema.GroupResource{Group: "", Resource: "secrets"}, - Name: snapCont.Annotations[velerov1api.DeleteSecretNameAnnotation], - Namespace: snapCont.Annotations[velerov1api.DeleteSecretNamespaceAnnotation], + Name: snapCont.Annotations[velerov1api.PrefixedSecretNameAnnotation], + Namespace: snapCont.Annotations[velerov1api.PrefixedSecretNamespaceAnnotation], }, ) } diff --git a/pkg/restore/actions/csi/volumesnapshotcontent_action_test.go b/pkg/restore/actions/csi/volumesnapshotcontent_action_test.go new file mode 100644 index 000000000..71ff67354 --- /dev/null +++ b/pkg/restore/actions/csi/volumesnapshotcontent_action_test.go @@ -0,0 +1,114 @@ +/* +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 csi + +import ( + "testing" + + snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v7/apis/volumesnapshot/v1" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/pkg/kuberesource" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" +) + +func TestVSCExecute(t *testing.T) { + tests := []struct { + name string + item runtime.Unstructured + vsc *snapshotv1api.VolumeSnapshotContent + restore *velerov1api.Restore + expectErr bool + expectedItems []velero.ResourceIdentifier + }{ + { + name: "Restore's RestorePVs is false", + restore: builder.ForRestore("velero", "restore").RestorePVs(false).Result(), + expectErr: false, + }, + { + name: "Normal case, additional items should return", + vsc: builder.ForVolumeSnapshotContent("test").ObjectMeta(builder.WithAnnotationsMap( + map[string]string{ + velerov1api.PrefixedSecretNameAnnotation: "name", + velerov1api.PrefixedSecretNamespaceAnnotation: "namespace", + }, + )).Result(), + restore: builder.ForRestore("velero", "restore").Result(), + expectErr: false, + expectedItems: []velero.ResourceIdentifier{ + { + GroupResource: kuberesource.Secrets, + Namespace: "namespace", + Name: "name", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + p, err := NewVolumeSnapshotContentRestoreItemAction(logrus.StandardLogger()) + require.NoError(t, err) + + action := p.(*volumeSnapshotContentRestoreItemAction) + + if test.vsc != nil { + vsMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(test.vsc) + require.NoError(t, err) + test.item = &unstructured.Unstructured{Object: vsMap} + } + + output, err := action.Execute( + &velero.RestoreItemActionExecuteInput{ + Item: test.item, + Restore: test.restore, + }, + ) + + if test.expectErr == false { + require.NoError(t, err) + } + + if len(test.expectedItems) > 0 { + require.Equal(t, test.expectedItems, output.AdditionalItems) + } + }) + } +} + +func TestVSCAppliesTo(t *testing.T) { + p := volumeSnapshotContentRestoreItemAction{ + log: logrus.StandardLogger(), + } + selector, err := p.AppliesTo() + + require.NoError(t, err) + + require.Equal( + t, + velero.ResourceSelector{ + IncludedResources: []string{"volumesnapshotcontents.snapshot.storage.k8s.io"}, + }, + selector, + ) +} diff --git a/pkg/util/csi/volume_snapshot.go b/pkg/util/csi/volume_snapshot.go index 94188b801..ff6d00fd8 100644 --- a/pkg/util/csi/volume_snapshot.go +++ b/pkg/util/csi/volume_snapshot.go @@ -447,14 +447,6 @@ func IsVolumeSnapshotContentHasDeleteSecret(vsc *snapshotv1api.VolumeSnapshotCon return nameExists && nsExists } -// IsVolumeSnapshotHasVSCDeleteSecret returns whether a volumesnapshot should set the deletesnapshot secret -// for the static volumesnapshotcontent that is created on restore -func IsVolumeSnapshotHasVSCDeleteSecret(vs *snapshotv1api.VolumeSnapshot) bool { - _, nameExists := vs.Annotations[velerov1api.DeleteSecretNameAnnotation] - _, nsExists := vs.Annotations[velerov1api.DeleteSecretNamespaceAnnotation] - return nameExists && nsExists -} - // IsVolumeSnapshotExists returns whether a specific volumesnapshot object exists. func IsVolumeSnapshotExists( ns, diff --git a/pkg/util/csi/volume_snapshot_test.go b/pkg/util/csi/volume_snapshot_test.go index a8200291a..09b91b856 100644 --- a/pkg/util/csi/volume_snapshot_test.go +++ b/pkg/util/csi/volume_snapshot_test.go @@ -1257,94 +1257,6 @@ func TestIsVolumeSnapshotContentHasDeleteSecret(t *testing.T) { } } -func TestIsVolumeSnapshotHasVSCDeleteSecret(t *testing.T) { - testCases := []struct { - name string - vs snapshotv1api.VolumeSnapshot - expected bool - }{ - { - name: "should find both annotations", - vs: snapshotv1api.VolumeSnapshot{ - ObjectMeta: metav1.ObjectMeta{ - Name: "vs-1", - Annotations: map[string]string{ - "velero.io/csi-deletesnapshotsecret-name": "snapDelSecret", - "velero.io/csi-deletesnapshotsecret-namespace": "awesome-ns", - }, - }, - }, - expected: true, - }, - { - name: "should not find both annotations name is missing", - vs: snapshotv1api.VolumeSnapshot{ - ObjectMeta: metav1.ObjectMeta{ - Name: "vs-1", - Annotations: map[string]string{ - "foo": "snapDelSecret", - "velero.io/csi-deletesnapshotsecret-namespace": "awesome-ns", - }, - }, - }, - expected: false, - }, - { - name: "should not find both annotations namespace is missing", - vs: snapshotv1api.VolumeSnapshot{ - ObjectMeta: metav1.ObjectMeta{ - Name: "vs-1", - Annotations: map[string]string{ - "velero.io/csi-deletesnapshotsecret-name": "snapDelSecret", - "foo": "awesome-ns", - }, - }, - }, - expected: false, - }, - { - name: "should not find annotation non-empty annotation", - vs: snapshotv1api.VolumeSnapshot{ - ObjectMeta: metav1.ObjectMeta{ - Name: "vs-1", - Annotations: map[string]string{ - "foo": "snapDelSecret", - "bar": "awesome-ns", - }, - }, - }, - expected: false, - }, - { - name: "should not find annotation empty annotation", - vs: snapshotv1api.VolumeSnapshot{ - ObjectMeta: metav1.ObjectMeta{ - Name: "vs-1", - Annotations: map[string]string{}, - }, - }, - expected: false, - }, - { - name: "should not find annotation nil annotation", - vs: snapshotv1api.VolumeSnapshot{ - ObjectMeta: metav1.ObjectMeta{ - Name: "vs-1", - Annotations: nil, - }, - }, - expected: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - actual := IsVolumeSnapshotHasVSCDeleteSecret(&tc.vs) - assert.Equal(t, tc.expected, actual) - }) - } -} - func TestIsVolumeSnapshotExists(t *testing.T) { vsExists := &snapshotv1api.VolumeSnapshot{ ObjectMeta: metav1.ObjectMeta{