diff --git a/pkg/apis/velero/v1/backup_repository_types.go b/pkg/apis/velero/v1/backup_repository_types.go index 300ecae9c..a64e3be68 100644 --- a/pkg/apis/velero/v1/backup_repository_types.go +++ b/pkg/apis/velero/v1/backup_repository_types.go @@ -51,6 +51,9 @@ const ( BackupRepositoryPhaseNew BackupRepositoryPhase = "New" BackupRepositoryPhaseReady BackupRepositoryPhase = "Ready" BackupRepositoryPhaseNotReady BackupRepositoryPhase = "NotReady" + + BackupRepositoryTypeRestic string = "restic" + BackupRepositoryTypeUnified string = "unified" ) // BackupRepositoryStatus is the current status of a BackupRepository. diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index 8a0251f1b..96dc264e8 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -82,6 +82,8 @@ import ( "github.com/vmware-tanzu/velero/internal/storage" "github.com/vmware-tanzu/velero/internal/util/managercontroller" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/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" ) @@ -248,7 +250,9 @@ type server struct { logger logrus.FieldLogger logLevel logrus.Level pluginRegistry clientmgmt.Registry - resticManager restic.RepositoryManager + repoManager repository.Manager + repoLocker *repository.RepoLocker + repoEnsurer *repository.RepositoryEnsurer metrics *metrics.ServerMetrics config serverConfig mgr manager.Manager @@ -536,22 +540,10 @@ func (s *server) initRestic() error { return err } - res, err := restic.NewRepositoryManager( - s.ctx, - s.namespace, - s.veleroClient, - s.sharedInformerFactory.Velero().V1().BackupRepositories(), - s.veleroClient.VeleroV1(), - s.mgr.GetClient(), - s.kubeClient.CoreV1(), - s.kubeClient.CoreV1(), - s.credentialFileStore, - s.logger, - ) - if err != nil { - return err - } - s.resticManager = res + s.repoLocker = repository.NewRepoLocker() + s.repoEnsurer = repository.NewRepositoryEnsurer(s.sharedInformerFactory.Velero().V1().BackupRepositories(), s.veleroClient.VeleroV1(), s.logger) + + s.repoManager = repository.NewManager(s.namespace, s.mgr.GetClient(), s.repoLocker, s.repoEnsurer, s.credentialFileStore, s.logger) return nil } @@ -643,7 +635,8 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string s.discoveryHelper, client.NewDynamicFactory(s.dynamicClient), podexec.NewPodCommandExecutor(s.kubeClientConfig, s.kubeClient.CoreV1().RESTClient()), - s.resticManager, + podvolume.NewBackupperFactory(s.repoLocker, s.repoEnsurer, s.veleroClient, s.kubeClient.CoreV1(), + s.kubeClient.CoreV1(), s.sharedInformerFactory.Velero().V1().BackupRepositories().Informer().HasSynced, s.logger), s.config.podVolumeOperationTimeout, s.config.defaultVolumesToRestic, s.config.clientPageSize, @@ -704,7 +697,8 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string client.NewDynamicFactory(s.dynamicClient), s.config.restoreResourcePriorities, s.kubeClient.CoreV1().Namespaces(), - s.resticManager, + podvolume.NewRestorerFactory(s.repoLocker, s.repoEnsurer, s.veleroClient, s.kubeClient.CoreV1(), + s.sharedInformerFactory.Velero().V1().BackupRepositories().Informer().HasSynced, s.logger), s.config.podVolumeOperationTimeout, s.config.resourceTerminatingTimeout, s.logger, @@ -812,7 +806,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string s.logger.Fatal(err, "unable to create controller", "controller", controller.Schedule) } - if err := controller.NewResticRepoReconciler(s.namespace, s.logger, s.mgr.GetClient(), s.config.defaultResticMaintenanceFrequency, s.resticManager).SetupWithManager(s.mgr); err != nil { + if err := controller.NewResticRepoReconciler(s.namespace, s.logger, s.mgr.GetClient(), s.config.defaultResticMaintenanceFrequency, s.repoManager).SetupWithManager(s.mgr); err != nil { s.logger.Fatal(err, "unable to create controller", "controller", controller.ResticRepo) } @@ -820,7 +814,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string s.logger, s.mgr.GetClient(), backupTracker, - s.resticManager, + s.repoManager, s.metrics, s.discoveryHelper, newPluginManager, diff --git a/pkg/controller/backup_deletion_controller.go b/pkg/controller/backup_deletion_controller.go index 52d358042..a616dcb71 100644 --- a/pkg/controller/backup_deletion_controller.go +++ b/pkg/controller/backup_deletion_controller.go @@ -40,6 +40,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/persistence" "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" "github.com/vmware-tanzu/velero/pkg/plugin/velero" + "github.com/vmware-tanzu/velero/pkg/repository" "github.com/vmware-tanzu/velero/pkg/restic" "github.com/vmware-tanzu/velero/pkg/util/filesystem" "github.com/vmware-tanzu/velero/pkg/util/kube" @@ -56,7 +57,7 @@ type backupDeletionReconciler struct { client.Client logger logrus.FieldLogger backupTracker BackupTracker - resticMgr restic.RepositoryManager + repoMgr repository.Manager metrics *metrics.ServerMetrics clock clock.Clock discoveryHelper discovery.Helper @@ -69,7 +70,7 @@ func NewBackupDeletionReconciler( logger logrus.FieldLogger, client client.Client, backupTracker BackupTracker, - resticMgr restic.RepositoryManager, + repoMgr repository.Manager, metrics *metrics.ServerMetrics, helper discovery.Helper, newPluginManager func(logrus.FieldLogger) clientmgmt.Manager, @@ -79,7 +80,7 @@ func NewBackupDeletionReconciler( Client: client, logger: logger, backupTracker: backupTracker, - resticMgr: resticMgr, + repoMgr: repoMgr, metrics: metrics, clock: clock.RealClock{}, discoveryHelper: helper, @@ -435,7 +436,7 @@ func (r *backupDeletionReconciler) deleteExistingDeletionRequests(ctx context.Co } func (r *backupDeletionReconciler) deleteResticSnapshots(ctx context.Context, backup *velerov1api.Backup) []error { - if r.resticMgr == nil { + if r.repoMgr == nil { return nil } @@ -449,7 +450,7 @@ func (r *backupDeletionReconciler) deleteResticSnapshots(ctx context.Context, ba var errs []error for _, snapshot := range snapshots { - if err := r.resticMgr.Forget(ctx2, snapshot); err != nil { + if err := r.repoMgr.Forget(ctx2, snapshot); err != nil { errs = append(errs, err) } } diff --git a/pkg/controller/restic_repository_controller.go b/pkg/controller/restic_repository_controller.go index c3ca1505a..d6cd869e3 100644 --- a/pkg/controller/restic_repository_controller.go +++ b/pkg/controller/restic_repository_controller.go @@ -30,6 +30,7 @@ import ( "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/repository/config" "github.com/vmware-tanzu/velero/pkg/restic" "github.com/vmware-tanzu/velero/pkg/util/kube" @@ -45,11 +46,11 @@ type ResticRepoReconciler struct { logger logrus.FieldLogger clock clock.Clock defaultMaintenanceFrequency time.Duration - repositoryManager restic.RepositoryManager + repositoryManager repository.Manager } func NewResticRepoReconciler(namespace string, logger logrus.FieldLogger, client client.Client, - defaultMaintenanceFrequency time.Duration, repositoryManager restic.RepositoryManager) *ResticRepoReconciler { + defaultMaintenanceFrequency time.Duration, repositoryManager repository.Manager) *ResticRepoReconciler { c := &ResticRepoReconciler{ client, namespace, @@ -163,7 +164,7 @@ func (r *ResticRepoReconciler) initializeRepo(ctx context.Context, req *velerov1 // ensureRepo checks to see if a repository exists, and attempts to initialize it if // it does not exist. An error is returned if the repository can't be connected to // or initialized. -func ensureRepo(repo *velerov1api.BackupRepository, repoManager restic.RepositoryManager) error { +func ensureRepo(repo *velerov1api.BackupRepository, repoManager repository.Manager) error { if err := repoManager.ConnectToRepo(repo); err != nil { // If the repository has not yet been initialized, the error message will always include // the following string. This is the only scenario where we should try to initialize it. diff --git a/pkg/controller/restic_repository_controller_test.go b/pkg/controller/restic_repository_controller_test.go index 28e899329..d693f510b 100644 --- a/pkg/controller/restic_repository_controller_test.go +++ b/pkg/controller/restic_repository_controller_test.go @@ -24,14 +24,14 @@ import ( ctrl "sigs.k8s.io/controller-runtime" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - resticmokes "github.com/vmware-tanzu/velero/pkg/restic/mocks" + repomokes "github.com/vmware-tanzu/velero/pkg/repository/mocks" velerotest "github.com/vmware-tanzu/velero/pkg/test" ) const defaultMaintenanceFrequency = 10 * time.Minute func mockResticRepoReconciler(t *testing.T, rr *velerov1api.BackupRepository, mockOn string, arg interface{}, ret interface{}) *ResticRepoReconciler { - mgr := &resticmokes.RepositoryManager{} + mgr := &repomokes.RepositoryManager{} if mockOn != "" { mgr.On(mockOn, arg).Return(ret) } diff --git a/pkg/repository/manager.go b/pkg/repository/manager.go new file mode 100644 index 000000000..eb700d106 --- /dev/null +++ b/pkg/repository/manager.go @@ -0,0 +1,188 @@ +/* +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 repository + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/vmware-tanzu/velero/internal/credentials" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/repository/provider" + "github.com/vmware-tanzu/velero/pkg/restic" + "github.com/vmware-tanzu/velero/pkg/util/filesystem" +) + +// Manager manages backup repositories. +type Manager interface { + // InitRepo initializes a repo with the specified name and identifier. + InitRepo(repo *velerov1api.BackupRepository) error + + // ConnectToRepo runs the 'restic snapshots' command against the + // specified repo, and returns an error if it fails. This is + // intended to be used to ensure that the repo exists/can be + // authenticated to. + ConnectToRepo(repo *velerov1api.BackupRepository) error + + // PruneRepo deletes unused data from a repo. + PruneRepo(repo *velerov1api.BackupRepository) error + + // UnlockRepo removes stale locks from a repo. + UnlockRepo(repo *velerov1api.BackupRepository) error + + // Forget removes a snapshot from the list of + // available snapshots in a repo. + Forget(context.Context, restic.SnapshotIdentifier) error +} + +type manager struct { + namespace string + providers map[string]provider.Provider + client client.Client + repoLocker *RepoLocker + repoEnsurer *RepositoryEnsurer + fileSystem filesystem.Interface + log logrus.FieldLogger +} + +// NewManager create a new repository manager. +func NewManager( + namespace string, + client client.Client, + repoLocker *RepoLocker, + repoEnsurer *RepositoryEnsurer, + credentialFileStore credentials.FileStore, + log logrus.FieldLogger, +) Manager { + mgr := &manager{ + namespace: namespace, + client: client, + providers: map[string]provider.Provider{}, + repoLocker: repoLocker, + repoEnsurer: repoEnsurer, + fileSystem: filesystem.NewFileSystem(), + log: log, + } + + mgr.providers[velerov1api.BackupRepositoryTypeRestic] = provider.NewResticRepositoryProvider(credentialFileStore, mgr.fileSystem, mgr.log) + + return mgr +} + +func (m *manager) InitRepo(repo *velerov1api.BackupRepository) error { + m.repoLocker.LockExclusive(repo.Name) + defer m.repoLocker.UnlockExclusive(repo.Name) + + prd, err := m.getRepositoryProvider(repo) + if err != nil { + return errors.WithStack(err) + } + param, err := m.assembleRepoParam(repo) + if err != nil { + return errors.WithStack(err) + } + return prd.InitRepo(context.Background(), param) +} + +func (m *manager) ConnectToRepo(repo *velerov1api.BackupRepository) error { + m.repoLocker.Lock(repo.Name) + defer m.repoLocker.Unlock(repo.Name) + + prd, err := m.getRepositoryProvider(repo) + if err != nil { + return errors.WithStack(err) + } + param, err := m.assembleRepoParam(repo) + if err != nil { + return errors.WithStack(err) + } + return prd.ConnectToRepo(context.Background(), param) +} + +func (m *manager) PruneRepo(repo *velerov1api.BackupRepository) error { + m.repoLocker.LockExclusive(repo.Name) + defer m.repoLocker.UnlockExclusive(repo.Name) + + prd, err := m.getRepositoryProvider(repo) + if err != nil { + return errors.WithStack(err) + } + param, err := m.assembleRepoParam(repo) + if err != nil { + return errors.WithStack(err) + } + return prd.PruneRepo(context.Background(), param) +} + +func (m *manager) UnlockRepo(repo *velerov1api.BackupRepository) error { + m.repoLocker.Lock(repo.Name) + defer m.repoLocker.Unlock(repo.Name) + + prd, err := m.getRepositoryProvider(repo) + if err != nil { + return errors.WithStack(err) + } + param, err := m.assembleRepoParam(repo) + if err != nil { + return errors.WithStack(err) + } + return prd.EnsureUnlockRepo(context.Background(), param) +} + +func (m *manager) Forget(ctx context.Context, snapshot restic.SnapshotIdentifier) error { + repo, err := m.repoEnsurer.EnsureRepo(ctx, m.namespace, snapshot.VolumeNamespace, snapshot.BackupStorageLocation) + if err != nil { + return err + } + + m.repoLocker.LockExclusive(repo.Name) + defer m.repoLocker.UnlockExclusive(repo.Name) + + prd, err := m.getRepositoryProvider(repo) + if err != nil { + return errors.WithStack(err) + } + param, err := m.assembleRepoParam(repo) + if err != nil { + return errors.WithStack(err) + } + return prd.Forget(context.Background(), snapshot.SnapshotID, param) +} + +func (m *manager) getRepositoryProvider(repo *velerov1api.BackupRepository) (provider.Provider, error) { + switch repo.Spec.RepositoryType { + case "", velerov1api.BackupRepositoryTypeRestic: + return m.providers[velerov1api.BackupRepositoryTypeRestic], nil + default: + return nil, fmt.Errorf("failed to get provider for repository %s", repo.Spec.RepositoryType) + } +} + +func (m *manager) assembleRepoParam(repo *velerov1api.BackupRepository) (provider.RepoParam, error) { + bsl := &velerov1api.BackupStorageLocation{} + if err := m.client.Get(context.Background(), client.ObjectKey{m.namespace, repo.Spec.BackupStorageLocation}, bsl); err != nil { + return provider.RepoParam{}, errors.WithStack(err) + } + return provider.RepoParam{ + BackupLocation: bsl, + BackupRepo: repo, + }, nil +} diff --git a/pkg/repository/manager_test.go b/pkg/repository/manager_test.go new file mode 100644 index 000000000..7692a8b21 --- /dev/null +++ b/pkg/repository/manager_test.go @@ -0,0 +1,47 @@ +/* +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 repository + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" +) + +func TestGetRepositoryProvider(t *testing.T) { + mgr := NewManager("", nil, nil, nil, nil, nil).(*manager) + repo := &velerov1.BackupRepository{} + + // empty repository type + provider, err := mgr.getRepositoryProvider(repo) + require.Nil(t, err) + assert.NotNil(t, provider) + + // valid repository type + repo.Spec.RepositoryType = velerov1.BackupRepositoryTypeRestic + provider, err = mgr.getRepositoryProvider(repo) + require.Nil(t, err) + assert.NotNil(t, provider) + + // invalid repository type + repo.Spec.RepositoryType = "unknown" + _, err = mgr.getRepositoryProvider(repo) + require.NotNil(t, err) +} diff --git a/pkg/restic/mocks/repository_manager.go b/pkg/repository/mocks/repository_manager.go similarity index 100% rename from pkg/restic/mocks/repository_manager.go rename to pkg/repository/mocks/repository_manager.go diff --git a/pkg/repository/provider/restic.go b/pkg/repository/provider/restic.go new file mode 100644 index 000000000..3659f1be7 --- /dev/null +++ b/pkg/repository/provider/restic.go @@ -0,0 +1,69 @@ +/* +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 provider + +import ( + "context" + + "github.com/sirupsen/logrus" + + "github.com/vmware-tanzu/velero/internal/credentials" + "github.com/vmware-tanzu/velero/pkg/repository/restic" + "github.com/vmware-tanzu/velero/pkg/util/filesystem" +) + +func NewResticRepositoryProvider(store credentials.FileStore, fs filesystem.Interface, log logrus.FieldLogger) Provider { + return &resticRepositoryProvider{ + svc: restic.NewRepositoryService(store, fs, log), + } +} + +type resticRepositoryProvider struct { + svc *restic.RepositoryService +} + +func (r *resticRepositoryProvider) InitRepo(ctx context.Context, param RepoParam) error { + return r.svc.InitRepo(param.BackupLocation, param.BackupRepo) +} + +func (r *resticRepositoryProvider) ConnectToRepo(ctx context.Context, param RepoParam) error { + return r.svc.ConnectToRepo(param.BackupLocation, param.BackupRepo) +} + +func (r *resticRepositoryProvider) PrepareRepo(ctx context.Context, param RepoParam) error { + if err := r.InitRepo(ctx, param); err != nil { + return err + } + return r.ConnectToRepo(ctx, param) +} + +func (r *resticRepositoryProvider) PruneRepo(ctx context.Context, param RepoParam) error { + return r.svc.PruneRepo(param.BackupLocation, param.BackupRepo) +} + +func (r *resticRepositoryProvider) PruneRepoQuick(ctx context.Context, param RepoParam) error { + // restic doesn't support this operation + return nil +} + +func (r *resticRepositoryProvider) EnsureUnlockRepo(ctx context.Context, param RepoParam) error { + return r.svc.UnlockRepo(param.BackupLocation, param.BackupRepo) +} + +func (r *resticRepositoryProvider) Forget(ctx context.Context, snapshotID string, param RepoParam) error { + return r.svc.Forget(param.BackupLocation, param.BackupRepo, snapshotID) +} diff --git a/pkg/repository/restic/repository.go b/pkg/repository/restic/repository.go new file mode 100644 index 000000000..fa88a9cc4 --- /dev/null +++ b/pkg/repository/restic/repository.go @@ -0,0 +1,122 @@ +/* +Copyright the Velero contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package restic + +import ( + "os" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/vmware-tanzu/velero/internal/credentials" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + repokey "github.com/vmware-tanzu/velero/pkg/repository/keys" + "github.com/vmware-tanzu/velero/pkg/restic" + veleroexec "github.com/vmware-tanzu/velero/pkg/util/exec" + "github.com/vmware-tanzu/velero/pkg/util/filesystem" +) + +func NewRepositoryService(store credentials.FileStore, fs filesystem.Interface, log logrus.FieldLogger) *RepositoryService { + return &RepositoryService{ + credentialsFileStore: store, + fileSystem: fs, + log: log, + } +} + +type RepositoryService struct { + credentialsFileStore credentials.FileStore + fileSystem filesystem.Interface + log logrus.FieldLogger +} + +func (r *RepositoryService) InitRepo(bsl *velerov1api.BackupStorageLocation, repo *velerov1api.BackupRepository) error { + return r.exec(restic.InitCommand(repo.Spec.ResticIdentifier), bsl) +} + +func (r *RepositoryService) ConnectToRepo(bsl *velerov1api.BackupStorageLocation, repo *velerov1api.BackupRepository) error { + snapshotsCmd := restic.SnapshotsCommand(repo.Spec.ResticIdentifier) + // use the '--latest=1' flag to minimize the amount of data fetched since + // we're just validating that the repo exists and can be authenticated + // to. + // "--last" is replaced by "--latest=1" in restic v0.12.1 + snapshotsCmd.ExtraFlags = append(snapshotsCmd.ExtraFlags, "--latest=1") + + return r.exec(snapshotsCmd, bsl) +} + +func (r *RepositoryService) PruneRepo(bsl *velerov1api.BackupStorageLocation, repo *velerov1api.BackupRepository) error { + return r.exec(restic.PruneCommand(repo.Spec.ResticIdentifier), bsl) +} + +func (r *RepositoryService) UnlockRepo(bsl *velerov1api.BackupStorageLocation, repo *velerov1api.BackupRepository) error { + return r.exec(restic.UnlockCommand(repo.Spec.ResticIdentifier), bsl) +} + +func (r *RepositoryService) Forget(bsl *velerov1api.BackupStorageLocation, repo *velerov1api.BackupRepository, snapshotID string) error { + return r.exec(restic.ForgetCommand(repo.Spec.ResticIdentifier, snapshotID), bsl) +} + +func (r *RepositoryService) exec(cmd *restic.Command, bsl *velerov1api.BackupStorageLocation) error { + file, err := r.credentialsFileStore.Path(repokey.RepoKeySelector()) + if err != nil { + return err + } + // ignore error since there's nothing we can do and it's a temp file. + defer os.Remove(file) + + cmd.PasswordFile = file + + // if there's a caCert on the ObjectStorage, write it to disk so that it can be passed to restic + var caCertFile string + if bsl.Spec.ObjectStorage != nil && bsl.Spec.ObjectStorage.CACert != nil { + caCertFile, err = restic.TempCACertFile(bsl.Spec.ObjectStorage.CACert, bsl.Name, r.fileSystem) + if err != nil { + return errors.Wrap(err, "error creating temp cacert file") + } + // ignore error since there's nothing we can do and it's a temp file. + defer os.Remove(caCertFile) + } + cmd.CACertFile = caCertFile + + env, err := restic.CmdEnv(bsl, r.credentialsFileStore) + if err != nil { + return err + } + cmd.Env = env + + // #4820: restrieve insecureSkipTLSVerify from BSL configuration for + // AWS plugin. If nothing is return, that means insecureSkipTLSVerify + // is not enable for Restic command. + skipTLSRet := restic.GetInsecureSkipTLSVerifyFromBSL(bsl, r.log) + if len(skipTLSRet) > 0 { + cmd.ExtraFlags = append(cmd.ExtraFlags, skipTLSRet) + } + + stdout, stderr, err := veleroexec.RunCommand(cmd.Cmd()) + r.log.WithFields(logrus.Fields{ + "repository": cmd.RepoName(), + "command": cmd.String(), + "stdout": stdout, + "stderr": stderr, + }).Debugf("Ran restic command") + if err != nil { + return errors.Wrapf(err, "error running command=%s, stdout=%s, stderr=%s", cmd.String(), stdout, stderr) + } + + return nil +} diff --git a/pkg/restic/common.go b/pkg/restic/common.go index 6e2671625..860f983f7 100644 --- a/pkg/restic/common.go +++ b/pkg/restic/common.go @@ -20,9 +20,11 @@ import ( "context" "fmt" "os" + "strconv" "time" "github.com/pkg/errors" + "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "sigs.k8s.io/controller-runtime/pkg/client" @@ -49,6 +51,14 @@ const ( // DefaultVolumesToRestic specifies whether restic should be used, by default, to // take backup of all pod volumes. DefaultVolumesToRestic = false + + // insecureSkipTLSVerifyKey is the flag in BackupStorageLocation's config + // to indicate whether to skip TLS verify to setup insecure HTTPS connection. + insecureSkipTLSVerifyKey = "insecureSkipTLSVerify" + + // resticInsecureTLSFlag is the flag for Restic command line to indicate + // skip TLS verify on https connection. + resticInsecureTLSFlag = "--insecure-tls" ) // SnapshotIdentifier uniquely identifies a restic snapshot @@ -176,3 +186,22 @@ func CmdEnv(backupLocation *velerov1api.BackupStorageLocation, credentialFileSto return env, nil } + +// GetInsecureSkipTLSVerifyFromBSL get insecureSkipTLSVerify flag from BSL configuration, +// Then return --insecure-tls flag with boolean value as result. +func GetInsecureSkipTLSVerifyFromBSL(backupLocation *velerov1api.BackupStorageLocation, logger logrus.FieldLogger) string { + result := "" + + if backupLocation == nil { + logger.Info("bsl is nil. return empty.") + return result + } + + if insecure, _ := strconv.ParseBool(backupLocation.Spec.Config[insecureSkipTLSVerifyKey]); insecure { + logger.Debugf("set --insecure-tls=true for Restic command according to BSL %s config", backupLocation.Name) + result = resticInsecureTLSFlag + "=true" + return result + } + + return result +} diff --git a/pkg/restic/common_test.go b/pkg/restic/common_test.go index fac82f901..b2acee773 100644 --- a/pkg/restic/common_test.go +++ b/pkg/restic/common_test.go @@ -22,6 +22,7 @@ import ( "sort" "testing" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1api "k8s.io/api/core/v1" @@ -216,3 +217,98 @@ func TestTempCACertFile(t *testing.T) { os.Remove(fileName) } + +func TestGetInsecureSkipTLSVerifyFromBSL(t *testing.T) { + log := logrus.StandardLogger() + tests := []struct { + name string + backupLocation *velerov1api.BackupStorageLocation + logger logrus.FieldLogger + expected string + }{ + { + "Test with nil BSL. Should return empty string.", + nil, + log, + "", + }, + { + "Test BSL with no configuration. Should return empty string.", + &velerov1api.BackupStorageLocation{ + Spec: velerov1api.BackupStorageLocationSpec{ + Provider: "azure", + }, + }, + log, + "", + }, + { + "Test with AWS BSL's insecureSkipTLSVerify set to false.", + &velerov1api.BackupStorageLocation{ + Spec: velerov1api.BackupStorageLocationSpec{ + Provider: "aws", + Config: map[string]string{ + "insecureSkipTLSVerify": "false", + }, + }, + }, + log, + "", + }, + { + "Test with AWS BSL's insecureSkipTLSVerify set to true.", + &velerov1api.BackupStorageLocation{ + Spec: velerov1api.BackupStorageLocationSpec{ + Provider: "aws", + Config: map[string]string{ + "insecureSkipTLSVerify": "true", + }, + }, + }, + log, + "--insecure-tls=true", + }, + { + "Test with Azure BSL's insecureSkipTLSVerify set to invalid.", + &velerov1api.BackupStorageLocation{ + Spec: velerov1api.BackupStorageLocationSpec{ + Provider: "azure", + Config: map[string]string{ + "insecureSkipTLSVerify": "invalid", + }, + }, + }, + log, + "", + }, + { + "Test with GCP without insecureSkipTLSVerify.", + &velerov1api.BackupStorageLocation{ + Spec: velerov1api.BackupStorageLocationSpec{ + Provider: "gcp", + Config: map[string]string{}, + }, + }, + log, + "", + }, + { + "Test with AWS without config.", + &velerov1api.BackupStorageLocation{ + Spec: velerov1api.BackupStorageLocationSpec{ + Provider: "aws", + }, + }, + log, + "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + res := GetInsecureSkipTLSVerifyFromBSL(test.backupLocation, test.logger) + + assert.Equal(t, test.expected, res) + }) + } +} diff --git a/pkg/restic/repository_keys.go b/pkg/restic/repository_keys.go deleted file mode 100644 index 28c190f70..000000000 --- a/pkg/restic/repository_keys.go +++ /dev/null @@ -1,75 +0,0 @@ -/* -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 restic - -import ( - "context" - - "github.com/pkg/errors" - corev1api "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - corev1client "k8s.io/client-go/kubernetes/typed/core/v1" - - "github.com/vmware-tanzu/velero/pkg/builder" -) - -const ( - credentialsSecretName = "velero-restic-credentials" - credentialsKey = "repository-password" - - encryptionKey = "static-passw0rd" -) - -func EnsureCommonRepositoryKey(secretClient corev1client.SecretsGetter, namespace string) error { - _, err := secretClient.Secrets(namespace).Get(context.TODO(), credentialsSecretName, metav1.GetOptions{}) - if err != nil && !apierrors.IsNotFound(err) { - return errors.WithStack(err) - } - if err == nil { - return nil - } - - // if we got here, we got an IsNotFound error, so we need to create the key - - secret := &corev1api.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: credentialsSecretName, - }, - Type: corev1api.SecretTypeOpaque, - Data: map[string][]byte{ - credentialsKey: []byte(encryptionKey), - }, - } - - if _, err = secretClient.Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{}); err != nil { - return errors.Wrapf(err, "error creating %s secret", credentialsSecretName) - } - - return nil -} - -// RepoKeySelector returns the SecretKeySelector which can be used to fetch -// the restic repository key. -func RepoKeySelector() *corev1api.SecretKeySelector { - // For now, all restic repos share the same key so we don't need the repoName to fetch it. - // When we move to full-backup encryption, we'll likely have a separate key per restic repo - // (all within the Velero server's namespace) so RepoKeySelector will need to select the key - // for that repo. - return builder.ForSecretKeySelector(credentialsSecretName, credentialsKey).Result() -} diff --git a/pkg/restic/repository_keys_test.go b/pkg/restic/repository_keys_test.go deleted file mode 100644 index 6af6641ce..000000000 --- a/pkg/restic/repository_keys_test.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -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 restic - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestRepoKeySelector(t *testing.T) { - selector := RepoKeySelector() - - require.Equal(t, credentialsSecretName, selector.Name) - require.Equal(t, credentialsKey, selector.Key) -} diff --git a/pkg/restic/repository_manager.go b/pkg/restic/repository_manager.go deleted file mode 100644 index 39961fc02..000000000 --- a/pkg/restic/repository_manager.go +++ /dev/null @@ -1,269 +0,0 @@ -/* -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 restic - -import ( - "context" - "os" - "strconv" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - 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" - velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned" - 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" -) - -// RepositoryManager executes commands against restic repositories. -type RepositoryManager interface { - // InitRepo initializes a repo with the specified name and identifier. - InitRepo(repo *velerov1api.BackupRepository) error - - // ConnectToRepo runs the 'restic snapshots' command against the - // specified repo, and returns an error if it fails. This is - // intended to be used to ensure that the repo exists/can be - // authenticated to. - ConnectToRepo(repo *velerov1api.BackupRepository) error - - // PruneRepo deletes unused data from a repo. - PruneRepo(repo *velerov1api.BackupRepository) error - - // UnlockRepo removes stale locks from a repo. - UnlockRepo(repo *velerov1api.BackupRepository) error - - // Forget removes a snapshot from the list of - // available snapshots in a repo. - Forget(context.Context, SnapshotIdentifier) error - - podvolume.BackupperFactory - - podvolume.RestorerFactory -} - -type repositoryManager struct { - namespace string - veleroClient clientset.Interface - repoLister velerov1listers.BackupRepositoryLister - repoInformerSynced cache.InformerSynced - kbClient kbclient.Client - log logrus.FieldLogger - 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 ( - // insecureSkipTLSVerifyKey is the flag in BackupStorageLocation's config - // to indicate whether to skip TLS verify to setup insecure HTTPS connection. - insecureSkipTLSVerifyKey = "insecureSkipTLSVerify" - - // resticInsecureTLSFlag is the flag for Restic command line to indicate - // skip TLS verify on https connection. - resticInsecureTLSFlag = "--insecure-tls" -) - -// NewRepositoryManager constructs a RepositoryManager. -func NewRepositoryManager( - ctx context.Context, - namespace string, - veleroClient clientset.Interface, - repoInformer velerov1informers.BackupRepositoryInformer, - repoClient velerov1client.BackupRepositoriesGetter, - kbClient kbclient.Client, - pvcClient corev1client.PersistentVolumeClaimsGetter, - pvClient corev1client.PersistentVolumesGetter, - credentialFileStore credentials.FileStore, - log logrus.FieldLogger, -) (RepositoryManager, error) { - rm := &repositoryManager{ - namespace: namespace, - veleroClient: veleroClient, - repoLister: repoInformer.Lister(), - repoInformerSynced: repoInformer.Informer().HasSynced, - kbClient: kbClient, - pvcClient: pvcClient, - pvClient: pvClient, - credentialsFileStore: credentialFileStore, - log: log, - ctx: ctx, - - 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) InitRepo(repo *velerov1api.BackupRepository) error { - // restic init requires an exclusive lock - rm.repoLocker.LockExclusive(repo.Name) - defer rm.repoLocker.UnlockExclusive(repo.Name) - - return rm.exec(InitCommand(repo.Spec.ResticIdentifier), repo.Spec.BackupStorageLocation) -} - -func (rm *repositoryManager) ConnectToRepo(repo *velerov1api.BackupRepository) error { - // restic snapshots requires a non-exclusive lock - rm.repoLocker.Lock(repo.Name) - defer rm.repoLocker.Unlock(repo.Name) - - snapshotsCmd := SnapshotsCommand(repo.Spec.ResticIdentifier) - // use the '--latest=1' flag to minimize the amount of data fetched since - // we're just validating that the repo exists and can be authenticated - // to. - // "--last" is replaced by "--latest=1" in restic v0.12.1 - snapshotsCmd.ExtraFlags = append(snapshotsCmd.ExtraFlags, "--latest=1") - - return rm.exec(snapshotsCmd, repo.Spec.BackupStorageLocation) -} - -func (rm *repositoryManager) PruneRepo(repo *velerov1api.BackupRepository) error { - // restic prune requires an exclusive lock - rm.repoLocker.LockExclusive(repo.Name) - defer rm.repoLocker.UnlockExclusive(repo.Name) - - return rm.exec(PruneCommand(repo.Spec.ResticIdentifier), repo.Spec.BackupStorageLocation) -} - -func (rm *repositoryManager) UnlockRepo(repo *velerov1api.BackupRepository) error { - // restic unlock requires a non-exclusive lock - rm.repoLocker.Lock(repo.Name) - defer rm.repoLocker.Unlock(repo.Name) - - return rm.exec(UnlockCommand(repo.Spec.ResticIdentifier), repo.Spec.BackupStorageLocation) -} - -func (rm *repositoryManager) Forget(ctx context.Context, snapshot SnapshotIdentifier) error { - // We can't wait for this in the constructor, because this informer is coming - // from the shared informer factory, which isn't started until *after* the repo - // manager is instantiated & passed to the controller constructors. We'd get a - // deadlock if we tried to wait for this in the constructor. - if !cache.WaitForCacheSync(ctx.Done(), rm.repoInformerSynced) { - return errors.New("timed out waiting for cache to sync") - } - - repo, err := rm.repoEnsurer.EnsureRepo(ctx, rm.namespace, snapshot.VolumeNamespace, snapshot.BackupStorageLocation) - if err != nil { - return err - } - - // restic forget requires an exclusive lock - rm.repoLocker.LockExclusive(repo.Name) - defer rm.repoLocker.UnlockExclusive(repo.Name) - - return rm.exec(ForgetCommand(repo.Spec.ResticIdentifier, snapshot.SnapshotID), repo.Spec.BackupStorageLocation) -} - -func (rm *repositoryManager) exec(cmd *Command, backupLocation string) error { - file, err := rm.credentialsFileStore.Path(repokey.RepoKeySelector()) - if err != nil { - return err - } - // ignore error since there's nothing we can do and it's a temp file. - defer os.Remove(file) - - cmd.PasswordFile = file - - loc := &velerov1api.BackupStorageLocation{} - if err := rm.kbClient.Get(context.Background(), kbclient.ObjectKey{ - Namespace: rm.namespace, - Name: backupLocation, - }, loc); err != nil { - return errors.Wrap(err, "error getting backup storage location") - } - - // if there's a caCert on the ObjectStorage, write it to disk so that it can be passed to restic - var caCertFile string - if loc.Spec.ObjectStorage != nil && loc.Spec.ObjectStorage.CACert != nil { - caCertFile, err = TempCACertFile(loc.Spec.ObjectStorage.CACert, backupLocation, rm.fileSystem) - if err != nil { - return errors.Wrap(err, "error creating temp cacert file") - } - // ignore error since there's nothing we can do and it's a temp file. - defer os.Remove(caCertFile) - } - cmd.CACertFile = caCertFile - - env, err := CmdEnv(loc, rm.credentialsFileStore) - if err != nil { - return err - } - cmd.Env = env - - // #4820: restrieve insecureSkipTLSVerify from BSL configuration for - // AWS plugin. If nothing is return, that means insecureSkipTLSVerify - // is not enable for Restic command. - skipTLSRet := GetInsecureSkipTLSVerifyFromBSL(loc, rm.log) - if len(skipTLSRet) > 0 { - cmd.ExtraFlags = append(cmd.ExtraFlags, skipTLSRet) - } - - stdout, stderr, err := veleroexec.RunCommand(cmd.Cmd()) - rm.log.WithFields(logrus.Fields{ - "repository": cmd.RepoName(), - "command": cmd.String(), - "stdout": stdout, - "stderr": stderr, - }).Debugf("Ran restic command") - if err != nil { - return errors.Wrapf(err, "error running command=%s, stdout=%s, stderr=%s", cmd.String(), stdout, stderr) - } - - return nil -} - -// GetInsecureSkipTLSVerifyFromBSL get insecureSkipTLSVerify flag from BSL configuration, -// Then return --insecure-tls flag with boolean value as result. -func GetInsecureSkipTLSVerifyFromBSL(backupLocation *velerov1api.BackupStorageLocation, logger logrus.FieldLogger) string { - result := "" - - if backupLocation == nil { - logger.Info("bsl is nil. return empty.") - return result - } - - if insecure, _ := strconv.ParseBool(backupLocation.Spec.Config[insecureSkipTLSVerifyKey]); insecure { - logger.Debugf("set --insecure-tls=true for Restic command according to BSL %s config", backupLocation.Name) - result = resticInsecureTLSFlag + "=true" - return result - } - - return result -} diff --git a/pkg/restic/repository_manager_test.go b/pkg/restic/repository_manager_test.go deleted file mode 100644 index 79d326bb8..000000000 --- a/pkg/restic/repository_manager_test.go +++ /dev/null @@ -1,121 +0,0 @@ -/* -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 restic - -import ( - "testing" - - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - - velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" -) - -func TestGetInsecureSkipTLSVerifyFromBSL(t *testing.T) { - log := logrus.StandardLogger() - tests := []struct { - name string - backupLocation *velerov1api.BackupStorageLocation - logger logrus.FieldLogger - expected string - }{ - { - "Test with nil BSL. Should return empty string.", - nil, - log, - "", - }, - { - "Test BSL with no configuration. Should return empty string.", - &velerov1api.BackupStorageLocation{ - Spec: velerov1api.BackupStorageLocationSpec{ - Provider: "azure", - }, - }, - log, - "", - }, - { - "Test with AWS BSL's insecureSkipTLSVerify set to false.", - &velerov1api.BackupStorageLocation{ - Spec: velerov1api.BackupStorageLocationSpec{ - Provider: "aws", - Config: map[string]string{ - "insecureSkipTLSVerify": "false", - }, - }, - }, - log, - "", - }, - { - "Test with AWS BSL's insecureSkipTLSVerify set to true.", - &velerov1api.BackupStorageLocation{ - Spec: velerov1api.BackupStorageLocationSpec{ - Provider: "aws", - Config: map[string]string{ - "insecureSkipTLSVerify": "true", - }, - }, - }, - log, - "--insecure-tls=true", - }, - { - "Test with Azure BSL's insecureSkipTLSVerify set to invalid.", - &velerov1api.BackupStorageLocation{ - Spec: velerov1api.BackupStorageLocationSpec{ - Provider: "azure", - Config: map[string]string{ - "insecureSkipTLSVerify": "invalid", - }, - }, - }, - log, - "", - }, - { - "Test with GCP without insecureSkipTLSVerify.", - &velerov1api.BackupStorageLocation{ - Spec: velerov1api.BackupStorageLocationSpec{ - Provider: "gcp", - Config: map[string]string{}, - }, - }, - log, - "", - }, - { - "Test with AWS without config.", - &velerov1api.BackupStorageLocation{ - Spec: velerov1api.BackupStorageLocationSpec{ - Provider: "aws", - }, - }, - log, - "", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - res := GetInsecureSkipTLSVerifyFromBSL(test.backupLocation, test.logger) - - assert.Equal(t, test.expected, res) - }) - } -}