Pass configured BSL credential to plugin via config (#3442)

* Load credentials and pass to ObjectStorage plugins

Update NewObjectBackupStore to take a CredentialsGetter which can be
used to get the credentials for a BackupStorageLocation if it has been
configured with a Credential. If the BSL has a credential, use that
SecretKeySelector to fetch the secret, write the contents to a temp file
and then pass that file through to the plugin via the config map using
the key `credentialsFile`. This relies on the plugin being able to use
this new config field.

This does not yet handle VolumeSnapshotLocations or ResticRepositories.

Signed-off-by: Bridget McErlean <bmcerlean@vmware.com>

* Address code reviews

Add godocs and comments.
Improve formatting and test names.

Signed-off-by: Bridget McErlean <bmcerlean@vmware.com>

* Address code reviews

Signed-off-by: Bridget McErlean <bmcerlean@vmware.com>
This commit is contained in:
Bridget McErlean
2021-03-04 13:43:15 -08:00
committed by GitHub
parent c46fe71b12
commit b9a8c0b254
19 changed files with 433 additions and 64 deletions
+1
View File
@@ -0,0 +1 @@
Add support for per-BSL credentials. Velero will now serialize the secret referenced by the `Credential` field in the BSL and pass this path through to Object Storage plugins via the `config` map using the `credentialsFile` key.
+88
View File
@@ -0,0 +1,88 @@
/*
Copyright the Velero contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package credentials
import (
"fmt"
"os"
"path/filepath"
"github.com/pkg/errors"
corev1api "k8s.io/api/core/v1"
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
"github.com/vmware-tanzu/velero/pkg/util/kube"
)
// FileStore defines operations for interacting with credentials
// that are stored on a file system.
type FileStore interface {
// Path returns a path on disk where the secret key defined by
// the given selector is serialized.
Path(selector *corev1api.SecretKeySelector) (string, error)
}
type namespacedFileStore struct {
client kbclient.Client
namespace string
fsRoot string
fs filesystem.Interface
}
// NewNamespacedFileStore returns a FileStore which can interact with credentials
// for the given namespace and will store them under the given fsRoot.
func NewNamespacedFileStore(client kbclient.Client, namespace string, fsRoot string, fs filesystem.Interface) (FileStore, error) {
fsNamespaceRoot := filepath.Join(fsRoot, namespace)
if err := fs.MkdirAll(fsNamespaceRoot, 0755); err != nil {
return nil, err
}
return &namespacedFileStore{
client: client,
namespace: namespace,
fsRoot: fsNamespaceRoot,
fs: fs,
}, nil
}
// Path returns a path on disk where the secret key defined by
// the given selector is serialized.
func (n *namespacedFileStore) Path(selector *corev1api.SecretKeySelector) (string, error) {
creds, err := kube.GetSecretKey(n.client, n.namespace, selector)
if err != nil {
return "", errors.Wrap(err, "unable to get key for secret")
}
keyFilePath := filepath.Join(n.fsRoot, fmt.Sprintf("%s-%s", selector.Name, selector.Key))
file, err := n.fs.OpenFile(keyFilePath, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return "", errors.Wrap(err, "unable to open credentials file for writing")
}
if _, err := file.Write(creds); err != nil {
return "", errors.Wrap(err, "unable to write credentials to store")
}
if err := file.Close(); err != nil {
return "", errors.Wrap(err, "unable to close credentials file")
}
return keyFilePath, nil
}
+93
View File
@@ -0,0 +1,93 @@
/*
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 credentials
import (
"context"
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"github.com/vmware-tanzu/velero/pkg/builder"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
)
func TestNamespacedFileStore(t *testing.T) {
testCases := []struct {
name string
namespace string
fsRoot string
secrets []*corev1.Secret
secretSelector *corev1.SecretKeySelector
wantErr string
expectedPath string
expectedContents string
}{
{
name: "returns an error if the secret can't be found",
secretSelector: builder.ForSecretKeySelector("non-existent-secret", "secret-key").Result(),
wantErr: "unable to get key for secret: secrets \"non-existent-secret\" not found",
},
{
name: "returns a filepath formed using fsRoot, namespace, secret name and key, with secret contents",
namespace: "ns1",
fsRoot: "/tmp/credentials",
secretSelector: builder.ForSecretKeySelector("credential", "key2").Result(),
secrets: []*corev1.Secret{
builder.ForSecret("ns1", "credential").Data(map[string][]byte{
"key1": []byte("ns1-secretdata1"),
"key2": []byte("ns1-secretdata2"),
"key3": []byte("ns1-secretdata3"),
}).Result(),
builder.ForSecret("ns2", "credential").Data(map[string][]byte{
"key2": []byte("ns2-secretdata2"),
}).Result(),
},
expectedPath: "/tmp/credentials/ns1/credential-key2",
expectedContents: "ns1-secretdata2",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := velerotest.NewFakeControllerRuntimeClient(t)
for _, secret := range tc.secrets {
require.NoError(t, client.Create(context.Background(), secret))
}
fs := velerotest.NewFakeFileSystem()
fileStore, err := NewNamespacedFileStore(client, tc.namespace, tc.fsRoot, fs)
require.NoError(t, err)
path, err := fileStore.Path(tc.secretSelector)
if tc.wantErr != "" {
require.EqualError(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
require.Equal(t, path, tc.expectedPath)
contents, err := fs.ReadFile(path)
require.NoError(t, err)
require.Equal(t, []byte(tc.expectedContents), contents)
})
}
}
+17 -1
View File
@@ -1,5 +1,5 @@
/*
Copyright 2020 the Velero contributors.
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.
@@ -19,6 +19,7 @@ package builder
import (
"time"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
@@ -83,6 +84,15 @@ func (b *BackupStorageLocationBuilder) Prefix(val string) *BackupStorageLocation
return b
}
// CACert sets the BackupStorageLocation's object storage CACert.
func (b *BackupStorageLocationBuilder) CACert(val []byte) *BackupStorageLocationBuilder {
if b.object.Spec.StorageType.ObjectStorage == nil {
b.object.Spec.StorageType.ObjectStorage = new(velerov1api.ObjectStorageLocation)
}
b.object.Spec.ObjectStorage.CACert = val
return b
}
// Default sets the BackupStorageLocation's is default or not
func (b *BackupStorageLocationBuilder) Default(isDefault bool) *BackupStorageLocationBuilder {
b.object.Spec.Default = isDefault
@@ -112,3 +122,9 @@ func (b *BackupStorageLocationBuilder) Phase(phase velerov1api.BackupStorageLoca
b.object.Status.Phase = phase
return b
}
// Credential sets the BackupStorageLocation's credential selector.
func (b *BackupStorageLocationBuilder) Credential(selector *corev1api.SecretKeySelector) *BackupStorageLocationBuilder {
b.object.Spec.Credential = selector
return b
}
+23 -6
View File
@@ -49,6 +49,7 @@ import (
snapshotv1beta1informers "github.com/kubernetes-csi/external-snapshotter/client/v4/informers/externalversions"
snapshotv1beta1listers "github.com/kubernetes-csi/external-snapshotter/client/v4/listers/volumesnapshot/v1beta1"
"github.com/vmware-tanzu/velero/internal/credentials"
"github.com/vmware-tanzu/velero/pkg/backup"
"github.com/vmware-tanzu/velero/pkg/buildinfo"
"github.com/vmware-tanzu/velero/pkg/client"
@@ -67,6 +68,7 @@ import (
"github.com/vmware-tanzu/velero/pkg/podexec"
"github.com/vmware-tanzu/velero/pkg/restic"
"github.com/vmware-tanzu/velero/pkg/restore"
"github.com/vmware-tanzu/velero/pkg/util/filesystem"
"github.com/vmware-tanzu/velero/pkg/util/logging"
ctrl "sigs.k8s.io/controller-runtime"
@@ -96,6 +98,8 @@ const (
defaultControllerWorkers = 1
// the default TTL for a backup
defaultBackupTTL = 30 * 24 * time.Hour
defaultCredentialsDirectory = "/tmp/credentials"
)
type serverConfig struct {
@@ -552,6 +556,19 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
newPluginManager := func(logger logrus.FieldLogger) clientmgmt.Manager {
return clientmgmt.NewManager(logger, s.logLevel, s.pluginRegistry)
}
// Create the credentials store which will fetch secrets from the Velero
// namespace and store them on the file system
credentialFileStore, err := credentials.NewNamespacedFileStore(
s.mgr.GetClient(),
s.namespace,
defaultCredentialsDirectory,
filesystem.NewFileSystem(),
)
cmd.CheckError(err)
backupStoreGetter := persistence.NewObjectBackupStoreGetter(credentialFileStore)
csiVSLister, csiVSCLister := s.getCSISnapshotListers()
backupSyncControllerRunInfo := func() controllerRunInfo {
@@ -566,7 +583,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
s.kubeClient,
s.config.defaultBackupLocation,
newPluginManager,
persistence.NewObjectBackupStoreGetter(),
backupStoreGetter,
s.logger,
)
@@ -609,7 +626,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
s.config.formatFlag.Parse(),
csiVSLister,
csiVSCLister,
persistence.NewObjectBackupStoreGetter(),
backupStoreGetter,
)
return controllerRunInfo{
@@ -666,7 +683,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
csiVSCLister,
s.csiSnapshotClient,
newPluginManager,
persistence.NewObjectBackupStoreGetter(),
backupStoreGetter,
s.metrics,
s.discoveryHelper,
)
@@ -705,7 +722,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
s.logger,
s.logLevel,
newPluginManager,
persistence.NewObjectBackupStoreGetter(),
backupStoreGetter,
s.metrics,
s.config.formatFlag.Parse(),
)
@@ -800,7 +817,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
ServerValidationFrequency: s.config.storeValidationFrequency,
},
NewPluginManager: newPluginManager,
BackupStoreGetter: persistence.NewObjectBackupStoreGetter(),
BackupStoreGetter: backupStoreGetter,
Log: s.logger,
}
if err := bslr.SetupWithManager(s.mgr); err != nil {
@@ -827,7 +844,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
Client: s.mgr.GetClient(),
Clock: clock.RealClock{},
NewPluginManager: newPluginManager,
BackupStoreGetter: persistence.NewObjectBackupStoreGetter(),
BackupStoreGetter: backupStoreGetter,
Log: s.logger,
}
if err := r.SetupWithManager(s.mgr); err != nil {
@@ -45,7 +45,6 @@ import (
"github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/fake"
informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions"
"github.com/vmware-tanzu/velero/pkg/metrics"
"github.com/vmware-tanzu/velero/pkg/persistence"
persistencemocks "github.com/vmware-tanzu/velero/pkg/persistence/mocks"
"github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt"
pluginmocks "github.com/vmware-tanzu/velero/pkg/plugin/mocks"
@@ -75,7 +74,7 @@ func TestBackupDeletionControllerProcessQueueItem(t *testing.T) {
nil, // csiSnapshotContentLister
nil, // csiSnapshotClient
nil, // new plugin manager func
persistence.NewObjectBackupStoreGetter(),
nil, // backupStoreGetter
metrics.NewServerMetrics(),
nil, // discovery helper
).(*backupDeletionController)
@@ -1131,7 +1130,7 @@ func TestBackupDeletionControllerDeleteExpiredRequests(t *testing.T) {
nil, // csiSnapshotContentLister
nil, // csiSnapshotClient
nil, // new plugin manager func
persistence.NewObjectBackupStoreGetter(),
nil, // backupStoreGetter
metrics.NewServerMetrics(),
nil, // discovery helper,
).(*backupDeletionController)
@@ -21,8 +21,6 @@ import (
"testing"
"time"
"github.com/vmware-tanzu/velero/pkg/persistence"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -573,7 +571,7 @@ func TestDeleteOrphanedBackups(t *testing.T) {
nil, // kubeClient
"",
nil, // new plugin manager func
persistence.NewObjectBackupStoreGetter(),
nil, // backupStoreGetter
velerotest.NewLogger(),
).(*backupSyncController)
@@ -667,7 +665,7 @@ func TestStorageLabelsInDeleteOrphanedBackups(t *testing.T) {
nil, // kubeClient
"",
nil, // new plugin manager func
persistence.NewObjectBackupStoreGetter(),
nil, // backupStoreGetter
velerotest.NewLogger(),
).(*backupSyncController)
@@ -221,7 +221,7 @@ func (c *podVolumeBackupController) processBackup(req *velerov1api.PodVolumeBack
log.WithField("path", path).Debugf("Found path matching glob")
// temp creds
credentialsFile, err := restic.TempCredentialsFile(c.kbClient, req.Namespace, req.Spec.Pod.Namespace, c.fileSystem)
credentialsFile, err := restic.TempCredentialsFile(c.kbClient, req.Namespace, c.fileSystem)
if err != nil {
log.WithError(err).Error("Error creating temp restic credentials file")
return c.fail(req, errors.Wrap(err, "error creating temp restic credentials file").Error(), log)
@@ -300,7 +300,7 @@ func (c *podVolumeRestoreController) processRestore(req *velerov1api.PodVolumeRe
return c.failRestore(req, errors.Wrap(err, "error getting volume directory name").Error(), log)
}
credsFile, err := restic.TempCredentialsFile(c.kbClient, req.Namespace, req.Spec.Pod.Namespace, c.fileSystem)
credsFile, err := restic.TempCredentialsFile(c.kbClient, req.Namespace, c.fileSystem)
if err != nil {
log.WithError(err).Error("Error creating temp restic credentials file")
return c.failRestore(req, errors.Wrap(err, "error creating temp restic credentials file").Error(), log)
+2 -3
View File
@@ -42,7 +42,6 @@ import (
informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions"
listers "github.com/vmware-tanzu/velero/pkg/generated/listers/velero/v1"
"github.com/vmware-tanzu/velero/pkg/metrics"
"github.com/vmware-tanzu/velero/pkg/persistence"
persistencemocks "github.com/vmware-tanzu/velero/pkg/persistence/mocks"
"github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt"
pluginmocks "github.com/vmware-tanzu/velero/pkg/plugin/mocks"
@@ -209,7 +208,7 @@ func TestProcessQueueItemSkips(t *testing.T) {
logger,
logrus.InfoLevel,
nil,
persistence.NewObjectBackupStoreGetter(),
nil, // backupStoreGetter
metrics.NewServerMetrics(),
formatFlag,
).(*restoreController)
@@ -666,7 +665,7 @@ func TestvalidateAndCompleteWhenScheduleNameSpecified(t *testing.T) {
logger,
logrus.DebugLevel,
nil,
persistence.NewObjectBackupStoreGetter(),
nil, // backupStoreGetter
nil,
formatFlag,
).(*restoreController)
+4 -2
View File
@@ -1,6 +1,6 @@
/*
Copyright 2018, 2019 the Velero contributors.
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.
@@ -31,7 +31,8 @@ type BucketData map[string][]byte
// that stores its data in-memory/in-proc. This is mainly intended to be used
// as a test fake.
type inMemoryObjectStore struct {
Data map[string]BucketData
Data map[string]BucketData
Config map[string]string
}
func newInMemoryObjectStore(buckets ...string) *inMemoryObjectStore {
@@ -51,6 +52,7 @@ func newInMemoryObjectStore(buckets ...string) *inMemoryObjectStore {
//
func (o *inMemoryObjectStore) Init(config map[string]string) error {
o.Config = config
return nil
}
+29 -15
View File
@@ -1,5 +1,5 @@
/*
Copyright 2018, 2020 the Velero contributors.
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.
@@ -25,10 +25,12 @@ import (
"time"
snapshotv1beta1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1beta1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
kerrors "k8s.io/apimachinery/pkg/util/errors"
"github.com/vmware-tanzu/velero/internal/credentials"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/scheme"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
@@ -96,12 +98,13 @@ type ObjectBackupStoreGetter interface {
Get(location *velerov1api.BackupStorageLocation, objectStoreGetter ObjectStoreGetter, logger logrus.FieldLogger) (BackupStore, error)
}
type objectBackupStoreGetter struct{}
type objectBackupStoreGetter struct {
credentialStore credentials.FileStore
}
// NewObjectBackupStoreGetter returns a ObjectBackupStoreGetter that can get a
// default velero.BackupStore.
func NewObjectBackupStoreGetter() ObjectBackupStoreGetter {
return &objectBackupStoreGetter{}
// NewObjectBackupStoreGetter returns a ObjectBackupStoreGetter that can get a velero.BackupStore.
func NewObjectBackupStoreGetter(credentialStore credentials.FileStore) ObjectBackupStoreGetter {
return &objectBackupStoreGetter{credentialStore: credentialStore}
}
func (b *objectBackupStoreGetter) Get(location *velerov1api.BackupStorageLocation, objectStoreGetter ObjectStoreGetter, logger logrus.FieldLogger) (BackupStore, error) {
@@ -127,16 +130,27 @@ func (b *objectBackupStoreGetter) Get(location *velerov1api.BackupStorageLocatio
// add the bucket name and prefix to the config map so that object stores
// can use them when initializing. The AWS object store uses the bucket
// name to determine the bucket's region when setting up its client.
if location.Spec.ObjectStorage != nil {
if location.Spec.Config == nil {
location.Spec.Config = make(map[string]string)
}
location.Spec.Config["bucket"] = bucket
location.Spec.Config["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 {
location.Spec.Config["caCert"] = string(location.Spec.ObjectStorage.CACert)
if location.Spec.Config == nil {
location.Spec.Config = make(map[string]string)
}
location.Spec.Config["bucket"] = bucket
location.Spec.Config["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 {
location.Spec.Config["caCert"] = string(location.Spec.ObjectStorage.CACert)
}
// If the BSL specifies a credential, fetch its path on disk and pass to
// plugin via the config.
if location.Spec.Credential != nil {
credsFile, err := b.credentialStore.Path(location.Spec.Credential)
if err != nil {
return nil, errors.Wrap(err, "unable to get credentials")
}
location.Spec.Config["credentialsFile"] = credsFile
}
objectStore, err := objectStoreGetter.GetObjectStore(location.Spec.Provider)
+102 -17
View File
@@ -1,5 +1,5 @@
/*
Copyright 2017 the Velero contributors.
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.
@@ -21,6 +21,7 @@ import (
"compress/gzip"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"sort"
@@ -32,6 +33,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"github.com/vmware-tanzu/velero/internal/credentials"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/builder"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
@@ -612,24 +614,37 @@ func TestNewObjectBackupStoreGetter(t *testing.T) {
name string
location *velerov1api.BackupStorageLocation
objectStoreGetter objectStoreGetter
credFileStore credentials.FileStore
fileStoreErr error
wantBucket string
wantPrefix string
wantErr string
}{
{
name: "location with no ObjectStorage field results in an error",
location: new(velerov1api.BackupStorageLocation),
wantErr: "backup storage location does not use object storage",
name: "when location does not use object storage, a backup store can't be retrieved",
location: new(velerov1api.BackupStorageLocation),
credFileStore: velerotest.NewFakeCredentialsFileStore("", nil),
wantErr: "backup storage location does not use object storage",
},
{
name: "location with no Provider field results in an error",
location: builder.ForBackupStorageLocation("", "").Bucket("").Result(),
wantErr: "object storage provider name must not be empty",
name: "when object storage does not specify a provider, a backup store can't be retrieved",
location: builder.ForBackupStorageLocation("", "").Bucket("").Result(),
credFileStore: velerotest.NewFakeCredentialsFileStore("", nil),
wantErr: "object storage provider name must not be empty",
},
{
name: "location with a Bucket field with a '/' in the middle results in an error",
location: builder.ForBackupStorageLocation("", "").Provider("provider-1").Bucket("invalid/bucket").Result(),
wantErr: "backup storage location's bucket name \"invalid/bucket\" must not contain a '/' (if using a prefix, put it in the 'Prefix' field instead)",
name: "when the Bucket field has a '/' in the middle, a backup store can't be retrieved",
location: builder.ForBackupStorageLocation("", "").Provider("provider-1").Bucket("invalid/bucket").Result(),
credFileStore: velerotest.NewFakeCredentialsFileStore("", nil),
wantErr: "backup storage location's bucket name \"invalid/bucket\" must not contain a '/' (if using a prefix, put it in the 'Prefix' field instead)",
},
{
name: "when the credential selector is invalid, a backup store can't be retrieved",
location: builder.ForBackupStorageLocation("", "").Provider("provider-1").Bucket("bucket").Credential(
builder.ForSecretKeySelector("does-not-exist", "does-not-exist").Result(),
).Result(),
credFileStore: velerotest.NewFakeCredentialsFileStore("", fmt.Errorf("secret does not exist")),
wantErr: "unable to get credentials: secret does not exist",
},
{
name: "when Bucket has a leading and trailing slash, they are both stripped",
@@ -637,7 +652,8 @@ func TestNewObjectBackupStoreGetter(t *testing.T) {
objectStoreGetter: objectStoreGetter{
"provider-1": newInMemoryObjectStore("bucket"),
},
wantBucket: "bucket",
credFileStore: velerotest.NewFakeCredentialsFileStore("", nil),
wantBucket: "bucket",
},
{
name: "when Prefix has a leading and trailing slash, the leading slash is stripped and the trailing slash is left",
@@ -645,8 +661,9 @@ func TestNewObjectBackupStoreGetter(t *testing.T) {
objectStoreGetter: objectStoreGetter{
"provider-1": newInMemoryObjectStore("bucket"),
},
wantBucket: "bucket",
wantPrefix: "prefix/",
credFileStore: velerotest.NewFakeCredentialsFileStore("", nil),
wantBucket: "bucket",
wantPrefix: "prefix/",
},
{
name: "when Prefix has no leading or trailing slash, a trailing slash is added",
@@ -654,17 +671,18 @@ func TestNewObjectBackupStoreGetter(t *testing.T) {
objectStoreGetter: objectStoreGetter{
"provider-1": newInMemoryObjectStore("bucket"),
},
wantBucket: "bucket",
wantPrefix: "prefix/",
credFileStore: velerotest.NewFakeCredentialsFileStore("", nil),
wantBucket: "bucket",
wantPrefix: "prefix/",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
getter := NewObjectBackupStoreGetter()
getter := NewObjectBackupStoreGetter(tc.credFileStore)
res, err := getter.Get(tc.location, tc.objectStoreGetter, velerotest.NewLogger())
if tc.wantErr != "" {
require.Equal(t, tc.wantErr, err.Error())
require.EqualError(t, err, tc.wantErr)
} else {
require.Nil(t, err)
@@ -678,6 +696,73 @@ func TestNewObjectBackupStoreGetter(t *testing.T) {
}
}
// TestNewObjectBackupStoreGetterConfig runs the NewObjectBackupStoreGetter constructor and ensures
// that it initializes the ObjectBackupStore with the correct config.
func TestNewObjectBackupStoreGetterConfig(t *testing.T) {
provider := "provider"
bucket := "bucket"
tests := []struct {
name string
location *velerov1api.BackupStorageLocation
getter ObjectBackupStoreGetter
credentialPath string
wantConfig map[string]string
}{
{
name: "location with bucket but no prefix has config initialized with bucket and empty prefix",
location: builder.ForBackupStorageLocation("", "").Provider(provider).Bucket(bucket).Result(),
getter: NewObjectBackupStoreGetter(velerotest.NewFakeCredentialsFileStore("", nil)),
wantConfig: map[string]string{
"bucket": "bucket",
"prefix": "",
},
},
{
name: "location with bucket and prefix has config initialized with bucket and prefix",
location: builder.ForBackupStorageLocation("", "").Provider(provider).Bucket(bucket).Prefix("prefix").Result(),
getter: NewObjectBackupStoreGetter(velerotest.NewFakeCredentialsFileStore("", nil)),
wantConfig: map[string]string{
"bucket": "bucket",
"prefix": "prefix",
},
},
{
name: "location with CACert is initialized with caCert",
location: builder.ForBackupStorageLocation("", "").Provider(provider).Bucket(bucket).CACert([]byte("cacert-data")).Result(),
getter: NewObjectBackupStoreGetter(velerotest.NewFakeCredentialsFileStore("", nil)),
wantConfig: map[string]string{
"bucket": "bucket",
"prefix": "",
"caCert": "cacert-data",
},
},
{
name: "location with Credential is initialized with path of serialized secret",
location: builder.ForBackupStorageLocation("", "").Provider(provider).Bucket(bucket).Credential(
builder.ForSecretKeySelector("does-not-exist", "does-not-exist").Result(),
).Result(),
getter: NewObjectBackupStoreGetter(velerotest.NewFakeCredentialsFileStore("/tmp/credentials/secret-file", nil)),
wantConfig: map[string]string{
"bucket": "bucket",
"prefix": "",
"credentialsFile": "/tmp/credentials/secret-file",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
objStore := newInMemoryObjectStore(bucket)
objStoreGetter := &objectStoreGetter{provider: objStore}
_, err := tc.getter.Get(tc.location, objStoreGetter, velerotest.NewLogger())
require.NoError(t, err)
require.Equal(t, tc.wantConfig, objStore.Config)
})
}
}
func encodeToBytes(obj runtime.Object) []byte {
res, err := encode.Encode(obj, "json")
if err != nil {
+5 -6
View File
@@ -238,11 +238,10 @@ func GetSnapshotsInBackup(backup *velerov1api.Backup, podVolumeBackupLister vele
return res, nil
}
// TempCredentialsFile creates a temp file containing a restic
// encryption key for the given repo and returns its path. The
// caller should generally call os.Remove() to remove the file
// when done with it.
func TempCredentialsFile(client kbclient.Client, veleroNamespace, repoName string, fs filesystem.Interface) (string, error) {
// TempCredentialsFile creates a temp file containing the restic
// encryption key and returns its path. The caller should generally
// call os.Remove() to remove the file when done with it.
func TempCredentialsFile(client kbclient.Client, veleroNamespace string, fs filesystem.Interface) (string, error) {
// 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
@@ -254,7 +253,7 @@ func TempCredentialsFile(client kbclient.Client, veleroNamespace, repoName strin
return "", err
}
file, err := fs.TempFile("", fmt.Sprintf("%s-%s", CredentialsSecretName, repoName))
file, err := fs.TempFile("", fmt.Sprintf("%s-%s", CredentialsSecretName, CredentialsKey))
if err != nil {
return "", errors.WithStack(err)
}
+2 -2
View File
@@ -374,14 +374,14 @@ func TestTempCredentialsFile(t *testing.T) {
)
// secret not in server: expect an error
fileName, err := TempCredentialsFile(fakeClient, "velero", "default", fs)
fileName, err := TempCredentialsFile(fakeClient, "velero", fs)
assert.Error(t, err)
// now add secret
require.NoError(t, fakeClient.Create(context.Background(), secret))
// secret in server: expect temp file to be created with password
fileName, err = TempCredentialsFile(fakeClient, "velero", "default", fs)
fileName, err = TempCredentialsFile(fakeClient, "velero", fs)
require.NoError(t, err)
contents, err := fs.ReadFile(fileName)
+1 -1
View File
@@ -227,7 +227,7 @@ func (rm *repositoryManager) Forget(ctx context.Context, snapshot SnapshotIdenti
}
func (rm *repositoryManager) exec(cmd *Command, backupLocation string) error {
file, err := TempCredentialsFile(rm.kbClient, rm.namespace, cmd.RepoName(), rm.fileSystem)
file, err := TempCredentialsFile(rm.kbClient, rm.namespace, rm.fileSystem)
if err != nil {
return err
}
+49
View File
@@ -0,0 +1,49 @@
/*
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 test
import (
corev1api "k8s.io/api/core/v1"
)
// FileStore defines operations for interacting with credentials
// that are stored on a file system.
type FileStore interface {
// Path returns a path on disk where the secret key defined by
// the given selector is serialized.
Path(selector *corev1api.SecretKeySelector) (string, error)
}
type fakeCredentialsFileStore struct {
path string
err error
}
// Path returns a path on disk where the secret key defined by
// the given selector is serialized.
func (f *fakeCredentialsFileStore) Path(*corev1api.SecretKeySelector) (string, error) {
return f.path, f.err
}
// NewFakeCredentialFileStore creates a FileStore which will return the given path
// and error when Path is called.
func NewFakeCredentialsFileStore(path string, err error) FileStore {
return &fakeCredentialsFileStore{
path: path,
err: err,
}
}
+5 -1
View File
@@ -1,5 +1,5 @@
/*
Copyright 2018 the Velero contributors.
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.
@@ -48,6 +48,10 @@ func (fs *FakeFileSystem) Create(name string) (io.WriteCloser, error) {
return fs.fs.Create(name)
}
func (fs *FakeFileSystem) OpenFile(name string, flag int, perm os.FileMode) (io.WriteCloser, error) {
return fs.fs.OpenFile(name, flag, perm)
}
func (fs *FakeFileSystem) RemoveAll(path string) error {
return fs.fs.RemoveAll(path)
}
+6 -1
View File
@@ -1,5 +1,5 @@
/*
Copyright 2017 the Velero contributors.
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.
@@ -28,6 +28,7 @@ type Interface interface {
TempDir(dir, prefix string) (string, error)
MkdirAll(path string, perm os.FileMode) error
Create(name string) (io.WriteCloser, error)
OpenFile(name string, flag int, perm os.FileMode) (io.WriteCloser, error)
RemoveAll(path string) error
ReadDir(dirname string) ([]os.FileInfo, error)
ReadFile(filename string) ([]byte, error)
@@ -60,6 +61,10 @@ func (fs *osFileSystem) Create(name string) (io.WriteCloser, error) {
return os.Create(name)
}
func (fs *osFileSystem) OpenFile(name string, flag int, perm os.FileMode) (io.WriteCloser, error) {
return os.OpenFile(name, flag, perm)
}
func (fs *osFileSystem) RemoveAll(path string) error {
return os.RemoveAll(path)
}