mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-09 01:26:30 +00:00
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:
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user