diff --git a/Tiltfile b/Tiltfile index 02da1df56..0d9a64263 100644 --- a/Tiltfile +++ b/Tiltfile @@ -7,7 +7,7 @@ k8s_yaml([ 'config/crd/v1/bases/velero.io_downloadrequests.yaml', 'config/crd/v1/bases/velero.io_podvolumebackups.yaml', 'config/crd/v1/bases/velero.io_podvolumerestores.yaml', - 'config/crd/v1/bases/velero.io_resticrepositories.yaml', + 'config/crd/v1/bases/velero.io_backuprepositories.yaml', 'config/crd/v1/bases/velero.io_restores.yaml', 'config/crd/v1/bases/velero.io_schedules.yaml', 'config/crd/v1/bases/velero.io_serverstatusrequests.yaml', diff --git a/changelogs/unreleased/5143-ywk253100 b/changelogs/unreleased/5143-ywk253100 new file mode 100644 index 000000000..cf5213645 --- /dev/null +++ b/changelogs/unreleased/5143-ywk253100 @@ -0,0 +1 @@ +This commit splits the pkg/restic package into several packages to support Kopia integration works \ No newline at end of file diff --git a/pkg/backup/backup.go b/pkg/backup/backup.go index 697be8500..d026b09cf 100644 --- a/pkg/backup/backup.go +++ b/pkg/backup/backup.go @@ -46,7 +46,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/plugin/framework" "github.com/vmware-tanzu/velero/pkg/plugin/velero" "github.com/vmware-tanzu/velero/pkg/podexec" - "github.com/vmware-tanzu/velero/pkg/restic" + "github.com/vmware-tanzu/velero/pkg/podvolume" "github.com/vmware-tanzu/velero/pkg/util/boolptr" "github.com/vmware-tanzu/velero/pkg/util/collections" ) @@ -74,7 +74,7 @@ type kubernetesBackupper struct { dynamicFactory client.DynamicFactory discoveryHelper discovery.Helper podCommandExecutor podexec.PodCommandExecutor - resticBackupperFactory restic.BackupperFactory + resticBackupperFactory podvolume.BackupperFactory resticTimeout time.Duration defaultVolumesToRestic bool clientPageSize int @@ -100,7 +100,7 @@ func NewKubernetesBackupper( discoveryHelper discovery.Helper, dynamicFactory client.DynamicFactory, podCommandExecutor podexec.PodCommandExecutor, - resticBackupperFactory restic.BackupperFactory, + resticBackupperFactory podvolume.BackupperFactory, resticTimeout time.Duration, defaultVolumesToRestic bool, clientPageSize int, @@ -234,7 +234,7 @@ func (kb *kubernetesBackupper) BackupWithResolvers(log logrus.FieldLogger, ctx, cancelFunc := context.WithTimeout(context.Background(), podVolumeTimeout) defer cancelFunc() - var resticBackupper restic.Backupper + var resticBackupper podvolume.Backupper if kb.resticBackupperFactory != nil { resticBackupper, err = kb.resticBackupperFactory.NewBackupper(ctx, backupRequest.Backup) if err != nil { diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index 53554e8b3..cf6a4269f 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -47,7 +47,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/discovery" "github.com/vmware-tanzu/velero/pkg/kuberesource" "github.com/vmware-tanzu/velero/pkg/plugin/velero" - "github.com/vmware-tanzu/velero/pkg/restic" + "github.com/vmware-tanzu/velero/pkg/podvolume" "github.com/vmware-tanzu/velero/pkg/test" testutil "github.com/vmware-tanzu/velero/pkg/test" kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" @@ -2595,7 +2595,7 @@ func TestBackupWithHooks(t *testing.T) { type fakeResticBackupperFactory struct{} -func (f *fakeResticBackupperFactory) NewBackupper(context.Context, *velerov1.Backup) (restic.Backupper, error) { +func (f *fakeResticBackupperFactory) NewBackupper(context.Context, *velerov1.Backup) (podvolume.Backupper, error) { return &fakeResticBackupper{}, nil } diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index bd40c3bdb..df5d48cd2 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -42,7 +42,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/features" "github.com/vmware-tanzu/velero/pkg/kuberesource" "github.com/vmware-tanzu/velero/pkg/plugin/velero" - "github.com/vmware-tanzu/velero/pkg/restic" + "github.com/vmware-tanzu/velero/pkg/podvolume" "github.com/vmware-tanzu/velero/pkg/util/boolptr" "github.com/vmware-tanzu/velero/pkg/volume" ) @@ -53,7 +53,7 @@ type itemBackupper struct { tarWriter tarWriter dynamicFactory client.DynamicFactory discoveryHelper discovery.Helper - resticBackupper restic.Backupper + resticBackupper podvolume.Backupper resticSnapshotTracker *pvcSnapshotTracker volumeSnapshotterGetter VolumeSnapshotterGetter @@ -149,7 +149,7 @@ func (ib *itemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstr // Get the list of volumes to back up using restic from the pod's annotations. Remove from this list // any volumes that use a PVC that we've already backed up (this would be in a read-write-many scenario, // where it's been backed up from another pod), since we don't need >1 backup per PVC. - for _, volume := range restic.GetPodVolumesUsingRestic(pod, boolptr.IsSetToTrue(ib.backupRequest.Spec.DefaultVolumesToRestic)) { + for _, volume := range podvolume.GetPodVolumesUsingRestic(pod, boolptr.IsSetToTrue(ib.backupRequest.Spec.DefaultVolumesToRestic)) { if found, pvcName := ib.resticSnapshotTracker.HasPVCForPodVolume(pod, volume); found { log.WithFields(map[string]interface{}{ "podVolume": volume, diff --git a/pkg/controller/restic_repository_controller.go b/pkg/controller/restic_repository_controller.go index 3f27de36f..c3ca1505a 100644 --- a/pkg/controller/restic_repository_controller.go +++ b/pkg/controller/restic_repository_controller.go @@ -26,7 +26,6 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/clock" - ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/pkg/restic/backupper.go b/pkg/podvolume/backupper.go similarity index 90% rename from pkg/restic/backupper.go rename to pkg/podvolume/backupper.go index 589b396f3..116a5c4e7 100644 --- a/pkg/restic/backupper.go +++ b/pkg/podvolume/backupper.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package podvolume import ( "context" @@ -30,7 +30,9 @@ import ( "k8s.io/client-go/tools/cache" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned" "github.com/vmware-tanzu/velero/pkg/label" + "github.com/vmware-tanzu/velero/pkg/repository" "github.com/vmware-tanzu/velero/pkg/util/boolptr" ) @@ -41,11 +43,12 @@ type Backupper interface { } type backupper struct { - ctx context.Context - repoManager *repositoryManager - repoEnsurer *repositoryEnsurer - pvcClient corev1client.PersistentVolumeClaimsGetter - pvClient corev1client.PersistentVolumesGetter + ctx context.Context + repoLocker *repository.RepoLocker + repoEnsurer *repository.RepositoryEnsurer + veleroClient clientset.Interface + pvcClient corev1client.PersistentVolumeClaimsGetter + pvClient corev1client.PersistentVolumesGetter results map[string]chan *velerov1api.PodVolumeBackup resultsLock sync.Mutex @@ -53,19 +56,21 @@ type backupper struct { func newBackupper( ctx context.Context, - repoManager *repositoryManager, - repoEnsurer *repositoryEnsurer, + repoLocker *repository.RepoLocker, + repoEnsurer *repository.RepositoryEnsurer, podVolumeBackupInformer cache.SharedIndexInformer, + veleroClient clientset.Interface, pvcClient corev1client.PersistentVolumeClaimsGetter, pvClient corev1client.PersistentVolumesGetter, log logrus.FieldLogger, ) *backupper { b := &backupper{ - ctx: ctx, - repoManager: repoManager, - repoEnsurer: repoEnsurer, - pvcClient: pvcClient, - pvClient: pvClient, + ctx: ctx, + repoLocker: repoLocker, + repoEnsurer: repoEnsurer, + veleroClient: veleroClient, + pvcClient: pvcClient, + pvClient: pvClient, results: make(map[string]chan *velerov1api.PodVolumeBackup), } @@ -109,8 +114,8 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api. // get a single non-exclusive lock since we'll wait for all individual // backups to be complete before releasing it. - b.repoManager.repoLocker.Lock(repo.Name) - defer b.repoManager.repoLocker.Unlock(repo.Name) + b.repoLocker.Lock(repo.Name) + defer b.repoLocker.Unlock(repo.Name) resultsChan := make(chan *velerov1api.PodVolumeBackup) @@ -176,9 +181,10 @@ func (b *backupper) BackupPodVolumes(backup *velerov1api.Backup, pod *corev1api. log.Warnf("Volume %s is declared in pod %s/%s but not mounted by any container, skipping", volumeName, pod.Namespace, pod.Name) continue } + // TODO: Remove the hard-coded uploader type before v1.10 FC volumeBackup := newPodVolumeBackup(backup, pod, volume, repo.Spec.ResticIdentifier, "restic", pvc) - if volumeBackup, err = b.repoManager.veleroClient.VeleroV1().PodVolumeBackups(volumeBackup.Namespace).Create(context.TODO(), volumeBackup, metav1.CreateOptions{}); err != nil { + if volumeBackup, err = b.veleroClient.VeleroV1().PodVolumeBackups(volumeBackup.Namespace).Create(context.TODO(), volumeBackup, metav1.CreateOptions{}); err != nil { errs = append(errs, err) continue } diff --git a/pkg/podvolume/backupper_factory.go b/pkg/podvolume/backupper_factory.go new file mode 100644 index 000000000..aaaa5e2ac --- /dev/null +++ b/pkg/podvolume/backupper_factory.go @@ -0,0 +1,88 @@ +/* +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 podvolume + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/tools/cache" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned" + velerov1informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions/velero/v1" + "github.com/vmware-tanzu/velero/pkg/repository" +) + +// BackupperFactory can construct pod volumes backuppers. +type BackupperFactory interface { + // NewBackupper returns a pod volumes backupper for use during a single Velero backup. + NewBackupper(context.Context, *velerov1api.Backup) (Backupper, error) +} + +func NewBackupperFactory(repoLocker *repository.RepoLocker, + repoEnsurer *repository.RepositoryEnsurer, + veleroClient clientset.Interface, + pvcClient corev1client.PersistentVolumeClaimsGetter, + pvClient corev1client.PersistentVolumesGetter, + repoInformerSynced cache.InformerSynced, + log logrus.FieldLogger) BackupperFactory { + return &backupperFactory{ + repoLocker: repoLocker, + repoEnsurer: repoEnsurer, + veleroClient: veleroClient, + pvcClient: pvcClient, + pvClient: pvClient, + repoInformerSynced: repoInformerSynced, + log: log, + } +} + +type backupperFactory struct { + repoLocker *repository.RepoLocker + repoEnsurer *repository.RepositoryEnsurer + veleroClient clientset.Interface + pvcClient corev1client.PersistentVolumeClaimsGetter + pvClient corev1client.PersistentVolumesGetter + repoInformerSynced cache.InformerSynced + log logrus.FieldLogger +} + +func (bf *backupperFactory) NewBackupper(ctx context.Context, backup *velerov1api.Backup) (Backupper, error) { + informer := velerov1informers.NewFilteredPodVolumeBackupInformer( + bf.veleroClient, + backup.Namespace, + 0, + cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, + func(opts *metav1.ListOptions) { + opts.LabelSelector = fmt.Sprintf("%s=%s", velerov1api.BackupUIDLabel, backup.UID) + }, + ) + + b := newBackupper(ctx, bf.repoLocker, bf.repoEnsurer, informer, bf.veleroClient, bf.pvcClient, bf.pvClient, bf.log) + + go informer.Run(ctx.Done()) + if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced, bf.repoInformerSynced) { + return nil, errors.New("timed out waiting for caches to sync") + } + + return b, nil +} diff --git a/pkg/restic/backupper_test.go b/pkg/podvolume/backupper_test.go similarity index 99% rename from pkg/restic/backupper_test.go rename to pkg/podvolume/backupper_test.go index 8969f6efa..fb0cacd1a 100644 --- a/pkg/restic/backupper_test.go +++ b/pkg/podvolume/backupper_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package podvolume import ( "context" diff --git a/pkg/restic/mocks/restorer.go b/pkg/podvolume/mocks/restorer.go similarity index 60% rename from pkg/restic/mocks/restorer.go rename to pkg/podvolume/mocks/restorer.go index 7f4f5c1d8..fd210aa00 100644 --- a/pkg/restic/mocks/restorer.go +++ b/pkg/podvolume/mocks/restorer.go @@ -2,8 +2,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import restic "github.com/vmware-tanzu/velero/pkg/restic" +import ( + mock "github.com/stretchr/testify/mock" + "github.com/vmware-tanzu/velero/pkg/podvolume" +) // Restorer is an autogenerated mock type for the Restorer type type Restorer struct { @@ -11,11 +13,11 @@ type Restorer struct { } // RestorePodVolumes provides a mock function with given fields: _a0 -func (_m *Restorer) RestorePodVolumes(_a0 restic.RestoreData) []error { +func (_m *Restorer) RestorePodVolumes(_a0 podvolume.RestoreData) []error { ret := _m.Called(_a0) var r0 []error - if rf, ok := ret.Get(0).(func(restic.RestoreData) []error); ok { + if rf, ok := ret.Get(0).(func(podvolume.RestoreData) []error); ok { r0 = rf(_a0) } else { if ret.Get(0) != nil { diff --git a/pkg/restic/restorer.go b/pkg/podvolume/restorer.go similarity index 87% rename from pkg/restic/restorer.go rename to pkg/podvolume/restorer.go index e747b0b6d..daa3a630d 100644 --- a/pkg/restic/restorer.go +++ b/pkg/podvolume/restorer.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package podvolume import ( "context" @@ -28,7 +28,9 @@ import ( "k8s.io/client-go/tools/cache" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned" "github.com/vmware-tanzu/velero/pkg/label" + "github.com/vmware-tanzu/velero/pkg/repository" "github.com/vmware-tanzu/velero/pkg/util/boolptr" ) @@ -46,10 +48,11 @@ type Restorer interface { } type restorer struct { - ctx context.Context - repoManager *repositoryManager - repoEnsurer *repositoryEnsurer - pvcClient corev1client.PersistentVolumeClaimsGetter + ctx context.Context + repoLocker *repository.RepoLocker + repoEnsurer *repository.RepositoryEnsurer + veleroClient clientset.Interface + pvcClient corev1client.PersistentVolumeClaimsGetter resultsLock sync.Mutex results map[string]chan *velerov1api.PodVolumeRestore @@ -57,17 +60,19 @@ type restorer struct { func newRestorer( ctx context.Context, - rm *repositoryManager, - repoEnsurer *repositoryEnsurer, + repoLocker *repository.RepoLocker, + repoEnsurer *repository.RepositoryEnsurer, podVolumeRestoreInformer cache.SharedIndexInformer, + veleroClient clientset.Interface, pvcClient corev1client.PersistentVolumeClaimsGetter, log logrus.FieldLogger, ) *restorer { r := &restorer{ - ctx: ctx, - repoManager: rm, - repoEnsurer: repoEnsurer, - pvcClient: pvcClient, + ctx: ctx, + repoLocker: repoLocker, + repoEnsurer: repoEnsurer, + veleroClient: veleroClient, + pvcClient: pvcClient, results: make(map[string]chan *velerov1api.PodVolumeRestore), } @@ -108,8 +113,8 @@ func (r *restorer) RestorePodVolumes(data RestoreData) []error { // get a single non-exclusive lock since we'll wait for all individual // restores to be complete before releasing it. - r.repoManager.repoLocker.Lock(repo.Name) - defer r.repoManager.repoLocker.Unlock(repo.Name) + r.repoLocker.Lock(repo.Name) + defer r.repoLocker.Unlock(repo.Name) resultsChan := make(chan *velerov1api.PodVolumeRestore) @@ -142,7 +147,7 @@ func (r *restorer) RestorePodVolumes(data RestoreData) []error { // TODO: Remove the hard-coded uploader type before v1.10 FC volumeRestore := newPodVolumeRestore(data.Restore, data.Pod, data.BackupLocation, volume, snapshot, repo.Spec.ResticIdentifier, "restic", pvc) - if err := errorOnly(r.repoManager.veleroClient.VeleroV1().PodVolumeRestores(volumeRestore.Namespace).Create(context.TODO(), volumeRestore, metav1.CreateOptions{})); err != nil { + if err := errorOnly(r.veleroClient.VeleroV1().PodVolumeRestores(volumeRestore.Namespace).Create(context.TODO(), volumeRestore, metav1.CreateOptions{})); err != nil { errs = append(errs, errors.WithStack(err)) continue } diff --git a/pkg/podvolume/restorer_factory.go b/pkg/podvolume/restorer_factory.go new file mode 100644 index 000000000..3432f4be3 --- /dev/null +++ b/pkg/podvolume/restorer_factory.go @@ -0,0 +1,85 @@ +/* +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 podvolume + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/tools/cache" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned" + velerov1informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions/velero/v1" + "github.com/vmware-tanzu/velero/pkg/repository" +) + +// RestorerFactory can construct pod volumes restorers. +type RestorerFactory interface { + // NewRestorer returns a pod volumes restorer for use during a single Velero restore. + NewRestorer(context.Context, *velerov1api.Restore) (Restorer, error) +} + +func NewRestorerFactory(repoLocker *repository.RepoLocker, + repoEnsurer *repository.RepositoryEnsurer, + veleroClient clientset.Interface, + pvcClient corev1client.PersistentVolumeClaimsGetter, + repoInformerSynced cache.InformerSynced, + log logrus.FieldLogger) RestorerFactory { + return &restorerFactory{ + repoLocker: repoLocker, + repoEnsurer: repoEnsurer, + veleroClient: veleroClient, + pvcClient: pvcClient, + repoInformerSynced: repoInformerSynced, + log: log, + } +} + +type restorerFactory struct { + repoLocker *repository.RepoLocker + repoEnsurer *repository.RepositoryEnsurer + veleroClient clientset.Interface + pvcClient corev1client.PersistentVolumeClaimsGetter + repoInformerSynced cache.InformerSynced + log logrus.FieldLogger +} + +func (rf *restorerFactory) NewRestorer(ctx context.Context, restore *velerov1api.Restore) (Restorer, error) { + informer := velerov1informers.NewFilteredPodVolumeRestoreInformer( + rf.veleroClient, + restore.Namespace, + 0, + cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, + func(opts *metav1.ListOptions) { + opts.LabelSelector = fmt.Sprintf("%s=%s", velerov1api.RestoreUIDLabel, restore.UID) + }, + ) + + r := newRestorer(ctx, rf.repoLocker, rf.repoEnsurer, informer, rf.veleroClient, rf.pvcClient, rf.log) + + go informer.Run(ctx.Done()) + if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced, rf.repoInformerSynced) { + return nil, errors.New("timed out waiting for cache to sync") + } + + return r, nil +} diff --git a/pkg/podvolume/util.go b/pkg/podvolume/util.go new file mode 100644 index 000000000..57baacc10 --- /dev/null +++ b/pkg/podvolume/util.go @@ -0,0 +1,198 @@ +/* +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 podvolume + +import ( + "strings" + + corev1api "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" +) + +const ( + // PVCNameAnnotation is the key for the annotation added to + // pod volume backups when they're for a PVC. + PVCNameAnnotation = "velero.io/pvc-name" + + // Deprecated. + // + // TODO(2.0): remove + podAnnotationPrefix = "snapshot.velero.io/" + + // VolumesToBackupAnnotation is the annotation on a pod whose mounted volumes + // need to be backed up using restic. + VolumesToBackupAnnotation = "backup.velero.io/backup-volumes" + + // VolumesToExcludeAnnotation is the annotation on a pod whose mounted volumes + // should be excluded from restic backup. + VolumesToExcludeAnnotation = "backup.velero.io/backup-volumes-excludes" +) + +// GetVolumeBackupsForPod returns a map, of volume name -> snapshot id, +// of the PodVolumeBackups that exist for the provided pod. +func GetVolumeBackupsForPod(podVolumeBackups []*velerov1api.PodVolumeBackup, pod *corev1api.Pod, sourcePodNs string) map[string]string { + volumes := make(map[string]string) + + for _, pvb := range podVolumeBackups { + if !isPVBMatchPod(pvb, pod.GetName(), sourcePodNs) { + continue + } + + // skip PVBs without a snapshot ID since there's nothing + // to restore (they could be failed, or for empty volumes). + if pvb.Status.SnapshotID == "" { + continue + } + + // If the volume came from a projected or DownwardAPI source, skip its restore. + // This allows backups affected by https://github.com/vmware-tanzu/velero/issues/3863 + // or https://github.com/vmware-tanzu/velero/issues/4053 to be restored successfully. + if volumeHasNonRestorableSource(pvb.Spec.Volume, pod.Spec.Volumes) { + continue + } + + volumes[pvb.Spec.Volume] = pvb.Status.SnapshotID + } + + if len(volumes) > 0 { + return volumes + } + + return getPodSnapshotAnnotations(pod) +} + +func isPVBMatchPod(pvb *velerov1api.PodVolumeBackup, podName string, namespace string) bool { + return podName == pvb.Spec.Pod.Name && namespace == pvb.Spec.Pod.Namespace +} + +// volumeHasNonRestorableSource checks if the given volume exists in the list of podVolumes +// and returns true if the volume's source is not restorable. This is true for volumes with +// a Projected or DownwardAPI source. +func volumeHasNonRestorableSource(volumeName string, podVolumes []corev1api.Volume) bool { + var volume corev1api.Volume + for _, v := range podVolumes { + if v.Name == volumeName { + volume = v + break + } + } + return volume.Projected != nil || volume.DownwardAPI != nil +} + +// getPodSnapshotAnnotations returns a map, of volume name -> snapshot id, +// of all snapshots for this pod. +// TODO(2.0) to remove +// Deprecated: we will stop using pod annotations to record restic snapshot IDs after they're taken, +// therefore we won't need to check if these annotations exist. +func getPodSnapshotAnnotations(obj metav1.Object) map[string]string { + var res map[string]string + + insertSafe := func(k, v string) { + if res == nil { + res = make(map[string]string) + } + res[k] = v + } + + for k, v := range obj.GetAnnotations() { + if strings.HasPrefix(k, podAnnotationPrefix) { + insertSafe(k[len(podAnnotationPrefix):], v) + } + } + + return res +} + +// GetVolumesToBackup returns a list of volume names to backup for +// the provided pod. +// Deprecated: Use GetPodVolumesUsingRestic instead. +func GetVolumesToBackup(obj metav1.Object) []string { + annotations := obj.GetAnnotations() + if annotations == nil { + return nil + } + + backupsValue := annotations[VolumesToBackupAnnotation] + if backupsValue == "" { + return nil + } + + return strings.Split(backupsValue, ",") +} + +func getVolumesToExclude(obj metav1.Object) []string { + annotations := obj.GetAnnotations() + if annotations == nil { + return nil + } + + return strings.Split(annotations[VolumesToExcludeAnnotation], ",") +} + +func contains(list []string, k string) bool { + for _, i := range list { + if i == k { + return true + } + } + return false +} + +// GetPodVolumesUsingRestic returns a list of volume names to backup for the provided pod. +func GetPodVolumesUsingRestic(pod *corev1api.Pod, defaultVolumesToRestic bool) []string { + if !defaultVolumesToRestic { + return GetVolumesToBackup(pod) + } + + volsToExclude := getVolumesToExclude(pod) + podVolumes := []string{} + for _, pv := range pod.Spec.Volumes { + // cannot backup hostpath volumes as they are not mounted into /var/lib/kubelet/pods + // and therefore not accessible to the restic daemon set. + if pv.HostPath != nil { + continue + } + // don't backup volumes mounting secrets. Secrets will be backed up separately. + if pv.Secret != nil { + continue + } + // don't backup volumes mounting config maps. Config maps will be backed up separately. + if pv.ConfigMap != nil { + continue + } + // don't backup volumes mounted as projected volumes, all data in those come from kube state. + if pv.Projected != nil { + continue + } + // don't backup DownwardAPI volumes, all data in those come from kube state. + if pv.DownwardAPI != nil { + continue + } + // don't backup volumes that are included in the exclude list. + if contains(volsToExclude, pv.Name) { + continue + } + // don't include volumes that mount the default service account token. + if strings.HasPrefix(pv.Name, "default-token") { + continue + } + podVolumes = append(podVolumes, pv.Name) + } + return podVolumes +} diff --git a/pkg/podvolume/util_test.go b/pkg/podvolume/util_test.go new file mode 100644 index 000000000..88b574668 --- /dev/null +++ b/pkg/podvolume/util_test.go @@ -0,0 +1,563 @@ +/* +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 podvolume + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + corev1api "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" +) + +func TestGetVolumeBackupsForPod(t *testing.T) { + tests := []struct { + name string + podVolumeBackups []*velerov1api.PodVolumeBackup + podVolumes []corev1api.Volume + podAnnotations map[string]string + podName string + sourcePodNs string + expected map[string]string + }{ + { + name: "nil annotations results in no volume backups returned", + podAnnotations: nil, + expected: nil, + }, + { + name: "empty annotations results in no volume backups returned", + podAnnotations: make(map[string]string), + expected: nil, + }, + { + name: "pod annotations with no snapshot annotation prefix results in no volume backups returned", + podAnnotations: map[string]string{"foo": "bar"}, + expected: nil, + }, + { + name: "pod annotation with only snapshot annotation prefix, results in volume backup with empty volume key", + podAnnotations: map[string]string{podAnnotationPrefix: "snapshotID"}, + expected: map[string]string{"": "snapshotID"}, + }, + { + name: "pod annotation with snapshot annotation prefix results in volume backup with volume name and snapshot ID", + podAnnotations: map[string]string{podAnnotationPrefix + "volume": "snapshotID"}, + expected: map[string]string{"volume": "snapshotID"}, + }, + { + name: "only pod annotations with snapshot annotation prefix are considered", + podAnnotations: map[string]string{"x": "y", podAnnotationPrefix + "volume1": "snapshot1", podAnnotationPrefix + "volume2": "snapshot2"}, + expected: map[string]string{"volume1": "snapshot1", "volume2": "snapshot2"}, + }, + { + name: "pod annotations are not considered if PVBs are provided", + podVolumeBackups: []*velerov1api.PodVolumeBackup{ + builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(), + builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(), + }, + podName: "TestPod", + sourcePodNs: "TestNS", + podAnnotations: map[string]string{"x": "y", podAnnotationPrefix + "foo": "bar", podAnnotationPrefix + "abc": "123"}, + expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"}, + }, + { + name: "volume backups are returned even if no pod annotations are present", + podVolumeBackups: []*velerov1api.PodVolumeBackup{ + builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(), + builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(), + }, + podName: "TestPod", + sourcePodNs: "TestNS", + expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"}, + }, + { + name: "only volumes from PVBs with snapshot IDs are returned", + podVolumeBackups: []*velerov1api.PodVolumeBackup{ + builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(), + builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(), + builder.ForPodVolumeBackup("velero", "pvb-3").PodName("TestPod").PodNamespace("TestNS").Volume("pvbtest3-foo").Result(), + builder.ForPodVolumeBackup("velero", "pvb-4").PodName("TestPod").PodNamespace("TestNS").Volume("pvbtest4-abc").Result(), + }, + podName: "TestPod", + sourcePodNs: "TestNS", + expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"}, + }, + { + name: "only volumes from PVBs for the given pod are returned", + podVolumeBackups: []*velerov1api.PodVolumeBackup{ + builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(), + builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(), + builder.ForPodVolumeBackup("velero", "pvb-3").PodName("TestAnotherPod").SnapshotID("snapshot3").Volume("pvbtest3-xyz").Result(), + }, + podName: "TestPod", + sourcePodNs: "TestNS", + expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"}, + }, + { + name: "only volumes from PVBs which match the pod name and source pod namespace are returned", + podVolumeBackups: []*velerov1api.PodVolumeBackup{ + builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(), + builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestAnotherPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(), + builder.ForPodVolumeBackup("velero", "pvb-3").PodName("TestPod").PodNamespace("TestAnotherNS").SnapshotID("snapshot3").Volume("pvbtest3-xyz").Result(), + }, + podName: "TestPod", + sourcePodNs: "TestNS", + expected: map[string]string{"pvbtest1-foo": "snapshot1"}, + }, + { + name: "volumes from PVBs that correspond to a pod volume from a projected source are not returned", + podVolumeBackups: []*velerov1api.PodVolumeBackup{ + builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvb-non-projected").Result(), + builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvb-projected").Result(), + }, + podVolumes: []corev1api.Volume{ + { + Name: "pvb-non-projected", + VolumeSource: corev1api.VolumeSource{ + PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{}, + }, + }, + { + Name: "pvb-projected", + VolumeSource: corev1api.VolumeSource{ + Projected: &corev1api.ProjectedVolumeSource{}, + }, + }, + }, + podName: "TestPod", + sourcePodNs: "TestNS", + expected: map[string]string{"pvb-non-projected": "snapshot1"}, + }, + { + name: "volumes from PVBs that correspond to a pod volume from a DownwardAPI source are not returned", + podVolumeBackups: []*velerov1api.PodVolumeBackup{ + builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvb-non-downwardapi").Result(), + builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvb-downwardapi").Result(), + }, + podVolumes: []corev1api.Volume{ + { + Name: "pvb-non-downwardapi", + VolumeSource: corev1api.VolumeSource{ + PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{}, + }, + }, + { + Name: "pvb-downwardapi", + VolumeSource: corev1api.VolumeSource{ + DownwardAPI: &corev1api.DownwardAPIVolumeSource{}, + }, + }, + }, + podName: "TestPod", + sourcePodNs: "TestNS", + expected: map[string]string{"pvb-non-downwardapi": "snapshot1"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pod := &corev1api.Pod{} + pod.Annotations = test.podAnnotations + pod.Name = test.podName + pod.Spec.Volumes = test.podVolumes + + res := GetVolumeBackupsForPod(test.podVolumeBackups, pod, test.sourcePodNs) + assert.Equal(t, test.expected, res) + }) + } +} + +func TestVolumeHasNonRestorableSource(t *testing.T) { + testCases := []struct { + name string + volumeName string + podVolumes []corev1api.Volume + expected bool + }{ + { + name: "volume name not in list of volumes", + volumeName: "missing-volume", + podVolumes: []corev1api.Volume{ + { + Name: "restorable", + VolumeSource: corev1api.VolumeSource{ + PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{}, + }, + }, + { + Name: "projected", + VolumeSource: corev1api.VolumeSource{ + Projected: &corev1api.ProjectedVolumeSource{}, + }, + }, + { + Name: "downwardapi", + VolumeSource: corev1api.VolumeSource{ + DownwardAPI: &corev1api.DownwardAPIVolumeSource{}, + }, + }, + }, + expected: false, + }, + { + name: "volume name in list of volumes but not projected or DownwardAPI", + volumeName: "restorable", + podVolumes: []corev1api.Volume{ + { + Name: "restorable", + VolumeSource: corev1api.VolumeSource{ + PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{}, + }, + }, + { + Name: "projected", + VolumeSource: corev1api.VolumeSource{ + Projected: &corev1api.ProjectedVolumeSource{}, + }, + }, + { + Name: "downwardapi", + VolumeSource: corev1api.VolumeSource{ + DownwardAPI: &corev1api.DownwardAPIVolumeSource{}, + }, + }, + }, + expected: false, + }, + { + name: "volume name in list of volumes and projected", + volumeName: "projected", + podVolumes: []corev1api.Volume{ + { + Name: "restorable", + VolumeSource: corev1api.VolumeSource{ + PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{}, + }, + }, + { + Name: "projected", + VolumeSource: corev1api.VolumeSource{ + Projected: &corev1api.ProjectedVolumeSource{}, + }, + }, + { + Name: "downwardapi", + VolumeSource: corev1api.VolumeSource{ + DownwardAPI: &corev1api.DownwardAPIVolumeSource{}, + }, + }, + }, + expected: true, + }, + { + name: "volume name in list of volumes and is a DownwardAPI volume", + volumeName: "downwardapi", + podVolumes: []corev1api.Volume{ + { + Name: "restorable", + VolumeSource: corev1api.VolumeSource{ + PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{}, + }, + }, + { + Name: "projected", + VolumeSource: corev1api.VolumeSource{ + Projected: &corev1api.ProjectedVolumeSource{}, + }, + }, + { + Name: "downwardapi", + VolumeSource: corev1api.VolumeSource{ + DownwardAPI: &corev1api.DownwardAPIVolumeSource{}, + }, + }, + }, + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := volumeHasNonRestorableSource(tc.volumeName, tc.podVolumes) + assert.Equal(t, tc.expected, actual) + }) + + } +} + +func TestGetVolumesToBackup(t *testing.T) { + tests := []struct { + name string + annotations map[string]string + expected []string + }{ + { + name: "nil annotations", + annotations: nil, + expected: nil, + }, + { + name: "no volumes to backup", + annotations: map[string]string{"foo": "bar"}, + expected: nil, + }, + { + name: "one volume to backup", + annotations: map[string]string{"foo": "bar", VolumesToBackupAnnotation: "volume-1"}, + expected: []string{"volume-1"}, + }, + { + name: "multiple volumes to backup", + annotations: map[string]string{"foo": "bar", VolumesToBackupAnnotation: "volume-1,volume-2,volume-3"}, + expected: []string{"volume-1", "volume-2", "volume-3"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pod := &corev1api.Pod{} + pod.Annotations = test.annotations + + res := GetVolumesToBackup(pod) + + // sort to ensure good compare of slices + sort.Strings(test.expected) + sort.Strings(res) + + assert.Equal(t, test.expected, res) + }) + } +} + +func TestGetPodVolumesUsingRestic(t *testing.T) { + testCases := []struct { + name string + pod *corev1api.Pod + expected []string + defaultVolumesToRestic bool + }{ + { + name: "should get PVs from VolumesToBackupAnnotation when defaultVolumesToRestic is false", + defaultVolumesToRestic: false, + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + VolumesToBackupAnnotation: "resticPV1,resticPV2,resticPV3", + }, + }, + }, + expected: []string{"resticPV1", "resticPV2", "resticPV3"}, + }, + { + name: "should get all pod volumes when defaultVolumesToRestic is true and no PVs are excluded", + defaultVolumesToRestic: true, + pod: &corev1api.Pod{ + Spec: corev1api.PodSpec{ + Volumes: []corev1api.Volume{ + // Restic Volumes + {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, + }, + }, + }, + expected: []string{"resticPV1", "resticPV2", "resticPV3"}, + }, + { + name: "should get all pod volumes except ones excluded when defaultVolumesToRestic is true", + defaultVolumesToRestic: true, + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3", + }, + }, + Spec: corev1api.PodSpec{ + Volumes: []corev1api.Volume{ + // Restic Volumes + {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, + /// Excluded from restic through annotation + {Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"}, + }, + }, + }, + expected: []string{"resticPV1", "resticPV2", "resticPV3"}, + }, + { + name: "should exclude default service account token from restic backup", + defaultVolumesToRestic: true, + pod: &corev1api.Pod{ + Spec: corev1api.PodSpec{ + Volumes: []corev1api.Volume{ + // Restic Volumes + {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, + /// Excluded from restic because colume mounting default service account token + {Name: "default-token-5xq45"}, + }, + }, + }, + expected: []string{"resticPV1", "resticPV2", "resticPV3"}, + }, + { + name: "should exclude host path volumes from restic backups", + defaultVolumesToRestic: true, + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3", + }, + }, + Spec: corev1api.PodSpec{ + Volumes: []corev1api.Volume{ + // Restic Volumes + {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, + /// Excluded from restic through annotation + {Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"}, + // Excluded from restic because hostpath + {Name: "hostPath1", VolumeSource: corev1api.VolumeSource{HostPath: &corev1api.HostPathVolumeSource{Path: "/hostpathVol"}}}, + }, + }, + }, + expected: []string{"resticPV1", "resticPV2", "resticPV3"}, + }, + { + name: "should exclude volumes mounting secrets", + defaultVolumesToRestic: true, + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3", + }, + }, + Spec: corev1api.PodSpec{ + Volumes: []corev1api.Volume{ + // Restic Volumes + {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, + /// Excluded from restic through annotation + {Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"}, + // Excluded from restic because hostpath + {Name: "superSecret", VolumeSource: corev1api.VolumeSource{Secret: &corev1api.SecretVolumeSource{SecretName: "super-secret"}}}, + }, + }, + }, + expected: []string{"resticPV1", "resticPV2", "resticPV3"}, + }, + { + name: "should exclude volumes mounting config maps", + defaultVolumesToRestic: true, + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3", + }, + }, + Spec: corev1api.PodSpec{ + Volumes: []corev1api.Volume{ + // Restic Volumes + {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, + /// Excluded from restic through annotation + {Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"}, + // Excluded from restic because hostpath + {Name: "appCOnfig", VolumeSource: corev1api.VolumeSource{ConfigMap: &corev1api.ConfigMapVolumeSource{LocalObjectReference: corev1api.LocalObjectReference{Name: "app-config"}}}}, + }, + }, + }, + expected: []string{"resticPV1", "resticPV2", "resticPV3"}, + }, + { + name: "should exclude projected volumes", + defaultVolumesToRestic: true, + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3", + }, + }, + Spec: corev1api.PodSpec{ + Volumes: []corev1api.Volume{ + {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, + { + Name: "projected", + VolumeSource: corev1api.VolumeSource{ + Projected: &corev1api.ProjectedVolumeSource{ + Sources: []corev1api.VolumeProjection{{ + Secret: &corev1api.SecretProjection{ + LocalObjectReference: corev1api.LocalObjectReference{}, + Items: nil, + Optional: nil, + }, + DownwardAPI: nil, + ConfigMap: nil, + ServiceAccountToken: nil, + }}, + DefaultMode: nil, + }, + }, + }, + }, + }, + }, + expected: []string{"resticPV1", "resticPV2", "resticPV3"}, + }, + { + name: "should exclude DownwardAPI volumes", + defaultVolumesToRestic: true, + pod: &corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3", + }, + }, + Spec: corev1api.PodSpec{ + Volumes: []corev1api.Volume{ + {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, + { + Name: "downwardAPI", + VolumeSource: corev1api.VolumeSource{ + DownwardAPI: &corev1api.DownwardAPIVolumeSource{ + Items: []corev1api.DownwardAPIVolumeFile{ + { + Path: "labels", + FieldRef: &corev1api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: []string{"resticPV1", "resticPV2", "resticPV3"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := GetPodVolumesUsingRestic(tc.pod, tc.defaultVolumesToRestic) + + sort.Strings(tc.expected) + sort.Strings(actual) + assert.Equal(t, tc.expected, actual) + }) + } +} diff --git a/pkg/repository/config/config.go b/pkg/repository/config/config.go index 24dc8d6b3..d7ed99b69 100644 --- a/pkg/repository/config/config.go +++ b/pkg/repository/config/config.go @@ -34,9 +34,7 @@ const ( AzureBackend BackendType = "velero.io/azure" GCPBackend BackendType = "velero.io/gcp" FSBackend BackendType = "velero.io/fs" -) -const ( // CredentialsFileKey is the key within a BSL config that is checked to see if // the BSL is using its own credentials, rather than those in the environment CredentialsFileKey = "credentialsFile" diff --git a/pkg/restic/repository_ensurer.go b/pkg/repository/ensurer.go similarity index 93% rename from pkg/restic/repository_ensurer.go rename to pkg/repository/ensurer.go index d764a49c7..15aa10701 100644 --- a/pkg/restic/repository_ensurer.go +++ b/pkg/repository/ensurer.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package repository import ( "context" @@ -35,8 +35,8 @@ import ( "github.com/vmware-tanzu/velero/pkg/label" ) -// repositoryEnsurer ensures that Velero restic repositories are created and ready. -type repositoryEnsurer struct { +// RepositoryEnsurer ensures that backup repositories are created and ready. +type RepositoryEnsurer struct { log logrus.FieldLogger repoLister velerov1listers.BackupRepositoryLister repoClient velerov1client.BackupRepositoriesGetter @@ -55,8 +55,8 @@ type repoKey struct { backupLocation string } -func newRepositoryEnsurer(repoInformer velerov1informers.BackupRepositoryInformer, repoClient velerov1client.BackupRepositoriesGetter, log logrus.FieldLogger) *repositoryEnsurer { - r := &repositoryEnsurer{ +func NewRepositoryEnsurer(repoInformer velerov1informers.BackupRepositoryInformer, repoClient velerov1client.BackupRepositoriesGetter, log logrus.FieldLogger) *RepositoryEnsurer { + r := &RepositoryEnsurer{ log: log, repoLister: repoInformer.Lister(), repoClient: repoClient, @@ -105,7 +105,7 @@ func repoLabels(volumeNamespace, backupLocation string) labels.Set { } } -func (r *repositoryEnsurer) EnsureRepo(ctx context.Context, namespace, volumeNamespace, backupLocation string) (*velerov1api.BackupRepository, error) { +func (r *RepositoryEnsurer) EnsureRepo(ctx context.Context, namespace, volumeNamespace, backupLocation string) (*velerov1api.BackupRepository, error) { log := r.log.WithField("volumeNamespace", volumeNamespace).WithField("backupLocation", backupLocation) // It's only safe to have one instance of this method executing concurrently for a @@ -190,7 +190,7 @@ func (r *repositoryEnsurer) EnsureRepo(ctx context.Context, namespace, volumeNam } } -func (r *repositoryEnsurer) getRepoChan(name string) chan *velerov1api.BackupRepository { +func (r *RepositoryEnsurer) getRepoChan(name string) chan *velerov1api.BackupRepository { r.repoChansLock.Lock() defer r.repoChansLock.Unlock() @@ -198,7 +198,7 @@ func (r *repositoryEnsurer) getRepoChan(name string) chan *velerov1api.BackupRep return r.repoChans[name] } -func (r *repositoryEnsurer) repoLock(volumeNamespace, backupLocation string) *sync.Mutex { +func (r *RepositoryEnsurer) repoLock(volumeNamespace, backupLocation string) *sync.Mutex { r.repoLocksMu.Lock() defer r.repoLocksMu.Unlock() diff --git a/pkg/restic/repo_locker.go b/pkg/repository/locker.go similarity index 79% rename from pkg/restic/repo_locker.go rename to pkg/repository/locker.go index 29434753e..20eea9635 100644 --- a/pkg/restic/repo_locker.go +++ b/pkg/repository/locker.go @@ -13,23 +13,24 @@ 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 restic + +package repository import "sync" -// repoLocker manages exclusive/non-exclusive locks for +// RepoLocker manages exclusive/non-exclusive locks for // operations against restic repositories. The semantics // of exclusive/non-exclusive locks are the same as for // a sync.RWMutex, where a non-exclusive lock is equivalent // to a read lock, and an exclusive lock is equivalent to // a write lock. -type repoLocker struct { +type RepoLocker struct { mu sync.Mutex locks map[string]*sync.RWMutex } -func newRepoLocker() *repoLocker { - return &repoLocker{ +func NewRepoLocker() *RepoLocker { + return &RepoLocker{ locks: make(map[string]*sync.RWMutex), } } @@ -37,28 +38,28 @@ func newRepoLocker() *repoLocker { // LockExclusive acquires an exclusive lock for the specified // repository. This function blocks until no other locks exist // for the repo. -func (rl *repoLocker) LockExclusive(name string) { +func (rl *RepoLocker) LockExclusive(name string) { rl.ensureLock(name).Lock() } // Lock acquires a non-exclusive lock for the specified // repository. This function blocks until no exclusive // locks exist for the repo. -func (rl *repoLocker) Lock(name string) { +func (rl *RepoLocker) Lock(name string) { rl.ensureLock(name).RLock() } // UnlockExclusive releases an exclusive lock for the repo. -func (rl *repoLocker) UnlockExclusive(name string) { +func (rl *RepoLocker) UnlockExclusive(name string) { rl.ensureLock(name).Unlock() } // Unlock releases a non-exclusive lock for the repo. -func (rl *repoLocker) Unlock(name string) { +func (rl *RepoLocker) Unlock(name string) { rl.ensureLock(name).RUnlock() } -func (rl *repoLocker) ensureLock(name string) *sync.RWMutex { +func (rl *RepoLocker) ensureLock(name string) *sync.RWMutex { rl.mu.Lock() defer rl.mu.Unlock() diff --git a/pkg/restic/common.go b/pkg/restic/common.go index 580bd2708..6e2671625 100644 --- a/pkg/restic/common.go +++ b/pkg/restic/common.go @@ -20,11 +20,9 @@ import ( "context" "fmt" "os" - "strings" "time" "github.com/pkg/errors" - corev1api "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "sigs.k8s.io/controller-runtime/pkg/client" @@ -51,178 +49,8 @@ const ( // DefaultVolumesToRestic specifies whether restic should be used, by default, to // take backup of all pod volumes. DefaultVolumesToRestic = false - - // PVCNameAnnotation is the key for the annotation added to - // pod volume backups when they're for a PVC. - PVCNameAnnotation = "velero.io/pvc-name" - - // VolumesToBackupAnnotation is the annotation on a pod whose mounted volumes - // need to be backed up using restic. - VolumesToBackupAnnotation = "backup.velero.io/backup-volumes" - - // VolumesToExcludeAnnotation is the annotation on a pod whose mounted volumes - // should be excluded from restic backup. - VolumesToExcludeAnnotation = "backup.velero.io/backup-volumes-excludes" - - // Deprecated. - // - // TODO(2.0): remove - podAnnotationPrefix = "snapshot.velero.io/" ) -// getPodSnapshotAnnotations returns a map, of volume name -> snapshot id, -// of all restic snapshots for this pod. -// TODO(2.0) to remove -// Deprecated: we will stop using pod annotations to record restic snapshot IDs after they're taken, -// therefore we won't need to check if these annotations exist. -func getPodSnapshotAnnotations(obj metav1.Object) map[string]string { - var res map[string]string - - insertSafe := func(k, v string) { - if res == nil { - res = make(map[string]string) - } - res[k] = v - } - - for k, v := range obj.GetAnnotations() { - if strings.HasPrefix(k, podAnnotationPrefix) { - insertSafe(k[len(podAnnotationPrefix):], v) - } - } - - return res -} - -func isPVBMatchPod(pvb *velerov1api.PodVolumeBackup, podName string, namespace string) bool { - return podName == pvb.Spec.Pod.Name && namespace == pvb.Spec.Pod.Namespace -} - -// volumeHasNonRestorableSource checks if the given volume exists in the list of podVolumes -// and returns true if the volume's source is not restorable. This is true for volumes with -// a Projected or DownwardAPI source. -func volumeHasNonRestorableSource(volumeName string, podVolumes []corev1api.Volume) bool { - var volume corev1api.Volume - for _, v := range podVolumes { - if v.Name == volumeName { - volume = v - break - } - } - return volume.Projected != nil || volume.DownwardAPI != nil -} - -// GetVolumeBackupsForPod returns a map, of volume name -> snapshot id, -// of the PodVolumeBackups that exist for the provided pod. -func GetVolumeBackupsForPod(podVolumeBackups []*velerov1api.PodVolumeBackup, pod *corev1api.Pod, sourcePodNs string) map[string]string { - volumes := make(map[string]string) - - for _, pvb := range podVolumeBackups { - if !isPVBMatchPod(pvb, pod.GetName(), sourcePodNs) { - continue - } - - // skip PVBs without a snapshot ID since there's nothing - // to restore (they could be failed, or for empty volumes). - if pvb.Status.SnapshotID == "" { - continue - } - - // If the volume came from a projected or DownwardAPI source, skip its restore. - // This allows backups affected by https://github.com/vmware-tanzu/velero/issues/3863 - // or https://github.com/vmware-tanzu/velero/issues/4053 to be restored successfully. - if volumeHasNonRestorableSource(pvb.Spec.Volume, pod.Spec.Volumes) { - continue - } - - volumes[pvb.Spec.Volume] = pvb.Status.SnapshotID - } - - if len(volumes) > 0 { - return volumes - } - - return getPodSnapshotAnnotations(pod) -} - -// GetVolumesToBackup returns a list of volume names to backup for -// the provided pod. -// Deprecated: Use GetPodVolumesUsingRestic instead. -func GetVolumesToBackup(obj metav1.Object) []string { - annotations := obj.GetAnnotations() - if annotations == nil { - return nil - } - - backupsValue := annotations[VolumesToBackupAnnotation] - if backupsValue == "" { - return nil - } - - return strings.Split(backupsValue, ",") -} - -func getVolumesToExclude(obj metav1.Object) []string { - annotations := obj.GetAnnotations() - if annotations == nil { - return nil - } - - return strings.Split(annotations[VolumesToExcludeAnnotation], ",") -} - -func contains(list []string, k string) bool { - for _, i := range list { - if i == k { - return true - } - } - return false -} - -// GetPodVolumesUsingRestic returns a list of volume names to backup for the provided pod. -func GetPodVolumesUsingRestic(pod *corev1api.Pod, defaultVolumesToRestic bool) []string { - if !defaultVolumesToRestic { - return GetVolumesToBackup(pod) - } - - volsToExclude := getVolumesToExclude(pod) - podVolumes := []string{} - for _, pv := range pod.Spec.Volumes { - // cannot backup hostpath volumes as they are not mounted into /var/lib/kubelet/pods - // and therefore not accessible to the restic daemon set. - if pv.HostPath != nil { - continue - } - // don't backup volumes mounting secrets. Secrets will be backed up separately. - if pv.Secret != nil { - continue - } - // don't backup volumes mounting config maps. Config maps will be backed up separately. - if pv.ConfigMap != nil { - continue - } - // don't backup volumes mounted as projected volumes, all data in those come from kube state. - if pv.Projected != nil { - continue - } - // don't backup DownwardAPI volumes, all data in those come from kube state. - if pv.DownwardAPI != nil { - continue - } - // don't backup volumes that are included in the exclude list. - if contains(volsToExclude, pv.Name) { - continue - } - // don't include volumes that mount the default service account token. - if strings.HasPrefix(pv.Name, "default-token") { - continue - } - podVolumes = append(podVolumes, pv.Name) - } - return podVolumes -} - // SnapshotIdentifier uniquely identifies a restic snapshot // taken by Velero. type SnapshotIdentifier struct { diff --git a/pkg/restic/common_test.go b/pkg/restic/common_test.go index 7f3e0c503..fac82f901 100644 --- a/pkg/restic/common_test.go +++ b/pkg/restic/common_test.go @@ -28,212 +28,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - "github.com/vmware-tanzu/velero/pkg/builder" velerotest "github.com/vmware-tanzu/velero/pkg/test" ) -func TestGetVolumeBackupsForPod(t *testing.T) { - tests := []struct { - name string - podVolumeBackups []*velerov1api.PodVolumeBackup - podVolumes []corev1api.Volume - podAnnotations map[string]string - podName string - sourcePodNs string - expected map[string]string - }{ - { - name: "nil annotations results in no volume backups returned", - podAnnotations: nil, - expected: nil, - }, - { - name: "empty annotations results in no volume backups returned", - podAnnotations: make(map[string]string), - expected: nil, - }, - { - name: "pod annotations with no snapshot annotation prefix results in no volume backups returned", - podAnnotations: map[string]string{"foo": "bar"}, - expected: nil, - }, - { - name: "pod annotation with only snapshot annotation prefix, results in volume backup with empty volume key", - podAnnotations: map[string]string{podAnnotationPrefix: "snapshotID"}, - expected: map[string]string{"": "snapshotID"}, - }, - { - name: "pod annotation with snapshot annotation prefix results in volume backup with volume name and snapshot ID", - podAnnotations: map[string]string{podAnnotationPrefix + "volume": "snapshotID"}, - expected: map[string]string{"volume": "snapshotID"}, - }, - { - name: "only pod annotations with snapshot annotation prefix are considered", - podAnnotations: map[string]string{"x": "y", podAnnotationPrefix + "volume1": "snapshot1", podAnnotationPrefix + "volume2": "snapshot2"}, - expected: map[string]string{"volume1": "snapshot1", "volume2": "snapshot2"}, - }, - { - name: "pod annotations are not considered if PVBs are provided", - podVolumeBackups: []*velerov1api.PodVolumeBackup{ - builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(), - builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(), - }, - podName: "TestPod", - sourcePodNs: "TestNS", - podAnnotations: map[string]string{"x": "y", podAnnotationPrefix + "foo": "bar", podAnnotationPrefix + "abc": "123"}, - expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"}, - }, - { - name: "volume backups are returned even if no pod annotations are present", - podVolumeBackups: []*velerov1api.PodVolumeBackup{ - builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(), - builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(), - }, - podName: "TestPod", - sourcePodNs: "TestNS", - expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"}, - }, - { - name: "only volumes from PVBs with snapshot IDs are returned", - podVolumeBackups: []*velerov1api.PodVolumeBackup{ - builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(), - builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(), - builder.ForPodVolumeBackup("velero", "pvb-3").PodName("TestPod").PodNamespace("TestNS").Volume("pvbtest3-foo").Result(), - builder.ForPodVolumeBackup("velero", "pvb-4").PodName("TestPod").PodNamespace("TestNS").Volume("pvbtest4-abc").Result(), - }, - podName: "TestPod", - sourcePodNs: "TestNS", - expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"}, - }, - { - name: "only volumes from PVBs for the given pod are returned", - podVolumeBackups: []*velerov1api.PodVolumeBackup{ - builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(), - builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(), - builder.ForPodVolumeBackup("velero", "pvb-3").PodName("TestAnotherPod").SnapshotID("snapshot3").Volume("pvbtest3-xyz").Result(), - }, - podName: "TestPod", - sourcePodNs: "TestNS", - expected: map[string]string{"pvbtest1-foo": "snapshot1", "pvbtest2-abc": "snapshot2"}, - }, - { - name: "only volumes from PVBs which match the pod name and source pod namespace are returned", - podVolumeBackups: []*velerov1api.PodVolumeBackup{ - builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvbtest1-foo").Result(), - builder.ForPodVolumeBackup("velero", "pvb-2").PodName("TestAnotherPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvbtest2-abc").Result(), - builder.ForPodVolumeBackup("velero", "pvb-3").PodName("TestPod").PodNamespace("TestAnotherNS").SnapshotID("snapshot3").Volume("pvbtest3-xyz").Result(), - }, - podName: "TestPod", - sourcePodNs: "TestNS", - expected: map[string]string{"pvbtest1-foo": "snapshot1"}, - }, - { - name: "volumes from PVBs that correspond to a pod volume from a projected source are not returned", - podVolumeBackups: []*velerov1api.PodVolumeBackup{ - builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvb-non-projected").Result(), - builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvb-projected").Result(), - }, - podVolumes: []corev1api.Volume{ - { - Name: "pvb-non-projected", - VolumeSource: corev1api.VolumeSource{ - PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{}, - }, - }, - { - Name: "pvb-projected", - VolumeSource: corev1api.VolumeSource{ - Projected: &corev1api.ProjectedVolumeSource{}, - }, - }, - }, - podName: "TestPod", - sourcePodNs: "TestNS", - expected: map[string]string{"pvb-non-projected": "snapshot1"}, - }, - { - name: "volumes from PVBs that correspond to a pod volume from a DownwardAPI source are not returned", - podVolumeBackups: []*velerov1api.PodVolumeBackup{ - builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot1").Volume("pvb-non-downwardapi").Result(), - builder.ForPodVolumeBackup("velero", "pvb-1").PodName("TestPod").PodNamespace("TestNS").SnapshotID("snapshot2").Volume("pvb-downwardapi").Result(), - }, - podVolumes: []corev1api.Volume{ - { - Name: "pvb-non-downwardapi", - VolumeSource: corev1api.VolumeSource{ - PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{}, - }, - }, - { - Name: "pvb-downwardapi", - VolumeSource: corev1api.VolumeSource{ - DownwardAPI: &corev1api.DownwardAPIVolumeSource{}, - }, - }, - }, - podName: "TestPod", - sourcePodNs: "TestNS", - expected: map[string]string{"pvb-non-downwardapi": "snapshot1"}, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - pod := &corev1api.Pod{} - pod.Annotations = test.podAnnotations - pod.Name = test.podName - pod.Spec.Volumes = test.podVolumes - - res := GetVolumeBackupsForPod(test.podVolumeBackups, pod, test.sourcePodNs) - assert.Equal(t, test.expected, res) - }) - } -} - -func TestGetVolumesToBackup(t *testing.T) { - tests := []struct { - name string - annotations map[string]string - expected []string - }{ - { - name: "nil annotations", - annotations: nil, - expected: nil, - }, - { - name: "no volumes to backup", - annotations: map[string]string{"foo": "bar"}, - expected: nil, - }, - { - name: "one volume to backup", - annotations: map[string]string{"foo": "bar", VolumesToBackupAnnotation: "volume-1"}, - expected: []string{"volume-1"}, - }, - { - name: "multiple volumes to backup", - annotations: map[string]string{"foo": "bar", VolumesToBackupAnnotation: "volume-1,volume-2,volume-3"}, - expected: []string{"volume-1", "volume-2", "volume-3"}, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - pod := &corev1api.Pod{} - pod.Annotations = test.annotations - - res := GetVolumesToBackup(pod) - - // sort to ensure good compare of slices - sort.Strings(test.expected) - sort.Strings(res) - - assert.Equal(t, test.expected, res) - }) - } -} - func TestGetSnapshotsInBackup(t *testing.T) { tests := []struct { name string @@ -419,410 +216,3 @@ func TestTempCACertFile(t *testing.T) { os.Remove(fileName) } - -func TestGetPodVolumesUsingRestic(t *testing.T) { - testCases := []struct { - name string - pod *corev1api.Pod - expected []string - defaultVolumesToRestic bool - }{ - { - name: "should get PVs from VolumesToBackupAnnotation when defaultVolumesToRestic is false", - defaultVolumesToRestic: false, - pod: &corev1api.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - VolumesToBackupAnnotation: "resticPV1,resticPV2,resticPV3", - }, - }, - }, - expected: []string{"resticPV1", "resticPV2", "resticPV3"}, - }, - { - name: "should get all pod volumes when defaultVolumesToRestic is true and no PVs are excluded", - defaultVolumesToRestic: true, - pod: &corev1api.Pod{ - Spec: corev1api.PodSpec{ - Volumes: []corev1api.Volume{ - // Restic Volumes - {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, - }, - }, - }, - expected: []string{"resticPV1", "resticPV2", "resticPV3"}, - }, - { - name: "should get all pod volumes except ones excluded when defaultVolumesToRestic is true", - defaultVolumesToRestic: true, - pod: &corev1api.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3", - }, - }, - Spec: corev1api.PodSpec{ - Volumes: []corev1api.Volume{ - // Restic Volumes - {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, - /// Excluded from restic through annotation - {Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"}, - }, - }, - }, - expected: []string{"resticPV1", "resticPV2", "resticPV3"}, - }, - { - name: "should exclude default service account token from restic backup", - defaultVolumesToRestic: true, - pod: &corev1api.Pod{ - Spec: corev1api.PodSpec{ - Volumes: []corev1api.Volume{ - // Restic Volumes - {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, - /// Excluded from restic because colume mounting default service account token - {Name: "default-token-5xq45"}, - }, - }, - }, - expected: []string{"resticPV1", "resticPV2", "resticPV3"}, - }, - { - name: "should exclude host path volumes from restic backups", - defaultVolumesToRestic: true, - pod: &corev1api.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3", - }, - }, - Spec: corev1api.PodSpec{ - Volumes: []corev1api.Volume{ - // Restic Volumes - {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, - /// Excluded from restic through annotation - {Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"}, - // Excluded from restic because hostpath - {Name: "hostPath1", VolumeSource: corev1api.VolumeSource{HostPath: &corev1api.HostPathVolumeSource{Path: "/hostpathVol"}}}, - }, - }, - }, - expected: []string{"resticPV1", "resticPV2", "resticPV3"}, - }, - { - name: "should exclude volumes mounting secrets", - defaultVolumesToRestic: true, - pod: &corev1api.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3", - }, - }, - Spec: corev1api.PodSpec{ - Volumes: []corev1api.Volume{ - // Restic Volumes - {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, - /// Excluded from restic through annotation - {Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"}, - // Excluded from restic because hostpath - {Name: "superSecret", VolumeSource: corev1api.VolumeSource{Secret: &corev1api.SecretVolumeSource{SecretName: "super-secret"}}}, - }, - }, - }, - expected: []string{"resticPV1", "resticPV2", "resticPV3"}, - }, - { - name: "should exclude volumes mounting config maps", - defaultVolumesToRestic: true, - pod: &corev1api.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3", - }, - }, - Spec: corev1api.PodSpec{ - Volumes: []corev1api.Volume{ - // Restic Volumes - {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, - /// Excluded from restic through annotation - {Name: "nonResticPV1"}, {Name: "nonResticPV2"}, {Name: "nonResticPV3"}, - // Excluded from restic because hostpath - {Name: "appCOnfig", VolumeSource: corev1api.VolumeSource{ConfigMap: &corev1api.ConfigMapVolumeSource{LocalObjectReference: corev1api.LocalObjectReference{Name: "app-config"}}}}, - }, - }, - }, - expected: []string{"resticPV1", "resticPV2", "resticPV3"}, - }, - { - name: "should exclude projected volumes", - defaultVolumesToRestic: true, - pod: &corev1api.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3", - }, - }, - Spec: corev1api.PodSpec{ - Volumes: []corev1api.Volume{ - {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, - { - Name: "projected", - VolumeSource: corev1api.VolumeSource{ - Projected: &corev1api.ProjectedVolumeSource{ - Sources: []corev1api.VolumeProjection{{ - Secret: &corev1api.SecretProjection{ - LocalObjectReference: corev1api.LocalObjectReference{}, - Items: nil, - Optional: nil, - }, - DownwardAPI: nil, - ConfigMap: nil, - ServiceAccountToken: nil, - }}, - DefaultMode: nil, - }, - }, - }, - }, - }, - }, - expected: []string{"resticPV1", "resticPV2", "resticPV3"}, - }, - { - name: "should exclude DownwardAPI volumes", - defaultVolumesToRestic: true, - pod: &corev1api.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - VolumesToExcludeAnnotation: "nonResticPV1,nonResticPV2,nonResticPV3", - }, - }, - Spec: corev1api.PodSpec{ - Volumes: []corev1api.Volume{ - {Name: "resticPV1"}, {Name: "resticPV2"}, {Name: "resticPV3"}, - { - Name: "downwardAPI", - VolumeSource: corev1api.VolumeSource{ - DownwardAPI: &corev1api.DownwardAPIVolumeSource{ - Items: []corev1api.DownwardAPIVolumeFile{ - { - Path: "labels", - FieldRef: &corev1api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.labels", - }, - }, - }, - }, - }, - }, - }, - }, - }, - expected: []string{"resticPV1", "resticPV2", "resticPV3"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - actual := GetPodVolumesUsingRestic(tc.pod, tc.defaultVolumesToRestic) - - sort.Strings(tc.expected) - sort.Strings(actual) - assert.Equal(t, tc.expected, actual) - }) - } -} - -func TestIsPVBMatchPod(t *testing.T) { - testCases := []struct { - name string - pvb velerov1api.PodVolumeBackup - podName string - sourcePodNs string - expected bool - }{ - { - name: "should match PVB and pod", - pvb: velerov1api.PodVolumeBackup{ - Spec: velerov1api.PodVolumeBackupSpec{ - Pod: corev1api.ObjectReference{ - Name: "matching-pod", - Namespace: "matching-namespace", - }, - }, - }, - podName: "matching-pod", - sourcePodNs: "matching-namespace", - expected: true, - }, - { - name: "should not match PVB and pod, pod name mismatch", - pvb: velerov1api.PodVolumeBackup{ - Spec: velerov1api.PodVolumeBackupSpec{ - Pod: corev1api.ObjectReference{ - Name: "matching-pod", - Namespace: "matching-namespace", - }, - }, - }, - podName: "not-matching-pod", - sourcePodNs: "matching-namespace", - expected: false, - }, - { - name: "should not match PVB and pod, pod namespace mismatch", - pvb: velerov1api.PodVolumeBackup{ - Spec: velerov1api.PodVolumeBackupSpec{ - Pod: corev1api.ObjectReference{ - Name: "matching-pod", - Namespace: "matching-namespace", - }, - }, - }, - podName: "matching-pod", - sourcePodNs: "not-matching-namespace", - expected: false, - }, - { - name: "should not match PVB and pod, pod name and namespace mismatch", - pvb: velerov1api.PodVolumeBackup{ - Spec: velerov1api.PodVolumeBackupSpec{ - Pod: corev1api.ObjectReference{ - Name: "matching-pod", - Namespace: "matching-namespace", - }, - }, - }, - podName: "not-matching-pod", - sourcePodNs: "not-matching-namespace", - expected: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - actual := isPVBMatchPod(&tc.pvb, tc.podName, tc.sourcePodNs) - assert.Equal(t, tc.expected, actual) - }) - - } -} - -func TestVolumeHasNonRestorableSource(t *testing.T) { - testCases := []struct { - name string - volumeName string - podVolumes []corev1api.Volume - expected bool - }{ - { - name: "volume name not in list of volumes", - volumeName: "missing-volume", - podVolumes: []corev1api.Volume{ - { - Name: "restorable", - VolumeSource: corev1api.VolumeSource{ - PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{}, - }, - }, - { - Name: "projected", - VolumeSource: corev1api.VolumeSource{ - Projected: &corev1api.ProjectedVolumeSource{}, - }, - }, - { - Name: "downwardapi", - VolumeSource: corev1api.VolumeSource{ - DownwardAPI: &corev1api.DownwardAPIVolumeSource{}, - }, - }, - }, - expected: false, - }, - { - name: "volume name in list of volumes but not projected or DownwardAPI", - volumeName: "restorable", - podVolumes: []corev1api.Volume{ - { - Name: "restorable", - VolumeSource: corev1api.VolumeSource{ - PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{}, - }, - }, - { - Name: "projected", - VolumeSource: corev1api.VolumeSource{ - Projected: &corev1api.ProjectedVolumeSource{}, - }, - }, - { - Name: "downwardapi", - VolumeSource: corev1api.VolumeSource{ - DownwardAPI: &corev1api.DownwardAPIVolumeSource{}, - }, - }, - }, - expected: false, - }, - { - name: "volume name in list of volumes and projected", - volumeName: "projected", - podVolumes: []corev1api.Volume{ - { - Name: "restorable", - VolumeSource: corev1api.VolumeSource{ - PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{}, - }, - }, - { - Name: "projected", - VolumeSource: corev1api.VolumeSource{ - Projected: &corev1api.ProjectedVolumeSource{}, - }, - }, - { - Name: "downwardapi", - VolumeSource: corev1api.VolumeSource{ - DownwardAPI: &corev1api.DownwardAPIVolumeSource{}, - }, - }, - }, - expected: true, - }, - { - name: "volume name in list of volumes and is a DownwardAPI volume", - volumeName: "downwardapi", - podVolumes: []corev1api.Volume{ - { - Name: "restorable", - VolumeSource: corev1api.VolumeSource{ - PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{}, - }, - }, - { - Name: "projected", - VolumeSource: corev1api.VolumeSource{ - Projected: &corev1api.ProjectedVolumeSource{}, - }, - }, - { - Name: "downwardapi", - VolumeSource: corev1api.VolumeSource{ - DownwardAPI: &corev1api.DownwardAPIVolumeSource{}, - }, - }, - }, - expected: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - actual := volumeHasNonRestorableSource(tc.volumeName, tc.podVolumes) - assert.Equal(t, tc.expected, actual) - }) - - } -} diff --git a/pkg/restic/mocks/repository_manager.go b/pkg/restic/mocks/repository_manager.go index de8770c37..553370674 100644 --- a/pkg/restic/mocks/repository_manager.go +++ b/pkg/restic/mocks/repository_manager.go @@ -23,6 +23,7 @@ import ( restic "github.com/vmware-tanzu/velero/pkg/restic" v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/podvolume" ) // RepositoryManager is an autogenerated mock type for the RepositoryManager type @@ -73,15 +74,15 @@ func (_m *RepositoryManager) InitRepo(repo *v1.BackupRepository) error { } // NewBackupper provides a mock function with given fields: _a0, _a1 -func (_m *RepositoryManager) NewBackupper(_a0 context.Context, _a1 *v1.Backup) (restic.Backupper, error) { +func (_m *RepositoryManager) NewBackupper(_a0 context.Context, _a1 *v1.Backup) (podvolume.Backupper, error) { ret := _m.Called(_a0, _a1) - var r0 restic.Backupper - if rf, ok := ret.Get(0).(func(context.Context, *v1.Backup) restic.Backupper); ok { + var r0 podvolume.Backupper + if rf, ok := ret.Get(0).(func(context.Context, *v1.Backup) podvolume.Backupper); ok { r0 = rf(_a0, _a1) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(restic.Backupper) + r0 = ret.Get(0).(podvolume.Backupper) } } @@ -96,15 +97,15 @@ func (_m *RepositoryManager) NewBackupper(_a0 context.Context, _a1 *v1.Backup) ( } // NewRestorer provides a mock function with given fields: _a0, _a1 -func (_m *RepositoryManager) NewRestorer(_a0 context.Context, _a1 *v1.Restore) (restic.Restorer, error) { +func (_m *RepositoryManager) NewRestorer(_a0 context.Context, _a1 *v1.Restore) (podvolume.Restorer, error) { ret := _m.Called(_a0, _a1) - var r0 restic.Restorer - if rf, ok := ret.Get(0).(func(context.Context, *v1.Restore) restic.Restorer); ok { + var r0 podvolume.Restorer + if rf, ok := ret.Get(0).(func(context.Context, *v1.Restore) podvolume.Restorer); ok { r0 = rf(_a0, _a1) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(restic.Restorer) + r0 = ret.Get(0).(podvolume.Restorer) } } diff --git a/pkg/restic/repository_manager.go b/pkg/restic/repository_manager.go index 69927b851..39961fc02 100644 --- a/pkg/restic/repository_manager.go +++ b/pkg/restic/repository_manager.go @@ -18,16 +18,13 @@ package restic import ( "context" - "fmt" "os" "strconv" "github.com/pkg/errors" "github.com/sirupsen/logrus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" - kbclient "sigs.k8s.io/controller-runtime/pkg/client" "github.com/vmware-tanzu/velero/internal/credentials" @@ -36,6 +33,8 @@ import ( velerov1client "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/typed/velero/v1" velerov1informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions/velero/v1" velerov1listers "github.com/vmware-tanzu/velero/pkg/generated/listers/velero/v1" + "github.com/vmware-tanzu/velero/pkg/podvolume" + "github.com/vmware-tanzu/velero/pkg/repository" repokey "github.com/vmware-tanzu/velero/pkg/repository/keys" veleroexec "github.com/vmware-tanzu/velero/pkg/util/exec" "github.com/vmware-tanzu/velero/pkg/util/filesystem" @@ -62,23 +61,9 @@ type RepositoryManager interface { // available snapshots in a repo. Forget(context.Context, SnapshotIdentifier) error - BackupperFactory + podvolume.BackupperFactory - RestorerFactory -} - -// BackupperFactory can construct restic backuppers. -type BackupperFactory interface { - // NewBackupper returns a restic backupper for use during a single - // Velero backup. - NewBackupper(context.Context, *velerov1api.Backup) (Backupper, error) -} - -// RestorerFactory can construct restic restorers. -type RestorerFactory interface { - // NewRestorer returns a restic restorer for use during a single - // Velero restore. - NewRestorer(context.Context, *velerov1api.Restore) (Restorer, error) + podvolume.RestorerFactory } type repositoryManager struct { @@ -88,13 +73,15 @@ type repositoryManager struct { repoInformerSynced cache.InformerSynced kbClient kbclient.Client log logrus.FieldLogger - repoLocker *repoLocker - repoEnsurer *repositoryEnsurer + repoLocker *repository.RepoLocker + repoEnsurer *repository.RepositoryEnsurer fileSystem filesystem.Interface ctx context.Context pvcClient corev1client.PersistentVolumeClaimsGetter pvClient corev1client.PersistentVolumesGetter credentialsFileStore credentials.FileStore + podvolume.BackupperFactory + podvolume.RestorerFactory } const ( @@ -132,56 +119,18 @@ func NewRepositoryManager( log: log, ctx: ctx, - repoLocker: newRepoLocker(), - repoEnsurer: newRepositoryEnsurer(repoInformer, repoClient, log), + repoLocker: repository.NewRepoLocker(), + repoEnsurer: repository.NewRepositoryEnsurer(repoInformer, repoClient, log), fileSystem: filesystem.NewFileSystem(), } + rm.BackupperFactory = podvolume.NewBackupperFactory(rm.repoLocker, rm.repoEnsurer, rm.veleroClient, rm.pvcClient, + rm.pvClient, rm.repoInformerSynced, rm.log) + rm.RestorerFactory = podvolume.NewRestorerFactory(rm.repoLocker, rm.repoEnsurer, rm.veleroClient, rm.pvcClient, + rm.repoInformerSynced, rm.log) return rm, nil } -func (rm *repositoryManager) NewBackupper(ctx context.Context, backup *velerov1api.Backup) (Backupper, error) { - informer := velerov1informers.NewFilteredPodVolumeBackupInformer( - rm.veleroClient, - backup.Namespace, - 0, - cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, - func(opts *metav1.ListOptions) { - opts.LabelSelector = fmt.Sprintf("%s=%s", velerov1api.BackupUIDLabel, backup.UID) - }, - ) - - b := newBackupper(ctx, rm, rm.repoEnsurer, informer, rm.pvcClient, rm.pvClient, rm.log) - - go informer.Run(ctx.Done()) - if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced, rm.repoInformerSynced) { - return nil, errors.New("timed out waiting for caches to sync") - } - - return b, nil -} - -func (rm *repositoryManager) NewRestorer(ctx context.Context, restore *velerov1api.Restore) (Restorer, error) { - informer := velerov1informers.NewFilteredPodVolumeRestoreInformer( - rm.veleroClient, - restore.Namespace, - 0, - cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, - func(opts *metav1.ListOptions) { - opts.LabelSelector = fmt.Sprintf("%s=%s", velerov1api.RestoreUIDLabel, restore.UID) - }, - ) - - r := newRestorer(ctx, rm, rm.repoEnsurer, informer, rm.pvcClient, rm.log) - - go informer.Run(ctx.Done()) - if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced, rm.repoInformerSynced) { - return nil, errors.New("timed out waiting for cache to sync") - } - - return r, nil -} - func (rm *repositoryManager) InitRepo(repo *velerov1api.BackupRepository) error { // restic init requires an exclusive lock rm.repoLocker.LockExclusive(repo.Name) diff --git a/pkg/restore/restic_restore_action.go b/pkg/restore/restic_restore_action.go index 91b4a6761..ba9f9cb0a 100644 --- a/pkg/restore/restic_restore_action.go +++ b/pkg/restore/restic_restore_action.go @@ -36,6 +36,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/label" "github.com/vmware-tanzu/velero/pkg/plugin/framework" "github.com/vmware-tanzu/velero/pkg/plugin/velero" + "github.com/vmware-tanzu/velero/pkg/podvolume" "github.com/vmware-tanzu/velero/pkg/restic" "github.com/vmware-tanzu/velero/pkg/util/kube" ) @@ -96,7 +97,7 @@ func (a *ResticRestoreAction) Execute(input *velero.RestoreItemActionExecuteInpu for i := range podVolumeBackupList.Items { podVolumeBackups = append(podVolumeBackups, &podVolumeBackupList.Items[i]) } - volumeSnapshots := restic.GetVolumeBackupsForPod(podVolumeBackups, &pod, podFromBackup.Namespace) + volumeSnapshots := podvolume.GetVolumeBackupsForPod(podVolumeBackups, &pod, podFromBackup.Namespace) if len(volumeSnapshots) == 0 { log.Debug("No restic backups found for pod") return velero.NewRestoreItemActionExecuteOutput(input.Item), nil diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 691027c7c..11008b55e 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -59,7 +59,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/plugin/framework" "github.com/vmware-tanzu/velero/pkg/plugin/velero" "github.com/vmware-tanzu/velero/pkg/podexec" - "github.com/vmware-tanzu/velero/pkg/restic" + "github.com/vmware-tanzu/velero/pkg/podvolume" "github.com/vmware-tanzu/velero/pkg/util/boolptr" "github.com/vmware-tanzu/velero/pkg/util/collections" "github.com/vmware-tanzu/velero/pkg/util/filesystem" @@ -104,7 +104,7 @@ type kubernetesRestorer struct { discoveryHelper discovery.Helper dynamicFactory client.DynamicFactory namespaceClient corev1.NamespaceInterface - resticRestorerFactory restic.RestorerFactory + resticRestorerFactory podvolume.RestorerFactory resticTimeout time.Duration resourceTerminatingTimeout time.Duration resourcePriorities []string @@ -122,7 +122,7 @@ func NewKubernetesRestorer( dynamicFactory client.DynamicFactory, resourcePriorities []string, namespaceClient corev1.NamespaceInterface, - resticRestorerFactory restic.RestorerFactory, + resticRestorerFactory podvolume.RestorerFactory, resticTimeout time.Duration, resourceTerminatingTimeout time.Duration, logger logrus.FieldLogger, @@ -248,7 +248,7 @@ func (kr *kubernetesRestorer) RestoreWithResolvers( ctx, cancelFunc := go_context.WithTimeout(go_context.Background(), podVolumeTimeout) defer cancelFunc() - var resticRestorer restic.Restorer + var resticRestorer podvolume.Restorer if kr.resticRestorerFactory != nil { resticRestorer, err = kr.resticRestorerFactory.NewRestorer(ctx, req.Restore) if err != nil { @@ -338,7 +338,7 @@ type restoreContext struct { restoreItemActions []framework.RestoreItemResolvedAction itemSnapshotterActions []framework.ItemSnapshotterResolvedAction volumeSnapshotterGetter VolumeSnapshotterGetter - resticRestorer restic.Restorer + resticRestorer podvolume.Restorer resticWaitGroup sync.WaitGroup resticErrs chan error pvsToProvision sets.String @@ -1394,7 +1394,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso // Do not create podvolumerestore when current restore excludes pv/pvc if ctx.resourceIncludesExcludes.ShouldInclude(kuberesource.PersistentVolumeClaims.String()) && ctx.resourceIncludesExcludes.ShouldInclude(kuberesource.PersistentVolumes.String()) && - len(restic.GetVolumeBackupsForPod(ctx.podVolumeBackups, pod, originalNamespace)) > 0 { + len(podvolume.GetVolumeBackupsForPod(ctx.podVolumeBackups, pod, originalNamespace)) > 0 { restorePodVolumeBackups(ctx, createdObj, originalNamespace) } } @@ -1549,7 +1549,7 @@ func restorePodVolumeBackups(ctx *restoreContext, createdObj *unstructured.Unstr return } - data := restic.RestoreData{ + data := podvolume.RestoreData{ Restore: ctx.restore, Pod: pod, PodVolumeBackups: ctx.podVolumeBackups, @@ -1631,7 +1631,7 @@ func hasResticBackup(unstructuredPV *unstructured.Unstructured, ctx *restoreCont var found bool for _, pvb := range ctx.podVolumeBackups { - if pvb.Spec.Pod.Namespace == pv.Spec.ClaimRef.Namespace && pvb.GetAnnotations()[restic.PVCNameAnnotation] == pv.Spec.ClaimRef.Name { + if pvb.Spec.Pod.Namespace == pv.Spec.ClaimRef.Namespace && pvb.GetAnnotations()[podvolume.PVCNameAnnotation] == pv.Spec.ClaimRef.Name { found = true break } diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index 7655253d1..404d45e1a 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -48,8 +48,8 @@ import ( velerov1informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions" "github.com/vmware-tanzu/velero/pkg/kuberesource" "github.com/vmware-tanzu/velero/pkg/plugin/velero" - "github.com/vmware-tanzu/velero/pkg/restic" - resticmocks "github.com/vmware-tanzu/velero/pkg/restic/mocks" + "github.com/vmware-tanzu/velero/pkg/podvolume" + uploadermocks "github.com/vmware-tanzu/velero/pkg/podvolume/mocks" "github.com/vmware-tanzu/velero/pkg/test" testutil "github.com/vmware-tanzu/velero/pkg/test" "github.com/vmware-tanzu/velero/pkg/util/kube" @@ -2681,10 +2681,10 @@ func TestRestorePersistentVolumes(t *testing.T) { } type fakeResticRestorerFactory struct { - restorer *resticmocks.Restorer + restorer *uploadermocks.Restorer } -func (f *fakeResticRestorerFactory) NewRestorer(context.Context, *velerov1api.Restore) (restic.Restorer, error) { +func (f *fakeResticRestorerFactory) NewRestorer(context.Context, *velerov1api.Restore) (podvolume.Restorer, error) { return f.restorer, nil } @@ -2749,7 +2749,7 @@ func TestRestoreWithRestic(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { h := newHarness(t) - restorer := new(resticmocks.Restorer) + restorer := new(uploadermocks.Restorer) defer restorer.AssertExpectations(t) h.restorer.resticRestorerFactory = &fakeResticRestorerFactory{ restorer: restorer, @@ -2773,7 +2773,7 @@ func TestRestoreWithRestic(t *testing.T) { // the restore process adds these labels before restoring, so we must add them here too otherwise they won't match pod.Labels = map[string]string{"velero.io/backup-name": tc.backup.Name, "velero.io/restore-name": tc.restore.Name} - expectedArgs := restic.RestoreData{ + expectedArgs := podvolume.RestoreData{ Restore: tc.restore, Pod: pod, PodVolumeBackups: tc.podVolumeBackups,