Merge pull request #9141 from kaovilai/9097
Some checks failed
Run the E2E test on kind / get-go-version (push) Failing after 1m8s
Run the E2E test on kind / build (push) Has been skipped
Run the E2E test on kind / setup-test-matrix (push) Successful in 3s
Run the E2E test on kind / run-e2e-test (push) Has been skipped
Main CI / get-go-version (push) Successful in 16s
Main CI / Build (push) Failing after 45s

feat: Enhance BackupStorageLocation with Secret-based CA certificate support
This commit is contained in:
lyndon-li
2025-12-19 13:13:47 +08:00
committed by GitHub
26 changed files with 1309 additions and 90 deletions

View File

@@ -17,6 +17,8 @@ limitations under the License.
package v1
import (
"errors"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
@@ -146,8 +148,15 @@ type ObjectStorageLocation struct {
Prefix string `json:"prefix,omitempty"`
// CACert defines a CA bundle to use when verifying TLS connections to the provider.
// Deprecated: Use CACertRef instead.
// +optional
CACert []byte `json:"caCert,omitempty"`
// CACertRef is a reference to a Secret containing the CA certificate bundle to use
// when verifying TLS connections to the provider. The Secret must be in the same
// namespace as the BackupStorageLocation.
// +optional
CACertRef *corev1api.SecretKeySelector `json:"caCertRef,omitempty"`
}
// BackupStorageLocationPhase is the lifecycle phase of a Velero BackupStorageLocation.
@@ -177,3 +186,13 @@ const (
// TODO(2.0): remove the AccessMode field from BackupStorageLocationStatus.
// TODO(2.0): remove the LastSyncedRevision field from BackupStorageLocationStatus.
// Validate validates the BackupStorageLocation to ensure that only one of CACert or CACertRef is set.
func (bsl *BackupStorageLocation) Validate() error {
if bsl.Spec.ObjectStorage != nil &&
bsl.Spec.ObjectStorage.CACert != nil &&
bsl.Spec.ObjectStorage.CACertRef != nil {
return errors.New("cannot specify both caCert and caCertRef in objectStorage")
}
return nil
}

View File

@@ -0,0 +1,121 @@
/*
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 v1
import (
"testing"
corev1api "k8s.io/api/core/v1"
)
func TestBackupStorageLocationValidate(t *testing.T) {
tests := []struct {
name string
bsl *BackupStorageLocation
expectError bool
}{
{
name: "valid - neither CACert nor CACertRef set",
bsl: &BackupStorageLocation{
Spec: BackupStorageLocationSpec{
StorageType: StorageType{
ObjectStorage: &ObjectStorageLocation{
Bucket: "test-bucket",
},
},
},
},
expectError: false,
},
{
name: "valid - only CACert set",
bsl: &BackupStorageLocation{
Spec: BackupStorageLocationSpec{
StorageType: StorageType{
ObjectStorage: &ObjectStorageLocation{
Bucket: "test-bucket",
CACert: []byte("test-cert"),
},
},
},
},
expectError: false,
},
{
name: "valid - only CACertRef set",
bsl: &BackupStorageLocation{
Spec: BackupStorageLocationSpec{
StorageType: StorageType{
ObjectStorage: &ObjectStorageLocation{
Bucket: "test-bucket",
CACertRef: &corev1api.SecretKeySelector{
LocalObjectReference: corev1api.LocalObjectReference{
Name: "ca-cert-secret",
},
Key: "ca.crt",
},
},
},
},
},
expectError: false,
},
{
name: "invalid - both CACert and CACertRef set",
bsl: &BackupStorageLocation{
Spec: BackupStorageLocationSpec{
StorageType: StorageType{
ObjectStorage: &ObjectStorageLocation{
Bucket: "test-bucket",
CACert: []byte("test-cert"),
CACertRef: &corev1api.SecretKeySelector{
LocalObjectReference: corev1api.LocalObjectReference{
Name: "ca-cert-secret",
},
Key: "ca.crt",
},
},
},
},
},
expectError: true,
},
{
name: "valid - no ObjectStorage",
bsl: &BackupStorageLocation{
Spec: BackupStorageLocationSpec{
StorageType: StorageType{
ObjectStorage: nil,
},
},
},
expectError: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.bsl.Validate()
if test.expectError && err == nil {
t.Errorf("expected error but got none")
}
if !test.expectError && err != nil {
t.Errorf("expected no error but got: %v", err)
}
})
}
}

View File

@@ -915,6 +915,11 @@ func (in *ObjectStorageLocation) DeepCopyInto(out *ObjectStorageLocation) {
*out = make([]byte, len(*in))
copy(*out, *in)
}
if in.CACertRef != nil {
in, out := &in.CACertRef, &out.CACertRef
*out = new(corev1.SecretKeySelector)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectStorageLocation.

View File

@@ -93,6 +93,15 @@ func (b *BackupStorageLocationBuilder) CACert(val []byte) *BackupStorageLocation
return b
}
// CACertRef sets the BackupStorageLocation's object storage CACertRef (Secret reference).
func (b *BackupStorageLocationBuilder) CACertRef(selector *corev1api.SecretKeySelector) *BackupStorageLocationBuilder {
if b.object.Spec.StorageType.ObjectStorage == nil {
b.object.Spec.StorageType.ObjectStorage = new(velerov1api.ObjectStorageLocation)
}
b.object.Spec.ObjectStorage.CACertRef = selector
return b
}
// Default sets the BackupStorageLocation's is default or not
func (b *BackupStorageLocationBuilder) Default(isDefault bool) *BackupStorageLocationBuilder {
b.object.Spec.Default = isDefault

View File

@@ -558,7 +558,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
return clientmgmt.NewManager(logger, s.logLevel, s.pluginRegistry)
}
backupStoreGetter := persistence.NewObjectBackupStoreGetter(s.credentialFileStore)
backupStoreGetter := persistence.NewObjectBackupStoreGetterWithSecretStore(s.credentialFileStore, s.credentialSecretStore)
backupTracker := controller.NewBackupTracker()

View File

@@ -20,7 +20,9 @@ import (
"context"
"github.com/pkg/errors"
corev1api "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
@@ -52,6 +54,7 @@ func GetCACertFromRestore(ctx context.Context, client kbclient.Client, namespace
}
// GetCACertFromBSL fetches a BackupStorageLocation directly and returns its cacert
// Priority order: caCertRef (from Secret) > caCert (inline, deprecated)
func GetCACertFromBSL(ctx context.Context, client kbclient.Client, namespace, bslName string) (string, error) {
if bslName == "" {
return "", nil
@@ -71,7 +74,44 @@ func GetCACertFromBSL(ctx context.Context, client kbclient.Client, namespace, bs
return "", errors.Wrapf(err, "error getting backup storage location %s", bslName)
}
if bsl.Spec.ObjectStorage != nil && len(bsl.Spec.ObjectStorage.CACert) > 0 {
if bsl.Spec.ObjectStorage == nil {
return "", nil
}
// Prefer caCertRef over inline caCert
if bsl.Spec.ObjectStorage.CACertRef != nil {
// Fetch certificate from Secret
secret := &corev1api.Secret{}
secretKey := types.NamespacedName{
Name: bsl.Spec.ObjectStorage.CACertRef.Name,
Namespace: namespace,
}
if err := client.Get(ctx, secretKey, secret); err != nil {
if apierrors.IsNotFound(err) {
return "", errors.Errorf("certificate secret %s not found in namespace %s",
bsl.Spec.ObjectStorage.CACertRef.Name, namespace)
}
return "", errors.Wrapf(err, "error getting certificate secret %s",
bsl.Spec.ObjectStorage.CACertRef.Name)
}
keyName := bsl.Spec.ObjectStorage.CACertRef.Key
if keyName == "" {
return "", errors.New("caCertRef key is empty")
}
certData, ok := secret.Data[keyName]
if !ok {
return "", errors.Errorf("key %s not found in secret %s",
keyName, bsl.Spec.ObjectStorage.CACertRef.Name)
}
return string(certData), nil
}
// Fall back to inline caCert (deprecated)
if len(bsl.Spec.ObjectStorage.CACert) > 0 {
return string(bsl.Spec.ObjectStorage.CACert), nil
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
@@ -294,6 +295,271 @@ func TestGetCACertFromBSL(t *testing.T) {
}
}
// TestGetCACertFromBSL_WithCACertRef tests the new caCertRef functionality
func TestGetCACertFromBSL_WithCACertRef(t *testing.T) {
testCases := []struct {
name string
bslName string
bsl *velerov1api.BackupStorageLocation
secret *corev1api.Secret
expectedCACert string
expectedError bool
errorContains string
}{
{
name: "BSL with caCertRef pointing to valid secret",
bslName: "test-bsl",
bsl: &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-bsl",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
CACertRef: &corev1api.SecretKeySelector{
LocalObjectReference: corev1api.LocalObjectReference{
Name: "test-secret",
},
Key: "ca-bundle.crt",
},
},
},
},
},
secret: &corev1api.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-secret",
},
Data: map[string][]byte{
"ca-bundle.crt": []byte("test-cacert-from-secret"),
},
},
expectedCACert: "test-cacert-from-secret",
expectedError: false,
},
{
name: "BSL with both caCertRef and caCert - caCertRef takes precedence",
bslName: "test-bsl",
bsl: &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-bsl",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
CACert: []byte("inline-cacert-deprecated"),
CACertRef: &corev1api.SecretKeySelector{
LocalObjectReference: corev1api.LocalObjectReference{
Name: "test-secret",
},
Key: "ca-bundle.crt",
},
},
},
},
},
secret: &corev1api.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-secret",
},
Data: map[string][]byte{
"ca-bundle.crt": []byte("cacert-from-secret-takes-precedence"),
},
},
expectedCACert: "cacert-from-secret-takes-precedence",
expectedError: false,
},
{
name: "BSL with caCertRef but secret not found",
bslName: "test-bsl",
bsl: &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-bsl",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
CACertRef: &corev1api.SecretKeySelector{
LocalObjectReference: corev1api.LocalObjectReference{
Name: "missing-secret",
},
Key: "ca-bundle.crt",
},
},
},
},
},
secret: nil,
expectedCACert: "",
expectedError: true,
errorContains: "certificate secret missing-secret not found",
},
{
name: "BSL with caCertRef but key not found in secret",
bslName: "test-bsl",
bsl: &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-bsl",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
CACertRef: &corev1api.SecretKeySelector{
LocalObjectReference: corev1api.LocalObjectReference{
Name: "test-secret",
},
Key: "missing-key",
},
},
},
},
},
secret: &corev1api.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-secret",
},
Data: map[string][]byte{
"ca-bundle.crt": []byte("test-cacert"),
},
},
expectedCACert: "",
expectedError: true,
errorContains: "key missing-key not found in secret test-secret",
},
{
name: "BSL with caCertRef but empty key",
bslName: "test-bsl",
bsl: &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-bsl",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
CACertRef: &corev1api.SecretKeySelector{
LocalObjectReference: corev1api.LocalObjectReference{
Name: "test-secret",
},
Key: "",
},
},
},
},
},
secret: &corev1api.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-secret",
},
Data: map[string][]byte{
"ca-bundle.crt": []byte("test-cacert"),
},
},
expectedCACert: "",
expectedError: true,
errorContains: "caCertRef key is empty",
},
{
name: "BSL with caCertRef containing multi-line PEM certificate",
bslName: "test-bsl",
bsl: &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-bsl",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
CACertRef: &corev1api.SecretKeySelector{
LocalObjectReference: corev1api.LocalObjectReference{
Name: "test-secret",
},
Key: "ca.pem",
},
},
},
},
},
secret: &corev1api.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-ns",
Name: "test-secret",
},
Data: map[string][]byte{
"ca.pem": []byte("-----BEGIN CERTIFICATE-----\nMIIDETC...\n-----END CERTIFICATE-----\n"),
},
},
expectedCACert: "-----BEGIN CERTIFICATE-----\nMIIDETC...\n-----END CERTIFICATE-----\n",
expectedError: false,
},
{
name: "BSL falls back to inline caCert when caCertRef is nil",
bslName: "test-bsl",
bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
Provider("aws").
Bucket("test-bucket").
CACert([]byte("fallback-inline-cacert")).
Result(),
secret: nil,
expectedCACert: "fallback-inline-cacert",
expectedError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var objs []runtime.Object
if tc.bsl != nil {
objs = append(objs, tc.bsl)
}
if tc.secret != nil {
objs = append(objs, tc.secret)
}
scheme := runtime.NewScheme()
_ = velerov1api.AddToScheme(scheme)
_ = corev1api.AddToScheme(scheme)
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithRuntimeObjects(objs...).
Build()
cacert, err := GetCACertFromBSL(t.Context(), fakeClient, "test-ns", tc.bslName)
if tc.expectedError {
require.Error(t, err)
if tc.errorContains != "" {
assert.Contains(t, err.Error(), tc.errorContains)
}
} else {
require.NoError(t, err)
assert.Equal(t, tc.expectedCACert, cacert)
}
})
}
}
// TestGetCACertFromBackup_ClientError tests error scenarios where client.Get returns non-NotFound errors
func TestGetCACertFromBackup_ClientError(t *testing.T) {
testCases := []struct {

View File

@@ -201,11 +201,22 @@ func (r *BackupRepoReconciler) needInvalidBackupRepo(oldObj client.Object, newOb
return true
}
// Check if either CACert or CACertRef has changed
if !bytes.Equal(oldStorage.CACert, newStorage.CACert) {
logger.Info("BSL's CACert has changed, invalid backup repositories")
return true
}
// Check if CACertRef has changed
if (oldStorage.CACertRef == nil && newStorage.CACertRef != nil) ||
(oldStorage.CACertRef != nil && newStorage.CACertRef == nil) ||
(oldStorage.CACertRef != nil && newStorage.CACertRef != nil &&
(oldStorage.CACertRef.Name != newStorage.CACertRef.Name ||
oldStorage.CACertRef.Key != newStorage.CACertRef.Key)) {
logger.Info("BSL's CACertRef has changed, invalid backup repositories")
return true
}
if !reflect.DeepEqual(oldConfig, newConfig) {
logger.Info("BSL's storage config has changed, invalid backup repositories")

View File

@@ -237,6 +237,12 @@ func (r *backupStorageLocationReconciler) Reconcile(ctx context.Context, req ctr
}
}()
// Validate the BackupStorageLocation spec
if err = location.Validate(); err != nil {
log.WithError(err).Error("BackupStorageLocation spec is invalid")
return
}
backupStore, err := r.backupStoreGetter.Get(&location, pluginManager, log)
if err != nil {
log.WithError(err).Error("Error getting a backup store")

View File

@@ -252,7 +252,7 @@ func (fs *fileSystemBR) boostRepoConnect(ctx context.Context, repositoryType str
return err
}
} else {
if err := repoProvider.NewResticRepositoryProvider(credentialGetter.FromFile, filesystem.NewFileSystem(), fs.log).BoostRepoConnect(ctx, repoProvider.RepoParam{BackupLocation: fs.backupLocation, BackupRepo: fs.backupRepo}); err != nil {
if err := repoProvider.NewResticRepositoryProvider(*credentialGetter, filesystem.NewFileSystem(), fs.log).BoostRepoConnect(ctx, repoProvider.RepoParam{BackupLocation: fs.backupLocation, BackupRepo: fs.backupRepo}); err != nil {
return err
}
}

View File

@@ -116,6 +116,7 @@ type ObjectBackupStoreGetter interface {
type objectBackupStoreGetter struct {
credentialStore credentials.FileStore
secretStore credentials.SecretStore
}
// NewObjectBackupStoreGetter returns a ObjectBackupStoreGetter that can get a velero.BackupStore.
@@ -123,6 +124,15 @@ func NewObjectBackupStoreGetter(credentialStore credentials.FileStore) ObjectBac
return &objectBackupStoreGetter{credentialStore: credentialStore}
}
// NewObjectBackupStoreGetterWithSecretStore returns an ObjectBackupStoreGetter with SecretStore
// support for resolving caCertRef from Kubernetes Secrets.
func NewObjectBackupStoreGetterWithSecretStore(credentialStore credentials.FileStore, secretStore credentials.SecretStore) ObjectBackupStoreGetter {
return &objectBackupStoreGetter{
credentialStore: credentialStore,
secretStore: secretStore,
}
}
func (b *objectBackupStoreGetter) Get(location *velerov1api.BackupStorageLocation, objectStoreGetter ObjectStoreGetter, logger logrus.FieldLogger) (BackupStore, error) {
if location.Spec.ObjectStorage == nil {
return nil, errors.New("backup storage location does not use object storage")
@@ -160,7 +170,16 @@ func (b *objectBackupStoreGetter) Get(location *velerov1api.BackupStorageLocatio
objectStoreConfig["prefix"] = prefix
// Only include a CACert if it's specified in order to maintain compatibility with plugins that don't expect it.
if location.Spec.ObjectStorage.CACert != nil {
// Prefer caCertRef (from Secret) over inline caCert (deprecated).
if location.Spec.ObjectStorage.CACertRef != nil {
if b.secretStore != nil {
caCertString, err := b.secretStore.Get(location.Spec.ObjectStorage.CACertRef)
if err != nil {
return nil, errors.Wrap(err, "error getting CA certificate from secret")
}
objectStoreConfig["caCert"] = caCertString
}
} else if location.Spec.ObjectStorage.CACert != nil {
objectStoreConfig["caCert"] = string(location.Spec.ObjectStorage.CACert)
}

View File

@@ -1017,6 +1017,32 @@ func TestNewObjectBackupStoreGetterConfig(t *testing.T) {
"credentialsFile": "/tmp/credentials/secret-file",
},
},
{
name: "location with CACertRef is initialized with caCert from secret",
location: builder.ForBackupStorageLocation("", "").Provider(provider).Bucket(bucket).CACertRef(
builder.ForSecretKeySelector("cacert-secret", "ca.crt").Result(),
).Result(),
getter: NewObjectBackupStoreGetterWithSecretStore(
velerotest.NewFakeCredentialsFileStore("", nil),
velerotest.NewFakeCredentialsSecretStore("cacert-from-secret", nil),
),
wantConfig: map[string]string{
"bucket": "bucket",
"prefix": "",
"caCert": "cacert-from-secret",
},
},
{
name: "location with CACertRef and no SecretStore uses no caCert",
location: builder.ForBackupStorageLocation("", "").Provider(provider).Bucket(bucket).CACertRef(
builder.ForSecretKeySelector("cacert-secret", "ca.crt").Result(),
).Result(),
getter: NewObjectBackupStoreGetter(velerotest.NewFakeCredentialsFileStore("", nil)),
wantConfig: map[string]string{
"bucket": "bucket",
"prefix": "",
},
},
}
for _, tc := range tests {

View File

@@ -109,7 +109,10 @@ func NewManager(
log: log,
}
mgr.providers[velerov1api.BackupRepositoryTypeRestic] = provider.NewResticRepositoryProvider(credentialFileStore, mgr.fileSystem, mgr.log)
mgr.providers[velerov1api.BackupRepositoryTypeRestic] = provider.NewResticRepositoryProvider(credentials.CredentialGetter{
FromFile: credentialFileStore,
FromSecret: credentialSecretStore,
}, mgr.fileSystem, mgr.log)
mgr.providers[velerov1api.BackupRepositoryTypeKopia] = provider.NewUnifiedRepoProvider(credentials.CredentialGetter{
FromFile: credentialFileStore,
FromSecret: credentialSecretStore,

View File

@@ -28,9 +28,9 @@ import (
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
)
func NewResticRepositoryProvider(store credentials.FileStore, fs filesystem.Interface, log logrus.FieldLogger) Provider {
func NewResticRepositoryProvider(credGetter credentials.CredentialGetter, fs filesystem.Interface, log logrus.FieldLogger) Provider {
return &resticRepositoryProvider{
svc: restic.NewRepositoryService(store, fs, log),
svc: restic.NewRepositoryService(credGetter, fs, log),
}
}

View File

@@ -59,7 +59,7 @@ var getGCPCredentials = repoconfig.GetGCPCredentials
var getS3BucketRegion = repoconfig.GetAWSBucketRegion
type localFuncTable struct {
getStorageVariables func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error)
getStorageVariables func(*velerov1api.BackupStorageLocation, string, string, map[string]string, credentials.CredentialGetter) (map[string]string, error)
getStorageCredentials func(*velerov1api.BackupStorageLocation, credentials.FileStore) (map[string]string, error)
}
@@ -427,7 +427,7 @@ func (urp *unifiedRepoProvider) GetStoreOptions(param any) (map[string]string, e
return map[string]string{}, errors.Errorf("invalid parameter, expect %T, actual %T", RepoParam{}, param)
}
storeVar, err := funcTable.getStorageVariables(repoParam.BackupLocation, urp.repoBackend, repoParam.BackupRepo.Spec.VolumeNamespace, repoParam.BackupRepo.Spec.RepositoryConfig)
storeVar, err := funcTable.getStorageVariables(repoParam.BackupLocation, urp.repoBackend, repoParam.BackupRepo.Spec.VolumeNamespace, repoParam.BackupRepo.Spec.RepositoryConfig, urp.credentialGetter)
if err != nil {
return map[string]string{}, errors.Wrap(err, "error to get storage variables")
}
@@ -539,7 +539,7 @@ func getStorageCredentials(backupLocation *velerov1api.BackupStorageLocation, cr
// so we would accept only the options that are well defined in the internal system.
// Users' inputs should not be treated as safe any time.
// We remove the unnecessary parameters and keep the modules/logics below safe
func getStorageVariables(backupLocation *velerov1api.BackupStorageLocation, repoBackend string, repoName string, backupRepoConfig map[string]string) (map[string]string, error) {
func getStorageVariables(backupLocation *velerov1api.BackupStorageLocation, repoBackend string, repoName string, backupRepoConfig map[string]string, credGetter credentials.CredentialGetter) (map[string]string, error) {
result := make(map[string]string)
backendType := repoconfig.GetBackendType(backupLocation.Spec.Provider, backupLocation.Spec.Config)
@@ -603,8 +603,23 @@ func getStorageVariables(backupLocation *velerov1api.BackupStorageLocation, repo
result[udmrepo.StoreOptionOssBucket] = bucket
result[udmrepo.StoreOptionPrefix] = prefix
if backupLocation.Spec.ObjectStorage != nil && backupLocation.Spec.ObjectStorage.CACert != nil {
result[udmrepo.StoreOptionCACert] = base64.StdEncoding.EncodeToString(backupLocation.Spec.ObjectStorage.CACert)
if backupLocation.Spec.ObjectStorage != nil {
var caCertData []byte
// Try CACertRef first (new method), then fall back to CACert (deprecated)
if backupLocation.Spec.ObjectStorage.CACertRef != nil {
caCertString, err := credGetter.FromSecret.Get(backupLocation.Spec.ObjectStorage.CACertRef)
if err != nil {
return nil, errors.Wrap(err, "error getting CA certificate from secret")
}
caCertData = []byte(caCertString)
} else if backupLocation.Spec.ObjectStorage.CACert != nil {
caCertData = backupLocation.Spec.ObjectStorage.CACert
}
if caCertData != nil {
result[udmrepo.StoreOptionCACert] = base64.StdEncoding.EncodeToString(caCertData)
}
}
result[udmrepo.StoreOptionOssRegion] = strings.Trim(region, "/")
result[udmrepo.StoreOptionFsPath] = config["fspath"]

View File

@@ -465,7 +465,7 @@ func TestGetStorageVariables(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
getS3BucketRegion = tc.getS3BucketRegion
actual, err := getStorageVariables(&tc.backupLocation, tc.repoBackend, tc.repoName, tc.repoConfig)
actual, err := getStorageVariables(&tc.backupLocation, tc.repoBackend, tc.repoName, tc.repoConfig, velerocredentials.CredentialGetter{})
require.Equal(t, tc.expected, actual)
@@ -554,7 +554,7 @@ func TestGetStoreOptions(t *testing.T) {
BackupRepo: &velerov1api.BackupRepository{},
},
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, errors.New("fake-error-2")
},
},
@@ -568,7 +568,7 @@ func TestGetStoreOptions(t *testing.T) {
BackupRepo: &velerov1api.BackupRepository{},
},
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -637,7 +637,7 @@ func TestPrepareRepo(t *testing.T) {
repoService: new(reposervicenmocks.BackupRepoService),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, errors.New("fake-store-option-error")
},
},
@@ -648,7 +648,7 @@ func TestPrepareRepo(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -666,7 +666,7 @@ func TestPrepareRepo(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -687,7 +687,7 @@ func TestPrepareRepo(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -703,12 +703,33 @@ func TestPrepareRepo(t *testing.T) {
},
expectedErr: "cannot create new backup repo for read-only backup storage location velero/fake-bsl",
},
{
name: "create fail",
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
return map[string]string{}, nil
},
},
repoService: new(reposervicenmocks.BackupRepoService),
retFuncCheck: func(ctx context.Context, repoOption udmrepo.RepoOptions) (bool, error) {
return false, nil
},
retFuncCreate: func(ctx context.Context, repoOption udmrepo.RepoOptions) error {
return errors.New("fake-error-1")
},
expectedErr: "error to create backup repo: fake-error-1",
},
{
name: "initialize error",
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -729,7 +750,7 @@ func TestPrepareRepo(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -812,7 +833,7 @@ func TestForget(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -836,7 +857,7 @@ func TestForget(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -864,7 +885,7 @@ func TestForget(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -962,7 +983,7 @@ func TestBatchForget(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -986,7 +1007,7 @@ func TestBatchForget(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -1015,7 +1036,7 @@ func TestBatchForget(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -1124,7 +1145,7 @@ func TestInitRepo(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -1142,7 +1163,7 @@ func TestInitRepo(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -1218,7 +1239,7 @@ func TestConnectToRepo(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -1236,7 +1257,7 @@ func TestConnectToRepo(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -1310,7 +1331,7 @@ func TestBoostRepoConnect(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -1337,7 +1358,7 @@ func TestBoostRepoConnect(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -1363,7 +1384,7 @@ func TestBoostRepoConnect(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -1450,7 +1471,7 @@ func TestPruneRepo(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {
@@ -1468,7 +1489,7 @@ func TestPruneRepo(t *testing.T) {
getter: new(credmock.SecretStore),
credStoreReturn: "fake-password",
funcTable: localFuncTable{
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string) (map[string]string, error) {
getStorageVariables: func(*velerov1api.BackupStorageLocation, string, string, map[string]string, velerocredentials.CredentialGetter) (map[string]string, error) {
return map[string]string{}, nil
},
getStorageCredentials: func(*velerov1api.BackupStorageLocation, velerocredentials.FileStore) (map[string]string, error) {

View File

@@ -31,18 +31,18 @@ import (
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
)
func NewRepositoryService(store credentials.FileStore, fs filesystem.Interface, log logrus.FieldLogger) *RepositoryService {
func NewRepositoryService(credGetter credentials.CredentialGetter, fs filesystem.Interface, log logrus.FieldLogger) *RepositoryService {
return &RepositoryService{
credentialsFileStore: store,
fileSystem: fs,
log: log,
credGetter: credGetter,
fileSystem: fs,
log: log,
}
}
type RepositoryService struct {
credentialsFileStore credentials.FileStore
fileSystem filesystem.Interface
log logrus.FieldLogger
credGetter credentials.CredentialGetter
fileSystem filesystem.Interface
log logrus.FieldLogger
}
func (r *RepositoryService) InitRepo(bsl *velerov1api.BackupStorageLocation, repo *velerov1api.BackupRepository) error {
@@ -77,7 +77,7 @@ func (r *RepositoryService) DefaultMaintenanceFrequency() time.Duration {
}
func (r *RepositoryService) exec(cmd *restic.Command, bsl *velerov1api.BackupStorageLocation) error {
file, err := r.credentialsFileStore.Path(repokey.RepoKeySelector())
file, err := r.credGetter.FromFile.Path(repokey.RepoKeySelector())
if err != nil {
return err
}
@@ -88,17 +88,37 @@ func (r *RepositoryService) exec(cmd *restic.Command, bsl *velerov1api.BackupSto
// 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")
if bsl.Spec.ObjectStorage != nil {
var caCertData []byte
// Try CACertRef first (new method), then fall back to CACert (deprecated)
if bsl.Spec.ObjectStorage.CACertRef != nil {
caCertString, err := r.credGetter.FromSecret.Get(bsl.Spec.ObjectStorage.CACertRef)
if err != nil {
return errors.Wrap(err, "error getting CA certificate from secret")
}
caCertData = []byte(caCertString)
} else if bsl.Spec.ObjectStorage.CACert != nil {
caCertData = bsl.Spec.ObjectStorage.CACert
}
if caCertData != nil {
caCertFile, err = restic.TempCACertFile(caCertData, 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)
}
// 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)
// CmdEnv uses credGetter.FromFile (not FromSecret) to get cloud provider credentials.
// FromFile materializes the BSL's Credential secret to a file path that cloud SDKs
// can read (e.g., AWS_SHARED_CREDENTIALS_FILE). This is different from caCertRef above,
// which uses FromSecret to read the CA certificate data directly into memory, then
// writes it to a temp file because restic CLI only accepts file paths (--cacert flag).
env, err := restic.CmdEnv(bsl, r.credGetter.FromFile)
if err != nil {
return err
}

View File

@@ -47,3 +47,31 @@ func NewFakeCredentialsFileStore(path string, err error) FileStore {
err: err,
}
}
// SecretStore defines operations for interacting with credentials
// that are stored in Secret.
type SecretStore interface {
// Get returns the secret key defined by the given selector
Get(selector *corev1api.SecretKeySelector) (string, error)
}
type fakeCredentialsSecretStore struct {
data string
err error
}
// Get returns the secret data.
func (f *fakeCredentialsSecretStore) Get(*corev1api.SecretKeySelector) (string, error) {
return f.data, f.err
}
// NewFakeCredentialsSecretStore creates a SecretStore which will return the given data
// and error when Get is called.
// data is the secret value to return (e.g., certificate content).
// err is the error to return, if any.
func NewFakeCredentialsSecretStore(data string, err error) SecretStore {
return &fakeCredentialsSecretStore{
data: data,
err: err,
}
}

View File

@@ -73,10 +73,25 @@ func NewResticUploaderProvider(
}
// if there's a caCert on the ObjectStorage, write it to disk so that it can be passed to restic
if bsl.Spec.ObjectStorage != nil && bsl.Spec.ObjectStorage.CACert != nil {
provider.caCertFile, err = resticTempCACertFileFunc(bsl.Spec.ObjectStorage.CACert, bsl.Name, filesystem.NewFileSystem())
if err != nil {
return nil, errors.Wrap(err, "error create temp cert file")
if bsl.Spec.ObjectStorage != nil {
var caCertData []byte
// Try CACertRef first (new method), then fall back to CACert (deprecated)
if bsl.Spec.ObjectStorage.CACertRef != nil {
caCertString, err := credGetter.FromSecret.Get(bsl.Spec.ObjectStorage.CACertRef)
if err != nil {
return nil, errors.Wrap(err, "error getting CA certificate from secret")
}
caCertData = []byte(caCertString)
} else if bsl.Spec.ObjectStorage.CACert != nil {
caCertData = bsl.Spec.ObjectStorage.CACert
}
if caCertData != nil {
provider.caCertFile, err = resticTempCACertFileFunc(caCertData, bsl.Name, filesystem.NewFileSystem())
if err != nil {
return nil, errors.Wrap(err, "error create temp cert file")
}
}
}