From 62d454225c7b21691fab4fe593e6a1f674a92a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wenkai=20Yin=28=E5=B0=B9=E6=96=87=E5=BC=80=29?= Date: Fri, 22 Jul 2022 10:10:47 +0800 Subject: [PATCH] Splic pkg/restic package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit splits the pkg/restic package into several packages to support Kopia integration works Fixes #5055 Signed-off-by: Wenkai Yin(尹文开) --- pkg/backup/backup.go | 8 +- pkg/backup/backup_test.go | 4 +- pkg/backup/item_backupper.go | 3 +- .../restic_repository_controller.go | 4 +- .../ensurer.go} | 16 +- .../repo_locker.go => repository/locker.go} | 21 +- pkg/{restic => repository/repoconfig}/aws.go | 8 +- .../repoconfig}/aws_test.go | 4 +- .../repoconfig}/azure.go | 8 +- .../repoconfig}/azure_test.go | 2 +- .../repoconfig}/config.go | 10 +- .../repoconfig}/config_test.go | 2 +- pkg/{restic => repository/repoconfig}/gcp.go | 8 +- .../repoconfig}/gcp_test.go | 4 +- pkg/restic/common.go | 105 +----- pkg/restic/common_test.go | 352 ------------------ pkg/restic/mocks/repository_manager.go | 17 +- pkg/restic/repository_manager.go | 79 +--- pkg/restore/restic_restore_action.go | 3 +- pkg/restore/restore.go | 16 +- pkg/restore/restore_test.go | 12 +- pkg/{restic => uploader}/backupper.go | 38 +- pkg/uploader/backupper_factory.go | 88 +++++ pkg/{restic => uploader}/backupper_test.go | 2 +- pkg/{restic => uploader}/mocks/restorer.go | 10 +- pkg/{restic => uploader}/restorer.go | 33 +- pkg/uploader/restorer_factory.go | 85 +++++ pkg/uploader/util.go | 112 ++++++ pkg/uploader/util_test.go | 303 +++++++++++++++ 29 files changed, 738 insertions(+), 619 deletions(-) rename pkg/{restic/repository_ensurer.go => repository/ensurer.go} (93%) rename pkg/{restic/repo_locker.go => repository/locker.go} (79%) rename pkg/{restic => repository/repoconfig}/aws.go (83%) rename pkg/{restic => repository/repoconfig}/aws_test.go (96%) rename pkg/{restic => repository/repoconfig}/azure.go (97%) rename pkg/{restic => repository/repoconfig}/azure_test.go (99%) rename pkg/{restic => repository/repoconfig}/config.go (92%) rename pkg/{restic => repository/repoconfig}/config_test.go (99%) rename pkg/{restic => repository/repoconfig}/gcp.go (81%) rename pkg/{restic => repository/repoconfig}/gcp_test.go (95%) rename pkg/{restic => uploader}/backupper.go (90%) create mode 100644 pkg/uploader/backupper_factory.go rename pkg/{restic => uploader}/backupper_test.go (99%) rename pkg/{restic => uploader}/mocks/restorer.go (60%) rename pkg/{restic => uploader}/restorer.go (87%) create mode 100644 pkg/uploader/restorer_factory.go create mode 100644 pkg/uploader/util.go create mode 100644 pkg/uploader/util_test.go diff --git a/pkg/backup/backup.go b/pkg/backup/backup.go index 697be85009..01f70837f8 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/uploader" "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 uploader.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 uploader.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 uploader.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 53554e8b38..0334506976 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -47,9 +47,9 @@ 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/test" testutil "github.com/vmware-tanzu/velero/pkg/test" + "github.com/vmware-tanzu/velero/pkg/uploader" kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" "github.com/vmware-tanzu/velero/pkg/volume" ) @@ -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) (uploader.Backupper, error) { return &fakeResticBackupper{}, nil } diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index bd40c3bdba..42104ae223 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -43,6 +43,7 @@ import ( "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/uploader" "github.com/vmware-tanzu/velero/pkg/util/boolptr" "github.com/vmware-tanzu/velero/pkg/volume" ) @@ -53,7 +54,7 @@ type itemBackupper struct { tarWriter tarWriter dynamicFactory client.DynamicFactory discoveryHelper discovery.Helper - resticBackupper restic.Backupper + resticBackupper uploader.Backupper resticSnapshotTracker *pvcSnapshotTracker volumeSnapshotterGetter VolumeSnapshotterGetter diff --git a/pkg/controller/restic_repository_controller.go b/pkg/controller/restic_repository_controller.go index 36f0e76a86..39a7964d91 100644 --- a/pkg/controller/restic_repository_controller.go +++ b/pkg/controller/restic_repository_controller.go @@ -26,11 +26,11 @@ 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" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/repository/repoconfig" "github.com/vmware-tanzu/velero/pkg/restic" "github.com/vmware-tanzu/velero/pkg/util/kube" ) @@ -127,7 +127,7 @@ func (r *ResticRepoReconciler) initializeRepo(ctx context.Context, req *velerov1 return r.patchResticRepository(ctx, req, repoNotReady(err.Error())) } - repoIdentifier, err := restic.GetRepoIdentifier(loc, req.Spec.VolumeNamespace) + repoIdentifier, err := repoconfig.GetRepoIdentifier(loc, req.Spec.VolumeNamespace) if err != nil { return r.patchResticRepository(ctx, req, func(rr *velerov1api.BackupRepository) { rr.Status.Message = err.Error() 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 d764a49c7d..15aa107014 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 29434753e9..20eea96359 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/aws.go b/pkg/repository/repoconfig/aws.go similarity index 83% rename from pkg/restic/aws.go rename to pkg/repository/repoconfig/aws.go index d97c5f0b77..edf596e706 100644 --- a/pkg/restic/aws.go +++ b/pkg/repository/repoconfig/aws.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package repoconfig const ( // AWS specific environment variable @@ -23,13 +23,13 @@ const ( awsCredentialsFileEnvVar = "AWS_SHARED_CREDENTIALS_FILE" ) -// getS3ResticEnvVars gets the environment variables that restic +// GetS3ResticEnvVars gets the environment variables that restic // relies on (AWS_PROFILE) based on info in the provided object // storage location config map. -func getS3ResticEnvVars(config map[string]string) (map[string]string, error) { +func GetS3ResticEnvVars(config map[string]string) (map[string]string, error) { result := make(map[string]string) - if credentialsFile, ok := config[credentialsFileKey]; ok { + if credentialsFile, ok := config[CredentialsFileKey]; ok { result[awsCredentialsFileEnvVar] = credentialsFile } diff --git a/pkg/restic/aws_test.go b/pkg/repository/repoconfig/aws_test.go similarity index 96% rename from pkg/restic/aws_test.go rename to pkg/repository/repoconfig/aws_test.go index 51f3ceb993..4b25394f9f 100644 --- a/pkg/restic/aws_test.go +++ b/pkg/repository/repoconfig/aws_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package repoconfig import ( "testing" @@ -55,7 +55,7 @@ func TestGetS3ResticEnvVars(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - actual, err := getS3ResticEnvVars(tc.config) + actual, err := GetS3ResticEnvVars(tc.config) require.NoError(t, err) diff --git a/pkg/restic/azure.go b/pkg/repository/repoconfig/azure.go similarity index 97% rename from pkg/restic/azure.go rename to pkg/repository/repoconfig/azure.go index 20324b8e36..c7931893ae 100644 --- a/pkg/restic/azure.go +++ b/pkg/repository/repoconfig/azure.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package repoconfig import ( "context" @@ -131,10 +131,10 @@ func mapLookup(data map[string]string) func(string) string { } } -// getAzureResticEnvVars gets the environment variables that restic +// GetAzureResticEnvVars gets the environment variables that restic // relies on (AZURE_ACCOUNT_NAME and AZURE_ACCOUNT_KEY) based // on info in the provided object storage location config map. -func getAzureResticEnvVars(config map[string]string) (map[string]string, error) { +func GetAzureResticEnvVars(config map[string]string) (map[string]string, error) { storageAccountKey, _, err := getStorageAccountKey(config) if err != nil { return nil, err @@ -158,7 +158,7 @@ func credentialsFileFromEnv() string { // selectCredentialsFile selects the Azure credentials file to use, retrieving it // from the given config or falling back to retrieving it from the environment. func selectCredentialsFile(config map[string]string) string { - if credentialsFile, ok := config[credentialsFileKey]; ok { + if credentialsFile, ok := config[CredentialsFileKey]; ok { return credentialsFile } diff --git a/pkg/restic/azure_test.go b/pkg/repository/repoconfig/azure_test.go similarity index 99% rename from pkg/restic/azure_test.go rename to pkg/repository/repoconfig/azure_test.go index acb2f25065..39345c4731 100644 --- a/pkg/restic/azure_test.go +++ b/pkg/repository/repoconfig/azure_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package repoconfig import ( "os" diff --git a/pkg/restic/config.go b/pkg/repository/repoconfig/config.go similarity index 92% rename from pkg/restic/config.go rename to pkg/repository/repoconfig/config.go index 1600f39fa8..fdc85ad49c 100644 --- a/pkg/restic/config.go +++ b/pkg/repository/repoconfig/config.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package repoconfig import ( "context" @@ -37,6 +37,10 @@ const ( AWSBackend BackendType = "velero.io/aws" AzureBackend BackendType = "velero.io/azure" GCPBackend BackendType = "velero.io/gcp" + + // 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" ) // this func is assigned to a package-level variable so it can be @@ -55,7 +59,7 @@ func getRepoPrefix(location *velerov1api.BackupStorageLocation) (string, error) prefix = layout.GetResticDir() } - backendType := getBackendType(location.Spec.Provider) + backendType := GetBackendType(location.Spec.Provider) if repoPrefix := location.Spec.Config["resticRepoPrefix"]; repoPrefix != "" { return repoPrefix, nil @@ -89,7 +93,7 @@ func getRepoPrefix(location *velerov1api.BackupStorageLocation) (string, error) return "", errors.New("restic repository prefix (resticRepoPrefix) not specified in backup storage location's config") } -func getBackendType(provider string) BackendType { +func GetBackendType(provider string) BackendType { if !strings.Contains(provider, "/") { provider = "velero.io/" + provider } diff --git a/pkg/restic/config_test.go b/pkg/repository/repoconfig/config_test.go similarity index 99% rename from pkg/restic/config_test.go rename to pkg/repository/repoconfig/config_test.go index 8418d68085..eb07892588 100644 --- a/pkg/restic/config_test.go +++ b/pkg/repository/repoconfig/config_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package repoconfig import ( "testing" diff --git a/pkg/restic/gcp.go b/pkg/repository/repoconfig/gcp.go similarity index 81% rename from pkg/restic/gcp.go rename to pkg/repository/repoconfig/gcp.go index 96d1edfe60..39cca4d53a 100644 --- a/pkg/restic/gcp.go +++ b/pkg/repository/repoconfig/gcp.go @@ -14,19 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package repoconfig const ( // GCP specific environment variable gcpCredentialsFileEnvVar = "GOOGLE_APPLICATION_CREDENTIALS" ) -// getGCPResticEnvVars gets the environment variables that restic relies +// GetGCPResticEnvVars gets the environment variables that restic relies // on based on info in the provided object storage location config map. -func getGCPResticEnvVars(config map[string]string) (map[string]string, error) { +func GetGCPResticEnvVars(config map[string]string) (map[string]string, error) { result := make(map[string]string) - if credentialsFile, ok := config[credentialsFileKey]; ok { + if credentialsFile, ok := config[CredentialsFileKey]; ok { result[gcpCredentialsFileEnvVar] = credentialsFile } diff --git a/pkg/restic/gcp_test.go b/pkg/repository/repoconfig/gcp_test.go similarity index 95% rename from pkg/restic/gcp_test.go rename to pkg/repository/repoconfig/gcp_test.go index 37f2bf2c70..3b8391ca15 100644 --- a/pkg/restic/gcp_test.go +++ b/pkg/repository/repoconfig/gcp_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package repoconfig import ( "testing" @@ -46,7 +46,7 @@ func TestGetGCPResticEnvVars(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - actual, err := getGCPResticEnvVars(tc.config) + actual, err := GetGCPResticEnvVars(tc.config) require.NoError(t, err) diff --git a/pkg/restic/common.go b/pkg/restic/common.go index 23c09e558e..c678414def 100644 --- a/pkg/restic/common.go +++ b/pkg/restic/common.go @@ -32,6 +32,7 @@ import ( "github.com/vmware-tanzu/velero/internal/credentials" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" "github.com/vmware-tanzu/velero/pkg/label" + "github.com/vmware-tanzu/velero/pkg/repository/repoconfig" "github.com/vmware-tanzu/velero/pkg/util/filesystem" ) @@ -51,10 +52,6 @@ const ( // 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" @@ -62,92 +59,8 @@ const ( // VolumesToExcludeAnnotation is the annotation on a pod whose mounted volumes // should be excluded from restic backup. VolumesToExcludeAnnotation = "backup.velero.io/backup-volumes-excludes" - - // 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" - - // 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. @@ -322,24 +235,24 @@ func CmdEnv(backupLocation *velerov1api.BackupStorageLocation, credentialFileSto if err != nil { return []string{}, errors.WithStack(err) } - config[credentialsFileKey] = credsFile + config[repoconfig.CredentialsFileKey] = credsFile } - backendType := getBackendType(backupLocation.Spec.Provider) + backendType := repoconfig.GetBackendType(backupLocation.Spec.Provider) switch backendType { - case AWSBackend: - customEnv, err = getS3ResticEnvVars(config) + case repoconfig.AWSBackend: + customEnv, err = repoconfig.GetS3ResticEnvVars(config) if err != nil { return []string{}, err } - case AzureBackend: - customEnv, err = getAzureResticEnvVars(config) + case repoconfig.AzureBackend: + customEnv, err = repoconfig.GetAzureResticEnvVars(config) if err != nil { return []string{}, err } - case GCPBackend: - customEnv, err = getGCPResticEnvVars(config) + case repoconfig.GCPBackend: + customEnv, err = repoconfig.GetGCPResticEnvVars(config) if err != nil { return []string{}, err } diff --git a/pkg/restic/common_test.go b/pkg/restic/common_test.go index 7f3e0c5032..b42f671aee 100644 --- a/pkg/restic/common_test.go +++ b/pkg/restic/common_test.go @@ -28,168 +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 @@ -633,196 +474,3 @@ func TestGetPodVolumesUsingRestic(t *testing.T) { }) } } - -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 de8770c375..75dc02af07 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/uploader" ) // 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) (uploader.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 uploader.Backupper + if rf, ok := ret.Get(0).(func(context.Context, *v1.Backup) uploader.Backupper); ok { r0 = rf(_a0, _a1) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(restic.Backupper) + r0 = ret.Get(0).(uploader.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) (uploader.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 uploader.Restorer + if rf, ok := ret.Get(0).(func(context.Context, *v1.Restore) uploader.Restorer); ok { r0 = rf(_a0, _a1) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(restic.Restorer) + r0 = ret.Get(0).(uploader.Restorer) } } diff --git a/pkg/restic/repository_manager.go b/pkg/restic/repository_manager.go index f0ab633868..85eb2612a7 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/repository" + "github.com/vmware-tanzu/velero/pkg/uploader" veleroexec "github.com/vmware-tanzu/velero/pkg/util/exec" "github.com/vmware-tanzu/velero/pkg/util/filesystem" ) @@ -61,23 +60,9 @@ type RepositoryManager interface { // available snapshots in a repo. Forget(context.Context, SnapshotIdentifier) error - BackupperFactory - - RestorerFactory -} + uploader.BackupperFactory -// 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) + uploader.RestorerFactory } type repositoryManager struct { @@ -87,13 +72,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 + uploader.BackupperFactory + uploader.RestorerFactory } const ( @@ -131,56 +118,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 = uploader.NewBackupperFactory(rm.repoLocker, rm.repoEnsurer, rm.veleroClient, rm.pvcClient, + rm.pvClient, rm.repoInformerSynced, rm.log) + rm.RestorerFactory = uploader.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 91b4a6761c..86a86b8b38 100644 --- a/pkg/restore/restic_restore_action.go +++ b/pkg/restore/restic_restore_action.go @@ -37,6 +37,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/plugin/framework" "github.com/vmware-tanzu/velero/pkg/plugin/velero" "github.com/vmware-tanzu/velero/pkg/restic" + "github.com/vmware-tanzu/velero/pkg/uploader" "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 := uploader.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 691027c7c6..d0ff845c29 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/uploader" "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 uploader.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 uploader.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 uploader.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 uploader.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(uploader.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 := uploader.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()[uploader.PVCNameAnnotation] == pv.Spec.ClaimRef.Name { found = true break } diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index 7655253d10..67c14cb1d2 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -48,10 +48,10 @@ 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/test" testutil "github.com/vmware-tanzu/velero/pkg/test" + "github.com/vmware-tanzu/velero/pkg/uploader" + uploadermocks "github.com/vmware-tanzu/velero/pkg/uploader/mocks" "github.com/vmware-tanzu/velero/pkg/util/kube" kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" "github.com/vmware-tanzu/velero/pkg/volume" @@ -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) (uploader.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 := uploader.RestoreData{ Restore: tc.restore, Pod: pod, PodVolumeBackups: tc.podVolumeBackups, diff --git a/pkg/restic/backupper.go b/pkg/uploader/backupper.go similarity index 90% rename from pkg/restic/backupper.go rename to pkg/uploader/backupper.go index 589b396f39..ed7cc9d7b9 100644 --- a/pkg/restic/backupper.go +++ b/pkg/uploader/backupper.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package uploader 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/uploader/backupper_factory.go b/pkg/uploader/backupper_factory.go new file mode 100644 index 0000000000..691309bd44 --- /dev/null +++ b/pkg/uploader/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 uploader + +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/uploader/backupper_test.go similarity index 99% rename from pkg/restic/backupper_test.go rename to pkg/uploader/backupper_test.go index 8969f6efaf..6e8f8e1ace 100644 --- a/pkg/restic/backupper_test.go +++ b/pkg/uploader/backupper_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package uploader import ( "context" diff --git a/pkg/restic/mocks/restorer.go b/pkg/uploader/mocks/restorer.go similarity index 60% rename from pkg/restic/mocks/restorer.go rename to pkg/uploader/mocks/restorer.go index 7f4f5c1d82..8cd747d83a 100644 --- a/pkg/restic/mocks/restorer.go +++ b/pkg/uploader/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/uploader" +) // 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 uploader.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(uploader.RestoreData) []error); ok { r0 = rf(_a0) } else { if ret.Get(0) != nil { diff --git a/pkg/restic/restorer.go b/pkg/uploader/restorer.go similarity index 87% rename from pkg/restic/restorer.go rename to pkg/uploader/restorer.go index e747b0b6d5..00225ae878 100644 --- a/pkg/restic/restorer.go +++ b/pkg/uploader/restorer.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package uploader 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/uploader/restorer_factory.go b/pkg/uploader/restorer_factory.go new file mode 100644 index 0000000000..44f4010dc4 --- /dev/null +++ b/pkg/uploader/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 uploader + +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/uploader/util.go b/pkg/uploader/util.go new file mode 100644 index 0000000000..3490ad5d1e --- /dev/null +++ b/pkg/uploader/util.go @@ -0,0 +1,112 @@ +/* +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 uploader + +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/" +) + +// 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 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 +} diff --git a/pkg/uploader/util_test.go b/pkg/uploader/util_test.go new file mode 100644 index 0000000000..fec7b680e5 --- /dev/null +++ b/pkg/uploader/util_test.go @@ -0,0 +1,303 @@ +/* +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 uploader + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1api "k8s.io/api/core/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) + }) + + } +}