From 0be9fc7d091a84be0c56bae0743a9cadc6d64f86 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Mon, 11 May 2026 17:31:40 +0800 Subject: [PATCH] add block uploader provider Signed-off-by: Lyndon-Li --- pkg/uploader/provider/block.go | 116 +++++++++++++++++ pkg/uploader/provider/block_test.go | 187 ++++++++++++++++++++++++++++ pkg/uploader/types.go | 3 +- 3 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 pkg/uploader/provider/block.go create mode 100644 pkg/uploader/provider/block_test.go diff --git a/pkg/uploader/provider/block.go b/pkg/uploader/provider/block.go new file mode 100644 index 000000000..c38387744 --- /dev/null +++ b/pkg/uploader/provider/block.go @@ -0,0 +1,116 @@ +/* +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" + "strings" + + "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" + repokeys "github.com/vmware-tanzu/velero/pkg/repository/keys" + "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" + "github.com/vmware-tanzu/velero/pkg/uploader" +) + +type blockProvider struct { + requestorType string + bkRepo udmrepo.BackupRepo + credGetter *credentials.CredentialGetter + log logrus.FieldLogger + canceling int32 +} + +// NewBlockUploaderProvider initialized with open or create a repository +func NewBlockUploaderProvider( + requestorType string, + ctx context.Context, + credGetter *credentials.CredentialGetter, + backupRepo *velerov1api.BackupRepository, + log logrus.FieldLogger, +) (Provider, error) { + bp := &blockProvider{ + requestorType: requestorType, + log: log, + credGetter: credGetter, + } + + repoUID := string(backupRepo.GetUID()) + repoOpt, err := udmrepo.NewRepoOptions( + udmrepo.WithPassword(bp, ""), + udmrepo.WithConfigFile("", repoUID), + udmrepo.WithDescription("Initial velero block uploader provider"), + ) + if err != nil { + return nil, errors.Wrapf(err, "error to get repo options") + } + + repoSvc := BackupRepoServiceCreateFunc(backupRepo.Spec.RepositoryType, log) + log.WithField("repoUID", repoUID).Info("Opening backup repo") + + bp.bkRepo, err = repoSvc.Open(ctx, *repoOpt) + if err != nil { + return nil, errors.Wrapf(err, "Failed to find backup repository") + } + + return bp, nil +} + +func (bp *blockProvider) Close(ctx context.Context) error { + return bp.bkRepo.Close(ctx) +} + +func (bp *blockProvider) GetPassword(param any) (string, error) { + if bp.credGetter.FromSecret == nil { + return "", errors.New("invalid credentials interface") + } + rawPass, err := bp.credGetter.FromSecret.Get(repokeys.RepoKeySelector()) + if err != nil { + return "", errors.Wrap(err, "error to get password") + } + + return strings.TrimSpace(rawPass), nil +} + +// TODO: implement in the following PRs +func (bp *blockProvider) RunBackup( + ctx context.Context, + path string, + realSource string, + tags map[string]string, + forceFull bool, + parentSnapshot string, + cbtParam CBTParam, + volMode uploader.PersistentVolumeMode, + uploaderCfg map[string]string, + updater uploader.ProgressUpdater) (string, bool, int64, int64, error) { + + return "", false, 0, 0, nil +} + +// TODO: implement in the following PRs +func (bp *blockProvider) RunRestore( + ctx context.Context, + snapshotID string, + volumePath string, + volMode uploader.PersistentVolumeMode, + uploaderCfg map[string]string, + updater uploader.ProgressUpdater) (int64, error) { + return 0, nil +} diff --git a/pkg/uploader/provider/block_test.go b/pkg/uploader/provider/block_test.go new file mode 100644 index 000000000..3584e35f1 --- /dev/null +++ b/pkg/uploader/provider/block_test.go @@ -0,0 +1,187 @@ +/* +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 ( + "testing" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + corev1api "k8s.io/api/core/v1" + + "github.com/vmware-tanzu/velero/internal/credentials" + "github.com/vmware-tanzu/velero/internal/credentials/mocks" + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/repository" + "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" + udmrepomocks "github.com/vmware-tanzu/velero/pkg/repository/udmrepo/mocks" +) + +func TestNewBlockUploaderProvider(t *testing.T) { + requestorType := "testRequestor" + ctx := t.Context() + backupRepo := repository.NewBackupRepository(velerov1api.DefaultNamespace, repository.BackupRepositoryKey{VolumeNamespace: "fake-volume-ns-02", BackupLocation: "fake-bsl-02", RepositoryType: "fake-repository-type-02"}) + mockLog := logrus.New() + + testCases := []struct { + name string + mockCredGetter *mocks.SecretStore + mockBackupRepoService udmrepo.BackupRepoService + expectedError string + }{ + { + name: "Success", + mockCredGetter: func() *mocks.SecretStore { + mockCredGetter := &mocks.SecretStore{} + mockCredGetter.On("Get", mock.Anything).Return("test", nil) + return mockCredGetter + }(), + mockBackupRepoService: func() udmrepo.BackupRepoService { + backupRepoService := &udmrepomocks.BackupRepoService{} + var backupRepo udmrepo.BackupRepo + backupRepoService.On("Open", t.Context(), mock.Anything).Return(backupRepo, nil) + return backupRepoService + }(), + expectedError: "", + }, + { + name: "Error to get repo options", + mockCredGetter: func() *mocks.SecretStore { + mockCredGetter := &mocks.SecretStore{} + mockCredGetter.On("Get", mock.Anything).Return("test", errors.New("failed to get password")) + return mockCredGetter + }(), + mockBackupRepoService: func() udmrepo.BackupRepoService { + backupRepoService := &udmrepomocks.BackupRepoService{} + var backupRepo udmrepo.BackupRepo + backupRepoService.On("Open", t.Context(), mock.Anything).Return(backupRepo, nil) + return backupRepoService + }(), + expectedError: "error to get repo options", + }, + { + name: "Error open repository service", + mockCredGetter: func() *mocks.SecretStore { + mockCredGetter := &mocks.SecretStore{} + mockCredGetter.On("Get", mock.Anything).Return("test", nil) + return mockCredGetter + }(), + mockBackupRepoService: func() udmrepo.BackupRepoService { + backupRepoService := &udmrepomocks.BackupRepoService{} + var backupRepo udmrepo.BackupRepo + backupRepoService.On("Open", t.Context(), mock.Anything).Return(backupRepo, errors.New("failed to init repository")) + return backupRepoService + }(), + expectedError: "Failed to find backup repository", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + credGetter := &credentials.CredentialGetter{FromSecret: tc.mockCredGetter} + BackupRepoServiceCreateFunc = func(string, logrus.FieldLogger) udmrepo.BackupRepoService { + return tc.mockBackupRepoService + } + _, err := NewBlockUploaderProvider(requestorType, ctx, credGetter, backupRepo, mockLog) + + if tc.expectedError != "" { + require.ErrorContains(t, err, tc.expectedError) + } else { + require.NoError(t, err) + } + + tc.mockCredGetter.AssertExpectations(t) + }) + } +} + +func TestBlockProviderClose(t *testing.T) { + mockBRepo := udmrepomocks.NewBackupRepo(t) + mockBRepo.On("Close", mock.Anything).Return(nil) + + bp := &blockProvider{ + bkRepo: mockBRepo, + } + + err := bp.Close(t.Context()) + assert.NoError(t, err) + mockBRepo.AssertExpectations(t) +} + +func TestBlockProviderGetPassword(t *testing.T) { + testCases := []struct { + name string + emptySecret bool + credGetterFunc func(*mocks.SecretStore, *corev1api.SecretKeySelector) + expectError bool + expectedPass string + }{ + { + name: "valid credentials interface", + credGetterFunc: func(ss *mocks.SecretStore, selector *corev1api.SecretKeySelector) { + ss.On("Get", selector).Return("test", nil) + }, + expectError: false, + expectedPass: "test", + }, + { + name: "empty from secret", + emptySecret: true, + expectError: true, + expectedPass: "", + }, + { + name: "ErrorGettingPassword", + credGetterFunc: func(ss *mocks.SecretStore, selector *corev1api.SecretKeySelector) { + ss.On("Get", selector).Return("", errors.New("error getting password")) + }, + expectError: true, + expectedPass: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + credGetter := &credentials.CredentialGetter{} + mockCredGetter := &mocks.SecretStore{} + if !tc.emptySecret { + credGetter.FromSecret = mockCredGetter + } + repoKeySelector := &corev1api.SecretKeySelector{LocalObjectReference: corev1api.LocalObjectReference{Name: "velero-repo-credentials"}, Key: "repository-password"} + + if tc.credGetterFunc != nil { + tc.credGetterFunc(mockCredGetter, repoKeySelector) + } + + bp := &blockProvider{ + credGetter: credGetter, + } + + password, err := bp.GetPassword(nil) + if tc.expectError { + require.Error(t, err, "Expected an error") + } else { + require.NoError(t, err, "Expected no error") + } + + assert.Equal(t, tc.expectedPass, password, "Expected password to match") + }) + } +} diff --git a/pkg/uploader/types.go b/pkg/uploader/types.go index 52f8ca5bf..cc3321012 100644 --- a/pkg/uploader/types.go +++ b/pkg/uploader/types.go @@ -23,6 +23,7 @@ import ( const ( KopiaType = "kopia" + BlockType = "velero-block" SnapshotRequesterTag = "snapshot-requester" SnapshotUploaderTag = "snapshot-uploader" ) @@ -40,7 +41,7 @@ const ( // It will return an error if it's invalid. func ValidateUploaderType(t string) (string, error) { t = strings.TrimSpace(t) - if t != KopiaType { + if t != KopiaType && t != BlockType { return "", fmt.Errorf("invalid uploader type '%s', valid type: '%s'", t, KopiaType) }