Merge pull request #710 from skriss/resumable-plugins-rebased

Resumable plugins rebased
This commit is contained in:
Nolan Brubaker
2018-07-31 11:57:11 -04:00
committed by GitHub
101 changed files with 5991 additions and 1891 deletions
+2 -10
View File
@@ -40,14 +40,13 @@ import (
"github.com/heptio/ark/pkg/restic"
"github.com/heptio/ark/pkg/util/collections"
kubeutil "github.com/heptio/ark/pkg/util/kube"
"github.com/heptio/ark/pkg/util/logging"
)
// Backupper performs backups.
type Backupper interface {
// Backup takes a backup using the specification in the api.Backup and writes backup and log data
// to the given writers.
Backup(backup *api.Backup, backupFile, logFile io.Writer, actions []ItemAction) error
Backup(logger logrus.FieldLogger, backup *api.Backup, backupFile io.Writer, actions []ItemAction) error
}
// kubernetesBackupper implements Backupper.
@@ -212,20 +211,13 @@ func getResourceHook(hookSpec api.BackupResourceHookSpec, discoveryHelper discov
// Backup backs up the items specified in the Backup, placing them in a gzip-compressed tar file
// written to backupFile. The finalized api.Backup is written to metadata.
func (kb *kubernetesBackupper) Backup(backup *api.Backup, backupFile, logFile io.Writer, actions []ItemAction) error {
func (kb *kubernetesBackupper) Backup(logger logrus.FieldLogger, backup *api.Backup, backupFile io.Writer, actions []ItemAction) error {
gzippedData := gzip.NewWriter(backupFile)
defer gzippedData.Close()
tw := tar.NewWriter(gzippedData)
defer tw.Close()
gzippedLog := gzip.NewWriter(logFile)
defer gzippedLog.Close()
logger := logrus.New()
logger.Out = gzippedLog
logger.Hooks.Add(&logging.ErrorLocationHook{})
logger.Hooks.Add(&logging.LogLocationHook{})
log := logger.WithField("backup", kubeutil.NamespaceAndName(backup))
log.Info("Starting backup")
+2 -2
View File
@@ -32,8 +32,8 @@ type backupPVAction struct {
log logrus.FieldLogger
}
func NewBackupPVAction(log logrus.FieldLogger) ItemAction {
return &backupPVAction{log: log}
func NewBackupPVAction(logger logrus.FieldLogger) ItemAction {
return &backupPVAction{log: logger}
}
func (a *backupPVAction) AppliesTo() (ResourceSelector, error) {
+5 -19
View File
@@ -18,8 +18,6 @@ package backup
import (
"bytes"
"compress/gzip"
"io"
"reflect"
"sort"
"testing"
@@ -46,6 +44,7 @@ import (
"github.com/heptio/ark/pkg/restic"
"github.com/heptio/ark/pkg/util/collections"
kubeutil "github.com/heptio/ark/pkg/util/kube"
"github.com/heptio/ark/pkg/util/logging"
arktest "github.com/heptio/ark/pkg/util/test"
)
@@ -549,22 +548,9 @@ func TestBackup(t *testing.T) {
groupBackupper.On("backupGroup", group).Return(err)
}
var backupFile, logFile bytes.Buffer
var backupFile bytes.Buffer
err = b.Backup(test.backup, &backupFile, &logFile, nil)
defer func() {
// print log if anything failed
if t.Failed() {
gzr, err := gzip.NewReader(&logFile)
require.NoError(t, err)
t.Log("Backup log contents:")
var buf bytes.Buffer
_, err = io.Copy(&buf, gzr)
require.NoError(t, err)
require.NoError(t, gzr.Close())
t.Log(buf.String())
}
}()
err = b.Backup(logging.DefaultLogger(logrus.DebugLevel), test.backup, &backupFile, nil)
if test.expectedError != nil {
assert.EqualError(t, err, test.expectedError.Error())
@@ -610,7 +596,7 @@ func TestBackupUsesNewCohabitatingResourcesForEachBackup(t *testing.T) {
mock.Anything,
).Return(&mockGroupBackupper{})
assert.NoError(t, b.Backup(&v1.Backup{}, &bytes.Buffer{}, &bytes.Buffer{}, nil))
assert.NoError(t, b.Backup(arktest.NewLogger(), &v1.Backup{}, &bytes.Buffer{}, nil))
groupBackupperFactory.AssertExpectations(t)
// mutate the cohabitatingResources map that was used in the first backup to simulate
@@ -642,7 +628,7 @@ func TestBackupUsesNewCohabitatingResourcesForEachBackup(t *testing.T) {
mock.Anything,
).Return(&mockGroupBackupper{})
assert.NoError(t, b.Backup(&v1.Backup{}, &bytes.Buffer{}, &bytes.Buffer{}, nil))
assert.NoError(t, b.Backup(arktest.NewLogger(), &v1.Backup{}, &bytes.Buffer{}, nil))
assert.NotEqual(t, firstCohabitatingResources, secondCohabitatingResources)
for _, resource := range secondCohabitatingResources {
assert.False(t, resource.seen)
-5
View File
@@ -41,7 +41,6 @@ import (
"github.com/heptio/ark/pkg/podexec"
"github.com/heptio/ark/pkg/restic"
"github.com/heptio/ark/pkg/util/collections"
"github.com/heptio/ark/pkg/util/logging"
)
type itemBackupperFactory interface {
@@ -320,10 +319,6 @@ func (ib *defaultItemBackupper) executeActions(
log.Info("Executing custom action")
if logSetter, ok := action.ItemAction.(logging.LogSetter); ok {
logSetter.SetLog(log)
}
updatedItem, additionalItemIdentifiers, err := action.Execute(obj, ib.backup)
if err != nil {
// We want this to show up in the log file at the place where the error occurs. When we return
+15
View File
@@ -1,3 +1,18 @@
/*
Copyright 2018 the Heptio Ark 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.
*/
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
+2 -2
View File
@@ -34,8 +34,8 @@ type podAction struct {
}
// NewPodAction creates a new ItemAction for pods.
func NewPodAction(log logrus.FieldLogger) ItemAction {
return &podAction{log: log}
func NewPodAction(logger logrus.FieldLogger) ItemAction {
return &podAction{log: logger}
}
// AppliesTo returns a ResourceSelector that applies only to pods.
+2 -2
View File
@@ -38,7 +38,7 @@ type serviceAccountAction struct {
}
// NewServiceAccountAction creates a new ItemAction for service accounts.
func NewServiceAccountAction(log logrus.FieldLogger, clusterRoleBindingListers map[string]ClusterRoleBindingLister, discoveryHelper arkdiscovery.Helper) (ItemAction, error) {
func NewServiceAccountAction(logger logrus.FieldLogger, clusterRoleBindingListers map[string]ClusterRoleBindingLister, discoveryHelper arkdiscovery.Helper) (ItemAction, error) {
// Look up the supported RBAC version
var supportedAPI metav1.GroupVersionForDiscovery
for _, ag := range discoveryHelper.APIGroups() {
@@ -58,7 +58,7 @@ func NewServiceAccountAction(log logrus.FieldLogger, clusterRoleBindingListers m
}
return &serviceAccountAction{
log: log,
log: logger,
clusterRoleBindings: crbs,
}, nil
}
+15
View File
@@ -1,3 +1,18 @@
/*
Copyright 2018 the Heptio Ark 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 buildinfo
import (
+15
View File
@@ -1,3 +1,18 @@
/*
Copyright 2018 the Heptio Ark 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 client
import (
+4 -2
View File
@@ -24,6 +24,7 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
@@ -40,6 +41,7 @@ const regionKey = "region"
var iopsVolumeTypes = sets.NewString("io1")
type blockStore struct {
log logrus.FieldLogger
ec2 *ec2.EC2
}
@@ -56,8 +58,8 @@ func getSession(config *aws.Config) (*session.Session, error) {
return sess, nil
}
func NewBlockStore() cloudprovider.BlockStore {
return &blockStore{}
func NewBlockStore(logger logrus.FieldLogger) cloudprovider.BlockStore {
return &blockStore{log: logger}
}
func (b *blockStore) Init(config map[string]string) error {
+4 -2
View File
@@ -26,6 +26,7 @@ import (
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/heptio/ark/pkg/cloudprovider"
)
@@ -38,13 +39,14 @@ const (
)
type objectStore struct {
log logrus.FieldLogger
s3 *s3.S3
s3Uploader *s3manager.Uploader
kmsKeyID string
}
func NewObjectStore() cloudprovider.ObjectStore {
return &objectStore{}
func NewObjectStore(logger logrus.FieldLogger) cloudprovider.ObjectStore {
return &objectStore{log: logger}
}
func (o *objectStore) Init(config map[string]string) error {
+4 -2
View File
@@ -31,6 +31,7 @@ import (
"github.com/Azure/go-autorest/autorest/azure"
"github.com/pkg/errors"
"github.com/satori/uuid"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/runtime"
@@ -52,6 +53,7 @@ const (
)
type blockStore struct {
log logrus.FieldLogger
disks *disk.DisksClient
snaps *disk.SnapshotsClient
subscription string
@@ -87,8 +89,8 @@ func getConfig() map[string]string {
return cfg
}
func NewBlockStore() cloudprovider.BlockStore {
return &blockStore{}
func NewBlockStore(logger logrus.FieldLogger) cloudprovider.BlockStore {
return &blockStore{log: logger}
}
func (b *blockStore) Init(config map[string]string) error {
+4 -2
View File
@@ -23,16 +23,18 @@ import (
"github.com/Azure/azure-sdk-for-go/storage"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/heptio/ark/pkg/cloudprovider"
)
type objectStore struct {
blobClient *storage.BlobStorageClient
log logrus.FieldLogger
}
func NewObjectStore() cloudprovider.ObjectStore {
return &objectStore{}
func NewObjectStore(logger logrus.FieldLogger) cloudprovider.ObjectStore {
return &objectStore{log: logger}
}
func (o *objectStore) Init(config map[string]string) error {
+8 -8
View File
@@ -28,15 +28,15 @@ import (
"github.com/heptio/ark/pkg/apis/ark/v1"
)
// backupCacheBucket holds the backups and error from a GetAllBackups call.
// backupCacheBucket holds the backups and error from a ListBackups call.
type backupCacheBucket struct {
backups []*v1.Backup
error error
}
// backupCache caches GetAllBackups calls, refreshing them periodically.
// backupCache caches ListBackups calls, refreshing them periodically.
type backupCache struct {
delegate BackupGetter
delegate BackupLister
lock sync.RWMutex
// This doesn't really need to be a map right now, but if we ever move to supporting multiple
// buckets, this will be ready for it.
@@ -44,10 +44,10 @@ type backupCache struct {
logger logrus.FieldLogger
}
var _ BackupGetter = &backupCache{}
var _ BackupLister = &backupCache{}
// NewBackupCache returns a new backup cache that refreshes from delegate every resyncPeriod.
func NewBackupCache(ctx context.Context, delegate BackupGetter, resyncPeriod time.Duration, logger logrus.FieldLogger) BackupGetter {
func NewBackupCache(ctx context.Context, delegate BackupLister, resyncPeriod time.Duration, logger logrus.FieldLogger) BackupLister {
c := &backupCache{
delegate: delegate,
buckets: make(map[string]*backupCacheBucket),
@@ -70,11 +70,11 @@ func (c *backupCache) refresh() {
for bucketName, bucket := range c.buckets {
c.logger.WithField("bucket", bucketName).Debug("Refreshing bucket")
bucket.backups, bucket.error = c.delegate.GetAllBackups(bucketName)
bucket.backups, bucket.error = c.delegate.ListBackups(bucketName)
}
}
func (c *backupCache) GetAllBackups(bucketName string) ([]*v1.Backup, error) {
func (c *backupCache) ListBackups(bucketName string) ([]*v1.Backup, error) {
c.lock.RLock()
bucket, found := c.buckets[bucketName]
c.lock.RUnlock()
@@ -88,7 +88,7 @@ func (c *backupCache) GetAllBackups(bucketName string) ([]*v1.Backup, error) {
logContext.Debug("Bucket is not in cache - doing a live lookup")
backups, err := c.delegate.GetAllBackups(bucketName)
backups, err := c.delegate.ListBackups(bucketName)
c.lock.Lock()
c.buckets[bucketName] = &backupCacheBucket{backups: backups, error: err}
c.lock.Unlock()
+17 -17
View File
@@ -25,13 +25,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/heptio/ark/pkg/apis/ark/v1"
cloudprovidermocks "github.com/heptio/ark/pkg/cloudprovider/mocks"
"github.com/heptio/ark/pkg/util/test"
)
func TestNewBackupCache(t *testing.T) {
var (
delegate = &test.FakeBackupService{}
delegate = &cloudprovidermocks.BackupLister{}
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
logger = test.NewLogger()
)
@@ -44,32 +44,32 @@ func TestNewBackupCache(t *testing.T) {
test.NewTestBackup().WithName("backup1").Backup,
test.NewTestBackup().WithName("backup2").Backup,
}
delegate.On("GetAllBackups", "bucket1").Return(bucket1, nil).Once()
delegate.On("ListBackups", "bucket1").Return(bucket1, nil).Once()
// should be updated via refresh
updatedBucket1 := []*v1.Backup{
test.NewTestBackup().WithName("backup2").Backup,
}
delegate.On("GetAllBackups", "bucket1").Return(updatedBucket1, nil)
delegate.On("ListBackups", "bucket1").Return(updatedBucket1, nil)
// nothing in cache, live lookup
bucket2 := []*v1.Backup{
test.NewTestBackup().WithName("backup5").Backup,
test.NewTestBackup().WithName("backup6").Backup,
}
delegate.On("GetAllBackups", "bucket2").Return(bucket2, nil).Once()
delegate.On("ListBackups", "bucket2").Return(bucket2, nil).Once()
// should be updated via refresh
updatedBucket2 := []*v1.Backup{
test.NewTestBackup().WithName("backup7").Backup,
}
delegate.On("GetAllBackups", "bucket2").Return(updatedBucket2, nil)
delegate.On("ListBackups", "bucket2").Return(updatedBucket2, nil)
backups, err := c.GetAllBackups("bucket1")
backups, err := c.ListBackups("bucket1")
assert.Equal(t, bucket1, backups)
assert.NoError(t, err)
backups, err = c.GetAllBackups("bucket2")
backups, err = c.ListBackups("bucket2")
assert.Equal(t, bucket2, backups)
assert.NoError(t, err)
@@ -84,14 +84,14 @@ func TestNewBackupCache(t *testing.T) {
}
}
backups, err = c.GetAllBackups("bucket1")
backups, err = c.ListBackups("bucket1")
if len(backups) == 1 {
if assert.Equal(t, updatedBucket1[0], backups[0]) {
done1 = true
}
}
backups, err = c.GetAllBackups("bucket2")
backups, err = c.ListBackups("bucket2")
if len(backups) == 1 {
if assert.Equal(t, updatedBucket2[0], backups[0]) {
done2 = true
@@ -103,7 +103,7 @@ func TestNewBackupCache(t *testing.T) {
func TestBackupCacheRefresh(t *testing.T) {
var (
delegate = &test.FakeBackupService{}
delegate = &cloudprovidermocks.BackupLister{}
logger = test.NewLogger()
)
@@ -120,9 +120,9 @@ func TestBackupCacheRefresh(t *testing.T) {
test.NewTestBackup().WithName("backup1").Backup,
test.NewTestBackup().WithName("backup2").Backup,
}
delegate.On("GetAllBackups", "bucket1").Return(bucket1, nil)
delegate.On("ListBackups", "bucket1").Return(bucket1, nil)
delegate.On("GetAllBackups", "bucket2").Return(nil, errors.New("bad"))
delegate.On("ListBackups", "bucket2").Return(nil, errors.New("bad"))
c.refresh()
@@ -135,7 +135,7 @@ func TestBackupCacheRefresh(t *testing.T) {
func TestBackupCacheGetAllBackupsUsesCacheIfPresent(t *testing.T) {
var (
delegate = &test.FakeBackupService{}
delegate = &cloudprovidermocks.BackupLister{}
logger = test.NewLogger()
bucket1 = []*v1.Backup{
test.NewTestBackup().WithName("backup1").Backup,
@@ -158,13 +158,13 @@ func TestBackupCacheGetAllBackupsUsesCacheIfPresent(t *testing.T) {
test.NewTestBackup().WithName("backup4").Backup,
}
delegate.On("GetAllBackups", "bucket2").Return(bucket2, nil)
delegate.On("ListBackups", "bucket2").Return(bucket2, nil)
backups, err := c.GetAllBackups("bucket1")
backups, err := c.ListBackups("bucket1")
assert.Equal(t, bucket1, backups)
assert.NoError(t, err)
backups, err = c.GetAllBackups("bucket2")
backups, err = c.ListBackups("bucket2")
assert.Equal(t, bucket2, backups)
assert.NoError(t, err)
}
+102 -120
View File
@@ -17,7 +17,6 @@ limitations under the License.
package cloudprovider
import (
"context"
"fmt"
"io"
"io/ioutil"
@@ -26,47 +25,16 @@ import (
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/runtime"
kerrors "k8s.io/apimachinery/pkg/util/errors"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/generated/clientset/versioned/scheme"
)
// BackupService contains methods for working with backups in object storage.
type BackupService interface {
BackupGetter
// UploadBackup uploads the specified Ark backup of a set of Kubernetes API objects, whose manifests are
// stored in the specified file, into object storage in an Ark bucket, tagged with Ark metadata. Returns
// an error if a problem is encountered accessing the file or performing the upload via the cloud API.
UploadBackup(bucket, name string, metadata, backup, log io.Reader) error
// DownloadBackup downloads an Ark backup with the specified object key from object storage via the cloud API.
// It returns the snapshot metadata and data (separately), or an error if a problem is encountered
// downloading or reading the file from the cloud API.
DownloadBackup(bucket, name string) (io.ReadCloser, error)
// DeleteBackupDir deletes all files in object storage for the given backup.
DeleteBackupDir(bucket, backupName string) error
// GetBackup gets the specified api.Backup from the given bucket in object storage.
GetBackup(bucket, name string) (*api.Backup, error)
// CreateSignedURL creates a pre-signed URL that can be used to download a file from object
// storage. The URL expires after ttl.
CreateSignedURL(target api.DownloadTarget, bucket, directory string, ttl time.Duration) (string, error)
// UploadRestoreLog uploads the restore's log file to object storage.
UploadRestoreLog(bucket, backup, restore string, log io.Reader) error
// UploadRestoreResults uploads the restore's results file to object storage.
UploadRestoreResults(bucket, backup, restore string, results io.Reader) error
}
// BackupGetter knows how to list backups in object storage.
type BackupGetter interface {
// GetAllBackups lists all the api.Backups in object storage for the given bucket.
GetAllBackups(bucket string) ([]*api.Backup, error)
// BackupLister knows how to list backups in object storage.
type BackupLister interface {
// ListBackups lists all the api.Backups in object storage for the given bucket.
ListBackups(bucket string) ([]*api.Backup, error)
}
const (
@@ -97,24 +65,6 @@ func getRestoreResultsKey(directory, restore string) string {
return fmt.Sprintf(restoreResultsFileFormatString, directory, restore)
}
type backupService struct {
objectStore ObjectStore
decoder runtime.Decoder
logger logrus.FieldLogger
}
var _ BackupService = &backupService{}
var _ BackupGetter = &backupService{}
// NewBackupService creates a backup service using the provided object store
func NewBackupService(objectStore ObjectStore, logger logrus.FieldLogger) BackupService {
return &backupService{
objectStore: objectStore,
decoder: scheme.Codecs.UniversalDecoder(api.SchemeGroupVersion),
logger: logger,
}
}
func seekToBeginning(r io.Reader) error {
seeker, ok := r.(io.Seeker)
if !ok {
@@ -125,7 +75,7 @@ func seekToBeginning(r io.Reader) error {
return err
}
func (br *backupService) seekAndPutObject(bucket, key string, file io.Reader) error {
func seekAndPutObject(objectStore ObjectStore, bucket, key string, file io.Reader) error {
if file == nil {
return nil
}
@@ -134,18 +84,34 @@ func (br *backupService) seekAndPutObject(bucket, key string, file io.Reader) er
return errors.WithStack(err)
}
return br.objectStore.PutObject(bucket, key, file)
return objectStore.PutObject(bucket, key, file)
}
func (br *backupService) UploadBackup(bucket, backupName string, metadata, backup, log io.Reader) error {
// Uploading the log file is best-effort; if it fails, we log the error but it doesn't impact the
// backup's status.
func UploadBackupLog(objectStore ObjectStore, bucket, backupName string, log io.Reader) error {
logKey := getBackupLogKey(backupName, backupName)
if err := br.seekAndPutObject(bucket, logKey, log); err != nil {
br.logger.WithError(err).WithFields(logrus.Fields{
"bucket": bucket,
"key": logKey,
}).Error("Error uploading log file")
return seekAndPutObject(objectStore, bucket, logKey, log)
}
func UploadBackupMetadata(objectStore ObjectStore, bucket, backupName string, metadata io.Reader) error {
metadataKey := getMetadataKey(backupName)
return seekAndPutObject(objectStore, bucket, metadataKey, metadata)
}
func DeleteBackupMetadata(objectStore ObjectStore, bucket, backupName string) error {
metadataKey := getMetadataKey(backupName)
return objectStore.DeleteObject(bucket, metadataKey)
}
func UploadBackupData(objectStore ObjectStore, bucket, backupName string, backup io.Reader) error {
backupKey := getBackupContentsKey(backupName, backupName)
return seekAndPutObject(objectStore, bucket, backupKey, backup)
}
func UploadBackup(logger logrus.FieldLogger, objectStore ObjectStore, bucket, backupName string, metadata, backup, log io.Reader) error {
if err := UploadBackupLog(objectStore, bucket, backupName, log); err != nil {
// Uploading the log file is best-effort; if it fails, we log the error but it doesn't impact the
// backup's status.
logger.WithError(err).WithField("bucket", bucket).Error("Error uploading log file")
}
if metadata == nil {
@@ -156,31 +122,49 @@ func (br *backupService) UploadBackup(bucket, backupName string, metadata, backu
}
// upload metadata file
metadataKey := getMetadataKey(backupName)
if err := br.seekAndPutObject(bucket, metadataKey, metadata); err != nil {
if err := UploadBackupMetadata(objectStore, bucket, backupName, metadata); err != nil {
// failure to upload metadata file is a hard-stop
return err
}
if backup != nil {
// upload tar file
if err := br.seekAndPutObject(bucket, getBackupContentsKey(backupName, backupName), backup); err != nil {
// try to delete the metadata file since the data upload failed
deleteErr := br.objectStore.DeleteObject(bucket, metadataKey)
return kerrors.NewAggregate([]error{err, deleteErr})
}
// upload tar file
if err := UploadBackupData(objectStore, bucket, backupName, backup); err != nil {
// try to delete the metadata file since the data upload failed
deleteErr := DeleteBackupMetadata(objectStore, bucket, backupName)
return kerrors.NewAggregate([]error{err, deleteErr})
}
return nil
}
func (br *backupService) DownloadBackup(bucket, backupName string) (io.ReadCloser, error) {
return br.objectStore.GetObject(bucket, getBackupContentsKey(backupName, backupName))
// DownloadBackupFunc is a function that can download backup metadata from a bucket in object storage.
type DownloadBackupFunc func(objectStore ObjectStore, bucket, backupName string) (io.ReadCloser, error)
// DownloadBackup downloads an Ark backup with the specified object key from object storage via the cloud API.
// It returns the snapshot metadata and data (separately), or an error if a problem is encountered
// downloading or reading the file from the cloud API.
func DownloadBackup(objectStore ObjectStore, bucket, backupName string) (io.ReadCloser, error) {
return objectStore.GetObject(bucket, getBackupContentsKey(backupName, backupName))
}
func (br *backupService) GetAllBackups(bucket string) ([]*api.Backup, error) {
prefixes, err := br.objectStore.ListCommonPrefixes(bucket, "/")
type liveBackupLister struct {
logger logrus.FieldLogger
objectStore ObjectStore
}
func NewLiveBackupLister(logger logrus.FieldLogger, objectStore ObjectStore) BackupLister {
return &liveBackupLister{
logger: logger,
objectStore: objectStore,
}
}
func (l *liveBackupLister) ListBackups(bucket string) ([]*api.Backup, error) {
return ListBackups(l.logger, l.objectStore, bucket)
}
func ListBackups(logger logrus.FieldLogger, objectStore ObjectStore, bucket string) ([]*api.Backup, error) {
prefixes, err := objectStore.ListCommonPrefixes(bucket, "/")
if err != nil {
return nil, err
}
@@ -191,9 +175,9 @@ func (br *backupService) GetAllBackups(bucket string) ([]*api.Backup, error) {
output := make([]*api.Backup, 0, len(prefixes))
for _, backupDir := range prefixes {
backup, err := br.GetBackup(bucket, backupDir)
backup, err := GetBackup(objectStore, bucket, backupDir)
if err != nil {
br.logger.WithError(err).WithField("dir", backupDir).Error("Error reading backup directory")
logger.WithError(err).WithField("dir", backupDir).Error("Error reading backup directory")
continue
}
@@ -203,10 +187,14 @@ func (br *backupService) GetAllBackups(bucket string) ([]*api.Backup, error) {
return output, nil
}
func (br *backupService) GetBackup(bucket, backupName string) (*api.Backup, error) {
//GetBackupFunc is a function that can retrieve backup metadata from an object store
type GetBackupFunc func(objectStore ObjectStore, bucket, backupName string) (*api.Backup, error)
// GetBackup gets the specified api.Backup from the given bucket in object storage.
func GetBackup(objectStore ObjectStore, bucket, backupName string) (*api.Backup, error) {
key := getMetadataKey(backupName)
res, err := br.objectStore.GetObject(bucket, key)
res, err := objectStore.GetObject(bucket, key)
if err != nil {
return nil, err
}
@@ -217,7 +205,8 @@ func (br *backupService) GetBackup(bucket, backupName string) (*api.Backup, erro
return nil, errors.WithStack(err)
}
obj, _, err := br.decoder.Decode(data, nil, nil)
decoder := scheme.Codecs.UniversalDecoder(api.SchemeGroupVersion)
obj, _, err := decoder.Decode(data, nil, nil)
if err != nil {
return nil, errors.WithStack(err)
}
@@ -230,19 +219,23 @@ func (br *backupService) GetBackup(bucket, backupName string) (*api.Backup, erro
return backup, nil
}
func (br *backupService) DeleteBackupDir(bucket, backupName string) error {
objects, err := br.objectStore.ListObjects(bucket, backupName+"/")
// DeleteBackupDirFunc is a function that can delete a backup directory from a bucket in object storage.
type DeleteBackupDirFunc func(logger logrus.FieldLogger, objectStore ObjectStore, bucket, backupName string) error
// DeleteBackupDir deletes all files in object storage for the given backup.
func DeleteBackupDir(logger logrus.FieldLogger, objectStore ObjectStore, bucket, backupName string) error {
objects, err := objectStore.ListObjects(bucket, backupName+"/")
if err != nil {
return err
}
var errs []error
for _, key := range objects {
br.logger.WithFields(logrus.Fields{
logger.WithFields(logrus.Fields{
"bucket": bucket,
"key": key,
}).Debug("Trying to delete object")
if err := br.objectStore.DeleteObject(bucket, key); err != nil {
if err := objectStore.DeleteObject(bucket, key); err != nil {
errs = append(errs, err)
}
}
@@ -250,51 +243,40 @@ func (br *backupService) DeleteBackupDir(bucket, backupName string) error {
return errors.WithStack(kerrors.NewAggregate(errs))
}
func (br *backupService) CreateSignedURL(target api.DownloadTarget, bucket, directory string, ttl time.Duration) (string, error) {
// CreateSignedURLFunc is a function that can create a signed URL for an object in object storage.
type CreateSignedURLFunc func(objectStore ObjectStore, target api.DownloadTarget, bucket, directory string, ttl time.Duration) (string, error)
// CreateSignedURL creates a pre-signed URL that can be used to download a file from object
// storage. The URL expires after ttl.
func CreateSignedURL(objectStore ObjectStore, target api.DownloadTarget, bucket, directory string, ttl time.Duration) (string, error) {
switch target.Kind {
case api.DownloadTargetKindBackupContents:
return br.objectStore.CreateSignedURL(bucket, getBackupContentsKey(directory, target.Name), ttl)
return objectStore.CreateSignedURL(bucket, getBackupContentsKey(directory, target.Name), ttl)
case api.DownloadTargetKindBackupLog:
return br.objectStore.CreateSignedURL(bucket, getBackupLogKey(directory, target.Name), ttl)
return objectStore.CreateSignedURL(bucket, getBackupLogKey(directory, target.Name), ttl)
case api.DownloadTargetKindRestoreLog:
return br.objectStore.CreateSignedURL(bucket, getRestoreLogKey(directory, target.Name), ttl)
return objectStore.CreateSignedURL(bucket, getRestoreLogKey(directory, target.Name), ttl)
case api.DownloadTargetKindRestoreResults:
return br.objectStore.CreateSignedURL(bucket, getRestoreResultsKey(directory, target.Name), ttl)
return objectStore.CreateSignedURL(bucket, getRestoreResultsKey(directory, target.Name), ttl)
default:
return "", errors.Errorf("unsupported download target kind %q", target.Kind)
}
}
func (br *backupService) UploadRestoreLog(bucket, backup, restore string, log io.Reader) error {
// UploadRestoreLogFunc is a function that can upload a restore log to a bucket in object storage.
type UploadRestoreLogFunc func(objectStore ObjectStore, bucket, backup, restore string, log io.Reader) error
// UploadRestoreLog uploads the restore's log file to object storage.
func UploadRestoreLog(objectStore ObjectStore, bucket, backup, restore string, log io.Reader) error {
key := getRestoreLogKey(backup, restore)
return br.objectStore.PutObject(bucket, key, log)
return objectStore.PutObject(bucket, key, log)
}
func (br *backupService) UploadRestoreResults(bucket, backup, restore string, results io.Reader) error {
// UploadRestoreResultsFunc is a function that can upload restore results to a bucket in object storage.
type UploadRestoreResultsFunc func(objectStore ObjectStore, bucket, backup, restore string, results io.Reader) error
// UploadRestoreResults uploads the restore's results file to object storage.
func UploadRestoreResults(objectStore ObjectStore, bucket, backup, restore string, results io.Reader) error {
key := getRestoreResultsKey(backup, restore)
return br.objectStore.PutObject(bucket, key, results)
}
// cachedBackupService wraps a real backup service with a cache for getting cloud backups.
type cachedBackupService struct {
BackupService
cache BackupGetter
}
// NewBackupServiceWithCachedBackupGetter returns a BackupService that uses a cache for
// GetAllBackups().
func NewBackupServiceWithCachedBackupGetter(
ctx context.Context,
delegate BackupService,
resyncPeriod time.Duration,
logger logrus.FieldLogger,
) BackupService {
return &cachedBackupService{
BackupService: delegate,
cache: NewBackupCache(ctx, delegate, resyncPeriod, logger),
}
}
func (c *cachedBackupService) GetAllBackups(bucketName string) ([]*api.Backup, error) {
return c.cache.GetAllBackups(bucketName)
return objectStore.PutObject(bucket, key, results)
}
+38 -48
View File
@@ -92,29 +92,27 @@ func TestUploadBackup(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var (
objStore = &testutil.ObjectStore{}
bucket = "test-bucket"
backupName = "test-backup"
logger = arktest.NewLogger()
objectStore = &testutil.ObjectStore{}
bucket = "test-bucket"
backupName = "test-backup"
logger = arktest.NewLogger()
)
defer objStore.AssertExpectations(t)
defer objectStore.AssertExpectations(t)
if test.metadata != nil {
objStore.On("PutObject", bucket, backupName+"/ark-backup.json", test.metadata).Return(test.metadataError)
objectStore.On("PutObject", bucket, backupName+"/ark-backup.json", test.metadata).Return(test.metadataError)
}
if test.backup != nil && test.expectBackupUpload {
objStore.On("PutObject", bucket, backupName+"/"+backupName+".tar.gz", test.backup).Return(test.backupError)
objectStore.On("PutObject", bucket, backupName+"/"+backupName+".tar.gz", test.backup).Return(test.backupError)
}
if test.log != nil {
objStore.On("PutObject", bucket, backupName+"/"+backupName+"-logs.gz", test.log).Return(test.logError)
objectStore.On("PutObject", bucket, backupName+"/"+backupName+"-logs.gz", test.log).Return(test.logError)
}
if test.expectMetadataDelete {
objStore.On("DeleteObject", bucket, backupName+"/ark-backup.json").Return(nil)
objectStore.On("DeleteObject", bucket, backupName+"/ark-backup.json").Return(nil)
}
backupService := NewBackupService(objStore, logger)
err := backupService.UploadBackup(bucket, backupName, test.metadata, test.backup, test.log)
err := UploadBackup(logger, objectStore, bucket, backupName, test.metadata, test.backup, test.log)
if test.expectedErr != "" {
assert.EqualError(t, err, test.expectedErr)
@@ -128,21 +126,19 @@ func TestUploadBackup(t *testing.T) {
func TestDownloadBackup(t *testing.T) {
var (
o = &testutil.ObjectStore{}
bucket = "b"
backup = "bak"
logger = arktest.NewLogger()
objectStore = &testutil.ObjectStore{}
bucket = "b"
backup = "bak"
)
o.On("GetObject", bucket, backup+"/"+backup+".tar.gz").Return(ioutil.NopCloser(strings.NewReader("foo")), nil)
objectStore.On("GetObject", bucket, backup+"/"+backup+".tar.gz").Return(ioutil.NopCloser(strings.NewReader("foo")), nil)
s := NewBackupService(o, logger)
rc, err := s.DownloadBackup(bucket, backup)
rc, err := DownloadBackup(objectStore, bucket, backup)
require.NoError(t, err)
require.NotNil(t, rc)
data, err := ioutil.ReadAll(rc)
require.NoError(t, err)
assert.Equal(t, "foo", string(data))
o.AssertExpectations(t)
objectStore.AssertExpectations(t)
}
func TestDeleteBackup(t *testing.T) {
@@ -165,26 +161,24 @@ func TestDeleteBackup(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var (
bucket = "bucket"
backup = "bak"
objects = []string{"bak/ark-backup.json", "bak/bak.tar.gz", "bak/bak.log.gz"}
objStore = &testutil.ObjectStore{}
logger = arktest.NewLogger()
bucket = "bucket"
backup = "bak"
objects = []string{"bak/ark-backup.json", "bak/bak.tar.gz", "bak/bak.log.gz"}
objectStore = &testutil.ObjectStore{}
logger = arktest.NewLogger()
)
objStore.On("ListObjects", bucket, backup+"/").Return(objects, test.listObjectsError)
for i, o := range objects {
objectStore.On("ListObjects", bucket, backup+"/").Return(objects, test.listObjectsError)
for i, obj := range objects {
var err error
if i < len(test.deleteErrors) {
err = test.deleteErrors[i]
}
objStore.On("DeleteObject", bucket, o).Return(err)
objectStore.On("DeleteObject", bucket, obj).Return(err)
}
backupService := NewBackupService(objStore, logger)
err := backupService.DeleteBackupDir(bucket, backup)
err := DeleteBackupDir(logger, objectStore, bucket, backup)
if test.expectedErr != "" {
assert.EqualError(t, err, test.expectedErr)
@@ -192,7 +186,7 @@ func TestDeleteBackup(t *testing.T) {
assert.NoError(t, err)
}
objStore.AssertExpectations(t)
objectStore.AssertExpectations(t)
})
}
}
@@ -239,18 +233,16 @@ func TestGetAllBackups(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var (
bucket = "bucket"
objStore = &testutil.ObjectStore{}
logger = arktest.NewLogger()
bucket = "bucket"
objectStore = &testutil.ObjectStore{}
logger = arktest.NewLogger()
)
objStore.On("ListCommonPrefixes", bucket, "/").Return([]string{"backup-1", "backup-2"}, nil)
objStore.On("GetObject", bucket, "backup-1/ark-backup.json").Return(ioutil.NopCloser(bytes.NewReader(test.storageData["backup-1/ark-backup.json"])), nil)
objStore.On("GetObject", bucket, "backup-2/ark-backup.json").Return(ioutil.NopCloser(bytes.NewReader(test.storageData["backup-2/ark-backup.json"])), nil)
objectStore.On("ListCommonPrefixes", bucket, "/").Return([]string{"backup-1", "backup-2"}, nil)
objectStore.On("GetObject", bucket, "backup-1/ark-backup.json").Return(ioutil.NopCloser(bytes.NewReader(test.storageData["backup-1/ark-backup.json"])), nil)
objectStore.On("GetObject", bucket, "backup-2/ark-backup.json").Return(ioutil.NopCloser(bytes.NewReader(test.storageData["backup-2/ark-backup.json"])), nil)
backupService := NewBackupService(objStore, logger)
res, err := backupService.GetAllBackups(bucket)
res, err := ListBackups(logger, objectStore, bucket)
if test.expectedErr != "" {
assert.EqualError(t, err, test.expectedErr)
@@ -260,7 +252,7 @@ func TestGetAllBackups(t *testing.T) {
assert.Equal(t, test.expectedRes, res)
objStore.AssertExpectations(t)
objectStore.AssertExpectations(t)
})
}
}
@@ -327,20 +319,18 @@ func TestCreateSignedURL(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var (
objectStorage = &testutil.ObjectStore{}
logger = arktest.NewLogger()
backupService = NewBackupService(objectStorage, logger)
objectStore = &testutil.ObjectStore{}
)
defer objectStore.AssertExpectations(t)
target := api.DownloadTarget{
Kind: test.targetKind,
Name: test.targetName,
}
objectStorage.On("CreateSignedURL", "bucket", test.expectedKey, time.Duration(0)).Return("url", nil)
url, err := backupService.CreateSignedURL(target, "bucket", test.directory, 0)
objectStore.On("CreateSignedURL", "bucket", test.expectedKey, time.Duration(0)).Return("url", nil)
url, err := CreateSignedURL(objectStore, target, "bucket", test.directory, 0)
require.NoError(t, err)
assert.Equal(t, "url", url)
objectStorage.AssertExpectations(t)
})
}
}
+2 -2
View File
@@ -44,8 +44,8 @@ type blockStore struct {
log logrus.FieldLogger
}
func NewBlockStore(log logrus.FieldLogger) cloudprovider.BlockStore {
return &blockStore{log: log}
func NewBlockStore(logger logrus.FieldLogger) cloudprovider.BlockStore {
return &blockStore{log: logger}
}
func (b *blockStore) Init(config map[string]string) error {
+4 -2
View File
@@ -26,6 +26,7 @@ import (
"cloud.google.com/go/storage"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2/google"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
@@ -50,14 +51,15 @@ func (w *writer) getWriteCloser(bucket, key string) io.WriteCloser {
}
type objectStore struct {
log logrus.FieldLogger
client *storage.Client
googleAccessID string
privateKey []byte
bucketWriter bucketWriter
}
func NewObjectStore() cloudprovider.ObjectStore {
return &objectStore{}
func NewObjectStore(logger logrus.FieldLogger) cloudprovider.ObjectStore {
return &objectStore{log: logger}
}
func (o *objectStore) Init(config map[string]string) error {
+2 -1
View File
@@ -21,6 +21,7 @@ import (
"strings"
"testing"
arktest "github.com/heptio/ark/pkg/util/test"
"github.com/stretchr/testify/assert"
)
@@ -87,7 +88,7 @@ func TestPutObject(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
wc := newMockWriteCloser(test.writeErr, test.closeErr)
o := NewObjectStore().(*objectStore)
o := NewObjectStore(arktest.NewLogger()).(*objectStore)
o.bucketWriter = newFakeWriter(wc)
err := o.PutObject("bucket", "key", strings.NewReader("contents"))
+48
View File
@@ -0,0 +1,48 @@
/*
Copyright 2018 the Heptio Ark 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.
*/
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import mock "github.com/stretchr/testify/mock"
import v1 "github.com/heptio/ark/pkg/apis/ark/v1"
// BackupLister is an autogenerated mock type for the BackupLister type
type BackupLister struct {
mock.Mock
}
// ListBackups provides a mock function with given fields: bucket
func (_m *BackupLister) ListBackups(bucket string) ([]*v1.Backup, error) {
ret := _m.Called(bucket)
var r0 []*v1.Backup
if rf, ok := ret.Get(0).(func(string) []*v1.Backup); ok {
r0 = rf(bucket)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*v1.Backup)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(bucket)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
+191
View File
@@ -0,0 +1,191 @@
/*
Copyright 2018 the Heptio Ark 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.
*/
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import mock "github.com/stretchr/testify/mock"
import runtime "k8s.io/apimachinery/pkg/runtime"
// BlockStore is an autogenerated mock type for the BlockStore type
type BlockStore struct {
mock.Mock
}
// CreateSnapshot provides a mock function with given fields: volumeID, volumeAZ, tags
func (_m *BlockStore) CreateSnapshot(volumeID string, volumeAZ string, tags map[string]string) (string, error) {
ret := _m.Called(volumeID, volumeAZ, tags)
var r0 string
if rf, ok := ret.Get(0).(func(string, string, map[string]string) string); ok {
r0 = rf(volumeID, volumeAZ, tags)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, map[string]string) error); ok {
r1 = rf(volumeID, volumeAZ, tags)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateVolumeFromSnapshot provides a mock function with given fields: snapshotID, volumeType, volumeAZ, iops
func (_m *BlockStore) CreateVolumeFromSnapshot(snapshotID string, volumeType string, volumeAZ string, iops *int64) (string, error) {
ret := _m.Called(snapshotID, volumeType, volumeAZ, iops)
var r0 string
if rf, ok := ret.Get(0).(func(string, string, string, *int64) string); ok {
r0 = rf(snapshotID, volumeType, volumeAZ, iops)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, *int64) error); ok {
r1 = rf(snapshotID, volumeType, volumeAZ, iops)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteSnapshot provides a mock function with given fields: snapshotID
func (_m *BlockStore) DeleteSnapshot(snapshotID string) error {
ret := _m.Called(snapshotID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(snapshotID)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetVolumeID provides a mock function with given fields: pv
func (_m *BlockStore) GetVolumeID(pv runtime.Unstructured) (string, error) {
ret := _m.Called(pv)
var r0 string
if rf, ok := ret.Get(0).(func(runtime.Unstructured) string); ok {
r0 = rf(pv)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(runtime.Unstructured) error); ok {
r1 = rf(pv)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetVolumeInfo provides a mock function with given fields: volumeID, volumeAZ
func (_m *BlockStore) GetVolumeInfo(volumeID string, volumeAZ string) (string, *int64, error) {
ret := _m.Called(volumeID, volumeAZ)
var r0 string
if rf, ok := ret.Get(0).(func(string, string) string); ok {
r0 = rf(volumeID, volumeAZ)
} else {
r0 = ret.Get(0).(string)
}
var r1 *int64
if rf, ok := ret.Get(1).(func(string, string) *int64); ok {
r1 = rf(volumeID, volumeAZ)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*int64)
}
}
var r2 error
if rf, ok := ret.Get(2).(func(string, string) error); ok {
r2 = rf(volumeID, volumeAZ)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Init provides a mock function with given fields: config
func (_m *BlockStore) Init(config map[string]string) error {
ret := _m.Called(config)
var r0 error
if rf, ok := ret.Get(0).(func(map[string]string) error); ok {
r0 = rf(config)
} else {
r0 = ret.Error(0)
}
return r0
}
// IsVolumeReady provides a mock function with given fields: volumeID, volumeAZ
func (_m *BlockStore) IsVolumeReady(volumeID string, volumeAZ string) (bool, error) {
ret := _m.Called(volumeID, volumeAZ)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string) bool); ok {
r0 = rf(volumeID, volumeAZ)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(volumeID, volumeAZ)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetVolumeID provides a mock function with given fields: pv, volumeID
func (_m *BlockStore) SetVolumeID(pv runtime.Unstructured, volumeID string) (runtime.Unstructured, error) {
ret := _m.Called(pv, volumeID)
var r0 runtime.Unstructured
if rf, ok := ret.Get(0).(func(runtime.Unstructured, string) runtime.Unstructured); ok {
r0 = rf(pv, volumeID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(runtime.Unstructured)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(runtime.Unstructured, string) error); ok {
r1 = rf(pv, volumeID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
+15
View File
@@ -1,3 +1,18 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
+15
View File
@@ -1,3 +1,18 @@
/*
Copyright 2018 the Heptio Ark contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restic
import (
+90 -89
View File
@@ -17,17 +17,14 @@ limitations under the License.
package plugin
import (
plugin "github.com/hashicorp/go-plugin"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/heptio/ark/pkg/backup"
"github.com/heptio/ark/pkg/client"
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/cloudprovider/aws"
"github.com/heptio/ark/pkg/cloudprovider/azure"
"github.com/heptio/ark/pkg/cloudprovider/gcp"
"github.com/heptio/ark/pkg/cmd"
arkdiscovery "github.com/heptio/ark/pkg/discovery"
arkplugin "github.com/heptio/ark/pkg/plugin"
"github.com/heptio/ark/pkg/restore"
@@ -37,98 +34,102 @@ func NewCommand(f client.Factory) *cobra.Command {
logger := arkplugin.NewLogger()
c := &cobra.Command{
Use: "run-plugin [KIND] [NAME]",
Use: "run-plugins",
Hidden: true,
Short: "INTERNAL COMMAND ONLY - not intended to be run directly by users",
Args: cobra.ExactArgs(2),
Run: func(c *cobra.Command, args []string) {
kind := args[0]
name := args[1]
logger.Debug("Executing run-plugins command")
logger = logger.WithFields(logrus.Fields{"kind": kind, "name": name})
serveConfig := &plugin.ServeConfig{
HandshakeConfig: arkplugin.Handshake,
GRPCServer: plugin.DefaultGRPCServer,
}
logger.Debug("Executing run-plugin command")
switch kind {
case "cloudprovider":
var (
objectStore cloudprovider.ObjectStore
blockStore cloudprovider.BlockStore
)
switch name {
case "aws":
objectStore, blockStore = aws.NewObjectStore(), aws.NewBlockStore()
case "azure":
objectStore, blockStore = azure.NewObjectStore(), azure.NewBlockStore()
case "gcp":
objectStore, blockStore = gcp.NewObjectStore(), gcp.NewBlockStore(logger)
default:
logger.Fatal("Unrecognized plugin name")
}
serveConfig.Plugins = map[string]plugin.Plugin{
string(arkplugin.PluginKindObjectStore): arkplugin.NewObjectStorePlugin(objectStore),
string(arkplugin.PluginKindBlockStore): arkplugin.NewBlockStorePlugin(blockStore),
}
case arkplugin.PluginKindBackupItemAction.String():
var action backup.ItemAction
switch name {
case "pv":
action = backup.NewBackupPVAction(logger)
case "pod":
action = backup.NewPodAction(logger)
case "serviceaccount":
clientset, err := f.KubeClient()
cmd.CheckError(err)
discoveryHelper, err := arkdiscovery.NewHelper(clientset.Discovery(), logger)
cmd.CheckError(err)
action, err = backup.NewServiceAccountAction(
logger,
backup.NewClusterRoleBindingListerMap(clientset),
discoveryHelper)
cmd.CheckError(err)
default:
logger.Fatal("Unrecognized plugin name")
}
serveConfig.Plugins = map[string]plugin.Plugin{
kind: arkplugin.NewBackupItemActionPlugin(action),
}
case arkplugin.PluginKindRestoreItemAction.String():
var action restore.ItemAction
switch name {
case "job":
action = restore.NewJobAction(logger)
case "pod":
action = restore.NewPodAction(logger)
case "svc":
action = restore.NewServiceAction(logger)
case "restic":
action = restore.NewResticRestoreAction(logger)
default:
logger.Fatal("Unrecognized plugin name")
}
serveConfig.Plugins = map[string]plugin.Plugin{
kind: arkplugin.NewRestoreItemActionPlugin(action),
}
default:
logger.Fatal("Unsupported plugin kind")
}
plugin.Serve(serveConfig)
arkplugin.NewServer(logger).
RegisterObjectStore("aws", newAwsObjectStore).
RegisterObjectStore("azure", newAzureObjectStore).
RegisterObjectStore("gcp", newGcpObjectStore).
RegisterBlockStore("aws", newAwsBlockStore).
RegisterBlockStore("azure", newAzureBlockStore).
RegisterBlockStore("gcp", newGcpBlockStore).
RegisterBackupItemAction("pv", newPVBackupItemAction).
RegisterBackupItemAction("pod", newPodBackupItemAction).
RegisterBackupItemAction("serviceaccount", newServiceAccountBackupItemAction(f)).
RegisterRestoreItemAction("job", newJobRestoreItemAction).
RegisterRestoreItemAction("pod", newPodRestoreItemAction).
RegisterRestoreItemAction("restic", newResticRestoreItemAction).
RegisterRestoreItemAction("service", newServiceRestoreItemAction).
Serve()
},
}
return c
}
func newAwsObjectStore(logger logrus.FieldLogger) (interface{}, error) {
return aws.NewObjectStore(logger), nil
}
func newAzureObjectStore(logger logrus.FieldLogger) (interface{}, error) {
return azure.NewObjectStore(logger), nil
}
func newGcpObjectStore(logger logrus.FieldLogger) (interface{}, error) {
return gcp.NewObjectStore(logger), nil
}
func newAwsBlockStore(logger logrus.FieldLogger) (interface{}, error) {
return aws.NewBlockStore(logger), nil
}
func newAzureBlockStore(logger logrus.FieldLogger) (interface{}, error) {
return azure.NewBlockStore(logger), nil
}
func newGcpBlockStore(logger logrus.FieldLogger) (interface{}, error) {
return gcp.NewBlockStore(logger), nil
}
func newPVBackupItemAction(logger logrus.FieldLogger) (interface{}, error) {
return backup.NewBackupPVAction(logger), nil
}
func newPodBackupItemAction(logger logrus.FieldLogger) (interface{}, error) {
return backup.NewPodAction(logger), nil
}
func newServiceAccountBackupItemAction(f client.Factory) arkplugin.HandlerInitializer {
return func(logger logrus.FieldLogger) (interface{}, error) {
// TODO(ncdc): consider a k8s style WantsKubernetesClientSet initialization approach
clientset, err := f.KubeClient()
if err != nil {
return nil, err
}
discoveryHelper, err := arkdiscovery.NewHelper(clientset.Discovery(), logger)
if err != nil {
return nil, err
}
action, err := backup.NewServiceAccountAction(
logger,
backup.NewClusterRoleBindingListerMap(clientset),
discoveryHelper)
if err != nil {
return nil, err
}
return action, nil
}
}
func newJobRestoreItemAction(logger logrus.FieldLogger) (interface{}, error) {
return restore.NewJobAction(logger), nil
}
func newPodRestoreItemAction(logger logrus.FieldLogger) (interface{}, error) {
return restore.NewPodAction(logger), nil
}
func newResticRestoreItemAction(logger logrus.FieldLogger) (interface{}, error) {
return restore.NewResticRestoreAction(logger), nil
}
func newServiceRestoreItemAction(logger logrus.FieldLogger) (interface{}, error) {
return restore.NewServiceAction(logger), nil
}
+29 -33
View File
@@ -149,7 +149,6 @@ type server struct {
kubeClient kubernetes.Interface
arkClient clientset.Interface
objectStore cloudprovider.ObjectStore
backupService cloudprovider.BackupService
snapshotService cloudprovider.SnapshotService
discoveryClient discovery.DiscoveryInterface
discoveryHelper arkdiscovery.Helper
@@ -158,6 +157,8 @@ type server struct {
ctx context.Context
cancelFunc context.CancelFunc
logger logrus.FieldLogger
logLevel logrus.Level
pluginRegistry plugin.Registry
pluginManager plugin.Manager
resticManager restic.RepositoryManager
metrics *metrics.ServerMetrics
@@ -179,7 +180,11 @@ func newServer(namespace, baseName, pluginDir, metricsAddr string, logger *logru
return nil, errors.WithStack(err)
}
pluginManager, err := plugin.NewManager(logger, logger.Level, pluginDir)
pluginRegistry := plugin.NewRegistry(pluginDir, logger, logger.Level)
if err := pluginRegistry.DiscoverPlugins(); err != nil {
return nil, err
}
pluginManager := plugin.NewManager(logger, logger.Level, pluginRegistry)
if err != nil {
return nil, err
}
@@ -200,10 +205,12 @@ func newServer(namespace, baseName, pluginDir, metricsAddr string, logger *logru
discoveryClient: arkClient.Discovery(),
dynamicClient: dynamicClient,
sharedInformerFactory: informers.NewFilteredSharedInformerFactory(arkClient, 0, namespace, nil),
ctx: ctx,
cancelFunc: cancelFunc,
logger: logger,
pluginManager: pluginManager,
ctx: ctx,
cancelFunc: cancelFunc,
logger: logger,
logLevel: logger.Level,
pluginRegistry: pluginRegistry,
pluginManager: pluginManager,
}
return s, nil
@@ -242,9 +249,11 @@ func (s *server) run() error {
s.watchConfig(originalConfig)
if err := s.initBackupService(config); err != nil {
objectStore, err := getObjectStore(config.BackupStorageProvider.CloudProviderConfig, s.pluginManager)
if err != nil {
return err
}
s.objectStore = objectStore
if err := s.initSnapshotService(config); err != nil {
return err
@@ -455,18 +464,6 @@ func (s *server) watchConfig(config *api.Config) {
})
}
func (s *server) initBackupService(config *api.Config) error {
s.logger.Info("Configuring cloud provider for backup service")
objectStore, err := getObjectStore(config.BackupStorageProvider.CloudProviderConfig, s.pluginManager)
if err != nil {
return err
}
s.objectStore = objectStore
s.backupService = cloudprovider.NewBackupService(objectStore, s.logger)
return nil
}
func (s *server) initSnapshotService(config *api.Config) error {
if config.PersistentVolumeProvider == nil {
s.logger.Info("PersistentVolumeProvider config not provided, volume snapshots and restores are disabled")
@@ -584,12 +581,9 @@ func (s *server) runControllers(config *api.Config) error {
cloudBackupCacheResyncPeriod := durationMin(config.GCSyncPeriod.Duration, config.BackupSyncPeriod.Duration)
s.logger.Infof("Caching cloud backups every %s", cloudBackupCacheResyncPeriod)
s.backupService = cloudprovider.NewBackupServiceWithCachedBackupGetter(
ctx,
s.backupService,
cloudBackupCacheResyncPeriod,
s.logger,
)
liveBackupLister := cloudprovider.NewLiveBackupLister(s.logger, s.objectStore)
cachedBackupLister := cloudprovider.NewBackupCache(ctx, liveBackupLister, cloudBackupCacheResyncPeriod, s.logger)
go func() {
metricsMux := http.NewServeMux()
@@ -604,7 +598,7 @@ func (s *server) runControllers(config *api.Config) error {
backupSyncController := controller.NewBackupSyncController(
s.arkClient.ArkV1(),
s.backupService,
cachedBackupLister,
config.BackupStorageProvider.Bucket,
config.BackupSyncPeriod.Duration,
s.namespace,
@@ -636,11 +630,12 @@ func (s *server) runControllers(config *api.Config) error {
s.sharedInformerFactory.Ark().V1().Backups(),
s.arkClient.ArkV1(),
backupper,
s.backupService,
config.BackupStorageProvider.CloudProviderConfig,
config.BackupStorageProvider.Bucket,
s.snapshotService != nil,
s.logger,
s.pluginManager,
s.logLevel,
s.pluginRegistry,
backupTracker,
s.metrics,
)
@@ -684,7 +679,7 @@ func (s *server) runControllers(config *api.Config) error {
s.arkClient.ArkV1(), // deleteBackupRequestClient
s.arkClient.ArkV1(), // backupClient
s.snapshotService,
s.backupService,
s.objectStore,
config.BackupStorageProvider.Bucket,
s.sharedInformerFactory.Ark().V1().Restores(),
s.arkClient.ArkV1(), // restoreClient
@@ -703,7 +698,6 @@ func (s *server) runControllers(config *api.Config) error {
restorer, err := restore.NewKubernetesRestorer(
s.discoveryHelper,
client.NewDynamicFactory(s.dynamicClient),
s.backupService,
s.snapshotService,
config.ResourcePriorities,
s.arkClient.ArkV1(),
@@ -720,14 +714,16 @@ func (s *server) runControllers(config *api.Config) error {
s.arkClient.ArkV1(),
s.arkClient.ArkV1(),
restorer,
s.backupService,
config.BackupStorageProvider.CloudProviderConfig,
config.BackupStorageProvider.Bucket,
s.sharedInformerFactory.Ark().V1().Backups(),
s.snapshotService != nil,
s.logger,
s.pluginManager,
s.logLevel,
s.pluginRegistry,
s.metrics,
)
wg.Add(1)
go func() {
restoreController.Run(ctx, 1)
@@ -738,7 +734,7 @@ func (s *server) runControllers(config *api.Config) error {
s.arkClient.ArkV1(),
s.sharedInformerFactory.Ark().V1().DownloadRequests(),
s.sharedInformerFactory.Ark().V1().Restores(),
s.backupService,
s.objectStore,
config.BackupStorageProvider.Bucket,
s.logger,
)
+86 -34
View File
@@ -18,6 +18,7 @@ package controller
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"fmt"
@@ -50,53 +51,63 @@ import (
"github.com/heptio/ark/pkg/util/collections"
"github.com/heptio/ark/pkg/util/encode"
kubeutil "github.com/heptio/ark/pkg/util/kube"
"github.com/heptio/ark/pkg/util/logging"
)
const backupVersion = 1
type backupController struct {
backupper backup.Backupper
backupService cloudprovider.BackupService
bucket string
pvProviderExists bool
lister listers.BackupLister
listerSynced cache.InformerSynced
client arkv1client.BackupsGetter
syncHandler func(backupName string) error
queue workqueue.RateLimitingInterface
clock clock.Clock
logger logrus.FieldLogger
pluginManager plugin.Manager
backupTracker BackupTracker
metrics *metrics.ServerMetrics
backupper backup.Backupper
objectStoreConfig api.CloudProviderConfig
bucket string
pvProviderExists bool
lister listers.BackupLister
listerSynced cache.InformerSynced
client arkv1client.BackupsGetter
syncHandler func(backupName string) error
queue workqueue.RateLimitingInterface
clock clock.Clock
logger logrus.FieldLogger
logLevel logrus.Level
pluginRegistry plugin.Registry
backupTracker BackupTracker
metrics *metrics.ServerMetrics
newPluginManager func(logger logrus.FieldLogger, logLevel logrus.Level, pluginRegistry plugin.Registry) plugin.Manager
}
func NewBackupController(
backupInformer informers.BackupInformer,
client arkv1client.BackupsGetter,
backupper backup.Backupper,
backupService cloudprovider.BackupService,
objectStoreConfig api.CloudProviderConfig,
bucket string,
pvProviderExists bool,
logger logrus.FieldLogger,
pluginManager plugin.Manager,
logLevel logrus.Level,
pluginRegistry plugin.Registry,
backupTracker BackupTracker,
metrics *metrics.ServerMetrics,
) Interface {
c := &backupController{
backupper: backupper,
backupService: backupService,
bucket: bucket,
pvProviderExists: pvProviderExists,
lister: backupInformer.Lister(),
listerSynced: backupInformer.Informer().HasSynced,
client: client,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "backup"),
clock: &clock.RealClock{},
logger: logger,
pluginManager: pluginManager,
backupTracker: backupTracker,
metrics: metrics,
backupper: backupper,
objectStoreConfig: objectStoreConfig,
bucket: bucket,
pvProviderExists: pvProviderExists,
lister: backupInformer.Lister(),
listerSynced: backupInformer.Informer().HasSynced,
client: client,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "backup"),
clock: &clock.RealClock{},
logger: logger,
logLevel: logLevel,
pluginRegistry: pluginRegistry,
backupTracker: backupTracker,
metrics: metrics,
newPluginManager: func(logger logrus.FieldLogger, logLevel logrus.Level, pluginRegistry plugin.Registry) plugin.Manager {
return plugin.NewManager(logger, logLevel, pluginRegistry)
},
}
c.syncHandler = c.processBackup
@@ -343,7 +354,22 @@ func (controller *backupController) runBackup(backup *api.Backup, bucket string)
if err != nil {
return errors.Wrap(err, "error creating temp file for backup log")
}
defer closeAndRemoveFile(logFile, log)
gzippedLogFile := gzip.NewWriter(logFile)
// Assuming we successfully uploaded the log file, this will have already been closed below. It is safe to call
// close multiple times. If we get an error closing this, there's not really anything we can do about it.
defer gzippedLogFile.Close()
defer closeAndRemoveFile(logFile, controller.logger)
// Log the backup to both a backup log file and to stdout. This will help see what happened if the upload of the
// backup log failed for whatever reason.
logger := logging.DefaultLogger(controller.logLevel)
logger.Out = io.MultiWriter(os.Stdout, gzippedLogFile)
log = logger.WithField("backup", kubeutil.NamespaceAndName(backup))
log.Info("Starting backup")
pluginManager := controller.newPluginManager(log, log.Level, controller.pluginRegistry)
defer pluginManager.CleanupClients()
backupFile, err := ioutil.TempFile("", "")
if err != nil {
@@ -351,18 +377,22 @@ func (controller *backupController) runBackup(backup *api.Backup, bucket string)
}
defer closeAndRemoveFile(backupFile, log)
actions, err := controller.pluginManager.GetBackupItemActions(backup.Name)
actions, err := pluginManager.GetBackupItemActions()
if err != nil {
return err
}
objectStore, err := getObjectStore(controller.objectStoreConfig, pluginManager)
if err != nil {
return err
}
defer controller.pluginManager.CloseBackupItemActions(backup.Name)
var errs []error
var backupJSONToUpload, backupFileToUpload io.Reader
// Do the actual backup
if err := controller.backupper.Backup(backup, backupFile, logFile, actions); err != nil {
if err := controller.backupper.Backup(log, backup, backupFile, actions); err != nil {
errs = append(errs, err)
backup.Status.Phase = api.BackupPhaseFailed
@@ -390,7 +420,11 @@ func (controller *backupController) runBackup(backup *api.Backup, bucket string)
backupSizeBytes = backupFileStat.Size()
}
if err := controller.backupService.UploadBackup(bucket, backup.Name, backupJSONToUpload, backupFileToUpload, logFile); err != nil {
if err := gzippedLogFile.Close(); err != nil {
controller.logger.WithError(err).Error("error closing gzippedLogFile")
}
if err := cloudprovider.UploadBackup(log, objectStore, bucket, backup.Name, backupJSONToUpload, backupFileToUpload, logFile); err != nil {
errs = append(errs, err)
}
@@ -406,6 +440,24 @@ func (controller *backupController) runBackup(backup *api.Backup, bucket string)
return kerrors.NewAggregate(errs)
}
// TODO(ncdc): move this to a better location that isn't backup specific
func getObjectStore(cloudConfig api.CloudProviderConfig, manager plugin.Manager) (cloudprovider.ObjectStore, error) {
if cloudConfig.Name == "" {
return nil, errors.New("object storage provider name must not be empty")
}
objectStore, err := manager.GetObjectStore(cloudConfig.Name)
if err != nil {
return nil, err
}
if err := objectStore.Init(cloudConfig.Config); err != nil {
return nil, err
}
return objectStore, nil
}
func closeAndRemoveFile(file *os.File, log logrus.FieldLogger) {
if err := file.Close(); err != nil {
log.WithError(err).WithField("file", file.Name()).Error("error closing file")
+39 -147
View File
@@ -19,28 +19,31 @@ package controller
import (
"bytes"
"encoding/json"
"fmt"
"io"
"strings"
"testing"
"time"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/clock"
core "k8s.io/client-go/testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/backup"
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/generated/clientset/versioned/fake"
informers "github.com/heptio/ark/pkg/generated/informers/externalversions"
"github.com/heptio/ark/pkg/metrics"
"github.com/heptio/ark/pkg/restore"
"github.com/heptio/ark/pkg/plugin"
pluginmocks "github.com/heptio/ark/pkg/plugin/mocks"
"github.com/heptio/ark/pkg/util/collections"
"github.com/heptio/ark/pkg/util/logging"
arktest "github.com/heptio/ark/pkg/util/test"
)
@@ -48,8 +51,8 @@ type fakeBackupper struct {
mock.Mock
}
func (b *fakeBackupper) Backup(backup *v1.Backup, data, log io.Writer, actions []backup.ItemAction) error {
args := b.Called(backup, data, log, actions)
func (b *fakeBackupper) Backup(logger logrus.FieldLogger, backup *v1.Backup, backupFile io.Writer, actions []backup.ItemAction) error {
args := b.Called(logger, backup, backupFile, actions)
return args.Error(0)
}
@@ -156,27 +159,35 @@ func TestProcessBackup(t *testing.T) {
var (
client = fake.NewSimpleClientset()
backupper = &fakeBackupper{}
cloudBackups = &arktest.BackupService{}
sharedInformers = informers.NewSharedInformerFactory(client, 0)
logger = arktest.NewLogger()
pluginManager = &MockManager{}
logger = logging.DefaultLogger(logrus.DebugLevel)
pluginRegistry = plugin.NewRegistry("/dir", logger, logrus.InfoLevel)
clockTime, _ = time.Parse("Mon Jan 2 15:04:05 2006", "Mon Jan 2 15:04:05 2006")
objectStore = &arktest.ObjectStore{}
pluginManager = &pluginmocks.Manager{}
)
defer backupper.AssertExpectations(t)
defer objectStore.AssertExpectations(t)
defer pluginManager.AssertExpectations(t)
c := NewBackupController(
sharedInformers.Ark().V1().Backups(),
client.ArkV1(),
backupper,
cloudBackups,
v1.CloudProviderConfig{Name: "myCloud"},
"bucket",
test.allowSnapshots,
logger,
pluginManager,
logrus.InfoLevel,
pluginRegistry,
NewBackupTracker(),
metrics.NewServerMetrics(),
).(*backupController)
c.clock = clock.NewFakeClock(clockTime)
c.newPluginManager = func(logger logrus.FieldLogger, logLevel logrus.Level, pluginRegistry plugin.Registry) plugin.Manager {
return pluginManager
}
var expiration, startTime time.Time
@@ -190,6 +201,11 @@ func TestProcessBackup(t *testing.T) {
if test.backup.Spec.TTL.Duration > 0 {
expiration = c.clock.Now().Add(test.backup.Spec.TTL.Duration)
}
}
if test.expectBackup {
pluginManager.On("GetObjectStore", "myCloud").Return(objectStore, nil)
objectStore.On("Init", mock.Anything).Return(nil)
// set up a Backup object to represent what we expect to be passed to backupper.Backup()
backup := test.backup.DeepCopy()
@@ -201,7 +217,14 @@ func TestProcessBackup(t *testing.T) {
backup.Status.Expiration.Time = expiration
backup.Status.StartTimestamp.Time = startTime
backup.Status.Version = 1
backupper.On("Backup", backup, mock.Anything, mock.Anything, mock.Anything).Return(nil)
backupper.On("Backup",
mock.Anything, // logger
backup,
mock.Anything, // backup file
mock.Anything, // actions
).Return(nil)
pluginManager.On("GetBackupItemActions").Return(nil, nil)
// Ensure we have a CompletionTimestamp when uploading.
// Failures will display the bytes in buf.
@@ -211,10 +234,11 @@ func TestProcessBackup(t *testing.T) {
return strings.Contains(json, timeString)
}
cloudBackups.On("UploadBackup", "bucket", backup.Name, mock.MatchedBy(completionTimestampIsPresent), mock.Anything, mock.Anything).Return(nil)
objectStore.On("PutObject", "bucket", fmt.Sprintf("%s/%s-logs.gz", test.backup.Name, test.backup.Name), mock.Anything).Return(nil)
objectStore.On("PutObject", "bucket", fmt.Sprintf("%s/ark-backup.json", test.backup.Name), mock.MatchedBy(completionTimestampIsPresent)).Return(nil)
objectStore.On("PutObject", "bucket", fmt.Sprintf("%s/%s.tar.gz", test.backup.Name, test.backup.Name), mock.Anything).Return(nil)
pluginManager.On("GetBackupItemActions", backup.Name).Return(nil, nil)
pluginManager.On("CloseBackupItemActions", backup.Name).Return(nil)
pluginManager.On("CleanupClients")
}
// this is necessary so the Patch() call returns the appropriate object
@@ -273,8 +297,7 @@ func TestProcessBackup(t *testing.T) {
require.NoError(t, err, "processBackup unexpected error: %v", err)
if !test.expectBackup {
assert.Empty(t, backupper.Calls)
assert.Empty(t, cloudBackups.Calls)
// the AssertExpectations calls above make sure we aren't running anything we shouldn't be
return
}
@@ -324,134 +347,3 @@ func TestProcessBackup(t *testing.T) {
})
}
}
// MockManager is an autogenerated mock type for the Manager type
type MockManager struct {
mock.Mock
}
// CloseBackupItemActions provides a mock function with given fields: backupName
func (_m *MockManager) CloseBackupItemActions(backupName string) error {
ret := _m.Called(backupName)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(backupName)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetBackupItemActions provides a mock function with given fields: backupName, logger, level
func (_m *MockManager) GetBackupItemActions(backupName string) ([]backup.ItemAction, error) {
ret := _m.Called(backupName)
var r0 []backup.ItemAction
if rf, ok := ret.Get(0).(func(string) []backup.ItemAction); ok {
r0 = rf(backupName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]backup.ItemAction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(backupName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CloseRestoreItemActions provides a mock function with given fields: restoreName
func (_m *MockManager) CloseRestoreItemActions(restoreName string) error {
ret := _m.Called(restoreName)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(restoreName)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetRestoreItemActions provides a mock function with given fields: restoreName, logger, level
func (_m *MockManager) GetRestoreItemActions(restoreName string) ([]restore.ItemAction, error) {
ret := _m.Called(restoreName)
var r0 []restore.ItemAction
if rf, ok := ret.Get(0).(func(string) []restore.ItemAction); ok {
r0 = rf(restoreName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]restore.ItemAction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(restoreName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetBlockStore provides a mock function with given fields: name
func (_m *MockManager) GetBlockStore(name string) (cloudprovider.BlockStore, error) {
ret := _m.Called(name)
var r0 cloudprovider.BlockStore
if rf, ok := ret.Get(0).(func(string) cloudprovider.BlockStore); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(cloudprovider.BlockStore)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetObjectStore provides a mock function with given fields: name
func (_m *MockManager) GetObjectStore(name string) (cloudprovider.ObjectStore, error) {
ret := _m.Called(name)
var r0 cloudprovider.ObjectStore
if rf, ok := ret.Get(0).(func(string) cloudprovider.ObjectStore); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(cloudprovider.ObjectStore)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CleanupClients provides a mock function
func (_m *MockManager) CleanupClients() {
_ = _m.Called()
return
}
+12 -9
View File
@@ -50,7 +50,7 @@ type backupDeletionController struct {
deleteBackupRequestLister listers.DeleteBackupRequestLister
backupClient arkv1client.BackupsGetter
snapshotService cloudprovider.SnapshotService
backupService cloudprovider.BackupService
objectStore cloudprovider.ObjectStore
bucket string
restoreLister listers.RestoreLister
restoreClient arkv1client.RestoresGetter
@@ -58,6 +58,7 @@ type backupDeletionController struct {
resticMgr restic.RepositoryManager
podvolumeBackupLister listers.PodVolumeBackupLister
deleteBackupDir cloudprovider.DeleteBackupDirFunc
processRequestFunc func(*v1.DeleteBackupRequest) error
clock clock.Clock
}
@@ -69,7 +70,7 @@ func NewBackupDeletionController(
deleteBackupRequestClient arkv1client.DeleteBackupRequestsGetter,
backupClient arkv1client.BackupsGetter,
snapshotService cloudprovider.SnapshotService,
backupService cloudprovider.BackupService,
objectStore cloudprovider.ObjectStore,
bucket string,
restoreInformer informers.RestoreInformer,
restoreClient arkv1client.RestoresGetter,
@@ -83,14 +84,16 @@ func NewBackupDeletionController(
deleteBackupRequestLister: deleteBackupRequestInformer.Lister(),
backupClient: backupClient,
snapshotService: snapshotService,
backupService: backupService,
objectStore: objectStore,
bucket: bucket,
restoreLister: restoreInformer.Lister(),
restoreClient: restoreClient,
backupTracker: backupTracker,
resticMgr: resticMgr,
podvolumeBackupLister: podvolumeBackupInformer.Lister(),
clock: &clock.RealClock{},
podvolumeBackupLister: podvolumeBackupInformer.Lister(),
deleteBackupDir: cloudprovider.DeleteBackupDir,
clock: &clock.RealClock{},
}
c.syncHandler = c.processQueueItem
@@ -254,10 +257,10 @@ func (c *backupDeletionController) processRequest(req *v1.DeleteBackupRequest) e
}
}
// Try to delete backup from object storage
log.Info("Removing backup from object storage")
if err := c.backupService.DeleteBackupDir(c.bucket, backup.Name); err != nil {
errs = append(errs, errors.Wrap(err, "error deleting backup from object storage").Error())
// Try to delete backup from backup storage
log.Info("Removing backup from backup storage")
if err := c.deleteBackupDir(log, c.objectStore, c.bucket, backup.Name); err != nil {
errs = append(errs, errors.Wrap(err, "error deleting backup from backup storage").Error())
}
// Try to delete restores
@@ -23,10 +23,12 @@ import (
"github.com/heptio/ark/pkg/apis/ark/v1"
pkgbackup "github.com/heptio/ark/pkg/backup"
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/generated/clientset/versioned/fake"
informers "github.com/heptio/ark/pkg/generated/informers/externalversions"
arktest "github.com/heptio/ark/pkg/util/test"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -106,7 +108,6 @@ func TestBackupDeletionControllerProcessQueueItem(t *testing.T) {
type backupDeletionControllerTestData struct {
client *fake.Clientset
sharedInformers informers.SharedInformerFactory
backupService *arktest.BackupService
snapshotService *arktest.FakeSnapshotService
controller *backupDeletionController
req *v1.DeleteBackupRequest
@@ -115,14 +116,12 @@ type backupDeletionControllerTestData struct {
func setupBackupDeletionControllerTest(objects ...runtime.Object) *backupDeletionControllerTestData {
client := fake.NewSimpleClientset(objects...)
sharedInformers := informers.NewSharedInformerFactory(client, 0)
backupService := &arktest.BackupService{}
snapshotService := &arktest.FakeSnapshotService{SnapshotsTaken: sets.NewString()}
req := pkgbackup.NewDeleteBackupRequest("foo", "uid")
data := &backupDeletionControllerTestData{
client: client,
sharedInformers: sharedInformers,
backupService: backupService,
snapshotService: snapshotService,
controller: NewBackupDeletionController(
arktest.NewLogger(),
@@ -130,7 +129,7 @@ func setupBackupDeletionControllerTest(objects ...runtime.Object) *backupDeletio
client.ArkV1(), // deleteBackupRequestClient
client.ArkV1(), // backupClient
snapshotService,
backupService,
nil, // objectStore
"bucket",
sharedInformers.Ark().V1().Restores(),
client.ArkV1(), // restoreClient
@@ -150,7 +149,6 @@ func setupBackupDeletionControllerTest(objects ...runtime.Object) *backupDeletio
func TestBackupDeletionControllerProcessRequest(t *testing.T) {
t.Run("missing spec.backupName", func(t *testing.T) {
td := setupBackupDeletionControllerTest()
defer td.backupService.AssertExpectations(t)
td.req.Spec.BackupName = ""
@@ -171,7 +169,6 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) {
t.Run("existing deletion requests for the backup are deleted", func(t *testing.T) {
td := setupBackupDeletionControllerTest()
defer td.backupService.AssertExpectations(t)
// add the backup to the tracker so the execution of processRequest doesn't progress
// past checking for an in-progress backup. this makes validation easier.
@@ -227,7 +224,6 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) {
t.Run("deleting an in progress backup isn't allowed", func(t *testing.T) {
td := setupBackupDeletionControllerTest()
defer td.backupService.AssertExpectations(t)
td.controller.backupTracker.Add(td.req.Namespace, td.req.Spec.BackupName)
@@ -248,7 +244,6 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) {
t.Run("patching to InProgress fails", func(t *testing.T) {
td := setupBackupDeletionControllerTest()
defer td.backupService.AssertExpectations(t)
td.client.PrependReactor("patch", "deletebackuprequests", func(action core.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("bad")
@@ -261,7 +256,6 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) {
t.Run("patching backup to Deleting fails", func(t *testing.T) {
backup := arktest.NewTestBackup().WithName("foo").WithSnapshot("pv-1", "snap-1").Backup
td := setupBackupDeletionControllerTest(backup)
defer td.backupService.AssertExpectations(t)
td.client.PrependReactor("patch", "deletebackuprequests", func(action core.Action) (bool, runtime.Object, error) {
return true, td.req, nil
@@ -276,7 +270,6 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) {
t.Run("unable to find backup", func(t *testing.T) {
td := setupBackupDeletionControllerTest()
defer td.backupService.AssertExpectations(t)
td.client.PrependReactor("get", "backups", func(action core.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewNotFound(v1.SchemeGroupVersion.WithResource("backups").GroupResource(), "foo")
@@ -315,7 +308,6 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) {
t.Run("no snapshot service, backup has snapshots", func(t *testing.T) {
td := setupBackupDeletionControllerTest()
td.controller.snapshotService = nil
defer td.backupService.AssertExpectations(t)
td.client.PrependReactor("get", "backups", func(action core.Action) (bool, runtime.Object, error) {
backup := arktest.NewTestBackup().WithName("backup-1").WithSnapshot("pv-1", "snap-1").Backup
@@ -366,8 +358,6 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) {
td.sharedInformers.Ark().V1().Restores().Informer().GetStore().Add(restore2)
td.sharedInformers.Ark().V1().Restores().Informer().GetStore().Add(restore3)
defer td.backupService.AssertExpectations(t)
// Clear out req labels to make sure the controller adds them
td.req.Labels = make(map[string]string)
@@ -384,7 +374,11 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) {
return true, backup, nil
})
td.backupService.On("DeleteBackupDir", td.controller.bucket, td.req.Spec.BackupName).Return(nil)
td.controller.deleteBackupDir = func(_ logrus.FieldLogger, _ cloudprovider.ObjectStore, bucket, backupName string) error {
require.Equal(t, "bucket", bucket)
require.Equal(t, td.req.Spec.BackupName, backupName)
return nil
}
err := td.controller.processRequest(td.req)
require.NoError(t, err)
+4 -4
View File
@@ -40,7 +40,7 @@ import (
type backupSyncController struct {
client arkv1client.BackupsGetter
backupService cloudprovider.BackupService
cloudBackupLister cloudprovider.BackupLister
bucket string
syncPeriod time.Duration
namespace string
@@ -51,7 +51,7 @@ type backupSyncController struct {
func NewBackupSyncController(
client arkv1client.BackupsGetter,
backupService cloudprovider.BackupService,
cloudBackupLister cloudprovider.BackupLister,
bucket string,
syncPeriod time.Duration,
namespace string,
@@ -64,7 +64,7 @@ func NewBackupSyncController(
}
return &backupSyncController{
client: client,
backupService: backupService,
cloudBackupLister: cloudBackupLister,
bucket: bucket,
syncPeriod: syncPeriod,
namespace: namespace,
@@ -92,7 +92,7 @@ const gcFinalizer = "gc.ark.heptio.com"
func (c *backupSyncController) run() {
c.logger.Info("Syncing backups from object storage")
backups, err := c.backupService.GetAllBackups(c.bucket)
backups, err := c.cloudBackupLister.ListBackups(c.bucket)
if err != nil {
c.logger.WithError(err).Error("error listing backups")
return
+13 -13
View File
@@ -27,6 +27,7 @@ import (
core "k8s.io/client-go/testing"
"github.com/heptio/ark/pkg/apis/ark/v1"
cloudprovidermocks "github.com/heptio/ark/pkg/cloudprovider/mocks"
"github.com/heptio/ark/pkg/generated/clientset/versioned/fake"
informers "github.com/heptio/ark/pkg/generated/informers/externalversions"
"github.com/heptio/ark/pkg/util/stringslice"
@@ -37,18 +38,18 @@ import (
func TestBackupSyncControllerRun(t *testing.T) {
tests := []struct {
name string
getAllBackupsError error
cloudBackups []*v1.Backup
namespace string
existingBackups sets.String
name string
listBackupsError error
cloudBackups []*v1.Backup
namespace string
existingBackups sets.String
}{
{
name: "no cloud backups",
},
{
name: "backup service returns error on GetAllBackups",
getAllBackupsError: errors.New("getAllBackups"),
name: "backup lister returns error on ListBackups",
listBackupsError: errors.New("listBackups"),
},
{
name: "normal case",
@@ -96,7 +97,7 @@ func TestBackupSyncControllerRun(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var (
bs = &arktest.BackupService{}
backupLister = &cloudprovidermocks.BackupLister{}
client = fake.NewSimpleClientset()
sharedInformers = informers.NewSharedInformerFactory(client, 0)
logger = arktest.NewLogger()
@@ -104,7 +105,7 @@ func TestBackupSyncControllerRun(t *testing.T) {
c := NewBackupSyncController(
client.ArkV1(),
bs,
backupLister,
"bucket",
time.Duration(0),
test.namespace,
@@ -112,7 +113,7 @@ func TestBackupSyncControllerRun(t *testing.T) {
logger,
).(*backupSyncController)
bs.On("GetAllBackups", "bucket").Return(test.cloudBackups, test.getAllBackupsError)
backupLister.On("ListBackups", "bucket").Return(test.cloudBackups, test.listBackupsError)
expectedActions := make([]core.Action, 0)
@@ -154,7 +155,6 @@ func TestBackupSyncControllerRun(t *testing.T) {
}
assert.Equal(t, expectedActions, client.Actions())
bs.AssertExpectations(t)
})
}
}
@@ -217,7 +217,7 @@ func TestDeleteUnused(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var (
bs = &arktest.BackupService{}
backupLister = &cloudprovidermocks.BackupLister{}
client = fake.NewSimpleClientset()
sharedInformers = informers.NewSharedInformerFactory(client, 0)
logger = arktest.NewLogger()
@@ -225,7 +225,7 @@ func TestDeleteUnused(t *testing.T) {
c := NewBackupSyncController(
client.ArkV1(),
bs,
backupLister,
"bucket",
time.Duration(0),
test.namespace,
@@ -49,12 +49,14 @@ type downloadRequestController struct {
downloadRequestListerSynced cache.InformerSynced
restoreLister listers.RestoreLister
restoreListerSynced cache.InformerSynced
backupService cloudprovider.BackupService
objectStore cloudprovider.ObjectStore
bucket string
syncHandler func(key string) error
queue workqueue.RateLimitingInterface
clock clock.Clock
logger logrus.FieldLogger
createSignedURL cloudprovider.CreateSignedURLFunc
}
// NewDownloadRequestController creates a new DownloadRequestController.
@@ -62,7 +64,7 @@ func NewDownloadRequestController(
downloadRequestClient arkv1client.DownloadRequestsGetter,
downloadRequestInformer informers.DownloadRequestInformer,
restoreInformer informers.RestoreInformer,
backupService cloudprovider.BackupService,
objectStore cloudprovider.ObjectStore,
bucket string,
logger logrus.FieldLogger,
) Interface {
@@ -72,11 +74,13 @@ func NewDownloadRequestController(
downloadRequestListerSynced: downloadRequestInformer.Informer().HasSynced,
restoreLister: restoreInformer.Lister(),
restoreListerSynced: restoreInformer.Informer().HasSynced,
backupService: backupService,
objectStore: objectStore,
bucket: bucket,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "downloadrequest"),
clock: &clock.RealClock{},
logger: logger,
createSignedURL: cloudprovider.CreateSignedURL,
}
c.syncHandler = c.processDownloadRequest
@@ -236,7 +240,7 @@ func (c *downloadRequestController) generatePreSignedURL(downloadRequest *v1.Dow
directory = downloadRequest.Spec.Target.Name
}
update.Status.DownloadURL, err = c.backupService.CreateSignedURL(downloadRequest.Spec.Target, c.bucket, directory, signedURLTTL)
update.Status.DownloadURL, err = c.createSignedURL(c.objectStore, downloadRequest.Spec.Target, c.bucket, directory, signedURLTTL)
if err != nil {
return err
}
@@ -28,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/util/clock"
"github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/generated/clientset/versioned/fake"
informers "github.com/heptio/ark/pkg/generated/informers/externalversions"
arktest "github.com/heptio/ark/pkg/util/test"
@@ -106,17 +107,15 @@ func TestProcessDownloadRequest(t *testing.T) {
sharedInformers = informers.NewSharedInformerFactory(client, 0)
downloadRequestsInformer = sharedInformers.Ark().V1().DownloadRequests()
restoresInformer = sharedInformers.Ark().V1().Restores()
backupService = &arktest.BackupService{}
logger = arktest.NewLogger()
clockTime, _ = time.Parse("Mon Jan 2 15:04:05 2006", "Mon Jan 2 15:04:05 2006")
)
defer backupService.AssertExpectations(t)
c := NewDownloadRequestController(
client.ArkV1(),
downloadRequestsInformer,
restoresInformer,
backupService,
nil, // objectStore
"bucket",
logger,
).(*downloadRequestController)
@@ -126,7 +125,7 @@ func TestProcessDownloadRequest(t *testing.T) {
var downloadRequest *v1.DownloadRequest
if tc.expectedPhase == v1.DownloadRequestPhaseProcessed {
target := v1.DownloadTarget{
expectedTarget := v1.DownloadTarget{
Kind: tc.targetKind,
Name: tc.targetName,
}
@@ -137,7 +136,7 @@ func TestProcessDownloadRequest(t *testing.T) {
Name: "dr1",
},
Spec: v1.DownloadRequestSpec{
Target: target,
Target: expectedTarget,
},
}
downloadRequestsInformer.Informer().GetStore().Add(downloadRequest)
@@ -146,7 +145,13 @@ func TestProcessDownloadRequest(t *testing.T) {
restoresInformer.Informer().GetStore().Add(tc.restore)
}
backupService.On("CreateSignedURL", target, "bucket", tc.expectedDir, 10*time.Minute).Return("signedURL", nil)
c.createSignedURL = func(objectStore cloudprovider.ObjectStore, target v1.DownloadTarget, bucket, directory string, ttl time.Duration) (string, error) {
require.Equal(t, expectedTarget, target)
require.Equal(t, "bucket", bucket)
require.Equal(t, tc.expectedDir, directory)
require.Equal(t, 10*time.Minute, ttl)
return "signedURL", nil
}
}
// method under test
+99 -58
View File
@@ -51,6 +51,7 @@ import (
"github.com/heptio/ark/pkg/util/boolptr"
"github.com/heptio/ark/pkg/util/collections"
kubeutil "github.com/heptio/ark/pkg/util/kube"
"github.com/heptio/ark/pkg/util/logging"
)
// nonRestorableResources is a blacklist for the restoration process. Any resources
@@ -74,7 +75,7 @@ type restoreController struct {
restoreClient arkv1client.RestoresGetter
backupClient arkv1client.BackupsGetter
restorer restore.Restorer
backupService cloudprovider.BackupService
objectStoreConfig api.CloudProviderConfig
bucket string
pvProviderExists bool
backupLister listers.BackupLister
@@ -84,8 +85,15 @@ type restoreController struct {
syncHandler func(restoreName string) error
queue workqueue.RateLimitingInterface
logger logrus.FieldLogger
pluginManager plugin.Manager
logLevel logrus.Level
pluginRegistry plugin.Registry
metrics *metrics.ServerMetrics
getBackup cloudprovider.GetBackupFunc
downloadBackup cloudprovider.DownloadBackupFunc
uploadRestoreLog cloudprovider.UploadRestoreLogFunc
uploadRestoreResults cloudprovider.UploadRestoreResultsFunc
newPluginManager func(logger logrus.FieldLogger, logLevel logrus.Level, pluginRegistry plugin.Registry) plugin.Manager
}
func NewRestoreController(
@@ -94,20 +102,22 @@ func NewRestoreController(
restoreClient arkv1client.RestoresGetter,
backupClient arkv1client.BackupsGetter,
restorer restore.Restorer,
backupService cloudprovider.BackupService,
objectStoreConfig api.CloudProviderConfig,
bucket string,
backupInformer informers.BackupInformer,
pvProviderExists bool,
logger logrus.FieldLogger,
pluginManager plugin.Manager,
logLevel logrus.Level,
pluginRegistry plugin.Registry,
metrics *metrics.ServerMetrics,
) Interface {
c := &restoreController{
namespace: namespace,
restoreClient: restoreClient,
backupClient: backupClient,
restorer: restorer,
backupService: backupService,
objectStoreConfig: objectStoreConfig,
bucket: bucket,
pvProviderExists: pvProviderExists,
backupLister: backupInformer.Lister(),
@@ -116,8 +126,17 @@ func NewRestoreController(
restoreListerSynced: restoreInformer.Informer().HasSynced,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "restore"),
logger: logger,
pluginManager: pluginManager,
logLevel: logLevel,
pluginRegistry: pluginRegistry,
metrics: metrics,
getBackup: cloudprovider.GetBackup,
downloadBackup: cloudprovider.DownloadBackup,
uploadRestoreLog: cloudprovider.UploadRestoreLog,
uploadRestoreResults: cloudprovider.UploadRestoreResults,
newPluginManager: func(logger logrus.FieldLogger, logLevel logrus.Level, pluginRegistry plugin.Registry) plugin.Manager {
return plugin.NewManager(logger, logLevel, pluginRegistry)
},
}
c.syncHandler = c.processRestore
@@ -230,7 +249,9 @@ func (c *restoreController) processRestore(key string) error {
logContext.Debug("Running processRestore")
ns, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
return errors.Wrap(err, "error splitting queue key")
logContext.WithError(err).Error("unable to process restore: error splitting queue key")
// Return nil here so we don't try to process the key any more
return nil
}
logContext.Debug("Getting Restore")
@@ -259,8 +280,21 @@ func (c *restoreController) processRestore(key string) error {
// don't modify items in the cache
restore = restore.DeepCopy()
// validation
if restore.Status.ValidationErrors = c.completeAndValidate(restore); len(restore.Status.ValidationErrors) > 0 {
pluginManager := c.newPluginManager(logContext, logContext.Level, c.pluginRegistry)
defer pluginManager.CleanupClients()
objectStore, err := getObjectStore(c.objectStoreConfig, pluginManager)
if err != nil {
return errors.Wrap(err, "error initializing object store")
}
actions, err := pluginManager.GetRestoreItemActions()
if err != nil {
return errors.Wrap(err, "error initializing restore item actions")
}
// complete & validate restore
if restore.Status.ValidationErrors = c.completeAndValidate(objectStore, restore); len(restore.Status.ValidationErrors) > 0 {
restore.Status.Phase = api.RestorePhaseFailedValidation
} else {
restore.Status.Phase = api.RestorePhaseInProgress
@@ -285,7 +319,11 @@ func (c *restoreController) processRestore(key string) error {
}
logContext.Debug("Running restore")
// execution & upload of restore
restoreWarnings, restoreErrors, restoreFailure := c.runRestore(restore, c.bucket)
restoreWarnings, restoreErrors, restoreFailure := c.runRestore(
restore,
actions,
objectStore,
)
restore.Status.Warnings = len(restoreWarnings.Ark) + len(restoreWarnings.Cluster)
for _, w := range restoreWarnings.Namespaces {
@@ -317,7 +355,7 @@ func (c *restoreController) processRestore(key string) error {
return nil
}
func (c *restoreController) completeAndValidate(restore *api.Restore) []string {
func (c *restoreController) completeAndValidate(objectStore cloudprovider.ObjectStore, restore *api.Restore) []string {
// add non-restorable resources to restore's excluded resources
excludedResources := sets.NewString(restore.Spec.ExcludedResources...)
for _, nonrestorable := range nonRestorableResources {
@@ -381,7 +419,7 @@ func (c *restoreController) completeAndValidate(restore *api.Restore) []string {
backup *api.Backup
err error
)
if backup, err = c.fetchBackup(c.bucket, restore.Spec.BackupName); err != nil {
if backup, err = c.fetchBackup(objectStore, restore.Spec.BackupName); err != nil {
return append(validationErrors, fmt.Sprintf("Error retrieving backup: %v", err))
}
@@ -424,7 +462,7 @@ func mostRecentCompletedBackup(backups []*api.Backup) *api.Backup {
return nil
}
func (c *restoreController) fetchBackup(bucket, name string) (*api.Backup, error) {
func (c *restoreController) fetchBackup(objectStore cloudprovider.ObjectStore, name string) (*api.Backup, error) {
backup, err := c.backupLister.Backups(c.namespace).Get(name)
if err == nil {
return backup, nil
@@ -437,7 +475,7 @@ func (c *restoreController) fetchBackup(bucket, name string) (*api.Backup, error
logContext := c.logger.WithField("backupName", name)
logContext.Debug("Backup not found in backupLister, checking object storage directly")
backup, err = c.backupService.GetBackup(bucket, name)
backup, err = c.getBackup(objectStore, c.bucket, name)
if err != nil {
return nil, err
}
@@ -457,39 +495,56 @@ func (c *restoreController) fetchBackup(bucket, name string) (*api.Backup, error
return backup, nil
}
func (c *restoreController) runRestore(restore *api.Restore, bucket string) (restoreWarnings, restoreErrors api.RestoreResult, restoreFailure error) {
logContext := c.logger.WithFields(
func (c *restoreController) runRestore(
restore *api.Restore,
actions []restore.ItemAction,
objectStore cloudprovider.ObjectStore,
) (restoreWarnings, restoreErrors api.RestoreResult, restoreFailure error) {
logFile, err := ioutil.TempFile("", "")
if err != nil {
c.logger.
WithFields(
logrus.Fields{
"restore": kubeutil.NamespaceAndName(restore),
"backup": restore.Spec.BackupName,
},
).
WithError(errors.WithStack(err)).
Error("Error creating log temp file")
restoreErrors.Ark = append(restoreErrors.Ark, err.Error())
return
}
gzippedLogFile := gzip.NewWriter(logFile)
// Assuming we successfully uploaded the log file, this will have already been closed below. It is safe to call
// close multiple times. If we get an error closing this, there's not really anything we can do about it.
defer gzippedLogFile.Close()
defer closeAndRemoveFile(logFile, c.logger)
// Log the backup to both a backup log file and to stdout. This will help see what happened if the upload of the
// backup log failed for whatever reason.
logger := logging.DefaultLogger(c.logLevel)
logger.Out = io.MultiWriter(os.Stdout, gzippedLogFile)
logContext := logger.WithFields(
logrus.Fields{
"restore": kubeutil.NamespaceAndName(restore),
"backup": restore.Spec.BackupName,
})
backup, err := c.fetchBackup(bucket, restore.Spec.BackupName)
backup, err := c.fetchBackup(objectStore, restore.Spec.BackupName)
if err != nil {
logContext.WithError(err).Error("Error getting backup")
restoreErrors.Ark = append(restoreErrors.Ark, err.Error())
return
}
var tempFiles []*os.File
backupFile, err := downloadToTempFile(restore.Spec.BackupName, c.backupService, bucket, c.logger)
backupFile, err := downloadToTempFile(objectStore, c.bucket, restore.Spec.BackupName, c.downloadBackup, c.logger)
if err != nil {
logContext.WithError(err).Error("Error downloading backup")
restoreErrors.Ark = append(restoreErrors.Ark, err.Error())
restoreFailure = err
return
}
tempFiles = append(tempFiles, backupFile)
logFile, err := ioutil.TempFile("", "")
if err != nil {
logContext.WithError(errors.WithStack(err)).Error("Error creating log temp file")
restoreErrors.Ark = append(restoreErrors.Ark, err.Error())
restoreFailure = err
return
}
tempFiles = append(tempFiles, logFile)
defer closeAndRemoveFile(backupFile, c.logger)
resultsFile, err := ioutil.TempFile("", "")
if err != nil {
@@ -498,44 +553,25 @@ func (c *restoreController) runRestore(restore *api.Restore, bucket string) (res
restoreFailure = err
return
}
tempFiles = append(tempFiles, resultsFile)
defer func() {
for _, file := range tempFiles {
if err := file.Close(); err != nil {
logContext.WithError(errors.WithStack(err)).WithField("file", file.Name()).Error("Error closing file")
restoreFailure = err
}
if err := os.Remove(file.Name()); err != nil {
logContext.WithError(errors.WithStack(err)).WithField("file", file.Name()).Error("Error removing file")
restoreFailure = err
}
}
}()
actions, err := c.pluginManager.GetRestoreItemActions(restore.Name)
if err != nil {
restoreErrors.Ark = append(restoreErrors.Ark, err.Error())
return
}
defer c.pluginManager.CloseRestoreItemActions(restore.Name)
defer closeAndRemoveFile(resultsFile, c.logger)
// Any return statement above this line means a total restore failure
// Some failures after this line *may* be a total restore failure
logContext.Info("starting restore")
restoreWarnings, restoreErrors = c.restorer.Restore(restore, backup, backupFile, logFile, actions)
restoreWarnings, restoreErrors = c.restorer.Restore(logContext, restore, backup, backupFile, actions)
logContext.Info("restore completed")
// Try to upload the log file. This is best-effort. If we fail, we'll add to the ark errors.
if err := gzippedLogFile.Close(); err != nil {
c.logger.WithError(err).Error("error closing gzippedLogFile")
}
// Reset the offset to 0 for reading
if _, err = logFile.Seek(0, 0); err != nil {
restoreErrors.Ark = append(restoreErrors.Ark, fmt.Sprintf("error resetting log file offset to 0: %v", err))
return
}
if err := c.backupService.UploadRestoreLog(bucket, restore.Spec.BackupName, restore.Name, logFile); err != nil {
if err := c.uploadRestoreLog(objectStore, c.bucket, restore.Spec.BackupName, restore.Name, logFile); err != nil {
restoreErrors.Ark = append(restoreErrors.Ark, fmt.Sprintf("error uploading log file to object storage: %v", err))
}
@@ -556,15 +592,20 @@ func (c *restoreController) runRestore(restore *api.Restore, bucket string) (res
logContext.WithError(errors.WithStack(err)).Error("Error resetting results file offset to 0")
return
}
if err := c.backupService.UploadRestoreResults(bucket, restore.Spec.BackupName, restore.Name, resultsFile); err != nil {
if err := c.uploadRestoreResults(objectStore, c.bucket, restore.Spec.BackupName, restore.Name, resultsFile); err != nil {
logContext.WithError(errors.WithStack(err)).Error("Error uploading results files to object storage")
}
return
}
func downloadToTempFile(backupName string, backupService cloudprovider.BackupService, bucket string, logger logrus.FieldLogger) (*os.File, error) {
readCloser, err := backupService.DownloadBackup(bucket, backupName)
func downloadToTempFile(
objectStore cloudprovider.ObjectStore,
bucket, backupName string,
downloadBackup cloudprovider.DownloadBackupFunc,
logger logrus.FieldLogger,
) (*os.File, error) {
readCloser, err := downloadBackup(objectStore, bucket, backupName)
if err != nil {
return nil, err
}
+136 -56
View File
@@ -25,6 +25,7 @@ import (
"testing"
"time"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@@ -35,9 +36,12 @@ import (
"k8s.io/client-go/tools/cache"
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/heptio/ark/pkg/generated/clientset/versioned/fake"
informers "github.com/heptio/ark/pkg/generated/informers/externalversions"
"github.com/heptio/ark/pkg/metrics"
"github.com/heptio/ark/pkg/plugin"
pluginmocks "github.com/heptio/ark/pkg/plugin/mocks"
"github.com/heptio/ark/pkg/restore"
"github.com/heptio/ark/pkg/util/collections"
arktest "github.com/heptio/ark/pkg/util/test"
@@ -79,9 +83,7 @@ func TestFetchBackup(t *testing.T) {
client = fake.NewSimpleClientset()
restorer = &fakeRestorer{}
sharedInformers = informers.NewSharedInformerFactory(client, 0)
backupSvc = &arktest.BackupService{}
logger = arktest.NewLogger()
pluginManager = &MockManager{}
)
c := NewRestoreController(
@@ -90,12 +92,13 @@ func TestFetchBackup(t *testing.T) {
client.ArkV1(),
client.ArkV1(),
restorer,
backupSvc,
api.CloudProviderConfig{},
"bucket",
sharedInformers.Ark().V1().Backups(),
false,
logger,
pluginManager,
logrus.InfoLevel,
nil, //pluginRegistry
metrics.NewServerMetrics(),
).(*restoreController)
@@ -104,20 +107,96 @@ func TestFetchBackup(t *testing.T) {
}
if test.backupServiceBackup != nil || test.backupServiceError != nil {
backupSvc.On("GetBackup", "bucket", test.backupName).Return(test.backupServiceBackup, test.backupServiceError)
c.getBackup = func(_ cloudprovider.ObjectStore, bucket, backup string) (*api.Backup, error) {
require.Equal(t, "bucket", bucket)
require.Equal(t, test.backupName, backup)
return test.backupServiceBackup, test.backupServiceError
}
}
backup, err := c.fetchBackup("bucket", test.backupName)
backup, err := c.fetchBackup(nil, test.backupName)
if assert.Equal(t, test.expectedErr, err != nil) {
assert.Equal(t, test.expectedRes, backup)
}
backupSvc.AssertExpectations(t)
})
}
}
func TestProcessRestoreSkips(t *testing.T) {
tests := []struct {
name string
restoreKey string
restore *api.Restore
expectError bool
}{
{
name: "invalid key returns error",
restoreKey: "invalid/key/value",
},
{
name: "missing restore returns error",
restoreKey: "foo/bar",
expectError: true,
},
{
name: "restore with phase InProgress does not get processed",
restoreKey: "foo/bar",
restore: arktest.NewTestRestore("foo", "bar", api.RestorePhaseInProgress).Restore,
},
{
name: "restore with phase Completed does not get processed",
restoreKey: "foo/bar",
restore: arktest.NewTestRestore("foo", "bar", api.RestorePhaseCompleted).Restore,
},
{
name: "restore with phase FailedValidation does not get processed",
restoreKey: "foo/bar",
restore: arktest.NewTestRestore("foo", "bar", api.RestorePhaseFailedValidation).Restore,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var (
client = fake.NewSimpleClientset()
restorer = &fakeRestorer{}
sharedInformers = informers.NewSharedInformerFactory(client, 0)
logger = arktest.NewLogger()
pluginManager = &pluginmocks.Manager{}
objectStore = &arktest.ObjectStore{}
)
defer restorer.AssertExpectations(t)
defer objectStore.AssertExpectations(t)
c := NewRestoreController(
api.DefaultNamespace,
sharedInformers.Ark().V1().Restores(),
client.ArkV1(),
client.ArkV1(),
restorer,
api.CloudProviderConfig{Name: "myCloud"},
"bucket",
sharedInformers.Ark().V1().Backups(),
false, // pvProviderExists
logger,
logrus.InfoLevel,
nil, // pluginRegistry
metrics.NewServerMetrics(),
).(*restoreController)
c.newPluginManager = func(logger logrus.FieldLogger, logLevel logrus.Level, pluginRegistry plugin.Registry) plugin.Manager {
return pluginManager
}
if test.restore != nil {
sharedInformers.Ark().V1().Restores().Informer().GetStore().Add(test.restore)
}
err := c.processRestore(test.restoreKey)
assert.Equal(t, test.expectError, err != nil)
})
}
}
func TestProcessRestore(t *testing.T) {
tests := []struct {
name string
@@ -136,31 +215,6 @@ func TestProcessRestore(t *testing.T) {
backupServiceDownloadBackupError error
expectedFinalPhase string
}{
{
name: "invalid key returns error",
restoreKey: "invalid/key/value",
expectedErr: true,
},
{
name: "missing restore returns error",
restoreKey: "foo/bar",
expectedErr: true,
},
{
name: "restore with phase InProgress does not get processed",
restore: arktest.NewTestRestore("foo", "bar", api.RestorePhaseInProgress).Restore,
expectedErr: false,
},
{
name: "restore with phase Completed does not get processed",
restore: arktest.NewTestRestore("foo", "bar", api.RestorePhaseCompleted).Restore,
expectedErr: false,
},
{
name: "restore with phase FailedValidation does not get processed",
restore: arktest.NewTestRestore("foo", "bar", api.RestorePhaseFailedValidation).Restore,
expectedErr: false,
},
{
name: "restore with both namespace in both includedNamespaces and excludedNamespaces fails validation",
restore: NewRestore("foo", "bar", "backup-1", "another-1", "*", api.RestorePhaseNew).WithExcludedNamespace("another-1").Restore,
@@ -318,13 +372,12 @@ func TestProcessRestore(t *testing.T) {
client = fake.NewSimpleClientset()
restorer = &fakeRestorer{}
sharedInformers = informers.NewSharedInformerFactory(client, 0)
backupSvc = &arktest.BackupService{}
logger = arktest.NewLogger()
pluginManager = &MockManager{}
pluginManager = &pluginmocks.Manager{}
objectStore = &arktest.ObjectStore{}
)
defer restorer.AssertExpectations(t)
defer backupSvc.AssertExpectations(t)
defer objectStore.AssertExpectations(t)
c := NewRestoreController(
api.DefaultNamespace,
@@ -332,16 +385,23 @@ func TestProcessRestore(t *testing.T) {
client.ArkV1(),
client.ArkV1(),
restorer,
backupSvc,
api.CloudProviderConfig{Name: "myCloud"},
"bucket",
sharedInformers.Ark().V1().Backups(),
test.allowRestoreSnapshots,
logger,
pluginManager,
logrus.InfoLevel,
nil, // pluginRegistry
metrics.NewServerMetrics(),
).(*restoreController)
c.newPluginManager = func(logger logrus.FieldLogger, logLevel logrus.Level, pluginRegistry plugin.Registry) plugin.Manager {
return pluginManager
}
if test.restore != nil {
pluginManager.On("GetObjectStore", "myCloud").Return(objectStore, nil)
objectStore.On("Init", mock.Anything).Return(nil)
sharedInformers.Ark().V1().Restores().Informer().GetStore().Add(test.restore)
// this is necessary so the Patch() call returns the appropriate object
@@ -391,11 +451,24 @@ func TestProcessRestore(t *testing.T) {
errors.Ark = append(errors.Ark, "error uploading log file to object storage: "+test.uploadLogError.Error())
}
if test.expectedRestorerCall != nil {
downloadedBackup := ioutil.NopCloser(bytes.NewReader([]byte("hello world")))
backupSvc.On("DownloadBackup", mock.Anything, mock.Anything).Return(downloadedBackup, nil)
restorer.On("Restore", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(warnings, errors)
backupSvc.On("UploadRestoreLog", "bucket", test.backup.Name, test.restore.Name, mock.Anything).Return(test.uploadLogError)
backupSvc.On("UploadRestoreResults", "bucket", test.backup.Name, test.restore.Name, mock.Anything).Return(nil)
c.downloadBackup = func(objectStore cloudprovider.ObjectStore, bucket, backup string) (io.ReadCloser, error) {
require.Equal(t, test.backup.Name, backup)
return ioutil.NopCloser(bytes.NewReader([]byte("hello world"))), nil
}
restorer.On("Restore", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(warnings, errors)
c.uploadRestoreLog = func(objectStore cloudprovider.ObjectStore, bucket, backup, restore string, log io.Reader) error {
require.Equal(t, test.backup.Name, backup)
require.Equal(t, test.restore.Name, restore)
return test.uploadLogError
}
c.uploadRestoreResults = func(objectStore cloudprovider.ObjectStore, bucket, backup, restore string, results io.Reader) error {
require.Equal(t, test.backup.Name, backup)
require.Equal(t, test.restore.Name, restore)
return nil
}
}
var (
@@ -410,21 +483,27 @@ func TestProcessRestore(t *testing.T) {
}
if test.backupServiceGetBackupError != nil {
backupSvc.On("GetBackup", "bucket", mock.Anything).Return(nil, test.backupServiceGetBackupError)
c.getBackup = func(_ cloudprovider.ObjectStore, bucket, backup string) (*api.Backup, error) {
require.Equal(t, "bucket", bucket)
require.Equal(t, test.restore.Spec.BackupName, backup)
return nil, test.backupServiceGetBackupError
}
}
if test.backupServiceDownloadBackupError != nil {
backupSvc.On("DownloadBackup", "bucket", test.restore.Spec.BackupName).Return(nil, test.backupServiceDownloadBackupError)
c.downloadBackup = func(_ cloudprovider.ObjectStore, bucket, backupName string) (io.ReadCloser, error) {
require.Equal(t, "bucket", bucket)
require.Equal(t, test.restore.Spec.BackupName, backupName)
return nil, test.backupServiceDownloadBackupError
}
}
if test.restore != nil {
pluginManager.On("GetRestoreItemActions", test.restore.Name).Return(nil, nil)
pluginManager.On("CloseRestoreItemActions", test.restore.Name).Return(nil)
pluginManager.On("GetRestoreItemActions").Return(nil, nil)
pluginManager.On("CleanupClients")
}
err = c.processRestore(key)
backupSvc.AssertExpectations(t)
restorer.AssertExpectations(t)
assert.Equal(t, test.expectedErr, err != nil, "got error %v", err)
actions := client.Actions()
@@ -524,11 +603,12 @@ func TestCompleteAndValidateWhenScheduleNameSpecified(t *testing.T) {
client.ArkV1(),
client.ArkV1(),
nil,
nil,
api.CloudProviderConfig{Name: "myCloud"},
"bucket",
sharedInformers.Ark().V1().Backups(),
false,
logger,
logrus.DebugLevel,
nil,
nil,
).(*restoreController)
@@ -552,7 +632,7 @@ func TestCompleteAndValidateWhenScheduleNameSpecified(t *testing.T) {
Backup,
))
errs := c.completeAndValidate(restore)
errs := c.completeAndValidate(nil, restore)
assert.Equal(t, []string{"No backups found for schedule"}, errs)
assert.Empty(t, restore.Spec.BackupName)
@@ -565,7 +645,7 @@ func TestCompleteAndValidateWhenScheduleNameSpecified(t *testing.T) {
Backup,
))
errs = c.completeAndValidate(restore)
errs = c.completeAndValidate(nil, restore)
assert.Equal(t, []string{"No completed backups found for schedule"}, errs)
assert.Empty(t, restore.Spec.BackupName)
@@ -589,7 +669,7 @@ func TestCompleteAndValidateWhenScheduleNameSpecified(t *testing.T) {
Backup,
))
errs = c.completeAndValidate(restore)
errs = c.completeAndValidate(nil, restore)
assert.Nil(t, errs)
assert.Equal(t, "bar", restore.Spec.BackupName)
}
@@ -708,13 +788,13 @@ type fakeRestorer struct {
}
func (r *fakeRestorer) Restore(
log logrus.FieldLogger,
restore *api.Restore,
backup *api.Backup,
backupReader io.Reader,
logger io.Writer,
actions []restore.ItemAction,
) (api.RestoreResult, api.RestoreResult) {
res := r.Called(restore, backup, backupReader, logger)
res := r.Called(log, restore, backup, backupReader, actions)
r.calledWithArg = *restore
+55 -24
View File
@@ -20,7 +20,7 @@ import (
"encoding/json"
"github.com/hashicorp/go-plugin"
"github.com/sirupsen/logrus"
"github.com/pkg/errors"
"golang.org/x/net/context"
"google.golang.org/grpc"
@@ -38,41 +38,41 @@ import (
// interface.
type BackupItemActionPlugin struct {
plugin.NetRPCUnsupportedPlugin
impl arkbackup.ItemAction
log *logrusAdapter
*pluginBase
}
// NewBackupItemActionPlugin constructs a BackupItemActionPlugin.
func NewBackupItemActionPlugin(itemAction arkbackup.ItemAction) *BackupItemActionPlugin {
func NewBackupItemActionPlugin(options ...pluginOption) *BackupItemActionPlugin {
return &BackupItemActionPlugin{
impl: itemAction,
pluginBase: newPluginBase(options...),
}
}
func (p *BackupItemActionPlugin) Kind() PluginKind {
return PluginKindBackupItemAction
}
//////////////////////////////////////////////////////////////////////////////
// client code
//////////////////////////////////////////////////////////////////////////////
// GRPCServer registers a BackupItemAction gRPC server.
func (p *BackupItemActionPlugin) GRPCServer(s *grpc.Server) error {
proto.RegisterBackupItemActionServer(s, &BackupItemActionGRPCServer{impl: p.impl})
return nil
}
// GRPCClient returns a BackupItemAction gRPC client.
// GRPCClient returns a clientDispenser for BackupItemAction gRPC clients.
func (p *BackupItemActionPlugin) GRPCClient(c *grpc.ClientConn) (interface{}, error) {
return &BackupItemActionGRPCClient{grpcClient: proto.NewBackupItemActionClient(c), log: p.log}, nil
return newClientDispenser(p.clientLogger, c, newBackupItemActionGRPCClient), nil
}
// BackupItemActionGRPCClient implements the backup/ItemAction interface and uses a
// gRPC client to make calls to the plugin server.
type BackupItemActionGRPCClient struct {
*clientBase
grpcClient proto.BackupItemActionClient
log *logrusAdapter
}
func newBackupItemActionGRPCClient(base *clientBase, clientConn *grpc.ClientConn) interface{} {
return &BackupItemActionGRPCClient{
clientBase: base,
grpcClient: proto.NewBackupItemActionClient(clientConn),
}
}
func (c *BackupItemActionGRPCClient) AppliesTo() (arkbackup.ResourceSelector, error) {
res, err := c.grpcClient.AppliesTo(context.Background(), &proto.Empty{})
res, err := c.grpcClient.AppliesTo(context.Background(), &proto.AppliesToRequest{Plugin: c.plugin})
if err != nil {
return arkbackup.ResourceSelector{}, err
}
@@ -98,6 +98,7 @@ func (c *BackupItemActionGRPCClient) Execute(item runtime.Unstructured, backup *
}
req := &proto.ExecuteRequest{
Plugin: c.plugin,
Item: itemJSON,
Backup: backupJSON,
}
@@ -130,18 +131,43 @@ func (c *BackupItemActionGRPCClient) Execute(item runtime.Unstructured, backup *
return &updatedItem, additionalItems, nil
}
func (c *BackupItemActionGRPCClient) SetLog(log logrus.FieldLogger) {
c.log.impl = log
//////////////////////////////////////////////////////////////////////////////
// server code
//////////////////////////////////////////////////////////////////////////////
// GRPCServer registers a BackupItemAction gRPC server.
func (p *BackupItemActionPlugin) GRPCServer(s *grpc.Server) error {
proto.RegisterBackupItemActionServer(s, &BackupItemActionGRPCServer{mux: p.serverMux})
return nil
}
// BackupItemActionGRPCServer implements the proto-generated BackupItemActionServer interface, and accepts
// gRPC calls and forwards them to an implementation of the pluggable interface.
type BackupItemActionGRPCServer struct {
impl arkbackup.ItemAction
mux *serverMux
}
func (s *BackupItemActionGRPCServer) AppliesTo(ctx context.Context, req *proto.Empty) (*proto.AppliesToResponse, error) {
resourceSelector, err := s.impl.AppliesTo()
func (s *BackupItemActionGRPCServer) getImpl(name string) (arkbackup.ItemAction, error) {
impl, err := s.mux.getHandler(name)
if err != nil {
return nil, err
}
itemAction, ok := impl.(arkbackup.ItemAction)
if !ok {
return nil, errors.Errorf("%T is not a backup item action", impl)
}
return itemAction, nil
}
func (s *BackupItemActionGRPCServer) AppliesTo(ctx context.Context, req *proto.AppliesToRequest) (*proto.AppliesToResponse, error) {
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
resourceSelector, err := impl.AppliesTo()
if err != nil {
return nil, err
}
@@ -156,6 +182,11 @@ func (s *BackupItemActionGRPCServer) AppliesTo(ctx context.Context, req *proto.E
}
func (s *BackupItemActionGRPCServer) Execute(ctx context.Context, req *proto.ExecuteRequest) (*proto.ExecuteResponse, error) {
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
var item unstructured.Unstructured
var backup api.Backup
@@ -166,7 +197,7 @@ func (s *BackupItemActionGRPCServer) Execute(ctx context.Context, req *proto.Exe
return nil, err
}
updatedItem, additionalItems, err := s.impl.Execute(&item, &backup)
updatedItem, additionalItems, err := impl.Execute(&item, &backup)
if err != nil {
return nil, err
}
+10 -2
View File
@@ -24,6 +24,7 @@ import (
"github.com/heptio/ark/pkg/backup"
"github.com/heptio/ark/pkg/backup/mocks"
proto "github.com/heptio/ark/pkg/plugin/generated"
arktest "github.com/heptio/ark/pkg/util/test"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -151,9 +152,15 @@ func TestBackupItemActionGRPCServerExecute(t *testing.T) {
itemAction.On("Execute", &validItemObject, &validBackupObject).Return(test.implUpdatedItem, test.implAdditionalItems, test.implError)
}
s := &BackupItemActionGRPCServer{impl: itemAction}
s := &BackupItemActionGRPCServer{mux: &serverMux{
serverLog: arktest.NewLogger(),
handlers: map[string]interface{}{
"xyz": itemAction,
},
}}
req := &proto.ExecuteRequest{
Plugin: "xyz",
Item: test.item,
Backup: test.backup,
}
@@ -162,9 +169,10 @@ func TestBackupItemActionGRPCServerExecute(t *testing.T) {
// Verify error
assert.Equal(t, test.expectError, err != nil)
if test.expectError {
if err != nil {
return
}
require.NotNil(t, resp)
// Verify updated item
updatedItem := test.implUpdatedItem
+97 -27
View File
@@ -20,6 +20,7 @@ import (
"encoding/json"
"github.com/hashicorp/go-plugin"
"github.com/pkg/errors"
"golang.org/x/net/context"
"google.golang.org/grpc"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -34,43 +35,44 @@ import (
// interface.
type BlockStorePlugin struct {
plugin.NetRPCUnsupportedPlugin
impl cloudprovider.BlockStore
*pluginBase
}
// NewBlockStorePlugin constructs a BlockStorePlugin.
func NewBlockStorePlugin(blockStore cloudprovider.BlockStore) *BlockStorePlugin {
func NewBlockStorePlugin(options ...pluginOption) *BlockStorePlugin {
return &BlockStorePlugin{
impl: blockStore,
pluginBase: newPluginBase(options...),
}
}
func (p *BlockStorePlugin) Kind() PluginKind {
return PluginKindBlockStore
}
// GRPCServer registers a BlockStore gRPC server.
func (p *BlockStorePlugin) GRPCServer(s *grpc.Server) error {
proto.RegisterBlockStoreServer(s, &BlockStoreGRPCServer{impl: p.impl})
return nil
}
//////////////////////////////////////////////////////////////////////////////
// client code
//////////////////////////////////////////////////////////////////////////////
// GRPCClient returns a BlockStore gRPC client.
func (p *BlockStorePlugin) GRPCClient(c *grpc.ClientConn) (interface{}, error) {
return &BlockStoreGRPCClient{grpcClient: proto.NewBlockStoreClient(c)}, nil
return newClientDispenser(p.clientLogger, c, newBlockStoreGRPCClient), nil
}
// BlockStoreGRPCClient implements the cloudprovider.BlockStore interface and uses a
// gRPC client to make calls to the plugin server.
type BlockStoreGRPCClient struct {
*clientBase
grpcClient proto.BlockStoreClient
}
func newBlockStoreGRPCClient(base *clientBase, clientConn *grpc.ClientConn) interface{} {
return &BlockStoreGRPCClient{
clientBase: base,
grpcClient: proto.NewBlockStoreClient(clientConn),
}
}
// Init prepares the BlockStore for usage using the provided map of
// configuration key-value pairs. It returns an error if the BlockStore
// cannot be initialized from the provided config.
func (c *BlockStoreGRPCClient) Init(config map[string]string) error {
_, err := c.grpcClient.Init(context.Background(), &proto.InitRequest{Config: config})
_, err := c.grpcClient.Init(context.Background(), &proto.InitRequest{Plugin: c.plugin, Config: config})
return err
}
@@ -79,6 +81,7 @@ func (c *BlockStoreGRPCClient) Init(config map[string]string) error {
// and with the specified type and IOPS (if using provisioned IOPS).
func (c *BlockStoreGRPCClient) CreateVolumeFromSnapshot(snapshotID, volumeType, volumeAZ string, iops *int64) (string, error) {
req := &proto.CreateVolumeRequest{
Plugin: c.plugin,
SnapshotID: snapshotID,
VolumeType: volumeType,
VolumeAZ: volumeAZ,
@@ -101,7 +104,7 @@ func (c *BlockStoreGRPCClient) CreateVolumeFromSnapshot(snapshotID, volumeType,
// GetVolumeInfo returns the type and IOPS (if using provisioned IOPS) for a specified block
// volume.
func (c *BlockStoreGRPCClient) GetVolumeInfo(volumeID, volumeAZ string) (string, *int64, error) {
res, err := c.grpcClient.GetVolumeInfo(context.Background(), &proto.GetVolumeInfoRequest{VolumeID: volumeID, VolumeAZ: volumeAZ})
res, err := c.grpcClient.GetVolumeInfo(context.Background(), &proto.GetVolumeInfoRequest{Plugin: c.plugin, VolumeID: volumeID, VolumeAZ: volumeAZ})
if err != nil {
return "", nil, err
}
@@ -116,7 +119,7 @@ func (c *BlockStoreGRPCClient) GetVolumeInfo(volumeID, volumeAZ string) (string,
// IsVolumeReady returns whether the specified volume is ready to be used.
func (c *BlockStoreGRPCClient) IsVolumeReady(volumeID, volumeAZ string) (bool, error) {
res, err := c.grpcClient.IsVolumeReady(context.Background(), &proto.IsVolumeReadyRequest{VolumeID: volumeID, VolumeAZ: volumeAZ})
res, err := c.grpcClient.IsVolumeReady(context.Background(), &proto.IsVolumeReadyRequest{Plugin: c.plugin, VolumeID: volumeID, VolumeAZ: volumeAZ})
if err != nil {
return false, err
}
@@ -128,6 +131,7 @@ func (c *BlockStoreGRPCClient) IsVolumeReady(volumeID, volumeAZ string) (bool, e
// set of tags to the snapshot.
func (c *BlockStoreGRPCClient) CreateSnapshot(volumeID, volumeAZ string, tags map[string]string) (string, error) {
req := &proto.CreateSnapshotRequest{
Plugin: c.plugin,
VolumeID: volumeID,
VolumeAZ: volumeAZ,
Tags: tags,
@@ -143,7 +147,7 @@ func (c *BlockStoreGRPCClient) CreateSnapshot(volumeID, volumeAZ string, tags ma
// DeleteSnapshot deletes the specified volume snapshot.
func (c *BlockStoreGRPCClient) DeleteSnapshot(snapshotID string) error {
_, err := c.grpcClient.DeleteSnapshot(context.Background(), &proto.DeleteSnapshotRequest{SnapshotID: snapshotID})
_, err := c.grpcClient.DeleteSnapshot(context.Background(), &proto.DeleteSnapshotRequest{Plugin: c.plugin, SnapshotID: snapshotID})
return err
}
@@ -155,6 +159,7 @@ func (c *BlockStoreGRPCClient) GetVolumeID(pv runtime.Unstructured) (string, err
}
req := &proto.GetVolumeIDRequest{
Plugin: c.plugin,
PersistentVolume: encodedPV,
}
@@ -173,6 +178,7 @@ func (c *BlockStoreGRPCClient) SetVolumeID(pv runtime.Unstructured, volumeID str
}
req := &proto.SetVolumeIDRequest{
Plugin: c.plugin,
PersistentVolume: encodedPV,
VolumeID: volumeID,
}
@@ -191,17 +197,46 @@ func (c *BlockStoreGRPCClient) SetVolumeID(pv runtime.Unstructured, volumeID str
return &updatedPV, nil
}
//////////////////////////////////////////////////////////////////////////////
// server code
//////////////////////////////////////////////////////////////////////////////
// GRPCServer registers a BlockStore gRPC server.
func (p *BlockStorePlugin) GRPCServer(s *grpc.Server) error {
proto.RegisterBlockStoreServer(s, &BlockStoreGRPCServer{mux: p.serverMux})
return nil
}
// BlockStoreGRPCServer implements the proto-generated BlockStoreServer interface, and accepts
// gRPC calls and forwards them to an implementation of the pluggable interface.
type BlockStoreGRPCServer struct {
impl cloudprovider.BlockStore
mux *serverMux
}
func (s *BlockStoreGRPCServer) getImpl(name string) (cloudprovider.BlockStore, error) {
impl, err := s.mux.getHandler(name)
if err != nil {
return nil, err
}
blockStore, ok := impl.(cloudprovider.BlockStore)
if !ok {
return nil, errors.Errorf("%T is not a block store", impl)
}
return blockStore, nil
}
// Init prepares the BlockStore for usage using the provided map of
// configuration key-value pairs. It returns an error if the BlockStore
// cannot be initialized from the provided config.
func (s *BlockStoreGRPCServer) Init(ctx context.Context, req *proto.InitRequest) (*proto.Empty, error) {
if err := s.impl.Init(req.Config); err != nil {
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
if err := impl.Init(req.Config); err != nil {
return nil, err
}
@@ -211,6 +246,11 @@ func (s *BlockStoreGRPCServer) Init(ctx context.Context, req *proto.InitRequest)
// CreateVolumeFromSnapshot creates a new block volume, initialized from the provided snapshot,
// and with the specified type and IOPS (if using provisioned IOPS).
func (s *BlockStoreGRPCServer) CreateVolumeFromSnapshot(ctx context.Context, req *proto.CreateVolumeRequest) (*proto.CreateVolumeResponse, error) {
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
snapshotID := req.SnapshotID
volumeType := req.VolumeType
volumeAZ := req.VolumeAZ
@@ -220,7 +260,7 @@ func (s *BlockStoreGRPCServer) CreateVolumeFromSnapshot(ctx context.Context, req
iops = &req.Iops
}
volumeID, err := s.impl.CreateVolumeFromSnapshot(snapshotID, volumeType, volumeAZ, iops)
volumeID, err := impl.CreateVolumeFromSnapshot(snapshotID, volumeType, volumeAZ, iops)
if err != nil {
return nil, err
}
@@ -231,7 +271,12 @@ func (s *BlockStoreGRPCServer) CreateVolumeFromSnapshot(ctx context.Context, req
// GetVolumeInfo returns the type and IOPS (if using provisioned IOPS) for a specified block
// volume.
func (s *BlockStoreGRPCServer) GetVolumeInfo(ctx context.Context, req *proto.GetVolumeInfoRequest) (*proto.GetVolumeInfoResponse, error) {
volumeType, iops, err := s.impl.GetVolumeInfo(req.VolumeID, req.VolumeAZ)
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
volumeType, iops, err := impl.GetVolumeInfo(req.VolumeID, req.VolumeAZ)
if err != nil {
return nil, err
}
@@ -249,7 +294,12 @@ func (s *BlockStoreGRPCServer) GetVolumeInfo(ctx context.Context, req *proto.Get
// IsVolumeReady returns whether the specified volume is ready to be used.
func (s *BlockStoreGRPCServer) IsVolumeReady(ctx context.Context, req *proto.IsVolumeReadyRequest) (*proto.IsVolumeReadyResponse, error) {
ready, err := s.impl.IsVolumeReady(req.VolumeID, req.VolumeAZ)
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
ready, err := impl.IsVolumeReady(req.VolumeID, req.VolumeAZ)
if err != nil {
return nil, err
}
@@ -260,7 +310,12 @@ func (s *BlockStoreGRPCServer) IsVolumeReady(ctx context.Context, req *proto.IsV
// CreateSnapshot creates a snapshot of the specified block volume, and applies the provided
// set of tags to the snapshot.
func (s *BlockStoreGRPCServer) CreateSnapshot(ctx context.Context, req *proto.CreateSnapshotRequest) (*proto.CreateSnapshotResponse, error) {
snapshotID, err := s.impl.CreateSnapshot(req.VolumeID, req.VolumeAZ, req.Tags)
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
snapshotID, err := impl.CreateSnapshot(req.VolumeID, req.VolumeAZ, req.Tags)
if err != nil {
return nil, err
}
@@ -270,7 +325,12 @@ func (s *BlockStoreGRPCServer) CreateSnapshot(ctx context.Context, req *proto.Cr
// DeleteSnapshot deletes the specified volume snapshot.
func (s *BlockStoreGRPCServer) DeleteSnapshot(ctx context.Context, req *proto.DeleteSnapshotRequest) (*proto.Empty, error) {
if err := s.impl.DeleteSnapshot(req.SnapshotID); err != nil {
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
if err := impl.DeleteSnapshot(req.SnapshotID); err != nil {
return nil, err
}
@@ -278,13 +338,18 @@ func (s *BlockStoreGRPCServer) DeleteSnapshot(ctx context.Context, req *proto.De
}
func (s *BlockStoreGRPCServer) GetVolumeID(ctx context.Context, req *proto.GetVolumeIDRequest) (*proto.GetVolumeIDResponse, error) {
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
var pv unstructured.Unstructured
if err := json.Unmarshal(req.PersistentVolume, &pv); err != nil {
return nil, err
}
volumeID, err := s.impl.GetVolumeID(&pv)
volumeID, err := impl.GetVolumeID(&pv)
if err != nil {
return nil, err
}
@@ -293,13 +358,18 @@ func (s *BlockStoreGRPCServer) GetVolumeID(ctx context.Context, req *proto.GetVo
}
func (s *BlockStoreGRPCServer) SetVolumeID(ctx context.Context, req *proto.SetVolumeIDRequest) (*proto.SetVolumeIDResponse, error) {
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
var pv unstructured.Unstructured
if err := json.Unmarshal(req.PersistentVolume, &pv); err != nil {
return nil, err
}
updatedPV, err := s.impl.SetVolumeID(&pv, req.VolumeID)
updatedPV, err := impl.SetVolumeID(&pv, req.VolumeID)
if err != nil {
return nil, err
}
+57 -26
View File
@@ -1,43 +1,74 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"os"
"os/exec"
"github.com/hashicorp/go-hclog"
hcplugin "github.com/hashicorp/go-plugin"
"github.com/sirupsen/logrus"
)
// clientBuilder builds go-plugin Clients.
type clientBuilder struct {
config *hcplugin.ClientConfig
commandName string
commandArgs []string
clientLogger logrus.FieldLogger
pluginLogger hclog.Logger
}
func newClientBuilder(baseConfig *hcplugin.ClientConfig) *clientBuilder {
return &clientBuilder{
config: baseConfig,
// newClientBuilder returns a new clientBuilder with commandName to name. If the command matches the currently running
// process (i.e. ark), this also sets commandArgs to the internal Ark command to run plugins.
func newClientBuilder(command string, logger logrus.FieldLogger, logLevel logrus.Level) *clientBuilder {
b := &clientBuilder{
commandName: command,
clientLogger: logger,
pluginLogger: newLogrusAdapter(logger, logLevel),
}
if command == os.Args[0] {
// For plugins compiled into the ark executable, we need to run "ark run-plugins"
b.commandArgs = []string{"run-plugins"}
}
return b
}
func newLogrusAdapter(pluginLogger logrus.FieldLogger, logLevel logrus.Level) *logrusAdapter {
return &logrusAdapter{impl: pluginLogger, level: logLevel}
}
func (b *clientBuilder) clientConfig() *hcplugin.ClientConfig {
return &hcplugin.ClientConfig{
HandshakeConfig: Handshake,
AllowedProtocols: []hcplugin.Protocol{hcplugin.ProtocolGRPC},
Plugins: map[string]hcplugin.Plugin{
string(PluginKindBackupItemAction): NewBackupItemActionPlugin(clientLogger(b.clientLogger)),
string(PluginKindBlockStore): NewBlockStorePlugin(clientLogger(b.clientLogger)),
string(PluginKindObjectStore): NewObjectStorePlugin(clientLogger(b.clientLogger)),
string(PluginKindPluginLister): &PluginListerPlugin{},
string(PluginKindRestoreItemAction): NewRestoreItemActionPlugin(clientLogger(b.clientLogger)),
},
Logger: b.pluginLogger,
Cmd: exec.Command(b.commandName, b.commandArgs...),
}
}
func (b *clientBuilder) withPlugin(kind PluginKind, plugin hcplugin.Plugin) *clientBuilder {
if b.config.Plugins == nil {
b.config.Plugins = make(map[string]hcplugin.Plugin)
}
b.config.Plugins[string(kind)] = plugin
return b
}
func (b *clientBuilder) withLogger(logger hclog.Logger) *clientBuilder {
b.config.Logger = logger
return b
}
func (b *clientBuilder) withCommand(name string, args ...string) *clientBuilder {
b.config.Cmd = exec.Command(name, args...)
return b
}
// client creates a new go-plugin Client with support for all of Ark's plugin kinds (BackupItemAction, BlockStore,
// ObjectStore, PluginLister, RestoreItemAction).
func (b *clientBuilder) client() *hcplugin.Client {
return hcplugin.NewClient(b.config)
return hcplugin.NewClient(b.clientConfig())
}
+64
View File
@@ -0,0 +1,64 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"os"
"os/exec"
"testing"
hcplugin "github.com/hashicorp/go-plugin"
"github.com/heptio/ark/pkg/util/test"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestNewClientBuilder(t *testing.T) {
logger := test.NewLogger()
logLevel := logrus.InfoLevel
cb := newClientBuilder("ark", logger, logLevel)
assert.Equal(t, cb.commandName, "ark")
assert.Empty(t, cb.commandArgs)
assert.Equal(t, newLogrusAdapter(logger, logLevel), cb.pluginLogger)
cb = newClientBuilder(os.Args[0], logger, logLevel)
assert.Equal(t, cb.commandName, os.Args[0])
assert.Equal(t, []string{"run-plugins"}, cb.commandArgs)
assert.Equal(t, newLogrusAdapter(logger, logLevel), cb.pluginLogger)
}
func TestClientConfig(t *testing.T) {
logger := test.NewLogger()
logLevel := logrus.InfoLevel
cb := newClientBuilder("ark", logger, logLevel)
expected := &hcplugin.ClientConfig{
HandshakeConfig: Handshake,
AllowedProtocols: []hcplugin.Protocol{hcplugin.ProtocolGRPC},
Plugins: map[string]hcplugin.Plugin{
string(PluginKindBackupItemAction): NewBackupItemActionPlugin(clientLogger(logger)),
string(PluginKindBlockStore): NewBlockStorePlugin(clientLogger(logger)),
string(PluginKindObjectStore): NewObjectStorePlugin(clientLogger(logger)),
string(PluginKindPluginLister): &PluginListerPlugin{},
string(PluginKindRestoreItemAction): NewRestoreItemActionPlugin(clientLogger(logger)),
},
Logger: cb.pluginLogger,
Cmd: exec.Command(cb.commandName, cb.commandArgs...),
}
cc := cb.clientConfig()
assert.Equal(t, expected, cc)
}
+74
View File
@@ -0,0 +1,74 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
)
// clientBase implements client and contains shared fields common to all clients.
type clientBase struct {
plugin string
logger logrus.FieldLogger
}
type ClientDispenser interface {
clientFor(name string) interface{}
}
// clientDispenser supports the initialization and retrieval of multiple implementations for a single plugin kind, such as
// "aws" and "azure" implementations of the object store plugin.
type clientDispenser struct {
// logger is the log the plugin should use.
logger logrus.FieldLogger
// clienConn is shared among all implementations for this client.
clientConn *grpc.ClientConn
// initFunc returns a client that implements a plugin interface, such as cloudprovider.ObjectStore.
initFunc clientInitFunc
// clients keeps track of all the initialized implementations.
clients map[string]interface{}
}
type clientInitFunc func(base *clientBase, clientConn *grpc.ClientConn) interface{}
// newClientDispenser creates a new clientDispenser.
func newClientDispenser(logger logrus.FieldLogger, clientConn *grpc.ClientConn, initFunc clientInitFunc) *clientDispenser {
return &clientDispenser{
clientConn: clientConn,
logger: logger,
initFunc: initFunc,
clients: make(map[string]interface{}),
}
}
// clientFor returns a gRPC client stub for the implementation of a plugin named name. If the client stub does not
// currently exist, clientFor creates it.
func (cd *clientDispenser) clientFor(name string) interface{} {
if client, found := cd.clients[name]; found {
return client
}
base := &clientBase{
plugin: name,
logger: cd.logger,
}
// Initialize the plugin (e.g. newBackupItemActionGRPCClient())
client := cd.initFunc(base, cd.clientConn)
cd.clients[name] = client
return client
}
+80
View File
@@ -0,0 +1,80 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"testing"
"github.com/heptio/ark/pkg/util/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)
type fakeClient struct {
base *clientBase
clientConn *grpc.ClientConn
}
func TestNewClientDispenser(t *testing.T) {
logger := test.NewLogger()
clientConn := new(grpc.ClientConn)
c := 3
initFunc := func(base *clientBase, clientConn *grpc.ClientConn) interface{} {
return c
}
cd := newClientDispenser(logger, clientConn, initFunc)
assert.Equal(t, clientConn, cd.clientConn)
assert.NotNil(t, cd.clients)
assert.Empty(t, cd.clients)
}
func TestClientFor(t *testing.T) {
logger := test.NewLogger()
clientConn := new(grpc.ClientConn)
c := new(fakeClient)
count := 0
initFunc := func(base *clientBase, clientConn *grpc.ClientConn) interface{} {
c.base = base
c.clientConn = clientConn
count++
return c
}
cd := newClientDispenser(logger, clientConn, initFunc)
actual := cd.clientFor("pod")
require.IsType(t, &fakeClient{}, actual)
typed := actual.(*fakeClient)
assert.Equal(t, 1, count)
assert.Equal(t, &typed, &c)
expectedBase := &clientBase{
plugin: "pod",
logger: logger,
}
assert.Equal(t, expectedBase, typed.base)
assert.Equal(t, clientConn, typed.clientConn)
// Make sure we reuse a previous client
actual = cd.clientFor("pod")
require.IsType(t, &fakeClient{}, actual)
typed = actual.(*fakeClient)
assert.Equal(t, 1, count)
}
-105
View File
@@ -1,105 +0,0 @@
package plugin
import (
"sync"
plugin "github.com/hashicorp/go-plugin"
"github.com/pkg/errors"
)
// clientKey is a unique ID for a plugin client.
type clientKey struct {
kind PluginKind
// scope is an additional identifier that allows multiple clients
// for the same kind/name to be differentiated. It will typically
// be the name of the applicable backup/restore for ItemAction
// clients, and blank for Object/BlockStore clients.
scope string
}
func newClientStore() *clientStore {
return &clientStore{
clients: make(map[clientKey]map[string]*plugin.Client),
lock: &sync.RWMutex{},
}
}
// clientStore is a repository of active plugin clients.
type clientStore struct {
// clients is a nested map, keyed first by clientKey (a
// combo of kind and "scope"), and second by plugin name.
// This enables easy listing of all clients for a given
// kind and scope (e.g. all BackupItemActions for a given
// backup), and efficient lookup by kind+name+scope (e.g.
// the AWS ObjectStore.)
clients map[clientKey]map[string]*plugin.Client
lock *sync.RWMutex
}
// get returns a plugin client for the given kind/name/scope, or an error if none
// is found.
func (s *clientStore) get(kind PluginKind, name, scope string) (*plugin.Client, error) {
s.lock.RLock()
defer s.lock.RUnlock()
if forScope, found := s.clients[clientKey{kind, scope}]; found {
if client, found := forScope[name]; found {
return client, nil
}
}
return nil, errors.New("client not found")
}
// list returns all plugin clients for the given kind/scope, or an
// error if none are found.
func (s *clientStore) list(kind PluginKind, scope string) ([]*plugin.Client, error) {
s.lock.RLock()
defer s.lock.RUnlock()
if forScope, found := s.clients[clientKey{kind, scope}]; found {
var clients []*plugin.Client
for _, client := range forScope {
clients = append(clients, client)
}
return clients, nil
}
return nil, errors.New("clients not found")
}
// add stores a plugin client for the given kind/name/scope.
func (s *clientStore) add(client *plugin.Client, kind PluginKind, name, scope string) {
s.lock.Lock()
defer s.lock.Unlock()
key := clientKey{kind, scope}
if _, found := s.clients[key]; !found {
s.clients[key] = make(map[string]*plugin.Client)
}
s.clients[key][name] = client
}
// delete removes the client with the given kind/name/scope from the store.
func (s *clientStore) delete(kind PluginKind, name, scope string) {
s.lock.Lock()
defer s.lock.Unlock()
if forScope, found := s.clients[clientKey{kind, scope}]; found {
delete(forScope, name)
}
}
// deleteAll removes all clients with the given kind/scope from
// the store.
func (s *clientStore) deleteAll(kind PluginKind, scope string) {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.clients, clientKey{kind, scope})
}
+39 -26
View File
@@ -8,6 +8,7 @@ It is generated from these files:
BackupItemAction.proto
BlockStore.proto
ObjectStore.proto
PluginLister.proto
RestoreItemAction.proto
Shared.proto
@@ -38,10 +39,13 @@ It has these top-level messages:
DeleteObjectRequest
CreateSignedURLRequest
CreateSignedURLResponse
PluginIdentifier
ListPluginsResponse
RestoreExecuteRequest
RestoreExecuteResponse
Empty
InitRequest
AppliesToRequest
AppliesToResponse
*/
package generated
@@ -67,8 +71,9 @@ var _ = math.Inf
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type ExecuteRequest struct {
Item []byte `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"`
Backup []byte `protobuf:"bytes,2,opt,name=backup,proto3" json:"backup,omitempty"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
Item []byte `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"`
Backup []byte `protobuf:"bytes,3,opt,name=backup,proto3" json:"backup,omitempty"`
}
func (m *ExecuteRequest) Reset() { *m = ExecuteRequest{} }
@@ -76,6 +81,13 @@ func (m *ExecuteRequest) String() string { return proto.CompactTextSt
func (*ExecuteRequest) ProtoMessage() {}
func (*ExecuteRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *ExecuteRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *ExecuteRequest) GetItem() []byte {
if m != nil {
return m.Item
@@ -171,7 +183,7 @@ const _ = grpc.SupportPackageIsVersion4
// Client API for BackupItemAction service
type BackupItemActionClient interface {
AppliesTo(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*AppliesToResponse, error)
AppliesTo(ctx context.Context, in *AppliesToRequest, opts ...grpc.CallOption) (*AppliesToResponse, error)
Execute(ctx context.Context, in *ExecuteRequest, opts ...grpc.CallOption) (*ExecuteResponse, error)
}
@@ -183,7 +195,7 @@ func NewBackupItemActionClient(cc *grpc.ClientConn) BackupItemActionClient {
return &backupItemActionClient{cc}
}
func (c *backupItemActionClient) AppliesTo(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*AppliesToResponse, error) {
func (c *backupItemActionClient) AppliesTo(ctx context.Context, in *AppliesToRequest, opts ...grpc.CallOption) (*AppliesToResponse, error) {
out := new(AppliesToResponse)
err := grpc.Invoke(ctx, "/generated.BackupItemAction/AppliesTo", in, out, c.cc, opts...)
if err != nil {
@@ -204,7 +216,7 @@ func (c *backupItemActionClient) Execute(ctx context.Context, in *ExecuteRequest
// Server API for BackupItemAction service
type BackupItemActionServer interface {
AppliesTo(context.Context, *Empty) (*AppliesToResponse, error)
AppliesTo(context.Context, *AppliesToRequest) (*AppliesToResponse, error)
Execute(context.Context, *ExecuteRequest) (*ExecuteResponse, error)
}
@@ -213,7 +225,7 @@ func RegisterBackupItemActionServer(s *grpc.Server, srv BackupItemActionServer)
}
func _BackupItemAction_AppliesTo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
in := new(AppliesToRequest)
if err := dec(in); err != nil {
return nil, err
}
@@ -225,7 +237,7 @@ func _BackupItemAction_AppliesTo_Handler(srv interface{}, ctx context.Context, d
FullMethod: "/generated.BackupItemAction/AppliesTo",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BackupItemActionServer).AppliesTo(ctx, req.(*Empty))
return srv.(BackupItemActionServer).AppliesTo(ctx, req.(*AppliesToRequest))
}
return interceptor(ctx, in, info, handler)
}
@@ -268,23 +280,24 @@ var _BackupItemAction_serviceDesc = grpc.ServiceDesc{
func init() { proto.RegisterFile("BackupItemAction.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 288 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x51, 0xc1, 0x4e, 0xc2, 0x40,
0x10, 0x4d, 0x01, 0xd1, 0x8e, 0x44, 0xc8, 0xc4, 0x90, 0xda, 0x60, 0x42, 0x7a, 0xe2, 0xd4, 0x03,
0x1e, 0xf5, 0x20, 0x26, 0xc4, 0x70, 0x5d, 0xfd, 0x81, 0xa5, 0x1d, 0x71, 0x23, 0xdd, 0x5d, 0x77,
0xb7, 0x09, 0x7e, 0x86, 0x7f, 0x6c, 0xba, 0x6d, 0x6a, 0x45, 0x6e, 0xfb, 0xe6, 0xcd, 0x9b, 0x7d,
0xf3, 0x06, 0xa6, 0x4f, 0x3c, 0xfb, 0x28, 0xf5, 0xc6, 0x51, 0xb1, 0xca, 0x9c, 0x50, 0x32, 0xd5,
0x46, 0x39, 0x85, 0xe1, 0x8e, 0x24, 0x19, 0xee, 0x28, 0x8f, 0x47, 0x2f, 0xef, 0xdc, 0x50, 0x5e,
0x13, 0xc9, 0x03, 0x5c, 0xad, 0x0f, 0x94, 0x95, 0x8e, 0x18, 0x7d, 0x96, 0x64, 0x1d, 0x22, 0x0c,
0x84, 0xa3, 0x22, 0x0a, 0xe6, 0xc1, 0x62, 0xc4, 0xfc, 0x1b, 0xa7, 0x30, 0xdc, 0xfa, 0xc1, 0x51,
0xcf, 0x57, 0x1b, 0x94, 0x48, 0x18, 0xb7, 0x6a, 0xab, 0x95, 0xb4, 0x74, 0x52, 0xfe, 0x0c, 0x63,
0x9e, 0xe7, 0xa2, 0xf2, 0xc3, 0xf7, 0x95, 0x37, 0x1b, 0xf5, 0xe6, 0xfd, 0xc5, 0xe5, 0xf2, 0x36,
0x6d, 0x7d, 0xa5, 0x8c, 0xac, 0x2a, 0x4d, 0x46, 0x9b, 0x9c, 0xa4, 0x13, 0x6f, 0x82, 0x0c, 0x3b,
0x56, 0x25, 0x07, 0xc0, 0xff, 0x6d, 0x78, 0x0d, 0x67, 0x3b, 0xa3, 0x4a, 0xed, 0xff, 0x0c, 0x59,
0x0d, 0x30, 0x86, 0x0b, 0xd3, 0xf4, 0x7a, 0xd7, 0x21, 0x6b, 0x31, 0xce, 0x20, 0x94, 0xbc, 0x20,
0xab, 0x79, 0x46, 0x51, 0xdf, 0x93, 0xbf, 0x85, 0x6a, 0x85, 0x0a, 0x44, 0x03, 0x4f, 0xf8, 0xf7,
0xf2, 0x3b, 0x80, 0xc9, 0x71, 0xb6, 0x78, 0x0f, 0xe1, 0x4a, 0xeb, 0xbd, 0x20, 0xfb, 0xaa, 0x70,
0xd2, 0xd9, 0x65, 0x5d, 0x68, 0xf7, 0x15, 0xcf, 0x3a, 0x95, 0xb6, 0xaf, 0x0d, 0xea, 0x11, 0xce,
0x9b, 0xec, 0xf0, 0xa6, 0x2b, 0xfd, 0x73, 0x8d, 0x38, 0x3e, 0x45, 0xd5, 0x13, 0xb6, 0x43, 0x7f,
0xc2, 0xbb, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x44, 0x09, 0x4d, 0x36, 0xf5, 0x01, 0x00, 0x00,
// 298 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x51, 0x4d, 0x4f, 0x02, 0x31,
0x10, 0xcd, 0x02, 0xa2, 0x1d, 0x89, 0x98, 0xc6, 0x90, 0x75, 0xc5, 0x84, 0x70, 0xe2, 0xc4, 0x01,
0xff, 0x80, 0x98, 0xa8, 0xe1, 0x5a, 0xf9, 0x03, 0x65, 0x77, 0xc4, 0xc6, 0xdd, 0xb6, 0xf6, 0x23,
0xe1, 0xc7, 0xf8, 0x63, 0xcd, 0x76, 0x9b, 0x75, 0x45, 0x6e, 0x7d, 0xf3, 0xde, 0x4c, 0xe7, 0xbd,
0x81, 0xc9, 0x13, 0xcf, 0x3f, 0xbd, 0xde, 0x38, 0xac, 0xd6, 0xb9, 0x13, 0x4a, 0x2e, 0xb5, 0x51,
0x4e, 0x51, 0xb2, 0x47, 0x89, 0x86, 0x3b, 0x2c, 0xb2, 0xd1, 0xdb, 0x07, 0x37, 0x58, 0x34, 0xc4,
0x7c, 0x0b, 0x57, 0xcf, 0x07, 0xcc, 0xbd, 0x43, 0x86, 0x5f, 0x1e, 0xad, 0xa3, 0x13, 0x18, 0xea,
0xd2, 0xef, 0x85, 0x4c, 0x93, 0x59, 0xb2, 0x20, 0x2c, 0x22, 0x4a, 0x61, 0x20, 0x1c, 0x56, 0x69,
0x6f, 0x96, 0x2c, 0x46, 0x2c, 0xbc, 0x6b, 0xed, 0x2e, 0x7c, 0x98, 0xf6, 0x43, 0x35, 0xa2, 0xb9,
0x84, 0x71, 0x3b, 0xd5, 0x6a, 0x25, 0x2d, 0xb6, 0xed, 0x49, 0xa7, 0xfd, 0x15, 0xc6, 0xbc, 0x28,
0x44, 0xbd, 0x27, 0x2f, 0xeb, 0x9d, 0x6d, 0xda, 0x9b, 0xf5, 0x17, 0x97, 0xab, 0xfb, 0x65, 0xbb,
0xef, 0x92, 0xa1, 0x55, 0xde, 0xe4, 0xb8, 0x29, 0x50, 0x3a, 0xf1, 0x2e, 0xd0, 0xb0, 0xe3, 0xae,
0xf9, 0x01, 0xe8, 0x7f, 0x19, 0xbd, 0x81, 0xb3, 0xbd, 0x51, 0x5e, 0x47, 0x23, 0x0d, 0xa0, 0x19,
0x5c, 0x98, 0xa8, 0x0d, 0x5e, 0x08, 0x6b, 0x31, 0x9d, 0x02, 0x91, 0xbc, 0x42, 0xab, 0x79, 0x8e,
0xc1, 0x12, 0x61, 0xbf, 0x85, 0xda, 0x42, 0x0d, 0xd2, 0x41, 0x20, 0xc2, 0x7b, 0xf5, 0x9d, 0xc0,
0xf5, 0x71, 0xe6, 0xf4, 0x05, 0xc8, 0x5a, 0xeb, 0x52, 0xa0, 0xdd, 0x2a, 0x7a, 0xd7, 0xf1, 0xd2,
0x56, 0x63, 0xd8, 0xd9, 0xf4, 0x34, 0x19, 0x33, 0x7b, 0x84, 0xf3, 0x18, 0x23, 0xbd, 0xed, 0x08,
0xff, 0x1e, 0x2c, 0xcb, 0x4e, 0x51, 0xcd, 0x84, 0xdd, 0x30, 0x5c, 0xf9, 0xe1, 0x27, 0x00, 0x00,
0xff, 0xff, 0x56, 0x1b, 0x70, 0xd6, 0x18, 0x02, 0x00, 0x00,
}
+108 -50
View File
@@ -18,10 +18,11 @@ var _ = fmt.Errorf
var _ = math.Inf
type CreateVolumeRequest struct {
SnapshotID string `protobuf:"bytes,1,opt,name=snapshotID" json:"snapshotID,omitempty"`
VolumeType string `protobuf:"bytes,2,opt,name=volumeType" json:"volumeType,omitempty"`
VolumeAZ string `protobuf:"bytes,3,opt,name=volumeAZ" json:"volumeAZ,omitempty"`
Iops int64 `protobuf:"varint,4,opt,name=iops" json:"iops,omitempty"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
SnapshotID string `protobuf:"bytes,2,opt,name=snapshotID" json:"snapshotID,omitempty"`
VolumeType string `protobuf:"bytes,3,opt,name=volumeType" json:"volumeType,omitempty"`
VolumeAZ string `protobuf:"bytes,4,opt,name=volumeAZ" json:"volumeAZ,omitempty"`
Iops int64 `protobuf:"varint,5,opt,name=iops" json:"iops,omitempty"`
}
func (m *CreateVolumeRequest) Reset() { *m = CreateVolumeRequest{} }
@@ -29,6 +30,13 @@ func (m *CreateVolumeRequest) String() string { return proto.CompactT
func (*CreateVolumeRequest) ProtoMessage() {}
func (*CreateVolumeRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} }
func (m *CreateVolumeRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *CreateVolumeRequest) GetSnapshotID() string {
if m != nil {
return m.SnapshotID
@@ -74,8 +82,9 @@ func (m *CreateVolumeResponse) GetVolumeID() string {
}
type GetVolumeInfoRequest struct {
VolumeID string `protobuf:"bytes,1,opt,name=volumeID" json:"volumeID,omitempty"`
VolumeAZ string `protobuf:"bytes,2,opt,name=volumeAZ" json:"volumeAZ,omitempty"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
VolumeID string `protobuf:"bytes,2,opt,name=volumeID" json:"volumeID,omitempty"`
VolumeAZ string `protobuf:"bytes,3,opt,name=volumeAZ" json:"volumeAZ,omitempty"`
}
func (m *GetVolumeInfoRequest) Reset() { *m = GetVolumeInfoRequest{} }
@@ -83,6 +92,13 @@ func (m *GetVolumeInfoRequest) String() string { return proto.Compact
func (*GetVolumeInfoRequest) ProtoMessage() {}
func (*GetVolumeInfoRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{2} }
func (m *GetVolumeInfoRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *GetVolumeInfoRequest) GetVolumeID() string {
if m != nil {
return m.VolumeID
@@ -122,8 +138,9 @@ func (m *GetVolumeInfoResponse) GetIops() int64 {
}
type IsVolumeReadyRequest struct {
VolumeID string `protobuf:"bytes,1,opt,name=volumeID" json:"volumeID,omitempty"`
VolumeAZ string `protobuf:"bytes,2,opt,name=volumeAZ" json:"volumeAZ,omitempty"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
VolumeID string `protobuf:"bytes,2,opt,name=volumeID" json:"volumeID,omitempty"`
VolumeAZ string `protobuf:"bytes,3,opt,name=volumeAZ" json:"volumeAZ,omitempty"`
}
func (m *IsVolumeReadyRequest) Reset() { *m = IsVolumeReadyRequest{} }
@@ -131,6 +148,13 @@ func (m *IsVolumeReadyRequest) String() string { return proto.Compact
func (*IsVolumeReadyRequest) ProtoMessage() {}
func (*IsVolumeReadyRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{4} }
func (m *IsVolumeReadyRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *IsVolumeReadyRequest) GetVolumeID() string {
if m != nil {
return m.VolumeID
@@ -162,9 +186,10 @@ func (m *IsVolumeReadyResponse) GetReady() bool {
}
type CreateSnapshotRequest struct {
VolumeID string `protobuf:"bytes,1,opt,name=volumeID" json:"volumeID,omitempty"`
VolumeAZ string `protobuf:"bytes,2,opt,name=volumeAZ" json:"volumeAZ,omitempty"`
Tags map[string]string `protobuf:"bytes,3,rep,name=tags" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
VolumeID string `protobuf:"bytes,2,opt,name=volumeID" json:"volumeID,omitempty"`
VolumeAZ string `protobuf:"bytes,3,opt,name=volumeAZ" json:"volumeAZ,omitempty"`
Tags map[string]string `protobuf:"bytes,4,rep,name=tags" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
}
func (m *CreateSnapshotRequest) Reset() { *m = CreateSnapshotRequest{} }
@@ -172,6 +197,13 @@ func (m *CreateSnapshotRequest) String() string { return proto.Compac
func (*CreateSnapshotRequest) ProtoMessage() {}
func (*CreateSnapshotRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{6} }
func (m *CreateSnapshotRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *CreateSnapshotRequest) GetVolumeID() string {
if m != nil {
return m.VolumeID
@@ -210,7 +242,8 @@ func (m *CreateSnapshotResponse) GetSnapshotID() string {
}
type DeleteSnapshotRequest struct {
SnapshotID string `protobuf:"bytes,1,opt,name=snapshotID" json:"snapshotID,omitempty"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
SnapshotID string `protobuf:"bytes,2,opt,name=snapshotID" json:"snapshotID,omitempty"`
}
func (m *DeleteSnapshotRequest) Reset() { *m = DeleteSnapshotRequest{} }
@@ -218,6 +251,13 @@ func (m *DeleteSnapshotRequest) String() string { return proto.Compac
func (*DeleteSnapshotRequest) ProtoMessage() {}
func (*DeleteSnapshotRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{8} }
func (m *DeleteSnapshotRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *DeleteSnapshotRequest) GetSnapshotID() string {
if m != nil {
return m.SnapshotID
@@ -226,7 +266,8 @@ func (m *DeleteSnapshotRequest) GetSnapshotID() string {
}
type GetVolumeIDRequest struct {
PersistentVolume []byte `protobuf:"bytes,1,opt,name=persistentVolume,proto3" json:"persistentVolume,omitempty"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
PersistentVolume []byte `protobuf:"bytes,2,opt,name=persistentVolume,proto3" json:"persistentVolume,omitempty"`
}
func (m *GetVolumeIDRequest) Reset() { *m = GetVolumeIDRequest{} }
@@ -234,6 +275,13 @@ func (m *GetVolumeIDRequest) String() string { return proto.CompactTe
func (*GetVolumeIDRequest) ProtoMessage() {}
func (*GetVolumeIDRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{9} }
func (m *GetVolumeIDRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *GetVolumeIDRequest) GetPersistentVolume() []byte {
if m != nil {
return m.PersistentVolume
@@ -258,8 +306,9 @@ func (m *GetVolumeIDResponse) GetVolumeID() string {
}
type SetVolumeIDRequest struct {
PersistentVolume []byte `protobuf:"bytes,1,opt,name=persistentVolume,proto3" json:"persistentVolume,omitempty"`
VolumeID string `protobuf:"bytes,2,opt,name=volumeID" json:"volumeID,omitempty"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
PersistentVolume []byte `protobuf:"bytes,2,opt,name=persistentVolume,proto3" json:"persistentVolume,omitempty"`
VolumeID string `protobuf:"bytes,3,opt,name=volumeID" json:"volumeID,omitempty"`
}
func (m *SetVolumeIDRequest) Reset() { *m = SetVolumeIDRequest{} }
@@ -267,6 +316,13 @@ func (m *SetVolumeIDRequest) String() string { return proto.CompactTe
func (*SetVolumeIDRequest) ProtoMessage() {}
func (*SetVolumeIDRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{11} }
func (m *SetVolumeIDRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *SetVolumeIDRequest) GetPersistentVolume() []byte {
if m != nil {
return m.PersistentVolume
@@ -619,39 +675,41 @@ var _BlockStore_serviceDesc = grpc.ServiceDesc{
func init() { proto.RegisterFile("BlockStore.proto", fileDescriptor1) }
var fileDescriptor1 = []byte{
// 540 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x55, 0x4d, 0x6f, 0xd3, 0x40,
0x10, 0x95, 0x3f, 0x40, 0xcd, 0xa4, 0x54, 0xd1, 0x26, 0xa9, 0x2c, 0x4b, 0x04, 0xe3, 0x53, 0x54,
0x89, 0x08, 0xc2, 0xa1, 0x15, 0x07, 0x44, 0x21, 0x05, 0x45, 0x54, 0x3d, 0xd8, 0x85, 0x03, 0x70,
0x31, 0x64, 0x48, 0xa3, 0x26, 0x5e, 0xe3, 0xdd, 0x54, 0xf2, 0x0f, 0xe0, 0xbf, 0xf1, 0xa3, 0x38,
0x20, 0xdb, 0xeb, 0x8f, 0x4d, 0x36, 0x09, 0x55, 0x6e, 0x9e, 0x99, 0x7d, 0x6f, 0xdf, 0xec, 0xbe,
0x59, 0x43, 0xeb, 0xed, 0x9c, 0xfe, 0xb8, 0xf5, 0x39, 0x8d, 0x71, 0x10, 0xc5, 0x94, 0x53, 0xd2,
0x98, 0x62, 0x88, 0x71, 0xc0, 0x71, 0x62, 0x1f, 0xfa, 0x37, 0x41, 0x8c, 0x93, 0xbc, 0xe0, 0xfe,
0xd6, 0xa0, 0xfd, 0x2e, 0xc6, 0x80, 0xe3, 0x67, 0x3a, 0x5f, 0x2e, 0xd0, 0xc3, 0x5f, 0x4b, 0x64,
0x9c, 0xf4, 0x00, 0x58, 0x18, 0x44, 0xec, 0x86, 0xf2, 0xf1, 0xc8, 0xd2, 0x1c, 0xad, 0xdf, 0xf0,
0x6a, 0x99, 0xb4, 0x7e, 0x97, 0x01, 0xae, 0x93, 0x08, 0x2d, 0x3d, 0xaf, 0x57, 0x19, 0x62, 0xc3,
0x41, 0x1e, 0x9d, 0x7f, 0xb1, 0x8c, 0xac, 0x5a, 0xc6, 0x84, 0x80, 0x39, 0xa3, 0x11, 0xb3, 0x4c,
0x47, 0xeb, 0x1b, 0x5e, 0xf6, 0xed, 0x0e, 0xa1, 0x23, 0xcb, 0x60, 0x11, 0x0d, 0x59, 0x8d, 0xa7,
0x54, 0x51, 0xc6, 0xee, 0x15, 0x74, 0x3e, 0x20, 0xcf, 0x01, 0xe3, 0xf0, 0x27, 0x2d, 0xb4, 0x6f,
0xc1, 0x48, 0xba, 0x74, 0x59, 0x97, 0xfb, 0x11, 0xba, 0x2b, 0x7c, 0x42, 0x84, 0xdc, 0xac, 0xb6,
0xd6, 0x6c, 0xd1, 0x90, 0x5e, 0x6b, 0xe8, 0x0a, 0x3a, 0x63, 0x56, 0x34, 0x13, 0x4c, 0x92, 0x7d,
0xc5, 0x3d, 0x83, 0xee, 0x0a, 0x9f, 0x10, 0xd7, 0x81, 0x07, 0x71, 0x9a, 0xc8, 0xd8, 0x0e, 0xbc,
0x3c, 0x70, 0xff, 0x68, 0xd0, 0xcd, 0x0f, 0xd4, 0x17, 0x97, 0xb6, 0xa7, 0x00, 0xf2, 0x1a, 0x4c,
0x1e, 0x4c, 0x99, 0x65, 0x38, 0x46, 0xbf, 0x39, 0x3c, 0x19, 0x94, 0x8e, 0x1a, 0x28, 0xf7, 0x19,
0x5c, 0x07, 0x53, 0x76, 0x11, 0xf2, 0x38, 0xf1, 0x32, 0x9c, 0x7d, 0x0a, 0x8d, 0x32, 0x45, 0x5a,
0x60, 0xdc, 0x62, 0x22, 0xf6, 0x4f, 0x3f, 0xd3, 0x36, 0xee, 0x82, 0xf9, 0xb2, 0xf0, 0x52, 0x1e,
0xbc, 0xd2, 0xcf, 0x34, 0xf7, 0x0c, 0x8e, 0x57, 0x77, 0xa8, 0xee, 0x65, 0x9b, 0x49, 0xdd, 0x53,
0xe8, 0x8e, 0x70, 0x8e, 0xeb, 0x67, 0xb0, 0x0b, 0xf8, 0x06, 0x48, 0xe5, 0x84, 0x51, 0x81, 0x3a,
0x81, 0x56, 0x84, 0x31, 0x9b, 0x31, 0x8e, 0xa1, 0x28, 0x66, 0xd8, 0x43, 0x6f, 0x2d, 0xef, 0xbe,
0x80, 0xb6, 0xc4, 0xf0, 0x1f, 0x76, 0xfe, 0x06, 0xc4, 0xdf, 0x6b, 0x53, 0x89, 0x5d, 0x5f, 0x61,
0x3f, 0x87, 0xb6, 0xaf, 0x10, 0x74, 0x0f, 0xfa, 0xe1, 0x5f, 0x13, 0xa0, 0x7a, 0x59, 0xc8, 0x73,
0x30, 0xc7, 0xe1, 0x8c, 0x93, 0xe3, 0x9a, 0x15, 0xd2, 0x84, 0x50, 0x6e, 0xb7, 0x6a, 0xf9, 0x8b,
0x45, 0xc4, 0x13, 0xf2, 0x15, 0xac, 0xfa, 0x90, 0xbf, 0x8f, 0xe9, 0xa2, 0xb8, 0x19, 0xd2, 0x5b,
0x33, 0x94, 0xf4, 0x20, 0xd9, 0x4f, 0x36, 0xd6, 0x45, 0x27, 0x1e, 0x3c, 0x92, 0xa6, 0x97, 0xd4,
0x11, 0xaa, 0x77, 0xc2, 0x76, 0x36, 0x2f, 0xa8, 0x38, 0xa5, 0xa1, 0x93, 0x38, 0x55, 0xe3, 0x2d,
0x71, 0xaa, 0xe7, 0xf5, 0x13, 0x1c, 0xc9, 0x76, 0x26, 0xce, 0xae, 0x59, 0xb2, 0x9f, 0x6e, 0x59,
0x21, 0x68, 0x47, 0x70, 0x24, 0x7b, 0x5d, 0xa2, 0x55, 0x8e, 0x81, 0xe2, 0x86, 0x2e, 0xa1, 0x59,
0xb3, 0x2d, 0x79, 0xac, 0x3c, 0xa1, 0xc2, 0x9b, 0x76, 0x6f, 0x53, 0x59, 0x68, 0xba, 0x84, 0xa6,
0xbf, 0x81, 0xcd, 0xdf, 0xce, 0xa6, 0xb0, 0xea, 0xf7, 0x87, 0xd9, 0x1f, 0xeb, 0xe5, 0xbf, 0x00,
0x00, 0x00, 0xff, 0xff, 0x76, 0x14, 0x93, 0xa6, 0xde, 0x06, 0x00, 0x00,
// 566 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xcd, 0x6e, 0xd3, 0x40,
0x10, 0x96, 0x63, 0xb7, 0x6a, 0x26, 0xa5, 0x8a, 0x36, 0x3f, 0xb2, 0x2c, 0x11, 0x8c, 0x4f, 0x51,
0x25, 0x22, 0x08, 0x07, 0x2a, 0x0e, 0x48, 0x05, 0x17, 0x64, 0x51, 0x09, 0xc9, 0x2e, 0x08, 0xc1,
0xc9, 0x90, 0x4d, 0x1a, 0xd5, 0xf1, 0x1a, 0xef, 0xa6, 0x92, 0x1f, 0x86, 0x77, 0xe3, 0x41, 0x38,
0x54, 0xb6, 0x37, 0xf1, 0xae, 0xb3, 0x69, 0x72, 0xc9, 0xcd, 0x33, 0xb3, 0xfb, 0xcd, 0x37, 0x3b,
0xdf, 0x8c, 0xa1, 0xfd, 0x3e, 0x22, 0xbf, 0xef, 0x02, 0x46, 0x52, 0x3c, 0x4a, 0x52, 0xc2, 0x08,
0x6a, 0xce, 0x70, 0x8c, 0xd3, 0x90, 0xe1, 0x89, 0x75, 0x1a, 0xdc, 0x86, 0x29, 0x9e, 0x94, 0x01,
0xe7, 0xaf, 0x06, 0x9d, 0x0f, 0x29, 0x0e, 0x19, 0xfe, 0x46, 0xa2, 0xe5, 0x02, 0xfb, 0xf8, 0xcf,
0x12, 0x53, 0x86, 0xfa, 0x70, 0x9c, 0x44, 0xcb, 0xd9, 0x3c, 0x36, 0x35, 0x5b, 0x1b, 0x36, 0x7d,
0x6e, 0xa1, 0x01, 0x00, 0x8d, 0xc3, 0x84, 0xde, 0x12, 0xe6, 0xb9, 0x66, 0xa3, 0x88, 0x09, 0x9e,
0x3c, 0x7e, 0x5f, 0x00, 0xdd, 0x64, 0x09, 0x36, 0xf5, 0x32, 0x5e, 0x79, 0x90, 0x05, 0x27, 0xa5,
0x75, 0xf9, 0xc3, 0x34, 0x8a, 0xe8, 0xda, 0x46, 0x08, 0x8c, 0x39, 0x49, 0xa8, 0x79, 0x64, 0x6b,
0x43, 0xdd, 0x2f, 0xbe, 0x9d, 0x31, 0x74, 0x65, 0x7a, 0x34, 0x21, 0x31, 0x15, 0x70, 0x3c, 0x97,
0x33, 0x5c, 0xdb, 0xce, 0x14, 0xba, 0x9f, 0x30, 0x2b, 0x2f, 0x78, 0xf1, 0x94, 0xec, 0xaa, 0x49,
0xc4, 0x6a, 0xc8, 0x58, 0x12, 0x5f, 0x5d, 0xe6, 0xeb, 0x7c, 0x86, 0x5e, 0x2d, 0x0f, 0x27, 0x27,
0x3f, 0x82, 0xb6, 0xf1, 0x08, 0xab, 0x42, 0x1b, 0x42, 0xa1, 0x53, 0xe8, 0x7a, 0x74, 0x55, 0x64,
0x38, 0xc9, 0x0e, 0x45, 0xfa, 0x05, 0xf4, 0x6a, 0x79, 0x38, 0xe9, 0x2e, 0x1c, 0xa5, 0xb9, 0xa3,
0xc8, 0x73, 0xe2, 0x97, 0x86, 0xf3, 0x4f, 0x83, 0x5e, 0xd9, 0x80, 0x80, 0x37, 0xf9, 0x40, 0xc4,
0xd0, 0x3b, 0x30, 0x58, 0x38, 0xa3, 0xa6, 0x61, 0xeb, 0xc3, 0xd6, 0xf8, 0x7c, 0xb4, 0x56, 0xec,
0x48, 0x99, 0x7f, 0x74, 0x13, 0xce, 0xe8, 0x55, 0xcc, 0xd2, 0xcc, 0x2f, 0xee, 0x59, 0x6f, 0xa0,
0xb9, 0x76, 0xa1, 0x36, 0xe8, 0x77, 0x38, 0xe3, 0xcc, 0xf2, 0xcf, 0xbc, 0xbc, 0xfb, 0x30, 0x5a,
0x62, 0xce, 0xa9, 0x34, 0xde, 0x36, 0x2e, 0x34, 0xe7, 0x02, 0xfa, 0xf5, 0x0c, 0x55, 0x1f, 0x05,
0xb1, 0x6b, 0x75, 0xb1, 0x3b, 0x5f, 0xa0, 0xe7, 0xe2, 0x08, 0xef, 0xff, 0x36, 0x3b, 0xa6, 0xc7,
0xf9, 0x0e, 0xa8, 0x52, 0x94, 0xbb, 0x0b, 0xed, 0x1c, 0xda, 0x09, 0x4e, 0xe9, 0x9c, 0x32, 0x1c,
0xf3, 0x4b, 0x05, 0xe6, 0xa9, 0xbf, 0xe1, 0x77, 0x5e, 0x41, 0x47, 0x42, 0xde, 0x63, 0x8c, 0x18,
0xa0, 0xe0, 0x20, 0x64, 0xa4, 0xac, 0x7a, 0x2d, 0xeb, 0x25, 0x74, 0x02, 0x05, 0x51, 0x15, 0xbc,
0xa6, 0x86, 0x1f, 0xff, 0x37, 0x00, 0xaa, 0x0d, 0x88, 0x5e, 0x82, 0xe1, 0xc5, 0x73, 0x86, 0xfa,
0x82, 0xa4, 0x72, 0x07, 0xaf, 0xc8, 0x6a, 0x0b, 0xfe, 0xab, 0x45, 0xc2, 0x32, 0xf4, 0x13, 0x4c,
0x71, 0xe9, 0x7c, 0x4c, 0xc9, 0x62, 0xd5, 0x61, 0x34, 0xd8, 0x10, 0xa6, 0xb4, 0x38, 0xad, 0x67,
0x5b, 0xe3, 0xbc, 0x12, 0x1f, 0x9e, 0x48, 0x5b, 0x03, 0x89, 0x37, 0x54, 0x7b, 0xcb, 0xb2, 0xb7,
0x1f, 0xa8, 0x30, 0xa5, 0xa1, 0x96, 0x30, 0x55, 0x6b, 0x45, 0xc2, 0x54, 0xef, 0x83, 0xaf, 0x70,
0x26, 0x8f, 0x05, 0xb2, 0x77, 0xcd, 0xa4, 0xf5, 0xfc, 0x91, 0x13, 0x1c, 0xd6, 0x85, 0x33, 0x79,
0x66, 0x24, 0x58, 0xe5, 0x38, 0x29, 0x3a, 0x74, 0x0d, 0x2d, 0x41, 0xce, 0xe8, 0xa9, 0xf2, 0x85,
0x56, 0x9a, 0xb5, 0x06, 0xdb, 0xc2, 0x9c, 0xd3, 0x35, 0xb4, 0x82, 0x2d, 0x68, 0xc1, 0xe3, 0x68,
0x0a, 0xa9, 0xfe, 0x3a, 0x2e, 0xfe, 0xac, 0xaf, 0x1f, 0x02, 0x00, 0x00, 0xff, 0xff, 0xa2, 0x65,
0xf8, 0xa4, 0x86, 0x07, 0x00, 0x00,
}
+92 -43
View File
@@ -18,9 +18,10 @@ var _ = fmt.Errorf
var _ = math.Inf
type PutObjectRequest struct {
Bucket string `protobuf:"bytes,1,opt,name=bucket" json:"bucket,omitempty"`
Key string `protobuf:"bytes,2,opt,name=key" json:"key,omitempty"`
Body []byte `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
Bucket string `protobuf:"bytes,2,opt,name=bucket" json:"bucket,omitempty"`
Key string `protobuf:"bytes,3,opt,name=key" json:"key,omitempty"`
Body []byte `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"`
}
func (m *PutObjectRequest) Reset() { *m = PutObjectRequest{} }
@@ -28,6 +29,13 @@ func (m *PutObjectRequest) String() string { return proto.CompactText
func (*PutObjectRequest) ProtoMessage() {}
func (*PutObjectRequest) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} }
func (m *PutObjectRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *PutObjectRequest) GetBucket() string {
if m != nil {
return m.Bucket
@@ -50,8 +58,9 @@ func (m *PutObjectRequest) GetBody() []byte {
}
type GetObjectRequest struct {
Bucket string `protobuf:"bytes,1,opt,name=bucket" json:"bucket,omitempty"`
Key string `protobuf:"bytes,2,opt,name=key" json:"key,omitempty"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
Bucket string `protobuf:"bytes,2,opt,name=bucket" json:"bucket,omitempty"`
Key string `protobuf:"bytes,3,opt,name=key" json:"key,omitempty"`
}
func (m *GetObjectRequest) Reset() { *m = GetObjectRequest{} }
@@ -59,6 +68,13 @@ func (m *GetObjectRequest) String() string { return proto.CompactText
func (*GetObjectRequest) ProtoMessage() {}
func (*GetObjectRequest) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{1} }
func (m *GetObjectRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *GetObjectRequest) GetBucket() string {
if m != nil {
return m.Bucket
@@ -90,8 +106,9 @@ func (m *Bytes) GetData() []byte {
}
type ListCommonPrefixesRequest struct {
Bucket string `protobuf:"bytes,1,opt,name=bucket" json:"bucket,omitempty"`
Delimiter string `protobuf:"bytes,2,opt,name=delimiter" json:"delimiter,omitempty"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
Bucket string `protobuf:"bytes,2,opt,name=bucket" json:"bucket,omitempty"`
Delimiter string `protobuf:"bytes,3,opt,name=delimiter" json:"delimiter,omitempty"`
}
func (m *ListCommonPrefixesRequest) Reset() { *m = ListCommonPrefixesRequest{} }
@@ -99,6 +116,13 @@ func (m *ListCommonPrefixesRequest) String() string { return proto.Co
func (*ListCommonPrefixesRequest) ProtoMessage() {}
func (*ListCommonPrefixesRequest) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{3} }
func (m *ListCommonPrefixesRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *ListCommonPrefixesRequest) GetBucket() string {
if m != nil {
return m.Bucket
@@ -130,8 +154,9 @@ func (m *ListCommonPrefixesResponse) GetPrefixes() []string {
}
type ListObjectsRequest struct {
Bucket string `protobuf:"bytes,1,opt,name=bucket" json:"bucket,omitempty"`
Prefix string `protobuf:"bytes,2,opt,name=prefix" json:"prefix,omitempty"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
Bucket string `protobuf:"bytes,2,opt,name=bucket" json:"bucket,omitempty"`
Prefix string `protobuf:"bytes,3,opt,name=prefix" json:"prefix,omitempty"`
}
func (m *ListObjectsRequest) Reset() { *m = ListObjectsRequest{} }
@@ -139,6 +164,13 @@ func (m *ListObjectsRequest) String() string { return proto.CompactTe
func (*ListObjectsRequest) ProtoMessage() {}
func (*ListObjectsRequest) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{5} }
func (m *ListObjectsRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *ListObjectsRequest) GetBucket() string {
if m != nil {
return m.Bucket
@@ -170,8 +202,9 @@ func (m *ListObjectsResponse) GetKeys() []string {
}
type DeleteObjectRequest struct {
Bucket string `protobuf:"bytes,1,opt,name=bucket" json:"bucket,omitempty"`
Key string `protobuf:"bytes,2,opt,name=key" json:"key,omitempty"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
Bucket string `protobuf:"bytes,2,opt,name=bucket" json:"bucket,omitempty"`
Key string `protobuf:"bytes,3,opt,name=key" json:"key,omitempty"`
}
func (m *DeleteObjectRequest) Reset() { *m = DeleteObjectRequest{} }
@@ -179,6 +212,13 @@ func (m *DeleteObjectRequest) String() string { return proto.CompactT
func (*DeleteObjectRequest) ProtoMessage() {}
func (*DeleteObjectRequest) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{7} }
func (m *DeleteObjectRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *DeleteObjectRequest) GetBucket() string {
if m != nil {
return m.Bucket
@@ -194,9 +234,10 @@ func (m *DeleteObjectRequest) GetKey() string {
}
type CreateSignedURLRequest struct {
Bucket string `protobuf:"bytes,1,opt,name=bucket" json:"bucket,omitempty"`
Key string `protobuf:"bytes,2,opt,name=key" json:"key,omitempty"`
Ttl int64 `protobuf:"varint,3,opt,name=ttl" json:"ttl,omitempty"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
Bucket string `protobuf:"bytes,2,opt,name=bucket" json:"bucket,omitempty"`
Key string `protobuf:"bytes,3,opt,name=key" json:"key,omitempty"`
Ttl int64 `protobuf:"varint,4,opt,name=ttl" json:"ttl,omitempty"`
}
func (m *CreateSignedURLRequest) Reset() { *m = CreateSignedURLRequest{} }
@@ -204,6 +245,13 @@ func (m *CreateSignedURLRequest) String() string { return proto.Compa
func (*CreateSignedURLRequest) ProtoMessage() {}
func (*CreateSignedURLRequest) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{8} }
func (m *CreateSignedURLRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *CreateSignedURLRequest) GetBucket() string {
if m != nil {
return m.Bucket
@@ -589,33 +637,34 @@ var _ObjectStore_serviceDesc = grpc.ServiceDesc{
func init() { proto.RegisterFile("ObjectStore.proto", fileDescriptor2) }
var fileDescriptor2 = []byte{
// 444 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x94, 0xdf, 0x8b, 0xd3, 0x40,
0x10, 0xc7, 0x89, 0xa9, 0xc5, 0xcc, 0x15, 0x8c, 0x73, 0x50, 0x6b, 0x4e, 0xa5, 0x2e, 0x0a, 0x15,
0xa1, 0x1c, 0xfa, 0xe2, 0xc3, 0x81, 0xe2, 0x9d, 0x88, 0x50, 0xb0, 0xa6, 0x0a, 0xbe, 0xa6, 0x97,
0xf1, 0x8c, 0xcd, 0x2f, 0x37, 0x13, 0x30, 0xff, 0x81, 0x7f, 0xb6, 0x64, 0xb3, 0xc6, 0x4d, 0x2e,
0x67, 0xf1, 0xde, 0x66, 0x67, 0xe7, 0xfb, 0x9d, 0xc9, 0xce, 0x87, 0xc0, 0x9d, 0x0f, 0xdb, 0xef,
0x74, 0xce, 0x1b, 0xce, 0x24, 0x2d, 0x73, 0x99, 0x71, 0x86, 0xce, 0x05, 0xa5, 0x24, 0x03, 0xa6,
0xd0, 0x9b, 0x6c, 0xbe, 0x05, 0x92, 0xc2, 0xe6, 0x42, 0xac, 0xc1, 0x5d, 0x97, 0xdc, 0x08, 0x7c,
0xfa, 0x51, 0x52, 0xc1, 0x38, 0x85, 0xf1, 0xb6, 0x3c, 0xdf, 0x11, 0xcf, 0xac, 0xb9, 0xb5, 0x70,
0x7c, 0x7d, 0x42, 0x17, 0xec, 0x1d, 0x55, 0xb3, 0x1b, 0x2a, 0x59, 0x87, 0x88, 0x30, 0xda, 0x66,
0x61, 0x35, 0xb3, 0xe7, 0xd6, 0x62, 0xe2, 0xab, 0x58, 0x9c, 0x80, 0xfb, 0x8e, 0xae, 0xeb, 0x28,
0x8e, 0xe0, 0xe6, 0x9b, 0x8a, 0xa9, 0xa8, 0xad, 0xc3, 0x80, 0x03, 0x25, 0x98, 0xf8, 0x2a, 0x16,
0x1f, 0xe1, 0xde, 0x2a, 0x2a, 0xf8, 0x34, 0x4b, 0x92, 0x2c, 0x5d, 0x4b, 0xfa, 0x1a, 0xfd, 0xa4,
0x62, 0x5f, 0x8f, 0xfb, 0xe0, 0x84, 0x14, 0x47, 0x49, 0xc4, 0x24, 0x75, 0xa7, 0xbf, 0x09, 0xf1,
0x12, 0xbc, 0x21, 0xcb, 0x22, 0xcf, 0xd2, 0x82, 0xd0, 0x83, 0x5b, 0xb9, 0xce, 0xcd, 0xac, 0xb9,
0xbd, 0x70, 0xfc, 0xf6, 0x2c, 0xce, 0x00, 0x6b, 0x65, 0xf3, 0xa1, 0x7b, 0xa7, 0x98, 0xc2, 0xb8,
0x51, 0xea, 0x11, 0xf4, 0x49, 0x3c, 0x85, 0xc3, 0x8e, 0x8b, 0x6e, 0x8c, 0x30, 0xda, 0x51, 0xf5,
0xa7, 0xa9, 0x8a, 0xc5, 0x2b, 0x38, 0x3c, 0xa3, 0x98, 0x98, 0xae, 0xfb, 0xb6, 0x9f, 0x60, 0x7a,
0x2a, 0x29, 0x60, 0xda, 0x44, 0x17, 0x29, 0x85, 0x9f, 0xfd, 0xd5, 0xff, 0x6f, 0xdc, 0x05, 0x9b,
0x39, 0x56, 0x0b, 0xb7, 0xfd, 0x3a, 0x14, 0xcf, 0xe0, 0xee, 0x25, 0x57, 0xfd, 0x15, 0x2e, 0xd8,
0xa5, 0x8c, 0xb5, 0x67, 0x1d, 0x3e, 0xff, 0x35, 0x82, 0x03, 0x83, 0x4e, 0x3c, 0x86, 0xd1, 0xfb,
0x34, 0x62, 0x9c, 0x2e, 0x5b, 0x40, 0x97, 0x75, 0x42, 0x0f, 0xe6, 0xb9, 0x46, 0xfe, 0x6d, 0x92,
0x73, 0x85, 0x27, 0xe0, 0xb4, 0xc0, 0xe2, 0x91, 0x71, 0xdd, 0xc7, 0xf8, 0xb2, 0x76, 0x61, 0xd5,
0xea, 0x16, 0xce, 0x8e, 0xba, 0x8f, 0x6c, 0x47, 0xad, 0x88, 0x3c, 0xb6, 0x30, 0x68, 0x56, 0xde,
0x85, 0x05, 0x1f, 0x1b, 0x95, 0x57, 0xe2, 0xe9, 0x3d, 0xd9, 0x53, 0xa5, 0x9f, 0x6c, 0x05, 0x07,
0x06, 0x0f, 0xf8, 0xa0, 0xa7, 0xea, 0xd2, 0xe6, 0x3d, 0xbc, 0xea, 0x5a, 0xbb, 0xbd, 0x86, 0x89,
0x89, 0x0c, 0x9a, 0xf5, 0x03, 0x2c, 0x0d, 0x3c, 0xf7, 0x17, 0xb8, 0xdd, 0xdb, 0x2e, 0x3e, 0x32,
0x8a, 0x86, 0x79, 0xf2, 0xc4, 0xbf, 0x4a, 0x9a, 0xd9, 0xb6, 0x63, 0xf5, 0x03, 0x7a, 0xf1, 0x3b,
0x00, 0x00, 0xff, 0xff, 0xf3, 0xb2, 0x85, 0x9a, 0xae, 0x04, 0x00, 0x00,
// 459 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x54, 0x51, 0x8b, 0xd3, 0x40,
0x10, 0x26, 0x26, 0x1e, 0x66, 0xae, 0x60, 0x9c, 0x83, 0x1a, 0x73, 0x2a, 0x75, 0x51, 0xa8, 0x08,
0xe5, 0xd0, 0x17, 0x1f, 0x7c, 0x10, 0x4f, 0x11, 0xa1, 0xe0, 0x91, 0x2a, 0xfa, 0xe0, 0x4b, 0x7a,
0x19, 0x7b, 0xb1, 0x69, 0x12, 0x37, 0x13, 0x30, 0xff, 0xc0, 0x9f, 0x2d, 0xbb, 0x59, 0xeb, 0xa6,
0xd7, 0xf3, 0xa0, 0xf4, 0x6d, 0xe6, 0xdb, 0xf9, 0x66, 0xbe, 0xec, 0x7e, 0x13, 0xb8, 0xf3, 0x71,
0xfe, 0x83, 0xce, 0x79, 0xc6, 0xa5, 0xa4, 0x49, 0x25, 0x4b, 0x2e, 0xd1, 0x5f, 0x50, 0x41, 0x32,
0x61, 0x4a, 0xa3, 0xc1, 0xec, 0x22, 0x91, 0x94, 0x76, 0x07, 0xe2, 0x02, 0x82, 0xb3, 0x86, 0x3b,
0x42, 0x4c, 0x3f, 0x1b, 0xaa, 0x19, 0x87, 0x70, 0x50, 0xe5, 0xcd, 0x22, 0x2b, 0x42, 0x67, 0xe4,
0x8c, 0xfd, 0xd8, 0x64, 0x0a, 0x9f, 0x37, 0xe7, 0x4b, 0xe2, 0xf0, 0x46, 0x87, 0x77, 0x19, 0x06,
0xe0, 0x2e, 0xa9, 0x0d, 0x5d, 0x0d, 0xaa, 0x10, 0x11, 0xbc, 0x79, 0x99, 0xb6, 0xa1, 0x37, 0x72,
0xc6, 0x83, 0x58, 0xc7, 0xe2, 0x13, 0x04, 0xef, 0x69, 0xdf, 0x93, 0xc4, 0x31, 0xdc, 0x7c, 0xd3,
0x32, 0xd5, 0x6a, 0x64, 0x9a, 0x70, 0xa2, 0x1b, 0x0d, 0x62, 0x1d, 0x8b, 0x0c, 0xee, 0x4d, 0xb3,
0x9a, 0x4f, 0xcb, 0xd5, 0xaa, 0x2c, 0xce, 0x24, 0x7d, 0xcf, 0x7e, 0x51, 0xbd, 0xeb, 0xec, 0xfb,
0xe0, 0xa7, 0x94, 0x67, 0xab, 0x8c, 0x49, 0x1a, 0x05, 0xff, 0x00, 0xf1, 0x12, 0xa2, 0x6d, 0xa3,
0xea, 0xaa, 0x2c, 0x6a, 0xc2, 0x08, 0x6e, 0x55, 0x06, 0x0b, 0x9d, 0x91, 0x3b, 0xf6, 0xe3, 0x75,
0x2e, 0xbe, 0x01, 0x2a, 0x66, 0x77, 0x31, 0x3b, 0xab, 0x53, 0xf5, 0xba, 0xa3, 0x91, 0x66, 0x32,
0xf1, 0x14, 0x8e, 0x7a, 0xdd, 0x8d, 0x20, 0x04, 0x6f, 0x49, 0xed, 0x5f, 0x31, 0x3a, 0x16, 0x5f,
0xe0, 0xe8, 0x2d, 0xe5, 0xc4, 0xb4, 0xef, 0x37, 0xca, 0x61, 0x78, 0x2a, 0x29, 0x61, 0x9a, 0x65,
0x8b, 0x82, 0xd2, 0xcf, 0xf1, 0x74, 0x7f, 0x4e, 0x0b, 0xc0, 0x65, 0xce, 0xb5, 0xd1, 0xdc, 0x58,
0x85, 0xe2, 0x19, 0xdc, 0xbd, 0x34, 0xcd, 0x7c, 0x75, 0x00, 0x6e, 0x23, 0x73, 0x33, 0x4b, 0x85,
0xcf, 0x7f, 0x7b, 0x70, 0x68, 0x6d, 0x0b, 0x9e, 0x80, 0xf7, 0xa1, 0xc8, 0x18, 0x87, 0x93, 0xf5,
0xc2, 0x4c, 0x14, 0x60, 0x04, 0x47, 0x81, 0x85, 0xbf, 0x5b, 0x55, 0xdc, 0xe2, 0x2b, 0xf0, 0xd7,
0x0b, 0x84, 0xc7, 0xd6, 0xf1, 0xe6, 0x5a, 0x5d, 0xe6, 0x8e, 0x1d, 0xc5, 0x5e, 0x2f, 0x45, 0x8f,
0xbd, 0xb9, 0x2a, 0x3d, 0xb6, 0x76, 0xfc, 0x89, 0x83, 0x49, 0x67, 0x9d, 0xbe, 0xe9, 0xf0, 0xb1,
0x55, 0x79, 0xa5, 0xfd, 0xa3, 0x27, 0xd7, 0x54, 0x99, 0x2b, 0x9b, 0xc2, 0xa1, 0xe5, 0x1f, 0x7c,
0xb0, 0xc1, 0xea, 0xbb, 0x36, 0x7a, 0x78, 0xd5, 0xb1, 0xe9, 0xf6, 0x1a, 0x06, 0xb6, 0xc5, 0xd0,
0xae, 0xdf, 0xe2, 0xbd, 0x2d, 0xd7, 0xfd, 0x15, 0x6e, 0x6f, 0xbc, 0x2e, 0x3e, 0xb2, 0x8a, 0xb6,
0xfb, 0x2c, 0x12, 0xff, 0x2b, 0xe9, 0xb4, 0xcd, 0x0f, 0xf4, 0x0f, 0xf1, 0xc5, 0x9f, 0x00, 0x00,
0x00, 0xff, 0xff, 0x59, 0xaf, 0x2a, 0xa0, 0x3e, 0x05, 0x00, 0x00,
}
+162
View File
@@ -0,0 +1,162 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: PluginLister.proto
package generated
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
type PluginIdentifier struct {
Command string `protobuf:"bytes,1,opt,name=command" json:"command,omitempty"`
Kind string `protobuf:"bytes,2,opt,name=kind" json:"kind,omitempty"`
Name string `protobuf:"bytes,3,opt,name=name" json:"name,omitempty"`
}
func (m *PluginIdentifier) Reset() { *m = PluginIdentifier{} }
func (m *PluginIdentifier) String() string { return proto.CompactTextString(m) }
func (*PluginIdentifier) ProtoMessage() {}
func (*PluginIdentifier) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{0} }
func (m *PluginIdentifier) GetCommand() string {
if m != nil {
return m.Command
}
return ""
}
func (m *PluginIdentifier) GetKind() string {
if m != nil {
return m.Kind
}
return ""
}
func (m *PluginIdentifier) GetName() string {
if m != nil {
return m.Name
}
return ""
}
type ListPluginsResponse struct {
Plugins []*PluginIdentifier `protobuf:"bytes,1,rep,name=plugins" json:"plugins,omitempty"`
}
func (m *ListPluginsResponse) Reset() { *m = ListPluginsResponse{} }
func (m *ListPluginsResponse) String() string { return proto.CompactTextString(m) }
func (*ListPluginsResponse) ProtoMessage() {}
func (*ListPluginsResponse) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{1} }
func (m *ListPluginsResponse) GetPlugins() []*PluginIdentifier {
if m != nil {
return m.Plugins
}
return nil
}
func init() {
proto.RegisterType((*PluginIdentifier)(nil), "generated.PluginIdentifier")
proto.RegisterType((*ListPluginsResponse)(nil), "generated.ListPluginsResponse")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for PluginLister service
type PluginListerClient interface {
ListPlugins(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*ListPluginsResponse, error)
}
type pluginListerClient struct {
cc *grpc.ClientConn
}
func NewPluginListerClient(cc *grpc.ClientConn) PluginListerClient {
return &pluginListerClient{cc}
}
func (c *pluginListerClient) ListPlugins(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*ListPluginsResponse, error) {
out := new(ListPluginsResponse)
err := grpc.Invoke(ctx, "/generated.PluginLister/ListPlugins", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for PluginLister service
type PluginListerServer interface {
ListPlugins(context.Context, *Empty) (*ListPluginsResponse, error)
}
func RegisterPluginListerServer(s *grpc.Server, srv PluginListerServer) {
s.RegisterService(&_PluginLister_serviceDesc, srv)
}
func _PluginLister_ListPlugins_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(PluginListerServer).ListPlugins(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/generated.PluginLister/ListPlugins",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PluginListerServer).ListPlugins(ctx, req.(*Empty))
}
return interceptor(ctx, in, info, handler)
}
var _PluginLister_serviceDesc = grpc.ServiceDesc{
ServiceName: "generated.PluginLister",
HandlerType: (*PluginListerServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ListPlugins",
Handler: _PluginLister_ListPlugins_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "PluginLister.proto",
}
func init() { proto.RegisterFile("PluginLister.proto", fileDescriptor3) }
var fileDescriptor3 = []byte{
// 201 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x0a, 0xc8, 0x29, 0x4d,
0xcf, 0xcc, 0xf3, 0xc9, 0x2c, 0x2e, 0x49, 0x2d, 0xd2, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2,
0x4c, 0x4f, 0xcd, 0x4b, 0x2d, 0x4a, 0x2c, 0x49, 0x4d, 0x91, 0xe2, 0x09, 0xce, 0x48, 0x2c, 0x4a,
0x4d, 0x81, 0x48, 0x28, 0x85, 0x70, 0x09, 0x40, 0x94, 0x7b, 0xa6, 0xa4, 0xe6, 0x95, 0x64, 0xa6,
0x65, 0xa6, 0x16, 0x09, 0x49, 0x70, 0xb1, 0x27, 0xe7, 0xe7, 0xe6, 0x26, 0xe6, 0xa5, 0x48, 0x30,
0x2a, 0x30, 0x6a, 0x70, 0x06, 0xc1, 0xb8, 0x42, 0x42, 0x5c, 0x2c, 0xd9, 0x99, 0x79, 0x29, 0x12,
0x4c, 0x60, 0x61, 0x30, 0x1b, 0x24, 0x96, 0x97, 0x98, 0x9b, 0x2a, 0xc1, 0x0c, 0x11, 0x03, 0xb1,
0x95, 0x7c, 0xb8, 0x84, 0x41, 0xd6, 0x43, 0x4c, 0x2e, 0x0e, 0x4a, 0x2d, 0x2e, 0xc8, 0xcf, 0x2b,
0x4e, 0x15, 0x32, 0xe5, 0x62, 0x2f, 0x80, 0x08, 0x49, 0x30, 0x2a, 0x30, 0x6b, 0x70, 0x1b, 0x49,
0xeb, 0xc1, 0xdd, 0xa5, 0x87, 0xee, 0x8c, 0x20, 0x98, 0x5a, 0x23, 0x7f, 0x2e, 0x1e, 0x64, 0x2f,
0x09, 0xd9, 0x73, 0x71, 0x23, 0x99, 0x2e, 0x24, 0x80, 0x64, 0x88, 0x6b, 0x6e, 0x41, 0x49, 0xa5,
0x94, 0x1c, 0x92, 0x08, 0x16, 0x77, 0x24, 0xb1, 0x81, 0xfd, 0x6e, 0x0c, 0x08, 0x00, 0x00, 0xff,
0xff, 0x0e, 0xb5, 0xe4, 0x0c, 0x2a, 0x01, 0x00, 0x00,
}
+32 -23
View File
@@ -18,14 +18,22 @@ var _ = fmt.Errorf
var _ = math.Inf
type RestoreExecuteRequest struct {
Item []byte `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"`
Restore []byte `protobuf:"bytes,2,opt,name=restore,proto3" json:"restore,omitempty"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
Item []byte `protobuf:"bytes,2,opt,name=item,proto3" json:"item,omitempty"`
Restore []byte `protobuf:"bytes,3,opt,name=restore,proto3" json:"restore,omitempty"`
}
func (m *RestoreExecuteRequest) Reset() { *m = RestoreExecuteRequest{} }
func (m *RestoreExecuteRequest) String() string { return proto.CompactTextString(m) }
func (*RestoreExecuteRequest) ProtoMessage() {}
func (*RestoreExecuteRequest) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{0} }
func (*RestoreExecuteRequest) Descriptor() ([]byte, []int) { return fileDescriptor4, []int{0} }
func (m *RestoreExecuteRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *RestoreExecuteRequest) GetItem() []byte {
if m != nil {
@@ -49,7 +57,7 @@ type RestoreExecuteResponse struct {
func (m *RestoreExecuteResponse) Reset() { *m = RestoreExecuteResponse{} }
func (m *RestoreExecuteResponse) String() string { return proto.CompactTextString(m) }
func (*RestoreExecuteResponse) ProtoMessage() {}
func (*RestoreExecuteResponse) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{1} }
func (*RestoreExecuteResponse) Descriptor() ([]byte, []int) { return fileDescriptor4, []int{1} }
func (m *RestoreExecuteResponse) GetItem() []byte {
if m != nil {
@@ -81,7 +89,7 @@ const _ = grpc.SupportPackageIsVersion4
// Client API for RestoreItemAction service
type RestoreItemActionClient interface {
AppliesTo(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*AppliesToResponse, error)
AppliesTo(ctx context.Context, in *AppliesToRequest, opts ...grpc.CallOption) (*AppliesToResponse, error)
Execute(ctx context.Context, in *RestoreExecuteRequest, opts ...grpc.CallOption) (*RestoreExecuteResponse, error)
}
@@ -93,7 +101,7 @@ func NewRestoreItemActionClient(cc *grpc.ClientConn) RestoreItemActionClient {
return &restoreItemActionClient{cc}
}
func (c *restoreItemActionClient) AppliesTo(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*AppliesToResponse, error) {
func (c *restoreItemActionClient) AppliesTo(ctx context.Context, in *AppliesToRequest, opts ...grpc.CallOption) (*AppliesToResponse, error) {
out := new(AppliesToResponse)
err := grpc.Invoke(ctx, "/generated.RestoreItemAction/AppliesTo", in, out, c.cc, opts...)
if err != nil {
@@ -114,7 +122,7 @@ func (c *restoreItemActionClient) Execute(ctx context.Context, in *RestoreExecut
// Server API for RestoreItemAction service
type RestoreItemActionServer interface {
AppliesTo(context.Context, *Empty) (*AppliesToResponse, error)
AppliesTo(context.Context, *AppliesToRequest) (*AppliesToResponse, error)
Execute(context.Context, *RestoreExecuteRequest) (*RestoreExecuteResponse, error)
}
@@ -123,7 +131,7 @@ func RegisterRestoreItemActionServer(s *grpc.Server, srv RestoreItemActionServer
}
func _RestoreItemAction_AppliesTo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Empty)
in := new(AppliesToRequest)
if err := dec(in); err != nil {
return nil, err
}
@@ -135,7 +143,7 @@ func _RestoreItemAction_AppliesTo_Handler(srv interface{}, ctx context.Context,
FullMethod: "/generated.RestoreItemAction/AppliesTo",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RestoreItemActionServer).AppliesTo(ctx, req.(*Empty))
return srv.(RestoreItemActionServer).AppliesTo(ctx, req.(*AppliesToRequest))
}
return interceptor(ctx, in, info, handler)
}
@@ -175,22 +183,23 @@ var _RestoreItemAction_serviceDesc = grpc.ServiceDesc{
Metadata: "RestoreItemAction.proto",
}
func init() { proto.RegisterFile("RestoreItemAction.proto", fileDescriptor3) }
func init() { proto.RegisterFile("RestoreItemAction.proto", fileDescriptor4) }
var fileDescriptor3 = []byte{
// 210 bytes of a gzipped FileDescriptorProto
var fileDescriptor4 = []byte{
// 230 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x0f, 0x4a, 0x2d, 0x2e,
0xc9, 0x2f, 0x4a, 0xf5, 0x2c, 0x49, 0xcd, 0x75, 0x4c, 0x2e, 0xc9, 0xcc, 0xcf, 0xd3, 0x2b, 0x28,
0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x4c, 0x4f, 0xcd, 0x4b, 0x2d, 0x4a, 0x2c, 0x49, 0x4d, 0x91, 0xe2,
0x09, 0xce, 0x48, 0x2c, 0x4a, 0x4d, 0x81, 0x48, 0x28, 0xb9, 0x72, 0x89, 0x42, 0xf5, 0xb8, 0x56,
0xa4, 0x26, 0x97, 0x96, 0xa4, 0x06, 0xa5, 0x16, 0x96, 0xa6, 0x16, 0x97, 0x08, 0x09, 0x71, 0xb1,
0x64, 0x96, 0xa4, 0xe6, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0xf0, 0x04, 0x81, 0xd9, 0x42, 0x12, 0x5c,
0xec, 0x45, 0x10, 0xc5, 0x12, 0x4c, 0x60, 0x61, 0x18, 0x57, 0xc9, 0x8d, 0x4b, 0x0c, 0xdd, 0x98,
0xe2, 0x82, 0xfc, 0xbc, 0xe2, 0x54, 0x5c, 0xe6, 0x94, 0x27, 0x16, 0xe5, 0x65, 0xe6, 0xa5, 0x83,
0xcd, 0xe1, 0x0c, 0x82, 0x71, 0x8d, 0x16, 0x30, 0x72, 0x09, 0x62, 0xf8, 0x41, 0xc8, 0x9a, 0x8b,
0xd3, 0xb1, 0xa0, 0x20, 0x27, 0x33, 0xb5, 0x38, 0x24, 0x5f, 0x48, 0x40, 0x0f, 0xee, 0x17, 0x3d,
0xd7, 0xdc, 0x82, 0x92, 0x4a, 0x29, 0x19, 0x24, 0x11, 0xb8, 0x3a, 0xb8, 0x03, 0xfc, 0xb8, 0xd8,
0xa1, 0x6e, 0x12, 0x52, 0x40, 0x52, 0x88, 0xd5, 0xd7, 0x52, 0x8a, 0x78, 0x54, 0x40, 0xcc, 0x4b,
0x62, 0x03, 0x07, 0x9c, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0xb9, 0x08, 0x09, 0x74, 0x6c, 0x01,
0x00, 0x00,
0x09, 0xce, 0x48, 0x2c, 0x4a, 0x4d, 0x81, 0x48, 0x28, 0xc5, 0x72, 0x89, 0x42, 0xf5, 0xb8, 0x56,
0xa4, 0x26, 0x97, 0x96, 0xa4, 0x06, 0xa5, 0x16, 0x96, 0xa6, 0x16, 0x97, 0x08, 0x89, 0x71, 0xb1,
0x15, 0xe4, 0x94, 0xa6, 0x67, 0xe6, 0x49, 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x06, 0x41, 0x79, 0x42,
0x42, 0x5c, 0x2c, 0x99, 0x25, 0xa9, 0xb9, 0x12, 0x4c, 0x0a, 0x8c, 0x1a, 0x3c, 0x41, 0x60, 0xb6,
0x90, 0x04, 0x17, 0x7b, 0x11, 0xc4, 0x10, 0x09, 0x66, 0xb0, 0x30, 0x8c, 0xab, 0xe4, 0xc6, 0x25,
0x86, 0x6e, 0x7c, 0x71, 0x41, 0x7e, 0x5e, 0x71, 0x2a, 0xdc, 0x1c, 0x46, 0x54, 0x73, 0xca, 0x13,
0x8b, 0xf2, 0x32, 0xf3, 0xd2, 0xc1, 0xc6, 0x73, 0x06, 0xc1, 0xb8, 0x46, 0xab, 0x19, 0xb9, 0x04,
0x31, 0xfc, 0x26, 0xe4, 0xc6, 0xc5, 0xe9, 0x58, 0x50, 0x90, 0x93, 0x99, 0x5a, 0x1c, 0x92, 0x2f,
0x24, 0xad, 0x07, 0xf7, 0xa3, 0x1e, 0x5c, 0x14, 0xea, 0x1b, 0x29, 0x19, 0xec, 0x92, 0x50, 0xb7,
0xf8, 0x71, 0xb1, 0x43, 0x9d, 0x27, 0xa4, 0x80, 0xa4, 0x10, 0x6b, 0xc0, 0x48, 0x29, 0xe2, 0x51,
0x01, 0x31, 0x2f, 0x89, 0x0d, 0x1c, 0xb6, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xaa, 0xe5,
0x97, 0xa4, 0x8f, 0x01, 0x00, 0x00,
}
+50 -24
View File
@@ -18,16 +18,24 @@ type Empty struct {
func (m *Empty) Reset() { *m = Empty{} }
func (m *Empty) String() string { return proto.CompactTextString(m) }
func (*Empty) ProtoMessage() {}
func (*Empty) Descriptor() ([]byte, []int) { return fileDescriptor4, []int{0} }
func (*Empty) Descriptor() ([]byte, []int) { return fileDescriptor5, []int{0} }
type InitRequest struct {
Config map[string]string `protobuf:"bytes,1,rep,name=config" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
Config map[string]string `protobuf:"bytes,2,rep,name=config" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
}
func (m *InitRequest) Reset() { *m = InitRequest{} }
func (m *InitRequest) String() string { return proto.CompactTextString(m) }
func (*InitRequest) ProtoMessage() {}
func (*InitRequest) Descriptor() ([]byte, []int) { return fileDescriptor4, []int{1} }
func (*InitRequest) Descriptor() ([]byte, []int) { return fileDescriptor5, []int{1} }
func (m *InitRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
func (m *InitRequest) GetConfig() map[string]string {
if m != nil {
@@ -36,6 +44,22 @@ func (m *InitRequest) GetConfig() map[string]string {
return nil
}
type AppliesToRequest struct {
Plugin string `protobuf:"bytes,1,opt,name=plugin" json:"plugin,omitempty"`
}
func (m *AppliesToRequest) Reset() { *m = AppliesToRequest{} }
func (m *AppliesToRequest) String() string { return proto.CompactTextString(m) }
func (*AppliesToRequest) ProtoMessage() {}
func (*AppliesToRequest) Descriptor() ([]byte, []int) { return fileDescriptor5, []int{2} }
func (m *AppliesToRequest) GetPlugin() string {
if m != nil {
return m.Plugin
}
return ""
}
type AppliesToResponse struct {
IncludedNamespaces []string `protobuf:"bytes,1,rep,name=includedNamespaces" json:"includedNamespaces,omitempty"`
ExcludedNamespaces []string `protobuf:"bytes,2,rep,name=excludedNamespaces" json:"excludedNamespaces,omitempty"`
@@ -47,7 +71,7 @@ type AppliesToResponse struct {
func (m *AppliesToResponse) Reset() { *m = AppliesToResponse{} }
func (m *AppliesToResponse) String() string { return proto.CompactTextString(m) }
func (*AppliesToResponse) ProtoMessage() {}
func (*AppliesToResponse) Descriptor() ([]byte, []int) { return fileDescriptor4, []int{2} }
func (*AppliesToResponse) Descriptor() ([]byte, []int) { return fileDescriptor5, []int{3} }
func (m *AppliesToResponse) GetIncludedNamespaces() []string {
if m != nil {
@@ -87,28 +111,30 @@ func (m *AppliesToResponse) GetSelector() string {
func init() {
proto.RegisterType((*Empty)(nil), "generated.Empty")
proto.RegisterType((*InitRequest)(nil), "generated.InitRequest")
proto.RegisterType((*AppliesToRequest)(nil), "generated.AppliesToRequest")
proto.RegisterType((*AppliesToResponse)(nil), "generated.AppliesToResponse")
}
func init() { proto.RegisterFile("Shared.proto", fileDescriptor4) }
func init() { proto.RegisterFile("Shared.proto", fileDescriptor5) }
var fileDescriptor4 = []byte{
// 257 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0xd1, 0xb1, 0x4e, 0xc3, 0x30,
0x10, 0x06, 0x60, 0xb9, 0x21, 0x85, 0x5c, 0x18, 0xa8, 0xc5, 0x10, 0x75, 0xaa, 0x32, 0x75, 0x40,
0x19, 0x60, 0x81, 0x6e, 0x08, 0x75, 0x60, 0x61, 0x30, 0xbc, 0x80, 0x49, 0x7e, 0x4a, 0x44, 0x6a,
0x1b, 0xdb, 0x41, 0x64, 0xe7, 0x6d, 0x79, 0x09, 0x14, 0x87, 0x96, 0x4a, 0x61, 0xf3, 0xdd, 0xff,
0xdd, 0xc9, 0x96, 0xe9, 0xf4, 0xf1, 0x55, 0x5a, 0x54, 0x85, 0xb1, 0xda, 0x6b, 0x9e, 0x6c, 0xa0,
0x60, 0xa5, 0x47, 0x95, 0x1f, 0x53, 0xbc, 0xde, 0x1a, 0xdf, 0xe5, 0x5f, 0x8c, 0xd2, 0x7b, 0x55,
0x7b, 0x81, 0xf7, 0x16, 0xce, 0xf3, 0x15, 0x4d, 0x4b, 0xad, 0x5e, 0xea, 0x4d, 0xc6, 0x16, 0xd1,
0x32, 0xbd, 0xcc, 0x8b, 0xfd, 0x50, 0x71, 0xe0, 0x8a, 0xbb, 0x80, 0xd6, 0xca, 0xdb, 0x4e, 0xfc,
0x4e, 0xcc, 0x6f, 0x28, 0x3d, 0x68, 0xf3, 0x33, 0x8a, 0xde, 0xd0, 0x65, 0x6c, 0xc1, 0x96, 0x89,
0xe8, 0x8f, 0xfc, 0x9c, 0xe2, 0x0f, 0xd9, 0xb4, 0xc8, 0x26, 0xa1, 0x37, 0x14, 0xab, 0xc9, 0x35,
0xcb, 0xbf, 0x19, 0xcd, 0x6e, 0x8d, 0x69, 0x6a, 0xb8, 0x27, 0x2d, 0xe0, 0x8c, 0x56, 0x0e, 0xbc,
0x20, 0x5e, 0xab, 0xb2, 0x69, 0x2b, 0x54, 0x0f, 0x72, 0x0b, 0x67, 0x64, 0x09, 0x17, 0x2e, 0x96,
0x88, 0x7f, 0x92, 0xde, 0xe3, 0x73, 0xe4, 0x27, 0x83, 0x1f, 0x27, 0xfc, 0x82, 0x66, 0xbb, 0x2d,
0x02, 0x4e, 0xb7, 0xb6, 0xe7, 0x51, 0xe0, 0xe3, 0xa0, 0xd7, 0xbb, 0x1d, 0x7f, 0xfa, 0x68, 0xd0,
0xa3, 0x80, 0xcf, 0xe9, 0xc4, 0xa1, 0x41, 0xe9, 0xb5, 0xcd, 0xe2, 0xf0, 0xdc, 0x7d, 0xfd, 0x3c,
0x0d, 0xff, 0x71, 0xf5, 0x13, 0x00, 0x00, 0xff, 0xff, 0x19, 0xd7, 0x88, 0x92, 0x9f, 0x01, 0x00,
0x00,
var fileDescriptor5 = []byte{
// 278 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x91, 0xcd, 0x4e, 0x84, 0x30,
0x14, 0x85, 0x53, 0x10, 0x94, 0x8b, 0x8b, 0x99, 0xc6, 0x18, 0x32, 0x2b, 0xc2, 0x8a, 0x18, 0xc3,
0x42, 0x37, 0x3a, 0x3b, 0x63, 0x66, 0xe1, 0xc6, 0x05, 0xfa, 0x02, 0x08, 0x57, 0x24, 0x32, 0x6d,
0xed, 0x8f, 0x91, 0x77, 0xf1, 0x0d, 0x7d, 0x09, 0x43, 0x61, 0x46, 0x12, 0x4c, 0x66, 0xd7, 0x73,
0xcf, 0x77, 0xbf, 0xb4, 0x29, 0x9c, 0x3e, 0xbd, 0x15, 0x12, 0xab, 0x4c, 0x48, 0xae, 0x39, 0x0d,
0x6a, 0x64, 0x28, 0x0b, 0x8d, 0x55, 0x72, 0x0c, 0xde, 0x66, 0x2b, 0x74, 0x97, 0x7c, 0x13, 0x08,
0x1f, 0x58, 0xa3, 0x73, 0xfc, 0x30, 0xa8, 0x34, 0x3d, 0x07, 0x5f, 0xb4, 0xa6, 0x6e, 0x58, 0x44,
0x62, 0x92, 0x06, 0xf9, 0x98, 0xe8, 0x1a, 0xfc, 0x92, 0xb3, 0xd7, 0xa6, 0x8e, 0x9c, 0xd8, 0x4d,
0xc3, 0xab, 0x24, 0xdb, 0xcb, 0xb2, 0xc9, 0x7e, 0x76, 0x6f, 0xa1, 0x0d, 0xd3, 0xb2, 0xcb, 0xc7,
0x8d, 0xd5, 0x2d, 0x84, 0x93, 0x31, 0x5d, 0x80, 0xfb, 0x8e, 0xdd, 0xe8, 0xef, 0x8f, 0xf4, 0x0c,
0xbc, 0xcf, 0xa2, 0x35, 0x18, 0x39, 0x76, 0x36, 0x84, 0xb5, 0x73, 0x43, 0x92, 0x0b, 0x58, 0xdc,
0x09, 0xd1, 0x36, 0xa8, 0x9e, 0xf9, 0x81, 0x2b, 0x26, 0x3f, 0x04, 0x96, 0x13, 0x58, 0x09, 0xce,
0x14, 0xd2, 0x0c, 0x68, 0xc3, 0xca, 0xd6, 0x54, 0x58, 0x3d, 0x16, 0x5b, 0x54, 0xa2, 0x28, 0x51,
0x45, 0x24, 0x76, 0xd3, 0x20, 0xff, 0xa7, 0xe9, 0x79, 0xfc, 0x9a, 0xf1, 0xce, 0xc0, 0xcf, 0x1b,
0x7a, 0x09, 0xcb, 0x9d, 0x25, 0x47, 0xc5, 0x8d, 0xec, 0x71, 0xd7, 0xe2, 0xf3, 0xa2, 0xa7, 0x77,
0x8e, 0x3f, 0xfa, 0x68, 0xa0, 0x67, 0x05, 0x5d, 0xc1, 0x89, 0xc2, 0x16, 0x4b, 0xcd, 0x65, 0xe4,
0xd9, 0xb7, 0xee, 0xf3, 0x8b, 0x6f, 0xff, 0xf4, 0xfa, 0x37, 0x00, 0x00, 0xff, 0xff, 0x73, 0x1b,
0xfe, 0x37, 0xe3, 0x01, 0x00, 0x00,
}
+3 -2
View File
@@ -22,6 +22,7 @@ import plugin "github.com/hashicorp/go-plugin"
type Interface interface {
plugin.Plugin
// Kind returns the PluginKind for the plugin.
Kind() PluginKind
// names returns a list of all the registered implementations for this plugin (such as "pod" and "pvc" for
// BackupItemAction).
names() []string
}
+45
View File
@@ -0,0 +1,45 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"testing"
"github.com/heptio/ark/pkg/util/logging"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestNewLogger(t *testing.T) {
l := NewLogger().(*logrus.Logger)
expectedFormatter := &logrus.JSONFormatter{
FieldMap: logrus.FieldMap{
logrus.FieldKeyMsg: "@message",
},
DisableTimestamp: true,
}
assert.Equal(t, expectedFormatter, l.Formatter)
expectedHooks := []logrus.Hook{
(&logging.LogLocationHook{}).WithLoggerName("plugin"),
&logging.HcLogLevelHook{},
}
for _, level := range logrus.AllLevels {
assert.Equal(t, expectedHooks, l.Hooks[level])
}
}
+15
View File
@@ -1,3 +1,18 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
+163 -375
View File
@@ -17,14 +17,8 @@ limitations under the License.
package plugin
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
plugin "github.com/hashicorp/go-plugin"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/heptio/ark/pkg/backup"
@@ -32,391 +26,185 @@ import (
"github.com/heptio/ark/pkg/restore"
)
// PluginKind is a type alias for a string that describes
// the kind of an Ark-supported plugin.
type PluginKind string
func (k PluginKind) String() string {
return string(k)
}
func baseConfig() *plugin.ClientConfig {
return &plugin.ClientConfig{
HandshakeConfig: Handshake,
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
Managed: true,
}
}
const (
// PluginKindObjectStore is the Kind string for
// an Object Store plugin.
PluginKindObjectStore PluginKind = "objectstore"
// PluginKindBlockStore is the Kind string for
// a Block Store plugin.
PluginKindBlockStore PluginKind = "blockstore"
// PluginKindCloudProvider is the Kind string for
// a CloudProvider plugin (i.e. an Object & Block
// store).
//
// NOTE that it is highly likely that in subsequent
// versions of Ark this kind of plugin will be replaced
// with a different mechanism for providing multiple
// plugin impls within a single binary. This should
// probably not be used.
PluginKindCloudProvider PluginKind = "cloudprovider"
// PluginKindBackupItemAction is the Kind string for
// a Backup ItemAction plugin.
PluginKindBackupItemAction PluginKind = "backupitemaction"
// PluginKindRestoreItemAction is the Kind string for
// a Restore ItemAction plugin.
PluginKindRestoreItemAction PluginKind = "restoreitemaction"
)
var AllPluginKinds = []PluginKind{
PluginKindObjectStore,
PluginKindBlockStore,
PluginKindCloudProvider,
PluginKindBackupItemAction,
PluginKindRestoreItemAction,
}
type pluginInfo struct {
kinds []PluginKind
name string
commandName string
commandArgs []string
}
// Manager exposes functions for getting implementations of the pluggable
// Ark interfaces.
// Manager manages the lifecycles of plugins.
type Manager interface {
// GetObjectStore returns the plugin implementation of the
// cloudprovider.ObjectStore interface with the specified name.
// GetObjectStore returns the ObjectStore plugin for name.
GetObjectStore(name string) (cloudprovider.ObjectStore, error)
// GetBlockStore returns the plugin implementation of the
// cloudprovider.BlockStore interface with the specified name.
// GetBlockStore returns the BlockStore plugin for name.
GetBlockStore(name string) (cloudprovider.BlockStore, error)
// GetBackupItemActions returns all backup.ItemAction plugins.
// These plugin instances should ONLY be used for a single backup
// (mainly because each one outputs to a per-backup log),
// and should be terminated upon completion of the backup with
// CloseBackupItemActions().
GetBackupItemActions(backupName string) ([]backup.ItemAction, error)
// GetBackupItemActions returns all backup item action plugins.
GetBackupItemActions() ([]backup.ItemAction, error)
// CloseBackupItemActions terminates the plugin sub-processes that
// are hosting BackupItemAction plugins for the given backup name.
CloseBackupItemActions(backupName string) error
// GetBackupItemAction returns the backup item action plugin for name.
GetBackupItemAction(name string) (backup.ItemAction, error)
// GetRestoreItemActions returns all restore.ItemAction plugins.
// These plugin instances should ONLY be used for a single restore
// (mainly because each one outputs to a per-restore log),
// and should be terminated upon completion of the restore with
// CloseRestoreItemActions().
GetRestoreItemActions(restoreName string) ([]restore.ItemAction, error)
// GetRestoreItemActions returns all restore item action plugins.
GetRestoreItemActions() ([]restore.ItemAction, error)
// CloseRestoreItemActions terminates the plugin sub-processes that
// are hosting RestoreItemAction plugins for the given restore name.
CloseRestoreItemActions(restoreName string) error
// GetRestoreItemAction returns the restore item action plugin for name.
GetRestoreItemAction(name string) (restore.ItemAction, error)
// CleanupClients kills all plugin subprocesses.
// CleanupClients terminates all of the Manager's running plugin processes.
CleanupClients()
}
// manager implements Manager.
type manager struct {
logger logrus.FieldLogger
logLevel logrus.Level
pluginRegistry *registry
clientStore *clientStore
pluginDir string
logger logrus.FieldLogger
logLevel logrus.Level
registry Registry
restartableProcessFactory RestartableProcessFactory
// lock guards restartableProcesses
lock sync.Mutex
restartableProcesses map[string]RestartableProcess
}
// NewManager constructs a manager for getting plugin implementations.
func NewManager(logger logrus.FieldLogger, level logrus.Level, pluginDir string) (Manager, error) {
m := &manager{
logger: logger,
logLevel: level,
pluginRegistry: newRegistry(),
clientStore: newClientStore(),
pluginDir: pluginDir,
// NewManager constructs a manager for getting plugins.
func NewManager(logger logrus.FieldLogger, level logrus.Level, registry Registry) Manager {
return &manager{
logger: logger,
logLevel: level,
registry: registry,
restartableProcessFactory: newRestartableProcessFactory(),
restartableProcesses: make(map[string]RestartableProcess),
}
if err := m.registerPlugins(); err != nil {
return nil, err
}
return m, nil
}
func pluginForKind(kind PluginKind) plugin.Plugin {
switch kind {
case PluginKindObjectStore:
return &ObjectStorePlugin{}
case PluginKindBlockStore:
return &BlockStorePlugin{}
default:
return nil
}
}
func getPluginInstance(client *plugin.Client, kind PluginKind) (interface{}, error) {
protocolClient, err := client.Client()
if err != nil {
return nil, errors.WithStack(err)
}
plugin, err := protocolClient.Dispense(string(kind))
if err != nil {
return nil, errors.WithStack(err)
}
return plugin, nil
}
func (m *manager) registerPlugins() error {
arkCommand := os.Args[0]
// first, register internal plugins
for _, provider := range []string{"aws", "gcp", "azure"} {
m.pluginRegistry.register(provider, arkCommand, []string{"run-plugin", "cloudprovider", provider}, PluginKindObjectStore, PluginKindBlockStore)
}
m.pluginRegistry.register("pv", arkCommand, []string{"run-plugin", string(PluginKindBackupItemAction), "pv"}, PluginKindBackupItemAction)
m.pluginRegistry.register("backup-pod", arkCommand, []string{"run-plugin", string(PluginKindBackupItemAction), "pod"}, PluginKindBackupItemAction)
m.pluginRegistry.register("serviceaccount", arkCommand, []string{"run-plugin", string(PluginKindBackupItemAction), "serviceaccount"}, PluginKindBackupItemAction)
m.pluginRegistry.register("job", arkCommand, []string{"run-plugin", string(PluginKindRestoreItemAction), "job"}, PluginKindRestoreItemAction)
m.pluginRegistry.register("restore-pod", arkCommand, []string{"run-plugin", string(PluginKindRestoreItemAction), "pod"}, PluginKindRestoreItemAction)
m.pluginRegistry.register("svc", arkCommand, []string{"run-plugin", string(PluginKindRestoreItemAction), "svc"}, PluginKindRestoreItemAction)
m.pluginRegistry.register("restic", arkCommand, []string{"run-plugin", string(PluginKindRestoreItemAction), "restic"}, PluginKindRestoreItemAction)
// second, register external plugins (these will override internal plugins, if applicable)
if _, err := os.Stat(m.pluginDir); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
files, err := ioutil.ReadDir(m.pluginDir)
if err != nil {
return err
}
for _, file := range files {
name, kind, err := parse(file.Name())
if err != nil {
continue
}
if kind == PluginKindCloudProvider {
m.pluginRegistry.register(name, filepath.Join(m.pluginDir, file.Name()), nil, PluginKindObjectStore, PluginKindBlockStore)
} else {
m.pluginRegistry.register(name, filepath.Join(m.pluginDir, file.Name()), nil, kind)
}
}
return nil
}
func parse(filename string) (string, PluginKind, error) {
for _, kind := range AllPluginKinds {
if prefix := fmt.Sprintf("ark-%s-", kind); strings.Index(filename, prefix) == 0 {
return strings.Replace(filename, prefix, "", -1), kind, nil
}
}
return "", "", errors.New("invalid file name")
}
// GetObjectStore returns the plugin implementation of the cloudprovider.ObjectStore
// interface with the specified name.
func (m *manager) GetObjectStore(name string) (cloudprovider.ObjectStore, error) {
pluginObj, err := m.getCloudProviderPlugin(name, PluginKindObjectStore)
if err != nil {
return nil, err
}
objStore, ok := pluginObj.(cloudprovider.ObjectStore)
if !ok {
return nil, errors.New("could not convert gRPC client to cloudprovider.ObjectStore")
}
return objStore, nil
}
// GetBlockStore returns the plugin implementation of the cloudprovider.BlockStore
// interface with the specified name.
func (m *manager) GetBlockStore(name string) (cloudprovider.BlockStore, error) {
pluginObj, err := m.getCloudProviderPlugin(name, PluginKindBlockStore)
if err != nil {
return nil, err
}
blockStore, ok := pluginObj.(cloudprovider.BlockStore)
if !ok {
return nil, errors.New("could not convert gRPC client to cloudprovider.BlockStore")
}
return blockStore, nil
}
func (m *manager) getCloudProviderPlugin(name string, kind PluginKind) (interface{}, error) {
client, err := m.clientStore.get(kind, name, "")
if err != nil {
pluginInfo, err := m.pluginRegistry.get(kind, name)
if err != nil {
return nil, err
}
// build a plugin client that can dispense all of the PluginKinds it's registered for
clientBuilder := newClientBuilder(baseConfig()).
withCommand(pluginInfo.commandName, pluginInfo.commandArgs...).
withLogger(&logrusAdapter{impl: m.logger, level: m.logLevel})
for _, kind := range pluginInfo.kinds {
clientBuilder.withPlugin(kind, pluginForKind(kind))
}
client = clientBuilder.client()
// register the plugin client for the appropriate kinds
for _, kind := range pluginInfo.kinds {
m.clientStore.add(client, kind, name, "")
}
}
pluginObj, err := getPluginInstance(client, kind)
if err != nil {
return nil, err
}
return pluginObj, nil
}
// GetBackupActions returns all backup.BackupAction plugins.
// These plugin instances should ONLY be used for a single backup
// (mainly because each one outputs to a per-backup log),
// and should be terminated upon completion of the backup with
// CloseBackupActions().
func (m *manager) GetBackupItemActions(backupName string) ([]backup.ItemAction, error) {
clients, err := m.clientStore.list(PluginKindBackupItemAction, backupName)
if err != nil {
pluginInfo, err := m.pluginRegistry.list(PluginKindBackupItemAction)
if err != nil {
return nil, err
}
// create clients for each
for _, plugin := range pluginInfo {
logger := &logrusAdapter{impl: m.logger, level: m.logLevel}
client := newClientBuilder(baseConfig()).
withCommand(plugin.commandName, plugin.commandArgs...).
withPlugin(PluginKindBackupItemAction, &BackupItemActionPlugin{log: logger}).
withLogger(logger).
client()
m.clientStore.add(client, PluginKindBackupItemAction, plugin.name, backupName)
clients = append(clients, client)
}
}
var backupActions []backup.ItemAction
for _, client := range clients {
plugin, err := getPluginInstance(client, PluginKindBackupItemAction)
if err != nil {
m.CloseBackupItemActions(backupName)
return nil, err
}
backupAction, ok := plugin.(backup.ItemAction)
if !ok {
m.CloseBackupItemActions(backupName)
return nil, errors.New("could not convert gRPC client to backup.ItemAction")
}
backupActions = append(backupActions, backupAction)
}
return backupActions, nil
}
// CloseBackupItemActions terminates the plugin sub-processes that
// are hosting BackupItemAction plugins for the given backup name.
func (m *manager) CloseBackupItemActions(backupName string) error {
return closeAll(m.clientStore, PluginKindBackupItemAction, backupName)
}
func (m *manager) GetRestoreItemActions(restoreName string) ([]restore.ItemAction, error) {
clients, err := m.clientStore.list(PluginKindRestoreItemAction, restoreName)
if err != nil {
pluginInfo, err := m.pluginRegistry.list(PluginKindRestoreItemAction)
if err != nil {
return nil, err
}
// create clients for each
for _, plugin := range pluginInfo {
logger := &logrusAdapter{impl: m.logger, level: m.logLevel}
client := newClientBuilder(baseConfig()).
withCommand(plugin.commandName, plugin.commandArgs...).
withPlugin(PluginKindRestoreItemAction, &RestoreItemActionPlugin{log: logger}).
withLogger(logger).
client()
m.clientStore.add(client, PluginKindRestoreItemAction, plugin.name, restoreName)
clients = append(clients, client)
}
}
var itemActions []restore.ItemAction
for _, client := range clients {
plugin, err := getPluginInstance(client, PluginKindRestoreItemAction)
if err != nil {
m.CloseRestoreItemActions(restoreName)
return nil, err
}
itemAction, ok := plugin.(restore.ItemAction)
if !ok {
m.CloseRestoreItemActions(restoreName)
return nil, errors.New("could not convert gRPC client to restore.ItemAction")
}
itemActions = append(itemActions, itemAction)
}
return itemActions, nil
}
// CloseRestoreItemActions terminates the plugin sub-processes that
// are hosting RestoreItemAction plugins for the given restore name.
func (m *manager) CloseRestoreItemActions(restoreName string) error {
return closeAll(m.clientStore, PluginKindRestoreItemAction, restoreName)
}
func closeAll(store *clientStore, kind PluginKind, scope string) error {
clients, err := store.list(kind, scope)
if err != nil {
return err
}
for _, client := range clients {
client.Kill()
}
store.deleteAll(kind, scope)
return nil
}
func (m *manager) CleanupClients() {
plugin.CleanupClients()
m.lock.Lock()
for _, restartableProcess := range m.restartableProcesses {
restartableProcess.stop()
}
m.lock.Unlock()
}
// getRestartableProcess returns a restartableProcess for a plugin identified by kind and name, creating a
// restartableProcess if it is the first time it has been requested.
func (m *manager) getRestartableProcess(kind PluginKind, name string) (RestartableProcess, error) {
m.lock.Lock()
defer m.lock.Unlock()
logger := m.logger.WithFields(logrus.Fields{
"kind": PluginKindObjectStore.String(),
"name": name,
})
logger.Debug("looking for plugin in registry")
info, err := m.registry.Get(kind, name)
if err != nil {
return nil, err
}
logger = logger.WithField("command", info.Command)
restartableProcess, found := m.restartableProcesses[info.Command]
if found {
logger.Debug("found preexisting restartable plugin process")
return restartableProcess, nil
}
logger.Debug("creating new restartable plugin process")
restartableProcess, err = m.restartableProcessFactory.newRestartableProcess(info.Command, m.logger, m.logLevel)
if err != nil {
return nil, err
}
m.restartableProcesses[info.Command] = restartableProcess
return restartableProcess, nil
}
// GetObjectStore returns a restartableObjectStore for name.
func (m *manager) GetObjectStore(name string) (cloudprovider.ObjectStore, error) {
restartableProcess, err := m.getRestartableProcess(PluginKindObjectStore, name)
if err != nil {
return nil, err
}
r := newRestartableObjectStore(name, restartableProcess)
return r, nil
}
// GetBlockStore returns a restartableBlockStore for name.
func (m *manager) GetBlockStore(name string) (cloudprovider.BlockStore, error) {
restartableProcess, err := m.getRestartableProcess(PluginKindBlockStore, name)
if err != nil {
return nil, err
}
r := newRestartableBlockStore(name, restartableProcess)
return r, nil
}
// GetBackupItemActions returns all backup item actions as restartableBackupItemActions.
func (m *manager) GetBackupItemActions() ([]backup.ItemAction, error) {
list := m.registry.List(PluginKindBackupItemAction)
actions := make([]backup.ItemAction, 0, len(list))
for i := range list {
id := list[i]
r, err := m.GetBackupItemAction(id.Name)
if err != nil {
return nil, err
}
actions = append(actions, r)
}
return actions, nil
}
// GetBackupItemAction returns a restartableBackupItemAction for name.
func (m *manager) GetBackupItemAction(name string) (backup.ItemAction, error) {
restartableProcess, err := m.getRestartableProcess(PluginKindBackupItemAction, name)
if err != nil {
return nil, err
}
r := newRestartableBackupItemAction(name, restartableProcess)
return r, nil
}
// GetRestoreItemActions returns all restore item actions as restartableRestoreItemActions.
func (m *manager) GetRestoreItemActions() ([]restore.ItemAction, error) {
list := m.registry.List(PluginKindRestoreItemAction)
actions := make([]restore.ItemAction, 0, len(list))
for i := range list {
id := list[i]
r, err := m.GetRestoreItemAction(id.Name)
if err != nil {
return nil, err
}
actions = append(actions, r)
}
return actions, nil
}
// GetRestoreItemAction returns a restartableRestoreItemAction for name.
func (m *manager) GetRestoreItemAction(name string) (restore.ItemAction, error) {
restartableProcess, err := m.getRestartableProcess(PluginKindRestoreItemAction, name)
if err != nil {
return nil, err
}
r := newRestartableRestoreItemAction(name, restartableProcess)
return r, nil
}
+475
View File
@@ -0,0 +1,475 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"fmt"
"testing"
"github.com/heptio/ark/pkg/util/test"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type mockRegistry struct {
mock.Mock
}
func (r *mockRegistry) DiscoverPlugins() error {
args := r.Called()
return args.Error(0)
}
func (r *mockRegistry) List(kind PluginKind) []PluginIdentifier {
args := r.Called(kind)
return args.Get(0).([]PluginIdentifier)
}
func (r *mockRegistry) Get(kind PluginKind, name string) (PluginIdentifier, error) {
args := r.Called(kind, name)
var id PluginIdentifier
if args.Get(0) != nil {
id = args.Get(0).(PluginIdentifier)
}
return id, args.Error(1)
}
func TestNewManager(t *testing.T) {
logger := test.NewLogger()
logLevel := logrus.InfoLevel
registry := &mockRegistry{}
defer registry.AssertExpectations(t)
m := NewManager(logger, logLevel, registry).(*manager)
assert.Equal(t, logger, m.logger)
assert.Equal(t, logLevel, m.logLevel)
assert.Equal(t, registry, m.registry)
assert.NotNil(t, m.restartableProcesses)
assert.Empty(t, m.restartableProcesses)
}
type mockRestartableProcessFactory struct {
mock.Mock
}
func (f *mockRestartableProcessFactory) newRestartableProcess(command string, logger logrus.FieldLogger, logLevel logrus.Level) (RestartableProcess, error) {
args := f.Called(command, logger, logLevel)
var rp RestartableProcess
if args.Get(0) != nil {
rp = args.Get(0).(RestartableProcess)
}
return rp, args.Error(1)
}
type mockRestartableProcess struct {
mock.Mock
}
func (rp *mockRestartableProcess) addReinitializer(key kindAndName, r reinitializer) {
rp.Called(key, r)
}
func (rp *mockRestartableProcess) reset() error {
args := rp.Called()
return args.Error(0)
}
func (rp *mockRestartableProcess) resetIfNeeded() error {
args := rp.Called()
return args.Error(0)
}
func (rp *mockRestartableProcess) getByKindAndName(key kindAndName) (interface{}, error) {
args := rp.Called(key)
return args.Get(0), args.Error(1)
}
func (rp *mockRestartableProcess) stop() {
rp.Called()
}
func TestGetRestartableProcess(t *testing.T) {
logger := test.NewLogger()
logLevel := logrus.InfoLevel
registry := &mockRegistry{}
defer registry.AssertExpectations(t)
m := NewManager(logger, logLevel, registry).(*manager)
factory := &mockRestartableProcessFactory{}
defer factory.AssertExpectations(t)
m.restartableProcessFactory = factory
// Test 1: registry error
pluginKind := PluginKindBackupItemAction
pluginName := "pod"
registry.On("Get", pluginKind, pluginName).Return(nil, errors.Errorf("registry")).Once()
rp, err := m.getRestartableProcess(pluginKind, pluginName)
assert.Nil(t, rp)
assert.EqualError(t, err, "registry")
// Test 2: registry ok, factory error
podID := PluginIdentifier{
Command: "/command",
Kind: pluginKind,
Name: pluginName,
}
registry.On("Get", pluginKind, pluginName).Return(podID, nil)
factory.On("newRestartableProcess", podID.Command, logger, logLevel).Return(nil, errors.Errorf("factory")).Once()
rp, err = m.getRestartableProcess(pluginKind, pluginName)
assert.Nil(t, rp)
assert.EqualError(t, err, "factory")
// Test 3: registry ok, factory ok
restartableProcess := &mockRestartableProcess{}
defer restartableProcess.AssertExpectations(t)
factory.On("newRestartableProcess", podID.Command, logger, logLevel).Return(restartableProcess, nil).Once()
rp, err = m.getRestartableProcess(pluginKind, pluginName)
require.NoError(t, err)
assert.Equal(t, restartableProcess, rp)
// Test 4: retrieve from cache
rp, err = m.getRestartableProcess(pluginKind, pluginName)
require.NoError(t, err)
assert.Equal(t, restartableProcess, rp)
}
func TestCleanupClients(t *testing.T) {
logger := test.NewLogger()
logLevel := logrus.InfoLevel
registry := &mockRegistry{}
defer registry.AssertExpectations(t)
m := NewManager(logger, logLevel, registry).(*manager)
for i := 0; i < 5; i++ {
rp := &mockRestartableProcess{}
defer rp.AssertExpectations(t)
rp.On("stop")
m.restartableProcesses[fmt.Sprintf("rp%d", i)] = rp
}
m.CleanupClients()
}
func TestGetObjectStore(t *testing.T) {
getPluginTest(t,
PluginKindObjectStore,
"aws",
func(m Manager, name string) (interface{}, error) {
return m.GetObjectStore(name)
},
func(name string, sharedPluginProcess RestartableProcess) interface{} {
return &restartableObjectStore{
key: kindAndName{kind: PluginKindObjectStore, name: name},
sharedPluginProcess: sharedPluginProcess,
}
},
true,
)
}
func TestGetBlockStore(t *testing.T) {
getPluginTest(t,
PluginKindBlockStore,
"aws",
func(m Manager, name string) (interface{}, error) {
return m.GetBlockStore(name)
},
func(name string, sharedPluginProcess RestartableProcess) interface{} {
return &restartableBlockStore{
key: kindAndName{kind: PluginKindBlockStore, name: name},
sharedPluginProcess: sharedPluginProcess,
}
},
true,
)
}
func TestGetBackupItemAction(t *testing.T) {
getPluginTest(t,
PluginKindBackupItemAction,
"pod",
func(m Manager, name string) (interface{}, error) {
return m.GetBackupItemAction(name)
},
func(name string, sharedPluginProcess RestartableProcess) interface{} {
return &restartableBackupItemAction{
key: kindAndName{kind: PluginKindBackupItemAction, name: name},
sharedPluginProcess: sharedPluginProcess,
}
},
false,
)
}
func TestGetRestoreItemAction(t *testing.T) {
getPluginTest(t,
PluginKindRestoreItemAction,
"pod",
func(m Manager, name string) (interface{}, error) {
return m.GetRestoreItemAction(name)
},
func(name string, sharedPluginProcess RestartableProcess) interface{} {
return &restartableRestoreItemAction{
key: kindAndName{kind: PluginKindRestoreItemAction, name: name},
sharedPluginProcess: sharedPluginProcess,
}
},
false,
)
}
func getPluginTest(
t *testing.T,
kind PluginKind,
name string,
getPluginFunc func(m Manager, name string) (interface{}, error),
expectedResultFunc func(name string, sharedPluginProcess RestartableProcess) interface{},
reinitializable bool,
) {
logger := test.NewLogger()
logLevel := logrus.InfoLevel
registry := &mockRegistry{}
defer registry.AssertExpectations(t)
m := NewManager(logger, logLevel, registry).(*manager)
factory := &mockRestartableProcessFactory{}
defer factory.AssertExpectations(t)
m.restartableProcessFactory = factory
pluginKind := kind
pluginName := name
pluginID := PluginIdentifier{
Command: "/command",
Kind: pluginKind,
Name: pluginName,
}
registry.On("Get", pluginKind, pluginName).Return(pluginID, nil)
restartableProcess := &mockRestartableProcess{}
defer restartableProcess.AssertExpectations(t)
// Test 1: error getting restartable process
factory.On("newRestartableProcess", pluginID.Command, logger, logLevel).Return(nil, errors.Errorf("newRestartableProcess")).Once()
actual, err := getPluginFunc(m, pluginName)
assert.Nil(t, actual)
assert.EqualError(t, err, "newRestartableProcess")
// Test 2: happy path
factory.On("newRestartableProcess", pluginID.Command, logger, logLevel).Return(restartableProcess, nil).Once()
expected := expectedResultFunc(name, restartableProcess)
if reinitializable {
key := kindAndName{kind: pluginID.Kind, name: pluginID.Name}
restartableProcess.On("addReinitializer", key, expected)
}
actual, err = getPluginFunc(m, pluginName)
require.NoError(t, err)
assert.Equal(t, expected, actual)
}
func TestGetBackupItemActions(t *testing.T) {
tests := []struct {
name string
names []string
newRestartableProcessError error
expectedError string
}{
{
name: "No items",
names: []string{},
},
{
name: "Error getting restartable process",
names: []string{"a", "b", "c"},
newRestartableProcessError: errors.Errorf("newRestartableProcess"),
expectedError: "newRestartableProcess",
},
{
name: "Happy path",
names: []string{"a", "b", "c"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
logger := test.NewLogger()
logLevel := logrus.InfoLevel
registry := &mockRegistry{}
defer registry.AssertExpectations(t)
m := NewManager(logger, logLevel, registry).(*manager)
factory := &mockRestartableProcessFactory{}
defer factory.AssertExpectations(t)
m.restartableProcessFactory = factory
pluginKind := PluginKindBackupItemAction
var pluginIDs []PluginIdentifier
for i := range tc.names {
pluginID := PluginIdentifier{
Command: "/command",
Kind: pluginKind,
Name: tc.names[i],
}
pluginIDs = append(pluginIDs, pluginID)
}
registry.On("List", pluginKind).Return(pluginIDs)
var expectedActions []interface{}
for i := range pluginIDs {
pluginID := pluginIDs[i]
pluginName := pluginID.Name
registry.On("Get", pluginKind, pluginName).Return(pluginID, nil)
restartableProcess := &mockRestartableProcess{}
defer restartableProcess.AssertExpectations(t)
expected := &restartableBackupItemAction{
key: kindAndName{kind: pluginKind, name: pluginName},
sharedPluginProcess: restartableProcess,
}
if tc.newRestartableProcessError != nil {
// Test 1: error getting restartable process
factory.On("newRestartableProcess", pluginID.Command, logger, logLevel).Return(nil, errors.Errorf("newRestartableProcess")).Once()
break
}
// Test 2: happy path
if i == 0 {
factory.On("newRestartableProcess", pluginID.Command, logger, logLevel).Return(restartableProcess, nil).Once()
}
expectedActions = append(expectedActions, expected)
}
backupItemActions, err := m.GetBackupItemActions()
if tc.newRestartableProcessError != nil {
assert.Nil(t, backupItemActions)
assert.EqualError(t, err, "newRestartableProcess")
} else {
require.NoError(t, err)
var actual []interface{}
for i := range backupItemActions {
actual = append(actual, backupItemActions[i])
}
assert.Equal(t, expectedActions, actual)
}
})
}
}
func TestGetRestoreItemActions(t *testing.T) {
tests := []struct {
name string
names []string
newRestartableProcessError error
expectedError string
}{
{
name: "No items",
names: []string{},
},
{
name: "Error getting restartable process",
names: []string{"a", "b", "c"},
newRestartableProcessError: errors.Errorf("newRestartableProcess"),
expectedError: "newRestartableProcess",
},
{
name: "Happy path",
names: []string{"a", "b", "c"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
logger := test.NewLogger()
logLevel := logrus.InfoLevel
registry := &mockRegistry{}
defer registry.AssertExpectations(t)
m := NewManager(logger, logLevel, registry).(*manager)
factory := &mockRestartableProcessFactory{}
defer factory.AssertExpectations(t)
m.restartableProcessFactory = factory
pluginKind := PluginKindRestoreItemAction
var pluginIDs []PluginIdentifier
for i := range tc.names {
pluginID := PluginIdentifier{
Command: "/command",
Kind: pluginKind,
Name: tc.names[i],
}
pluginIDs = append(pluginIDs, pluginID)
}
registry.On("List", pluginKind).Return(pluginIDs)
var expectedActions []interface{}
for i := range pluginIDs {
pluginID := pluginIDs[i]
pluginName := pluginID.Name
registry.On("Get", pluginKind, pluginName).Return(pluginID, nil)
restartableProcess := &mockRestartableProcess{}
defer restartableProcess.AssertExpectations(t)
expected := &restartableRestoreItemAction{
key: kindAndName{kind: pluginKind, name: pluginName},
sharedPluginProcess: restartableProcess,
}
if tc.newRestartableProcessError != nil {
// Test 1: error getting restartable process
factory.On("newRestartableProcess", pluginID.Command, logger, logLevel).Return(nil, errors.Errorf("newRestartableProcess")).Once()
break
}
// Test 2: happy path
if i == 0 {
factory.On("newRestartableProcess", pluginID.Command, logger, logLevel).Return(restartableProcess, nil).Once()
}
expectedActions = append(expectedActions, expected)
}
restoreItemActions, err := m.GetRestoreItemActions()
if tc.newRestartableProcessError != nil {
assert.Nil(t, restoreItemActions)
assert.EqualError(t, err, "newRestartableProcess")
} else {
require.NoError(t, err)
var actual []interface{}
for i := range restoreItemActions {
actual = append(actual, restoreItemActions[i])
}
assert.Equal(t, expectedActions, actual)
}
})
}
}
+171
View File
@@ -0,0 +1,171 @@
/*
Copyright 2018 the Heptio Ark 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.
*/
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import backup "github.com/heptio/ark/pkg/backup"
import cloudprovider "github.com/heptio/ark/pkg/cloudprovider"
import mock "github.com/stretchr/testify/mock"
import restore "github.com/heptio/ark/pkg/restore"
// Manager is an autogenerated mock type for the Manager type
type Manager struct {
mock.Mock
}
// CleanupClients provides a mock function with given fields:
func (_m *Manager) CleanupClients() {
_m.Called()
}
// GetBackupItemAction provides a mock function with given fields: name
func (_m *Manager) GetBackupItemAction(name string) (backup.ItemAction, error) {
ret := _m.Called(name)
var r0 backup.ItemAction
if rf, ok := ret.Get(0).(func(string) backup.ItemAction); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(backup.ItemAction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetBackupItemActions provides a mock function with given fields:
func (_m *Manager) GetBackupItemActions() ([]backup.ItemAction, error) {
ret := _m.Called()
var r0 []backup.ItemAction
if rf, ok := ret.Get(0).(func() []backup.ItemAction); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]backup.ItemAction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetBlockStore provides a mock function with given fields: name
func (_m *Manager) GetBlockStore(name string) (cloudprovider.BlockStore, error) {
ret := _m.Called(name)
var r0 cloudprovider.BlockStore
if rf, ok := ret.Get(0).(func(string) cloudprovider.BlockStore); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(cloudprovider.BlockStore)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetObjectStore provides a mock function with given fields: name
func (_m *Manager) GetObjectStore(name string) (cloudprovider.ObjectStore, error) {
ret := _m.Called(name)
var r0 cloudprovider.ObjectStore
if rf, ok := ret.Get(0).(func(string) cloudprovider.ObjectStore); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(cloudprovider.ObjectStore)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRestoreItemAction provides a mock function with given fields: name
func (_m *Manager) GetRestoreItemAction(name string) (restore.ItemAction, error) {
ret := _m.Called(name)
var r0 restore.ItemAction
if rf, ok := ret.Get(0).(func(string) restore.ItemAction); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(restore.ItemAction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRestoreItemActions provides a mock function with given fields:
func (_m *Manager) GetRestoreItemActions() ([]restore.ItemAction, error) {
ret := _m.Called()
var r0 []restore.ItemAction
if rf, ok := ret.Get(0).(func() []restore.ItemAction); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]restore.ItemAction)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
+49
View File
@@ -0,0 +1,49 @@
/*
Copyright 2018 the Heptio Ark 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.
*/
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import logrus "github.com/sirupsen/logrus"
import mock "github.com/stretchr/testify/mock"
import plugin "github.com/heptio/ark/pkg/plugin"
// ProcessFactory is an autogenerated mock type for the ProcessFactory type
type ProcessFactory struct {
mock.Mock
}
// newProcess provides a mock function with given fields: command, logger, logLevel
func (_m *ProcessFactory) newProcess(command string, logger logrus.FieldLogger, logLevel logrus.Level) (plugin.Process, error) {
ret := _m.Called(command, logger, logLevel)
var r0 plugin.Process
if rf, ok := ret.Get(0).(func(string, logrus.FieldLogger, logrus.Level) plugin.Process); ok {
r0 = rf(command, logger, logLevel)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(plugin.Process)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, logrus.FieldLogger, logrus.Level) error); ok {
r1 = rf(command, logger, logLevel)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
+90 -28
View File
@@ -21,6 +21,7 @@ import (
"time"
"github.com/hashicorp/go-plugin"
"github.com/pkg/errors"
"golang.org/x/net/context"
"google.golang.org/grpc"
@@ -35,43 +36,45 @@ const byteChunkSize = 16384
// interface.
type ObjectStorePlugin struct {
plugin.NetRPCUnsupportedPlugin
impl cloudprovider.ObjectStore
*pluginBase
}
// NewObjectStorePlugin construct an ObjectStorePlugin.
func NewObjectStorePlugin(objectStore cloudprovider.ObjectStore) *ObjectStorePlugin {
func NewObjectStorePlugin(options ...pluginOption) *ObjectStorePlugin {
return &ObjectStorePlugin{
impl: objectStore,
pluginBase: newPluginBase(options...),
}
}
func (p *ObjectStorePlugin) Kind() PluginKind {
return PluginKindObjectStore
}
// GRPCServer registers an ObjectStore gRPC server.
func (p *ObjectStorePlugin) GRPCServer(s *grpc.Server) error {
proto.RegisterObjectStoreServer(s, &ObjectStoreGRPCServer{impl: p.impl})
return nil
}
//////////////////////////////////////////////////////////////////////////////
// client code
//////////////////////////////////////////////////////////////////////////////
// GRPCClient returns an ObjectStore gRPC client.
func (p *ObjectStorePlugin) GRPCClient(c *grpc.ClientConn) (interface{}, error) {
return &ObjectStoreGRPCClient{grpcClient: proto.NewObjectStoreClient(c)}, nil
return newClientDispenser(p.clientLogger, c, newObjectStoreGRPCClient), nil
}
// ObjectStoreGRPCClient implements the cloudprovider.ObjectStore interface and uses a
// gRPC client to make calls to the plugin server.
type ObjectStoreGRPCClient struct {
*clientBase
grpcClient proto.ObjectStoreClient
}
func newObjectStoreGRPCClient(base *clientBase, clientConn *grpc.ClientConn) interface{} {
return &ObjectStoreGRPCClient{
clientBase: base,
grpcClient: proto.NewObjectStoreClient(clientConn),
}
}
// Init prepares the ObjectStore for usage using the provided map of
// configuration key-value pairs. It returns an error if the ObjectStore
// cannot be initialized from the provided config.
func (c *ObjectStoreGRPCClient) Init(config map[string]string) error {
_, err := c.grpcClient.Init(context.Background(), &proto.InitRequest{Config: config})
_, err := c.grpcClient.Init(context.Background(), &proto.InitRequest{Plugin: c.plugin, Config: config})
return err
}
@@ -98,7 +101,7 @@ func (c *ObjectStoreGRPCClient) PutObject(bucket, key string, body io.Reader) er
return err
}
if err := stream.Send(&proto.PutObjectRequest{Bucket: bucket, Key: key, Body: chunk[0:n]}); err != nil {
if err := stream.Send(&proto.PutObjectRequest{Plugin: c.plugin, Bucket: bucket, Key: key, Body: chunk[0:n]}); err != nil {
return err
}
}
@@ -107,7 +110,7 @@ func (c *ObjectStoreGRPCClient) PutObject(bucket, key string, body io.Reader) er
// GetObject retrieves the object with the given key from the specified
// bucket in object storage.
func (c *ObjectStoreGRPCClient) GetObject(bucket, key string) (io.ReadCloser, error) {
stream, err := c.grpcClient.GetObject(context.Background(), &proto.GetObjectRequest{Bucket: bucket, Key: key})
stream, err := c.grpcClient.GetObject(context.Background(), &proto.GetObjectRequest{Plugin: c.plugin, Bucket: bucket, Key: key})
if err != nil {
return nil, err
}
@@ -132,7 +135,7 @@ func (c *ObjectStoreGRPCClient) GetObject(bucket, key string) (io.ReadCloser, er
// before the provided delimiter (this is often used to simulate a directory
// hierarchy in object storage).
func (c *ObjectStoreGRPCClient) ListCommonPrefixes(bucket, delimiter string) ([]string, error) {
res, err := c.grpcClient.ListCommonPrefixes(context.Background(), &proto.ListCommonPrefixesRequest{Bucket: bucket, Delimiter: delimiter})
res, err := c.grpcClient.ListCommonPrefixes(context.Background(), &proto.ListCommonPrefixesRequest{Plugin: c.plugin, Bucket: bucket, Delimiter: delimiter})
if err != nil {
return nil, err
}
@@ -142,7 +145,7 @@ func (c *ObjectStoreGRPCClient) ListCommonPrefixes(bucket, delimiter string) ([]
// ListObjects gets a list of all objects in bucket that have the same prefix.
func (c *ObjectStoreGRPCClient) ListObjects(bucket, prefix string) ([]string, error) {
res, err := c.grpcClient.ListObjects(context.Background(), &proto.ListObjectsRequest{Bucket: bucket, Prefix: prefix})
res, err := c.grpcClient.ListObjects(context.Background(), &proto.ListObjectsRequest{Plugin: c.plugin, Bucket: bucket, Prefix: prefix})
if err != nil {
return nil, err
}
@@ -153,7 +156,7 @@ func (c *ObjectStoreGRPCClient) ListObjects(bucket, prefix string) ([]string, er
// DeleteObject removes object with the specified key from the given
// bucket.
func (c *ObjectStoreGRPCClient) DeleteObject(bucket, key string) error {
_, err := c.grpcClient.DeleteObject(context.Background(), &proto.DeleteObjectRequest{Bucket: bucket, Key: key})
_, err := c.grpcClient.DeleteObject(context.Background(), &proto.DeleteObjectRequest{Plugin: c.plugin, Bucket: bucket, Key: key})
return err
}
@@ -161,6 +164,7 @@ func (c *ObjectStoreGRPCClient) DeleteObject(bucket, key string) error {
// CreateSignedURL creates a pre-signed URL for the given bucket and key that expires after ttl.
func (c *ObjectStoreGRPCClient) CreateSignedURL(bucket, key string, ttl time.Duration) (string, error) {
res, err := c.grpcClient.CreateSignedURL(context.Background(), &proto.CreateSignedURLRequest{
Plugin: c.plugin,
Bucket: bucket,
Key: key,
Ttl: int64(ttl),
@@ -172,17 +176,46 @@ func (c *ObjectStoreGRPCClient) CreateSignedURL(bucket, key string, ttl time.Dur
return res.Url, nil
}
//////////////////////////////////////////////////////////////////////////////
// server code
//////////////////////////////////////////////////////////////////////////////
// GRPCServer registers an ObjectStore gRPC server.
func (p *ObjectStorePlugin) GRPCServer(s *grpc.Server) error {
proto.RegisterObjectStoreServer(s, &ObjectStoreGRPCServer{mux: p.serverMux})
return nil
}
// ObjectStoreGRPCServer implements the proto-generated ObjectStoreServer interface, and accepts
// gRPC calls and forwards them to an implementation of the pluggable interface.
type ObjectStoreGRPCServer struct {
impl cloudprovider.ObjectStore
mux *serverMux
}
func (s *ObjectStoreGRPCServer) getImpl(name string) (cloudprovider.ObjectStore, error) {
impl, err := s.mux.getHandler(name)
if err != nil {
return nil, err
}
itemAction, ok := impl.(cloudprovider.ObjectStore)
if !ok {
return nil, errors.Errorf("%T is not an object store", impl)
}
return itemAction, nil
}
// Init prepares the ObjectStore for usage using the provided map of
// configuration key-value pairs. It returns an error if the ObjectStore
// cannot be initialized from the provided config.
func (s *ObjectStoreGRPCServer) Init(ctx context.Context, req *proto.InitRequest) (*proto.Empty, error) {
if err := s.impl.Init(req.Config); err != nil {
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
if err := impl.Init(req.Config); err != nil {
return nil, err
}
@@ -199,6 +232,11 @@ func (s *ObjectStoreGRPCServer) PutObject(stream proto.ObjectStore_PutObjectServ
return err
}
impl, err := s.getImpl(firstChunk.Plugin)
if err != nil {
return err
}
bucket := firstChunk.Bucket
key := firstChunk.Key
@@ -220,7 +258,7 @@ func (s *ObjectStoreGRPCServer) PutObject(stream proto.ObjectStore_PutObjectServ
return nil
}
if err := s.impl.PutObject(bucket, key, &StreamReadCloser{receive: receive, close: close}); err != nil {
if err := impl.PutObject(bucket, key, &StreamReadCloser{receive: receive, close: close}); err != nil {
return err
}
@@ -230,7 +268,12 @@ func (s *ObjectStoreGRPCServer) PutObject(stream proto.ObjectStore_PutObjectServ
// GetObject retrieves the object with the given key from the specified
// bucket in object storage.
func (s *ObjectStoreGRPCServer) GetObject(req *proto.GetObjectRequest, stream proto.ObjectStore_GetObjectServer) error {
rdr, err := s.impl.GetObject(req.Bucket, req.Key)
impl, err := s.getImpl(req.Plugin)
if err != nil {
return err
}
rdr, err := impl.GetObject(req.Bucket, req.Key)
if err != nil {
return err
}
@@ -255,7 +298,12 @@ func (s *ObjectStoreGRPCServer) GetObject(req *proto.GetObjectRequest, stream pr
// before the provided delimiter (this is often used to simulate a directory
// hierarchy in object storage).
func (s *ObjectStoreGRPCServer) ListCommonPrefixes(ctx context.Context, req *proto.ListCommonPrefixesRequest) (*proto.ListCommonPrefixesResponse, error) {
prefixes, err := s.impl.ListCommonPrefixes(req.Bucket, req.Delimiter)
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
prefixes, err := impl.ListCommonPrefixes(req.Bucket, req.Delimiter)
if err != nil {
return nil, err
}
@@ -265,7 +313,12 @@ func (s *ObjectStoreGRPCServer) ListCommonPrefixes(ctx context.Context, req *pro
// ListObjects gets a list of all objects in bucket that have the same prefix.
func (s *ObjectStoreGRPCServer) ListObjects(ctx context.Context, req *proto.ListObjectsRequest) (*proto.ListObjectsResponse, error) {
keys, err := s.impl.ListObjects(req.Bucket, req.Prefix)
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
keys, err := impl.ListObjects(req.Bucket, req.Prefix)
if err != nil {
return nil, err
}
@@ -276,17 +329,26 @@ func (s *ObjectStoreGRPCServer) ListObjects(ctx context.Context, req *proto.List
// DeleteObject removes object with the specified key from the given
// bucket.
func (s *ObjectStoreGRPCServer) DeleteObject(ctx context.Context, req *proto.DeleteObjectRequest) (*proto.Empty, error) {
err := s.impl.DeleteObject(req.Bucket, req.Key)
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
if err := impl.DeleteObject(req.Bucket, req.Key); err != nil {
return nil, err
}
return &proto.Empty{}, nil
}
// CreateSignedURL creates a pre-signed URL for the given bucket and key that expires after ttl.
func (s *ObjectStoreGRPCServer) CreateSignedURL(ctx context.Context, req *proto.CreateSignedURLRequest) (*proto.CreateSignedURLResponse, error) {
url, err := s.impl.CreateSignedURL(req.Bucket, req.Key, time.Duration(req.Ttl))
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
url, err := impl.CreateSignedURL(req.Bucket, req.Key, time.Duration(req.Ttl))
if err != nil {
return nil, err
}
+46
View File
@@ -0,0 +1,46 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import "github.com/sirupsen/logrus"
type pluginBase struct {
clientLogger logrus.FieldLogger
*serverMux
}
func newPluginBase(options ...pluginOption) *pluginBase {
base := new(pluginBase)
for _, option := range options {
option(base)
}
return base
}
type pluginOption func(base *pluginBase)
func clientLogger(logger logrus.FieldLogger) pluginOption {
return func(base *pluginBase) {
base.clientLogger = logger
}
}
func serverLogger(logger logrus.FieldLogger) pluginOption {
return func(base *pluginBase) {
base.serverMux = newServerMux(logger)
}
}
+39
View File
@@ -0,0 +1,39 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"testing"
"github.com/heptio/ark/pkg/util/test"
"github.com/stretchr/testify/assert"
)
func TestClientLogger(t *testing.T) {
base := &pluginBase{}
logger := test.NewLogger()
f := clientLogger(logger)
f(base)
assert.Equal(t, logger, base.clientLogger)
}
func TestServerLogger(t *testing.T) {
base := &pluginBase{}
logger := test.NewLogger()
f := serverLogger(logger)
f(base)
assert.Equal(t, newServerMux(logger), base.serverMux)
}
+55
View File
@@ -0,0 +1,55 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"k8s.io/apimachinery/pkg/util/sets"
)
// PluginKind is a type alias for a string that describes
// the kind of an Ark-supported plugin.
type PluginKind string
// String returns the string for k.
func (k PluginKind) String() string {
return string(k)
}
const (
// PluginKindObjectStore represents an object store plugin.
PluginKindObjectStore PluginKind = "ObjectStore"
// PluginKindBlockStore represents a block store plugin.
PluginKindBlockStore PluginKind = "BlockStore"
// PluginKindBackupItemAction represents a backup item action plugin.
PluginKindBackupItemAction PluginKind = "BackupItemAction"
// PluginKindRestoreItemAction represents a restore item action plugin.
PluginKindRestoreItemAction PluginKind = "RestoreItemAction"
// PluginKindPluginLister represents a plugin lister plugin.
PluginKindPluginLister PluginKind = "PluginLister"
)
// allPluginKinds contains all the valid plugin kinds that Ark supports, excluding PluginLister because that is not a
// kind that a developer would ever need to implement (it's handled by Ark and the Ark plugin library code).
var allPluginKinds = sets.NewString(
PluginKindObjectStore.String(),
PluginKindBlockStore.String(),
PluginKindBackupItemAction.String(),
PluginKindRestoreItemAction.String(),
)
+34
View File
@@ -0,0 +1,34 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/util/sets"
)
func TestAllPluginKinds(t *testing.T) {
expected := sets.NewString(
PluginKindObjectStore.String(),
PluginKindBlockStore.String(),
PluginKindBackupItemAction.String(),
PluginKindRestoreItemAction.String(),
)
assert.True(t, expected.Equal(allPluginKinds))
}
+141
View File
@@ -0,0 +1,141 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
plugin "github.com/hashicorp/go-plugin"
proto "github.com/heptio/ark/pkg/plugin/generated"
"github.com/pkg/errors"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
// PluginIdenitifer uniquely identifies a plugin by command, kind, and name.
type PluginIdentifier struct {
Command string
Kind PluginKind
Name string
}
// PluginLister lists plugins.
type PluginLister interface {
ListPlugins() ([]PluginIdentifier, error)
}
// pluginLister implements PluginLister.
type pluginLister struct {
plugins []PluginIdentifier
}
// NewPluginLister returns a new PluginLister for plugins.
func NewPluginLister(plugins ...PluginIdentifier) PluginLister {
return &pluginLister{plugins: plugins}
}
// ListPlugins returns the pluginLister's plugins.
func (pl *pluginLister) ListPlugins() ([]PluginIdentifier, error) {
return pl.plugins, nil
}
// PluginListerPlugin is a go-plugin Plugin for a PluginLister.
type PluginListerPlugin struct {
plugin.NetRPCUnsupportedPlugin
impl PluginLister
}
// NewPluginListerPlugin creates a new PluginListerPlugin with impl as the server-side implementation.
func NewPluginListerPlugin(impl PluginLister) *PluginListerPlugin {
return &PluginListerPlugin{impl: impl}
}
//////////////////////////////////////////////////////////////////////////////
// client code
//////////////////////////////////////////////////////////////////////////////
// GRPCClient returns a PluginLister gRPC client.
func (p *PluginListerPlugin) GRPCClient(c *grpc.ClientConn) (interface{}, error) {
return &PluginListerGRPCClient{grpcClient: proto.NewPluginListerClient(c)}, nil
}
// PluginListerGRPCClient implements PluginLister and uses a gRPC client to make calls to the plugin server.
type PluginListerGRPCClient struct {
grpcClient proto.PluginListerClient
}
// ListPlugins uses the gRPC client to request the list of plugins from the server. It translates the protobuf response
// to []PluginIdentifier.
func (c *PluginListerGRPCClient) ListPlugins() ([]PluginIdentifier, error) {
resp, err := c.grpcClient.ListPlugins(context.Background(), &proto.Empty{})
if err != nil {
return nil, err
}
ret := make([]PluginIdentifier, len(resp.Plugins))
for i, id := range resp.Plugins {
if !allPluginKinds.Has(id.Kind) {
return nil, errors.Errorf("invalid plugin kind: %s", id.Kind)
}
ret[i] = PluginIdentifier{
Command: id.Command,
Kind: PluginKind(id.Kind),
Name: id.Name,
}
}
return ret, nil
}
//////////////////////////////////////////////////////////////////////////////
// server code
//////////////////////////////////////////////////////////////////////////////
// GRPCServer registers a PluginLister gRPC server.
func (p *PluginListerPlugin) GRPCServer(s *grpc.Server) error {
proto.RegisterPluginListerServer(s, &PluginListerGRPCServer{impl: p.impl})
return nil
}
// PluginListerGRPCServer implements the proto-generated PluginLister gRPC service interface. It accepts gRPC calls,
// forwards them to impl, and translates the responses to protobuf.
type PluginListerGRPCServer struct {
impl PluginLister
}
// ListPlugins returns a list of registered plugins, delegating to s.impl to perform the listing.
func (s *PluginListerGRPCServer) ListPlugins(ctx context.Context, req *proto.Empty) (*proto.ListPluginsResponse, error) {
list, err := s.impl.ListPlugins()
if err != nil {
return nil, err
}
plugins := make([]*proto.PluginIdentifier, len(list))
for i, id := range list {
if !allPluginKinds.Has(id.Kind.String()) {
return nil, errors.Errorf("invalid plugin kind: %s", id.Kind)
}
plugins[i] = &proto.PluginIdentifier{
Command: id.Command,
Kind: id.Kind.String(),
Name: id.Name,
}
}
ret := &proto.ListPluginsResponse{
Plugins: plugins,
}
return ret, nil
}
+96
View File
@@ -0,0 +1,96 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
plugin "github.com/hashicorp/go-plugin"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type ProcessFactory interface {
newProcess(command string, logger logrus.FieldLogger, logLevel logrus.Level) (Process, error)
}
type processFactory struct {
}
func newProcessFactory() ProcessFactory {
return &processFactory{}
}
func (pf *processFactory) newProcess(command string, logger logrus.FieldLogger, logLevel logrus.Level) (Process, error) {
return newProcess(command, logger, logLevel)
}
type Process interface {
dispense(key kindAndName) (interface{}, error)
exited() bool
kill()
}
type process struct {
client *plugin.Client
protocolClient plugin.ClientProtocol
}
func newProcess(command string, logger logrus.FieldLogger, logLevel logrus.Level) (Process, error) {
builder := newClientBuilder(command, logger.WithField("cmd", command), logLevel)
// This creates a new go-plugin Client that has its own unique exec.Cmd for launching the plugin process.
client := builder.client()
// This launches the plugin process.
protocolClient, err := client.Client()
if err != nil {
return nil, err
}
p := &process{
client: client,
protocolClient: protocolClient,
}
return p, nil
}
func (r *process) dispense(key kindAndName) (interface{}, error) {
// This calls GRPCClient(clientConn) on the plugin instance registered for key.name.
dispensed, err := r.protocolClient.Dispense(key.kind.String())
if err != nil {
return nil, errors.WithStack(err)
}
// Currently all plugins except for PluginLister dispense clientDispenser instances.
if clientDispenser, ok := dispensed.(ClientDispenser); ok {
if key.name == "" {
return nil, errors.Errorf("%s plugin requested but name is missing", key.kind.String())
}
// Get the instance that implements our plugin interface (e.g. cloudprovider.ObjectStore) that is a gRPC-based
// client
dispensed = clientDispenser.clientFor(key.name)
}
return dispensed, nil
}
func (r *process) exited() bool {
return r.client.Exited()
}
func (r *process) kill() {
r.client.Kill()
}
+121
View File
@@ -0,0 +1,121 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type mockClientProtocol struct {
mock.Mock
}
func (cp *mockClientProtocol) Close() error {
args := cp.Called()
return args.Error(0)
}
func (cp *mockClientProtocol) Dispense(name string) (interface{}, error) {
args := cp.Called(name)
return args.Get(0), args.Error(1)
}
func (cp *mockClientProtocol) Ping() error {
args := cp.Called()
return args.Error(0)
}
type mockClientDispenser struct {
mock.Mock
}
func (cd *mockClientDispenser) clientFor(name string) interface{} {
args := cd.Called(name)
return args.Get(0)
}
func TestDispense(t *testing.T) {
tests := []struct {
name string
missingKeyName bool
dispenseError error
clientDispenser bool
expectedError string
}{
{
name: "protocol client dispense error",
dispenseError: errors.Errorf("protocol client dispense"),
expectedError: "protocol client dispense",
},
{
name: "plugin lister, no error",
},
{
name: "client dispenser, missing key name",
clientDispenser: true,
missingKeyName: true,
expectedError: "ObjectStore plugin requested but name is missing",
},
{
name: "client dispenser, have key name",
clientDispenser: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
p := new(process)
protocolClient := new(mockClientProtocol)
defer protocolClient.AssertExpectations(t)
p.protocolClient = protocolClient
clientDispenser := new(mockClientDispenser)
defer clientDispenser.AssertExpectations(t)
var client interface{}
key := kindAndName{}
if tc.clientDispenser {
key.kind = PluginKindObjectStore
protocolClient.On("Dispense", key.kind.String()).Return(clientDispenser, tc.dispenseError)
if !tc.missingKeyName {
key.name = "aws"
client = &BackupItemActionGRPCClient{}
clientDispenser.On("clientFor", key.name).Return(client)
}
} else {
key.kind = PluginKindPluginLister
client = &PluginListerGRPCClient{}
protocolClient.On("Dispense", key.kind.String()).Return(client, tc.dispenseError)
}
dispensed, err := p.dispense(key)
if tc.expectedError != "" {
assert.EqualError(t, err, tc.expectedError)
return
}
require.NoError(t, err)
assert.Equal(t, client, dispensed)
})
}
}
+4 -3
View File
@@ -4,8 +4,9 @@ package generated;
import "Shared.proto";
message ExecuteRequest {
bytes item = 1;
bytes backup = 2;
string plugin = 1;
bytes item = 2;
bytes backup = 3;
}
message ExecuteResponse {
@@ -21,6 +22,6 @@ message ResourceIdentifier {
}
service BackupItemAction {
rpc AppliesTo(Empty) returns (AppliesToResponse);
rpc AppliesTo(AppliesToRequest) returns (AppliesToResponse);
rpc Execute(ExecuteRequest) returns (ExecuteResponse);
}
+22 -15
View File
@@ -4,10 +4,11 @@ package generated;
import "Shared.proto";
message CreateVolumeRequest {
string snapshotID = 1;
string volumeType = 2;
string volumeAZ = 3;
int64 iops = 4;
string plugin = 1;
string snapshotID = 2;
string volumeType = 3;
string volumeAZ = 4;
int64 iops = 5;
}
message CreateVolumeResponse {
@@ -15,8 +16,9 @@ message CreateVolumeResponse {
}
message GetVolumeInfoRequest {
string volumeID = 1;
string volumeAZ = 2;
string plugin = 1;
string volumeID = 2;
string volumeAZ = 3;
}
message GetVolumeInfoResponse {
@@ -25,8 +27,9 @@ message GetVolumeInfoResponse {
}
message IsVolumeReadyRequest {
string volumeID = 1;
string volumeAZ = 2;
string plugin = 1;
string volumeID = 2;
string volumeAZ = 3;
}
message IsVolumeReadyResponse {
@@ -34,9 +37,10 @@ message IsVolumeReadyResponse {
}
message CreateSnapshotRequest {
string volumeID = 1;
string volumeAZ = 2;
map<string, string> tags = 3;
string plugin = 1;
string volumeID = 2;
string volumeAZ = 3;
map<string, string> tags = 4;
}
message CreateSnapshotResponse {
@@ -44,11 +48,13 @@ message CreateSnapshotResponse {
}
message DeleteSnapshotRequest {
string snapshotID = 1;
string plugin = 1;
string snapshotID = 2;
}
message GetVolumeIDRequest {
bytes persistentVolume = 1;
string plugin = 1;
bytes persistentVolume = 2;
}
message GetVolumeIDResponse {
@@ -56,8 +62,9 @@ message GetVolumeIDResponse {
}
message SetVolumeIDRequest {
bytes persistentVolume = 1;
string volumeID = 2;
string plugin = 1;
bytes persistentVolume = 2;
string volumeID = 3;
}
message SetVolumeIDResponse {
+20 -14
View File
@@ -4,14 +4,16 @@ package generated;
import "Shared.proto";
message PutObjectRequest {
string bucket = 1;
string key = 2;
bytes body = 3;
string plugin = 1;
string bucket = 2;
string key = 3;
bytes body = 4;
}
message GetObjectRequest {
string bucket = 1;
string key = 2;
string plugin = 1;
string bucket = 2;
string key = 3;
}
message Bytes {
@@ -19,8 +21,9 @@ message Bytes {
}
message ListCommonPrefixesRequest {
string bucket = 1;
string delimiter = 2;
string plugin = 1;
string bucket = 2;
string delimiter = 3;
}
message ListCommonPrefixesResponse {
@@ -28,8 +31,9 @@ message ListCommonPrefixesResponse {
}
message ListObjectsRequest {
string bucket = 1;
string prefix = 2;
string plugin = 1;
string bucket = 2;
string prefix = 3;
}
message ListObjectsResponse {
@@ -37,15 +41,17 @@ message ListObjectsResponse {
}
message DeleteObjectRequest {
string bucket = 1;
string key = 2;
string plugin = 1;
string bucket = 2;
string key = 3;
}
message CreateSignedURLRequest {
string bucket = 1;
string key = 2;
int64 ttl = 3;
string plugin = 1;
string bucket = 2;
string key = 3;
int64 ttl = 4;
}
message CreateSignedURLResponse {
+18
View File
@@ -0,0 +1,18 @@
syntax = "proto3";
package generated;
import "Shared.proto";
message PluginIdentifier {
string command = 1;
string kind = 2;
string name = 3;
}
message ListPluginsResponse {
repeated PluginIdentifier plugins = 1;
}
service PluginLister {
rpc ListPlugins(Empty) returns (ListPluginsResponse);
}
+4 -3
View File
@@ -4,8 +4,9 @@ package generated;
import "Shared.proto";
message RestoreExecuteRequest {
bytes item = 1;
bytes restore = 2;
string plugin = 1;
bytes item = 2;
bytes restore = 3;
}
message RestoreExecuteResponse {
@@ -14,6 +15,6 @@ message RestoreExecuteResponse {
}
service RestoreItemAction {
rpc AppliesTo(Empty) returns (AppliesToResponse);
rpc AppliesTo(AppliesToRequest) returns (AppliesToResponse);
rpc Execute(RestoreExecuteRequest) returns (RestoreExecuteResponse);
}
+6 -1
View File
@@ -4,7 +4,12 @@ package generated;
message Empty {}
message InitRequest {
map<string, string> config = 1;
string plugin = 1;
map<string, string> config = 2;
}
message AppliesToRequest {
string plugin = 1;
}
message AppliesToResponse {
+222 -44
View File
@@ -1,68 +1,246 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"fmt"
"os"
"path/filepath"
"github.com/heptio/ark/pkg/util/filesystem"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// registry is a simple store of plugin binary information. If a binary
// is registered as supporting multiple PluginKinds, it will be
// gettable/listable for all of those kinds.
// Registry manages information about available plugins.
type Registry interface {
// DiscoverPlugins discovers all available plugins.
DiscoverPlugins() error
// List returns all PluginIdentifiers for kind.
List(kind PluginKind) []PluginIdentifier
// Get returns the PluginIdentifier for kind and name.
Get(kind PluginKind, name string) (PluginIdentifier, error)
}
// kindAndName is a convenience struct that combines a PluginKind and a name.
type kindAndName struct {
kind PluginKind
name string
}
// registry implements Registry.
type registry struct {
// plugins is a nested map, keyed first by PluginKind,
// and second by name. this is to allow easy listing
// of plugins for a kind, as well as efficient lookup
// of a plugin by kind+name.
plugins map[PluginKind]map[string]pluginInfo
// dir is the directory to search for plugins.
dir string
logger logrus.FieldLogger
logLevel logrus.Level
processFactory ProcessFactory
fs filesystem.Interface
pluginsByID map[kindAndName]PluginIdentifier
pluginsByKind map[PluginKind][]PluginIdentifier
}
func newRegistry() *registry {
// NewRegistry returns a new registry.
func NewRegistry(dir string, logger logrus.FieldLogger, logLevel logrus.Level) Registry {
return &registry{
plugins: make(map[PluginKind]map[string]pluginInfo),
dir: dir,
logger: logger,
logLevel: logLevel,
processFactory: newProcessFactory(),
fs: filesystem.NewFileSystem(),
pluginsByID: make(map[kindAndName]PluginIdentifier),
pluginsByKind: make(map[PluginKind][]PluginIdentifier),
}
}
// register adds a binary to the registry. If the binary supports multiple
// PluginKinds, it will be stored for each of those kinds so subsequent gets/lists
// for any supported kind will return it.
func (r *registry) register(name, commandName string, commandArgs []string, kinds ...PluginKind) {
for _, kind := range kinds {
if r.plugins[kind] == nil {
r.plugins[kind] = make(map[string]pluginInfo)
}
r.plugins[kind][name] = pluginInfo{
kinds: kinds,
name: name,
commandName: commandName,
commandArgs: commandArgs,
}
func (r *registry) DiscoverPlugins() error {
plugins, err := r.readPluginsDir(r.dir)
if err != nil {
return err
}
// Start by adding ark's internal plugins
commands := []string{os.Args[0]}
// Then add the discovered plugin executables
commands = append(commands, plugins...)
return r.discoverPlugins(commands)
}
// list returns info about all plugin binaries that implement the given
func (r *registry) discoverPlugins(commands []string) error {
for _, command := range commands {
plugins, err := r.listPlugins(command)
if err != nil {
return err
}
for _, plugin := range plugins {
r.logger.WithFields(logrus.Fields{
"kind": plugin.Kind,
"name": plugin.Name,
"command": command,
}).Info("registering plugin")
if err := r.register(plugin); err != nil {
return err
}
}
}
return nil
}
// List returns info about all plugin binaries that implement the given
// PluginKind.
func (r *registry) list(kind PluginKind) ([]pluginInfo, error) {
var res []pluginInfo
if plugins, found := r.plugins[kind]; found {
for _, itm := range plugins {
res = append(res, itm)
}
return res, nil
}
return nil, errors.New("plugins not found")
func (r *registry) List(kind PluginKind) []PluginIdentifier {
return r.pluginsByKind[kind]
}
// get returns info about a plugin with the given name and kind, or an
// Get returns info about a plugin with the given name and kind, or an
// error if one cannot be found.
func (r *registry) get(kind PluginKind, name string) (pluginInfo, error) {
if forKind := r.plugins[kind]; forKind != nil {
if plugin, found := r.plugins[kind][name]; found {
return plugin, nil
func (r *registry) Get(kind PluginKind, name string) (PluginIdentifier, error) {
p, found := r.pluginsByID[kindAndName{kind: kind, name: name}]
if !found {
return PluginIdentifier{}, newPluginNotFoundError(kind, name)
}
return p, nil
}
// readPluginsDir recursively reads dir looking for plugins.
func (r *registry) readPluginsDir(dir string) ([]string, error) {
if _, err := r.fs.Stat(dir); err != nil {
if os.IsNotExist(err) {
return []string{}, nil
}
return nil, errors.WithStack(err)
}
return pluginInfo{}, errors.New("plugin not found")
files, err := r.fs.ReadDir(dir)
if err != nil {
return nil, errors.WithStack(err)
}
fullPaths := make([]string, 0, len(files))
for _, file := range files {
fullPath := filepath.Join(dir, file.Name())
if file.IsDir() {
subDirPaths, err := r.readPluginsDir(fullPath)
if err != nil {
return nil, err
}
fullPaths = append(fullPaths, subDirPaths...)
continue
}
if !executable(file) {
continue
}
fullPaths = append(fullPaths, fullPath)
}
return fullPaths, nil
}
// executable determines if a file is executable.
func executable(info os.FileInfo) bool {
/*
When we AND the mode with 0111:
- 0100 (user executable)
- 0010 (group executable)
- 0001 (other executable)
the result will be 0 if and only if none of the executable bits is set.
*/
return (info.Mode() & 0111) != 0
}
// listPlugins executes command, queries it for registered plugins, and returns the list of PluginIdentifiers.
func (r *registry) listPlugins(command string) ([]PluginIdentifier, error) {
process, err := r.processFactory.newProcess(command, r.logger, r.logLevel)
if err != nil {
return nil, err
}
defer process.kill()
plugin, err := process.dispense(kindAndName{kind: PluginKindPluginLister})
if err != nil {
return nil, err
}
lister, ok := plugin.(PluginLister)
if !ok {
return nil, errors.Errorf("%T is not a PluginLister", plugin)
}
return lister.ListPlugins()
}
// register registers a PluginIdentifier with the registry.
func (r *registry) register(id PluginIdentifier) error {
key := kindAndName{kind: id.Kind, name: id.Name}
if existing, found := r.pluginsByID[key]; found {
return newDuplicatePluginRegistrationError(existing, id)
}
r.pluginsByID[key] = id
r.pluginsByKind[id.Kind] = append(r.pluginsByKind[id.Kind], id)
return nil
}
// pluginNotFoundError indicates a plugin could not be located for kind and name.
type pluginNotFoundError struct {
kind PluginKind
name string
}
// newPluginNotFoundError returns a new pluginNotFoundError for kind and name.
func newPluginNotFoundError(kind PluginKind, name string) *pluginNotFoundError {
return &pluginNotFoundError{
kind: kind,
name: name,
}
}
func (e *pluginNotFoundError) Error() string {
return fmt.Sprintf("unable to locate %v plugin named %s", e.kind, e.name)
}
type duplicatePluginRegistrationError struct {
existing PluginIdentifier
duplicate PluginIdentifier
}
func newDuplicatePluginRegistrationError(existing, duplicate PluginIdentifier) *duplicatePluginRegistrationError {
return &duplicatePluginRegistrationError{
existing: existing,
duplicate: duplicate,
}
}
func (e *duplicatePluginRegistrationError) Error() string {
return fmt.Sprintf(
"unable to register plugin (kind=%s, name=%s, command=%s) because another plugin is already registered for this kind and name (command=%s)",
string(e.duplicate.Kind),
e.duplicate.Name,
e.duplicate.Command,
e.existing.Command,
)
}
+129
View File
@@ -0,0 +1,129 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"os"
"sort"
"testing"
"github.com/heptio/ark/pkg/util/test"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewRegistry(t *testing.T) {
logger := test.NewLogger()
logLevel := logrus.InfoLevel
dir := "/plugins"
r := NewRegistry(dir, logger, logLevel).(*registry)
assert.Equal(t, dir, r.dir)
assert.Equal(t, logger, r.logger)
assert.Equal(t, logLevel, r.logLevel)
assert.NotNil(t, r.pluginsByID)
assert.Empty(t, r.pluginsByID)
assert.NotNil(t, r.pluginsByKind)
assert.Empty(t, r.pluginsByKind)
}
type fakeFileInfo struct {
os.FileInfo
mode os.FileMode
}
func (f *fakeFileInfo) Mode() os.FileMode {
return f.mode
}
func TestExecutable(t *testing.T) {
tests := []struct {
name string
mode uint32
expectExecutable bool
}{
{
name: "no perms",
mode: 0000,
},
{
name: "r--r--r--",
mode: 0444,
},
{
name: "rw-rw-rw-",
mode: 0666,
},
{
name: "--x------",
mode: 0100,
expectExecutable: true,
},
{
name: "-----x---",
mode: 0010,
expectExecutable: true,
},
{
name: "--------x",
mode: 0001,
expectExecutable: true,
},
{
name: "rwxrwxrwx",
mode: 0777,
expectExecutable: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
info := &fakeFileInfo{
mode: os.FileMode(test.mode),
}
assert.Equal(t, test.expectExecutable, executable(info))
})
}
}
func TestReadPluginsDir(t *testing.T) {
logger := test.NewLogger()
logLevel := logrus.InfoLevel
dir := "/plugins"
r := NewRegistry(dir, logger, logLevel).(*registry)
r.fs = test.NewFakeFileSystem().
WithFileAndMode("/plugins/executable1", []byte("plugin1"), 0755).
WithFileAndMode("/plugins/nonexecutable2", []byte("plugin2"), 0644).
WithFileAndMode("/plugins/executable3", []byte("plugin3"), 0755).
WithFileAndMode("/plugins/nested/executable4", []byte("plugin4"), 0755).
WithFileAndMode("/plugins/nested/nonexecutable5", []byte("plugin4"), 0644)
plugins, err := r.readPluginsDir(dir)
require.NoError(t, err)
expected := []string{
"/plugins/executable1",
"/plugins/executable3",
"/plugins/nested/executable4",
}
sort.Strings(plugins)
sort.Strings(expected)
assert.Equal(t, expected, plugins)
}
@@ -0,0 +1,86 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/backup"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
)
// restartableBackupItemAction is a backup item action for a given implementation (such as "pod"). It is associated with
// a restartableProcess, which may be shared and used to run multiple plugins. At the beginning of each method
// call, the restartableBackupItemAction asks its restartableProcess to restart itself if needed (e.g. if the
// process terminated for any reason), then it proceeds with the actual call.
type restartableBackupItemAction struct {
key kindAndName
sharedPluginProcess RestartableProcess
}
// newRestartableBackupItemAction returns a new restartableBackupItemAction.
func newRestartableBackupItemAction(name string, sharedPluginProcess RestartableProcess) *restartableBackupItemAction {
r := &restartableBackupItemAction{
key: kindAndName{kind: PluginKindBackupItemAction, name: name},
sharedPluginProcess: sharedPluginProcess,
}
return r
}
// getBackupItemAction returns the backup item action for this restartableBackupItemAction. It does *not* restart the
// plugin process.
func (r *restartableBackupItemAction) getBackupItemAction() (backup.ItemAction, error) {
plugin, err := r.sharedPluginProcess.getByKindAndName(r.key)
if err != nil {
return nil, err
}
backupItemAction, ok := plugin.(backup.ItemAction)
if !ok {
return nil, errors.Errorf("%T is not a backup.ItemAction!", plugin)
}
return backupItemAction, nil
}
// getDelegate restarts the plugin process (if needed) and returns the backup item action for this restartableBackupItemAction.
func (r *restartableBackupItemAction) getDelegate() (backup.ItemAction, error) {
if err := r.sharedPluginProcess.resetIfNeeded(); err != nil {
return nil, err
}
return r.getBackupItemAction()
}
// AppliesTo restarts the plugin's process if needed, then delegates the call.
func (r *restartableBackupItemAction) AppliesTo() (backup.ResourceSelector, error) {
delegate, err := r.getDelegate()
if err != nil {
return backup.ResourceSelector{}, err
}
return delegate.AppliesTo()
}
// Execute restarts the plugin's process if needed, then delegates the call.
func (r *restartableBackupItemAction) Execute(item runtime.Unstructured, backup *api.Backup) (runtime.Unstructured, []backup.ResourceIdentifier, error) {
delegate, err := r.getDelegate()
if err != nil {
return nil, nil, err
}
return delegate.Execute(item, backup)
}
@@ -0,0 +1,146 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"testing"
"github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/backup"
"github.com/heptio/ark/pkg/backup/mocks"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)
func TestRestartableGetBackupItemAction(t *testing.T) {
tests := []struct {
name string
plugin interface{}
getError error
expectedError string
}{
{
name: "error getting by kind and name",
getError: errors.Errorf("get error"),
expectedError: "get error",
},
{
name: "wrong type",
plugin: 3,
expectedError: "int is not a backup.ItemAction!",
},
{
name: "happy path",
plugin: new(mocks.ItemAction),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
p := new(mockRestartableProcess)
defer p.AssertExpectations(t)
name := "pod"
key := kindAndName{kind: PluginKindBackupItemAction, name: name}
p.On("getByKindAndName", key).Return(tc.plugin, tc.getError)
r := newRestartableBackupItemAction(name, p)
a, err := r.getBackupItemAction()
if tc.expectedError != "" {
assert.EqualError(t, err, tc.expectedError)
return
}
require.NoError(t, err)
assert.Equal(t, tc.plugin, a)
})
}
}
func TestRestartableBackupItemActionGetDelegate(t *testing.T) {
p := new(mockRestartableProcess)
defer p.AssertExpectations(t)
// Reset error
p.On("resetIfNeeded").Return(errors.Errorf("reset error")).Once()
name := "pod"
r := newRestartableBackupItemAction(name, p)
a, err := r.getDelegate()
assert.Nil(t, a)
assert.EqualError(t, err, "reset error")
// Happy path
p.On("resetIfNeeded").Return(nil)
expected := new(mocks.ItemAction)
key := kindAndName{kind: PluginKindBackupItemAction, name: name}
p.On("getByKindAndName", key).Return(expected, nil)
a, err = r.getDelegate()
assert.NoError(t, err)
assert.Equal(t, expected, a)
}
func TestRestartableBackupItemActionDelegatedFunctions(t *testing.T) {
b := new(v1.Backup)
pv := &unstructured.Unstructured{
Object: map[string]interface{}{
"color": "blue",
},
}
pvToReturn := &unstructured.Unstructured{
Object: map[string]interface{}{
"color": "green",
},
}
additionalItems := []backup.ResourceIdentifier{
{
GroupResource: schema.GroupResource{Group: "ark.heptio.com", Resource: "backups"},
},
}
runRestartableDelegateTests(
t,
PluginKindBackupItemAction,
func(key kindAndName, p RestartableProcess) interface{} {
return &restartableBackupItemAction{
key: key,
sharedPluginProcess: p,
}
},
func() mockable {
return new(mocks.ItemAction)
},
restartableDelegateTest{
function: "AppliesTo",
inputs: []interface{}{},
expectedErrorOutputs: []interface{}{backup.ResourceSelector{}, errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{backup.ResourceSelector{IncludedNamespaces: []string{"a"}}, errors.Errorf("delegate error")},
},
restartableDelegateTest{
function: "Execute",
inputs: []interface{}{pv, b},
expectedErrorOutputs: []interface{}{nil, ([]backup.ResourceIdentifier)(nil), errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{pvToReturn, additionalItems, errors.Errorf("delegate error")},
},
)
}
+167
View File
@@ -0,0 +1,167 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
)
// restartableBlockStore is an object store for a given implementation (such as "aws"). It is associated with
// a restartableProcess, which may be shared and used to run multiple plugins. At the beginning of each method
// call, the restartableBlockStore asks its restartableProcess to restart itself if needed (e.g. if the
// process terminated for any reason), then it proceeds with the actual call.
type restartableBlockStore struct {
key kindAndName
sharedPluginProcess RestartableProcess
config map[string]string
}
// newRestartableBlockStore returns a new restartableBlockStore.
func newRestartableBlockStore(name string, sharedPluginProcess RestartableProcess) *restartableBlockStore {
key := kindAndName{kind: PluginKindBlockStore, name: name}
r := &restartableBlockStore{
key: key,
sharedPluginProcess: sharedPluginProcess,
}
// Register our reinitializer so we can reinitialize after a restart with r.config.
sharedPluginProcess.addReinitializer(key, r)
return r
}
// reinitialize reinitializes a re-dispensed plugin using the initial data passed to Init().
func (r *restartableBlockStore) reinitialize(dispensed interface{}) error {
blockStore, ok := dispensed.(cloudprovider.BlockStore)
if !ok {
return errors.Errorf("%T is not a cloudprovider.BlockStore!", dispensed)
}
return r.init(blockStore, r.config)
}
// getBlockStore returns the block store for this restartableBlockStore. It does *not* restart the
// plugin process.
func (r *restartableBlockStore) getBlockStore() (cloudprovider.BlockStore, error) {
plugin, err := r.sharedPluginProcess.getByKindAndName(r.key)
if err != nil {
return nil, err
}
blockStore, ok := plugin.(cloudprovider.BlockStore)
if !ok {
return nil, errors.Errorf("%T is not a cloudprovider.BlockStore!", plugin)
}
return blockStore, nil
}
// getDelegate restarts the plugin process (if needed) and returns the block store for this restartableBlockStore.
func (r *restartableBlockStore) getDelegate() (cloudprovider.BlockStore, error) {
if err := r.sharedPluginProcess.resetIfNeeded(); err != nil {
return nil, err
}
return r.getBlockStore()
}
// Init initializes the block store instance using config. If this is the first invocation, r stores config for future
// reinitialization needs. Init does NOT restart the shared plugin process. Init may only be called once.
func (r *restartableBlockStore) Init(config map[string]string) error {
if r.config != nil {
return errors.Errorf("already initialized")
}
// Not using getDelegate() to avoid possible infinite recursion
delegate, err := r.getBlockStore()
if err != nil {
return err
}
r.config = config
return r.init(delegate, config)
}
// init calls Init on blockStore with config. This is split out from Init() so that both Init() and reinitialize() may
// call it using a specific BlockStore.
func (r *restartableBlockStore) init(blockStore cloudprovider.BlockStore, config map[string]string) error {
return blockStore.Init(config)
}
// CreateVolumeFromSnapshot restarts the plugin's process if needed, then delegates the call.
func (r *restartableBlockStore) CreateVolumeFromSnapshot(snapshotID string, volumeType string, volumeAZ string, iops *int64) (volumeID string, err error) {
delegate, err := r.getDelegate()
if err != nil {
return "", err
}
return delegate.CreateVolumeFromSnapshot(snapshotID, volumeType, volumeAZ, iops)
}
// GetVolumeID restarts the plugin's process if needed, then delegates the call.
func (r *restartableBlockStore) GetVolumeID(pv runtime.Unstructured) (string, error) {
delegate, err := r.getDelegate()
if err != nil {
return "", err
}
return delegate.GetVolumeID(pv)
}
// SetVolumeID restarts the plugin's process if needed, then delegates the call.
func (r *restartableBlockStore) SetVolumeID(pv runtime.Unstructured, volumeID string) (runtime.Unstructured, error) {
delegate, err := r.getDelegate()
if err != nil {
return nil, err
}
return delegate.SetVolumeID(pv, volumeID)
}
// GetVolumeInfo restarts the plugin's process if needed, then delegates the call.
func (r *restartableBlockStore) GetVolumeInfo(volumeID string, volumeAZ string) (string, *int64, error) {
delegate, err := r.getDelegate()
if err != nil {
return "", nil, err
}
return delegate.GetVolumeInfo(volumeID, volumeAZ)
}
// IsVolumeReady restarts the plugin's process if needed, then delegates the call.
func (r *restartableBlockStore) IsVolumeReady(volumeID string, volumeAZ string) (ready bool, err error) {
delegate, err := r.getDelegate()
if err != nil {
return false, err
}
return delegate.IsVolumeReady(volumeID, volumeAZ)
}
// CreateSnapshot restarts the plugin's process if needed, then delegates the call.
func (r *restartableBlockStore) CreateSnapshot(volumeID string, volumeAZ string, tags map[string]string) (snapshotID string, err error) {
delegate, err := r.getDelegate()
if err != nil {
return "", err
}
return delegate.CreateSnapshot(volumeID, volumeAZ, tags)
}
// DeleteSnapshot restarts the plugin's process if needed, then delegates the call.
func (r *restartableBlockStore) DeleteSnapshot(snapshotID string) error {
delegate, err := r.getDelegate()
if err != nil {
return err
}
return delegate.DeleteSnapshot(snapshotID)
}
+251
View File
@@ -0,0 +1,251 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"testing"
"github.com/Azure/go-autorest/autorest/to"
"github.com/heptio/ark/pkg/cloudprovider/mocks"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestRestartableGetBlockStore(t *testing.T) {
tests := []struct {
name string
plugin interface{}
getError error
expectedError string
}{
{
name: "error getting by kind and name",
getError: errors.Errorf("get error"),
expectedError: "get error",
},
{
name: "wrong type",
plugin: 3,
expectedError: "int is not a cloudprovider.BlockStore!",
},
{
name: "happy path",
plugin: new(mocks.BlockStore),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
p := new(mockRestartableProcess)
p.Test(t)
defer p.AssertExpectations(t)
name := "aws"
key := kindAndName{kind: PluginKindBlockStore, name: name}
p.On("getByKindAndName", key).Return(tc.plugin, tc.getError)
r := &restartableBlockStore{
key: key,
sharedPluginProcess: p,
}
a, err := r.getBlockStore()
if tc.expectedError != "" {
assert.EqualError(t, err, tc.expectedError)
return
}
require.NoError(t, err)
assert.Equal(t, tc.plugin, a)
})
}
}
func TestRestartableBlockStoreReinitialize(t *testing.T) {
p := new(mockRestartableProcess)
p.Test(t)
defer p.AssertExpectations(t)
name := "aws"
key := kindAndName{kind: PluginKindBlockStore, name: name}
r := &restartableBlockStore{
key: key,
sharedPluginProcess: p,
config: map[string]string{
"color": "blue",
},
}
err := r.reinitialize(3)
assert.EqualError(t, err, "int is not a cloudprovider.BlockStore!")
blockStore := new(mocks.BlockStore)
blockStore.Test(t)
defer blockStore.AssertExpectations(t)
blockStore.On("Init", r.config).Return(errors.Errorf("init error")).Once()
err = r.reinitialize(blockStore)
assert.EqualError(t, err, "init error")
blockStore.On("Init", r.config).Return(nil)
err = r.reinitialize(blockStore)
assert.NoError(t, err)
}
func TestRestartableBlockStoreGetDelegate(t *testing.T) {
p := new(mockRestartableProcess)
p.Test(t)
defer p.AssertExpectations(t)
// Reset error
p.On("resetIfNeeded").Return(errors.Errorf("reset error")).Once()
name := "aws"
key := kindAndName{kind: PluginKindBlockStore, name: name}
r := &restartableBlockStore{
key: key,
sharedPluginProcess: p,
}
a, err := r.getDelegate()
assert.Nil(t, a)
assert.EqualError(t, err, "reset error")
// Happy path
p.On("resetIfNeeded").Return(nil)
blockStore := new(mocks.BlockStore)
blockStore.Test(t)
defer blockStore.AssertExpectations(t)
p.On("getByKindAndName", key).Return(blockStore, nil)
a, err = r.getDelegate()
assert.NoError(t, err)
assert.Equal(t, blockStore, a)
}
func TestRestartableBlockStoreInit(t *testing.T) {
p := new(mockRestartableProcess)
p.Test(t)
defer p.AssertExpectations(t)
// getBlockStore error
name := "aws"
key := kindAndName{kind: PluginKindBlockStore, name: name}
r := &restartableBlockStore{
key: key,
sharedPluginProcess: p,
}
p.On("getByKindAndName", key).Return(nil, errors.Errorf("getByKindAndName error")).Once()
config := map[string]string{
"color": "blue",
}
err := r.Init(config)
assert.EqualError(t, err, "getByKindAndName error")
// Delegate returns error
blockStore := new(mocks.BlockStore)
blockStore.Test(t)
defer blockStore.AssertExpectations(t)
p.On("getByKindAndName", key).Return(blockStore, nil)
blockStore.On("Init", config).Return(errors.Errorf("Init error")).Once()
err = r.Init(config)
assert.EqualError(t, err, "Init error")
// wipe this out because the previous failed Init call set it
r.config = nil
// Happy path
blockStore.On("Init", config).Return(nil)
err = r.Init(config)
assert.NoError(t, err)
assert.Equal(t, config, r.config)
// Calling Init twice is forbidden
err = r.Init(config)
assert.EqualError(t, err, "already initialized")
}
func TestRestartableBlockStoreDelegatedFunctions(t *testing.T) {
pv := &unstructured.Unstructured{
Object: map[string]interface{}{
"color": "blue",
},
}
pvToReturn := &unstructured.Unstructured{
Object: map[string]interface{}{
"color": "green",
},
}
runRestartableDelegateTests(
t,
PluginKindBlockStore,
func(key kindAndName, p RestartableProcess) interface{} {
return &restartableBlockStore{
key: key,
sharedPluginProcess: p,
}
},
func() mockable {
return new(mocks.BlockStore)
},
restartableDelegateTest{
function: "CreateVolumeFromSnapshot",
inputs: []interface{}{"snapshotID", "volumeID", "volumeAZ", to.Int64Ptr(10000)},
expectedErrorOutputs: []interface{}{"", errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{"volumeID", errors.Errorf("delegate error")},
},
restartableDelegateTest{
function: "GetVolumeID",
inputs: []interface{}{pv},
expectedErrorOutputs: []interface{}{"", errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{"volumeID", errors.Errorf("delegate error")},
},
restartableDelegateTest{
function: "SetVolumeID",
inputs: []interface{}{pv, "volumeID"},
expectedErrorOutputs: []interface{}{nil, errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{pvToReturn, errors.Errorf("delegate error")},
},
restartableDelegateTest{
function: "GetVolumeInfo",
inputs: []interface{}{"volumeID", "volumeAZ"},
expectedErrorOutputs: []interface{}{"", (*int64)(nil), errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{"volumeType", to.Int64Ptr(10000), errors.Errorf("delegate error")},
},
restartableDelegateTest{
function: "IsVolumeReady",
inputs: []interface{}{"volumeID", "volumeAZ"},
expectedErrorOutputs: []interface{}{false, errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{true, errors.Errorf("delegate error")},
},
restartableDelegateTest{
function: "CreateSnapshot",
inputs: []interface{}{"volumeID", "volumeAZ", map[string]string{"a": "b"}},
expectedErrorOutputs: []interface{}{"", errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{"snapshotID", errors.Errorf("delegate error")},
},
restartableDelegateTest{
function: "DeleteSnapshot",
inputs: []interface{}{"snapshotID"},
expectedErrorOutputs: []interface{}{errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{errors.Errorf("delegate error")},
},
)
}
+118
View File
@@ -0,0 +1,118 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"reflect"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type restartableDelegateTest struct {
function string
inputs []interface{}
expectedErrorOutputs []interface{}
expectedDelegateOutputs []interface{}
}
type mockable interface {
Test(t mock.TestingT)
On(method string, args ...interface{}) *mock.Call
AssertExpectations(t mock.TestingT) bool
}
func runRestartableDelegateTests(
t *testing.T,
kind PluginKind,
newRestartable func(key kindAndName, p RestartableProcess) interface{},
newMock func() mockable,
tests ...restartableDelegateTest,
) {
for _, tc := range tests {
t.Run(tc.function, func(t *testing.T) {
p := new(mockRestartableProcess)
p.Test(t)
defer p.AssertExpectations(t)
// getDelegate error
p.On("resetIfNeeded").Return(errors.Errorf("reset error")).Once()
name := "delegateName"
key := kindAndName{kind: kind, name: name}
r := newRestartable(key, p)
// Get the method we're going to call using reflection
method := reflect.ValueOf(r).MethodByName(tc.function)
require.NotEmpty(t, method)
// Convert the test case inputs ([]interface{}) to []reflect.Value
var inputValues []reflect.Value
for i := range tc.inputs {
inputValues = append(inputValues, reflect.ValueOf(tc.inputs[i]))
}
// Invoke the method being tested
actual := method.Call(inputValues)
// This function asserts that the actual outputs match the expected outputs
checkOutputs := func(expected []interface{}, actual []reflect.Value) {
require.Equal(t, len(expected), len(actual))
for i := range actual {
// Get the underlying value from the reflect.Value
a := actual[i].Interface()
// Check if it's an error
actualErr, actualErrOk := a.(error)
// Check if the expected output element is an error
expectedErr, expectedErrOk := expected[i].(error)
// If both are errors, use EqualError
if actualErrOk && expectedErrOk {
assert.EqualError(t, actualErr, expectedErr.Error())
continue
}
// Otherwise, use plain Equal
assert.Equal(t, expected[i], a)
}
}
// Make sure we get what we expected when getDelegate returned an error
checkOutputs(tc.expectedErrorOutputs, actual)
// Invoke delegate, make sure all returned values are passed through
p.On("resetIfNeeded").Return(nil)
delegate := newMock()
delegate.Test(t)
defer delegate.AssertExpectations(t)
p.On("getByKindAndName", key).Return(delegate, nil)
// Set up the mocked method in the delegate
delegate.On(tc.function, tc.inputs...).Return(tc.expectedDelegateOutputs...)
// Invoke the method being tested
actual = method.Call(inputValues)
// Make sure we get what we expected when invoking the delegate
checkOutputs(tc.expectedDelegateOutputs, actual)
})
}
}
+163
View File
@@ -0,0 +1,163 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"io"
"time"
"github.com/heptio/ark/pkg/cloudprovider"
"github.com/pkg/errors"
)
// restartableObjectStore is an object store for a given implementation (such as "aws"). It is associated with
// a restartableProcess, which may be shared and used to run multiple plugins. At the beginning of each method
// call, the restartableObjectStore asks its restartableProcess to restart itself if needed (e.g. if the
// process terminated for any reason), then it proceeds with the actual call.
type restartableObjectStore struct {
key kindAndName
sharedPluginProcess RestartableProcess
// config contains the data used to initialize the plugin. It is used to reinitialize the plugin in the event its
// sharedPluginProcess gets restarted.
config map[string]string
}
// newRestartableObjectStore returns a new restartableObjectStore.
func newRestartableObjectStore(name string, sharedPluginProcess RestartableProcess) *restartableObjectStore {
key := kindAndName{kind: PluginKindObjectStore, name: name}
r := &restartableObjectStore{
key: key,
sharedPluginProcess: sharedPluginProcess,
}
// Register our reinitializer so we can reinitialize after a restart with r.config.
sharedPluginProcess.addReinitializer(key, r)
return r
}
// reinitialize reinitializes a re-dispensed plugin using the initial data passed to Init().
func (r *restartableObjectStore) reinitialize(dispensed interface{}) error {
objectStore, ok := dispensed.(cloudprovider.ObjectStore)
if !ok {
return errors.Errorf("%T is not a cloudprovider.ObjectStore!", dispensed)
}
return r.init(objectStore, r.config)
}
// getObjectStore returns the object store for this restartableObjectStore. It does *not* restart the
// plugin process.
func (r *restartableObjectStore) getObjectStore() (cloudprovider.ObjectStore, error) {
plugin, err := r.sharedPluginProcess.getByKindAndName(r.key)
if err != nil {
return nil, err
}
objectStore, ok := plugin.(cloudprovider.ObjectStore)
if !ok {
return nil, errors.Errorf("%T is not a cloudprovider.ObjectStore!", plugin)
}
return objectStore, nil
}
// getDelegate restarts the plugin process (if needed) and returns the object store for this restartableObjectStore.
func (r *restartableObjectStore) getDelegate() (cloudprovider.ObjectStore, error) {
if err := r.sharedPluginProcess.resetIfNeeded(); err != nil {
return nil, err
}
return r.getObjectStore()
}
// Init initializes the object store instance using config. If this is the first invocation, r stores config for future
// reinitialization needs. Init does NOT restart the shared plugin process. Init may only be called once.
func (r *restartableObjectStore) Init(config map[string]string) error {
if r.config != nil {
return errors.Errorf("already initialized")
}
// Not using getDelegate() to avoid possible infinite recursion
delegate, err := r.getObjectStore()
if err != nil {
return err
}
r.config = config
return r.init(delegate, config)
}
// init calls Init on objectStore with config. This is split out from Init() so that both Init() and reinitialize() may
// call it using a specific ObjectStore.
func (r *restartableObjectStore) init(objectStore cloudprovider.ObjectStore, config map[string]string) error {
return objectStore.Init(config)
}
// PutObject restarts the plugin's process if needed, then delegates the call.
func (r *restartableObjectStore) PutObject(bucket string, key string, body io.Reader) error {
delegate, err := r.getDelegate()
if err != nil {
return err
}
return delegate.PutObject(bucket, key, body)
}
// GetObject restarts the plugin's process if needed, then delegates the call.
func (r *restartableObjectStore) GetObject(bucket string, key string) (io.ReadCloser, error) {
delegate, err := r.getDelegate()
if err != nil {
return nil, err
}
return delegate.GetObject(bucket, key)
}
// ListCommonPrefixes restarts the plugin's process if needed, then delegates the call.
func (r *restartableObjectStore) ListCommonPrefixes(bucket string, delimiter string) ([]string, error) {
delegate, err := r.getDelegate()
if err != nil {
return nil, err
}
return delegate.ListCommonPrefixes(bucket, delimiter)
}
// ListObjects restarts the plugin's process if needed, then delegates the call.
func (r *restartableObjectStore) ListObjects(bucket string, prefix string) ([]string, error) {
delegate, err := r.getDelegate()
if err != nil {
return nil, err
}
return delegate.ListObjects(bucket, prefix)
}
// DeleteObject restarts the plugin's process if needed, then delegates the call.
func (r *restartableObjectStore) DeleteObject(bucket string, key string) error {
delegate, err := r.getDelegate()
if err != nil {
return err
}
return delegate.DeleteObject(bucket, key)
}
// CreateSignedURL restarts the plugin's process if needed, then delegates the call.
func (r *restartableObjectStore) CreateSignedURL(bucket string, key string, ttl time.Duration) (string, error) {
delegate, err := r.getDelegate()
if err != nil {
return "", err
}
return delegate.CreateSignedURL(bucket, key, ttl)
}
+234
View File
@@ -0,0 +1,234 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"io/ioutil"
"strings"
"testing"
"time"
"github.com/heptio/ark/pkg/util/test"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRestartableGetObjectStore(t *testing.T) {
tests := []struct {
name string
plugin interface{}
getError error
expectedError string
}{
{
name: "error getting by kind and name",
getError: errors.Errorf("get error"),
expectedError: "get error",
},
{
name: "wrong type",
plugin: 3,
expectedError: "int is not a cloudprovider.ObjectStore!",
},
{
name: "happy path",
plugin: new(test.ObjectStore),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
p := new(mockRestartableProcess)
p.Test(t)
defer p.AssertExpectations(t)
name := "aws"
key := kindAndName{kind: PluginKindObjectStore, name: name}
p.On("getByKindAndName", key).Return(tc.plugin, tc.getError)
r := &restartableObjectStore{
key: key,
sharedPluginProcess: p,
}
a, err := r.getObjectStore()
if tc.expectedError != "" {
assert.EqualError(t, err, tc.expectedError)
return
}
require.NoError(t, err)
assert.Equal(t, tc.plugin, a)
})
}
}
func TestRestartableObjectStoreReinitialize(t *testing.T) {
p := new(mockRestartableProcess)
p.Test(t)
defer p.AssertExpectations(t)
name := "aws"
key := kindAndName{kind: PluginKindObjectStore, name: name}
r := &restartableObjectStore{
key: key,
sharedPluginProcess: p,
config: map[string]string{
"color": "blue",
},
}
err := r.reinitialize(3)
assert.EqualError(t, err, "int is not a cloudprovider.ObjectStore!")
objectStore := new(test.ObjectStore)
objectStore.Test(t)
defer objectStore.AssertExpectations(t)
objectStore.On("Init", r.config).Return(errors.Errorf("init error")).Once()
err = r.reinitialize(objectStore)
assert.EqualError(t, err, "init error")
objectStore.On("Init", r.config).Return(nil)
err = r.reinitialize(objectStore)
assert.NoError(t, err)
}
func TestRestartableObjectStoreGetDelegate(t *testing.T) {
p := new(mockRestartableProcess)
p.Test(t)
defer p.AssertExpectations(t)
// Reset error
p.On("resetIfNeeded").Return(errors.Errorf("reset error")).Once()
name := "aws"
key := kindAndName{kind: PluginKindObjectStore, name: name}
r := &restartableObjectStore{
key: key,
sharedPluginProcess: p,
}
a, err := r.getDelegate()
assert.Nil(t, a)
assert.EqualError(t, err, "reset error")
// Happy path
p.On("resetIfNeeded").Return(nil)
objectStore := new(test.ObjectStore)
objectStore.Test(t)
defer objectStore.AssertExpectations(t)
p.On("getByKindAndName", key).Return(objectStore, nil)
a, err = r.getDelegate()
assert.NoError(t, err)
assert.Equal(t, objectStore, a)
}
func TestRestartableObjectStoreInit(t *testing.T) {
p := new(mockRestartableProcess)
p.Test(t)
defer p.AssertExpectations(t)
// getObjectStore error
name := "aws"
key := kindAndName{kind: PluginKindObjectStore, name: name}
r := &restartableObjectStore{
key: key,
sharedPluginProcess: p,
}
p.On("getByKindAndName", key).Return(nil, errors.Errorf("getByKindAndName error")).Once()
config := map[string]string{
"color": "blue",
}
err := r.Init(config)
assert.EqualError(t, err, "getByKindAndName error")
// Delegate returns error
objectStore := new(test.ObjectStore)
objectStore.Test(t)
defer objectStore.AssertExpectations(t)
p.On("getByKindAndName", key).Return(objectStore, nil)
objectStore.On("Init", config).Return(errors.Errorf("Init error")).Once()
err = r.Init(config)
assert.EqualError(t, err, "Init error")
// wipe this out because the previous failed Init call set it
r.config = nil
// Happy path
objectStore.On("Init", config).Return(nil)
err = r.Init(config)
assert.NoError(t, err)
assert.Equal(t, config, r.config)
// Calling Init twice is forbidden
err = r.Init(config)
assert.EqualError(t, err, "already initialized")
}
func TestRestartableObjectStoreDelegatedFunctions(t *testing.T) {
runRestartableDelegateTests(
t,
PluginKindObjectStore,
func(key kindAndName, p RestartableProcess) interface{} {
return &restartableObjectStore{
key: key,
sharedPluginProcess: p,
}
},
func() mockable {
return new(test.ObjectStore)
},
restartableDelegateTest{
function: "PutObject",
inputs: []interface{}{"bucket", "key", strings.NewReader("body")},
expectedErrorOutputs: []interface{}{errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{errors.Errorf("delegate error")},
},
restartableDelegateTest{
function: "GetObject",
inputs: []interface{}{"bucket", "key"},
expectedErrorOutputs: []interface{}{nil, errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{ioutil.NopCloser(strings.NewReader("object")), errors.Errorf("delegate error")},
},
restartableDelegateTest{
function: "ListCommonPrefixes",
inputs: []interface{}{"bucket", "delimeter"},
expectedErrorOutputs: []interface{}{([]string)(nil), errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{[]string{"a", "b"}, errors.Errorf("delegate error")},
},
restartableDelegateTest{
function: "ListObjects",
inputs: []interface{}{"bucket", "prefix"},
expectedErrorOutputs: []interface{}{([]string)(nil), errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{[]string{"a", "b"}, errors.Errorf("delegate error")},
},
restartableDelegateTest{
function: "DeleteObject",
inputs: []interface{}{"bucket", "key"},
expectedErrorOutputs: []interface{}{errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{errors.Errorf("delegate error")},
},
restartableDelegateTest{
function: "CreateSignedURL",
inputs: []interface{}{"bucket", "key", 30 * time.Minute},
expectedErrorOutputs: []interface{}{"", errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{"signedURL", errors.Errorf("delegate error")},
},
)
}
+190
View File
@@ -0,0 +1,190 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"sync"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
type RestartableProcessFactory interface {
newRestartableProcess(command string, logger logrus.FieldLogger, logLevel logrus.Level) (RestartableProcess, error)
}
type restartableProcessFactory struct {
}
func newRestartableProcessFactory() RestartableProcessFactory {
return &restartableProcessFactory{}
}
func (rpf *restartableProcessFactory) newRestartableProcess(command string, logger logrus.FieldLogger, logLevel logrus.Level) (RestartableProcess, error) {
return newRestartableProcess(command, logger, logLevel)
}
type RestartableProcess interface {
addReinitializer(key kindAndName, r reinitializer)
reset() error
resetIfNeeded() error
getByKindAndName(key kindAndName) (interface{}, error)
stop()
}
// restartableProcess encapsulates the lifecycle for all plugins contained in a single executable file. It is able
// to restart a plugin process if it is terminated for any reason. If this happens, all plugins are reinitialized using
// the original configuration data.
type restartableProcess struct {
command string
logger logrus.FieldLogger
logLevel logrus.Level
// lock guards all of the fields below
lock sync.RWMutex
process Process
plugins map[kindAndName]interface{}
reinitializers map[kindAndName]reinitializer
resetFailures int
}
// reinitializer is capable of reinitializing a restartable plugin instance using the newly dispensed plugin.
type reinitializer interface {
// reinitialize reinitializes a restartable plugin instance using the newly dispensed plugin.
reinitialize(dispensed interface{}) error
}
// newRestartableProcess creates a new restartableProcess for the given command and options.
func newRestartableProcess(command string, logger logrus.FieldLogger, logLevel logrus.Level) (RestartableProcess, error) {
p := &restartableProcess{
command: command,
logger: logger,
logLevel: logLevel,
plugins: make(map[kindAndName]interface{}),
reinitializers: make(map[kindAndName]reinitializer),
}
// This launches the process
err := p.reset()
return p, err
}
// addReinitializer registers the reinitializer r for key.
func (p *restartableProcess) addReinitializer(key kindAndName, r reinitializer) {
p.lock.Lock()
defer p.lock.Unlock()
p.reinitializers[key] = r
}
// reset acquires the lock and calls resetLH.
func (p *restartableProcess) reset() error {
p.lock.Lock()
defer p.lock.Unlock()
return p.resetLH()
}
// resetLH (re)launches the plugin process. It redispenses all previously dispensed plugins and reinitializes all the
// registered reinitializers using the newly dispensed plugins.
//
// Callers of resetLH *must* acquire the lock before calling it.
func (p *restartableProcess) resetLH() error {
if p.resetFailures > 10 {
return errors.Errorf("unable to restart plugin process: execeeded maximum number of reset failures")
}
process, err := newProcess(p.command, p.logger, p.logLevel)
if err != nil {
p.resetFailures++
return err
}
p.process = process
// Redispense any previously dispensed plugins, reinitializing if necessary.
// Start by creating a new map to hold the newly dispensed plugins.
newPlugins := make(map[kindAndName]interface{})
for key := range p.plugins {
// Re-dispense
dispensed, err := p.process.dispense(key)
if err != nil {
p.resetFailures++
return err
}
// Store in the new map
newPlugins[key] = dispensed
// Reinitialize
if r, found := p.reinitializers[key]; found {
if err := r.reinitialize(dispensed); err != nil {
p.resetFailures++
return err
}
}
}
// Make sure we update p's plugins!
p.plugins = newPlugins
p.resetFailures = 0
return nil
}
// resetIfNeeded checks if the plugin process has exited and resets p if it has.
func (p *restartableProcess) resetIfNeeded() error {
p.lock.Lock()
defer p.lock.Unlock()
if p.process.exited() {
p.logger.Info("Plugin process exited - restarting.")
return p.resetLH()
}
return nil
}
// getByKindAndName acquires the lock and calls getByKindAndNameLH.
func (p *restartableProcess) getByKindAndName(key kindAndName) (interface{}, error) {
p.lock.Lock()
defer p.lock.Unlock()
return p.getByKindAndNameLH(key)
}
// getByKindAndNameLH returns the dispensed plugin for key. If the plugin hasn't been dispensed before, it dispenses a
// new one.
func (p *restartableProcess) getByKindAndNameLH(key kindAndName) (interface{}, error) {
dispensed, found := p.plugins[key]
if found {
return dispensed, nil
}
dispensed, err := p.process.dispense(key)
if err != nil {
return nil, err
}
p.plugins[key] = dispensed
return p.plugins[key], nil
}
// stop terminates the plugin process.
func (p *restartableProcess) stop() {
p.lock.Lock()
p.process.kill()
p.lock.Unlock()
}
@@ -0,0 +1,87 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
api "github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/restore"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
)
// restartableRestoreItemAction is a restore item action for a given implementation (such as "pod"). It is associated with
// a restartableProcess, which may be shared and used to run multiple plugins. At the beginning of each method
// call, the restartableRestoreItemAction asks its restartableProcess to restart itself if needed (e.g. if the
// process terminated for any reason), then it proceeds with the actual call.
type restartableRestoreItemAction struct {
key kindAndName
sharedPluginProcess RestartableProcess
config map[string]string
}
// newRestartableRestoreItemAction returns a new restartableRestoreItemAction.
func newRestartableRestoreItemAction(name string, sharedPluginProcess RestartableProcess) *restartableRestoreItemAction {
r := &restartableRestoreItemAction{
key: kindAndName{kind: PluginKindRestoreItemAction, name: name},
sharedPluginProcess: sharedPluginProcess,
}
return r
}
// getRestoreItemAction returns the restore item action for this restartableRestoreItemAction. It does *not* restart the
// plugin process.
func (r *restartableRestoreItemAction) getRestoreItemAction() (restore.ItemAction, error) {
plugin, err := r.sharedPluginProcess.getByKindAndName(r.key)
if err != nil {
return nil, err
}
restoreItemAction, ok := plugin.(restore.ItemAction)
if !ok {
return nil, errors.Errorf("%T is not a restore.ItemAction!", plugin)
}
return restoreItemAction, nil
}
// getDelegate restarts the plugin process (if needed) and returns the restore item action for this restartableRestoreItemAction.
func (r *restartableRestoreItemAction) getDelegate() (restore.ItemAction, error) {
if err := r.sharedPluginProcess.resetIfNeeded(); err != nil {
return nil, err
}
return r.getRestoreItemAction()
}
// AppliesTo restarts the plugin's process if needed, then delegates the call.
func (r *restartableRestoreItemAction) AppliesTo() (restore.ResourceSelector, error) {
delegate, err := r.getDelegate()
if err != nil {
return restore.ResourceSelector{}, err
}
return delegate.AppliesTo()
}
// Execute restarts the plugin's process if needed, then delegates the call.
func (r *restartableRestoreItemAction) Execute(obj runtime.Unstructured, restore *api.Restore) (res runtime.Unstructured, warning error, err error) {
delegate, err := r.getDelegate()
if err != nil {
return nil, nil, err
}
return delegate.Execute(obj, restore)
}
@@ -0,0 +1,139 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"testing"
"github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/restore"
"github.com/heptio/ark/pkg/restore/mocks"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestRestartableGetRestoreItemAction(t *testing.T) {
tests := []struct {
name string
plugin interface{}
getError error
expectedError string
}{
{
name: "error getting by kind and name",
getError: errors.Errorf("get error"),
expectedError: "get error",
},
{
name: "wrong type",
plugin: 3,
expectedError: "int is not a restore.ItemAction!",
},
{
name: "happy path",
plugin: new(mocks.ItemAction),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
p := new(mockRestartableProcess)
defer p.AssertExpectations(t)
name := "pod"
key := kindAndName{kind: PluginKindRestoreItemAction, name: name}
p.On("getByKindAndName", key).Return(tc.plugin, tc.getError)
r := newRestartableRestoreItemAction(name, p)
a, err := r.getRestoreItemAction()
if tc.expectedError != "" {
assert.EqualError(t, err, tc.expectedError)
return
}
require.NoError(t, err)
assert.Equal(t, tc.plugin, a)
})
}
}
func TestRestartableRestoreItemActionGetDelegate(t *testing.T) {
p := new(mockRestartableProcess)
defer p.AssertExpectations(t)
// Reset error
p.On("resetIfNeeded").Return(errors.Errorf("reset error")).Once()
name := "pod"
r := newRestartableRestoreItemAction(name, p)
a, err := r.getDelegate()
assert.Nil(t, a)
assert.EqualError(t, err, "reset error")
// Happy path
p.On("resetIfNeeded").Return(nil)
expected := new(mocks.ItemAction)
key := kindAndName{kind: PluginKindRestoreItemAction, name: name}
p.On("getByKindAndName", key).Return(expected, nil)
a, err = r.getDelegate()
assert.NoError(t, err)
assert.Equal(t, expected, a)
}
func TestRestartableRestoreItemActionDelegatedFunctions(t *testing.T) {
r := new(v1.Restore)
pv := &unstructured.Unstructured{
Object: map[string]interface{}{
"color": "blue",
},
}
pvToReturn := &unstructured.Unstructured{
Object: map[string]interface{}{
"color": "green",
},
}
runRestartableDelegateTests(
t,
PluginKindRestoreItemAction,
func(key kindAndName, p RestartableProcess) interface{} {
return &restartableRestoreItemAction{
key: key,
sharedPluginProcess: p,
}
},
func() mockable {
return new(mocks.ItemAction)
},
restartableDelegateTest{
function: "AppliesTo",
inputs: []interface{}{},
expectedErrorOutputs: []interface{}{restore.ResourceSelector{}, errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{restore.ResourceSelector{IncludedNamespaces: []string{"a"}}, errors.Errorf("delegate error")},
},
restartableDelegateTest{
function: "Execute",
inputs: []interface{}{pv, r},
expectedErrorOutputs: []interface{}{nil, nil, errors.Errorf("reset error")},
expectedDelegateOutputs: []interface{}{pvToReturn, errors.Errorf("delegate warning"), errors.Errorf("delegate error")},
},
)
}
+53 -23
View File
@@ -21,7 +21,6 @@ import (
"github.com/hashicorp/go-plugin"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
"google.golang.org/grpc"
@@ -38,41 +37,41 @@ import (
// interface.
type RestoreItemActionPlugin struct {
plugin.NetRPCUnsupportedPlugin
impl restore.ItemAction
log *logrusAdapter
*pluginBase
}
// NewRestoreItemActionPlugin constructs a RestoreItemActionPlugin.
func NewRestoreItemActionPlugin(itemAction restore.ItemAction) *RestoreItemActionPlugin {
func NewRestoreItemActionPlugin(options ...pluginOption) *RestoreItemActionPlugin {
return &RestoreItemActionPlugin{
impl: itemAction,
pluginBase: newPluginBase(options...),
}
}
func (p *RestoreItemActionPlugin) Kind() PluginKind {
return PluginKindRestoreItemAction
}
// GRPCServer registers a RestoreItemAction gRPC server.
func (p *RestoreItemActionPlugin) GRPCServer(s *grpc.Server) error {
proto.RegisterRestoreItemActionServer(s, &RestoreItemActionGRPCServer{impl: p.impl})
return nil
}
//////////////////////////////////////////////////////////////////////////////
// client code
//////////////////////////////////////////////////////////////////////////////
// GRPCClient returns a RestoreItemAction gRPC client.
func (p *RestoreItemActionPlugin) GRPCClient(c *grpc.ClientConn) (interface{}, error) {
return &RestoreItemActionGRPCClient{grpcClient: proto.NewRestoreItemActionClient(c), log: p.log}, nil
return newClientDispenser(p.clientLogger, c, newRestoreItemActionGRPCClient), nil
}
// RestoreItemActionGRPCClient implements the backup/ItemAction interface and uses a
// gRPC client to make calls to the plugin server.
type RestoreItemActionGRPCClient struct {
*clientBase
grpcClient proto.RestoreItemActionClient
log *logrusAdapter
}
func newRestoreItemActionGRPCClient(base *clientBase, clientConn *grpc.ClientConn) interface{} {
return &RestoreItemActionGRPCClient{
clientBase: base,
grpcClient: proto.NewRestoreItemActionClient(clientConn),
}
}
func (c *RestoreItemActionGRPCClient) AppliesTo() (restore.ResourceSelector, error) {
res, err := c.grpcClient.AppliesTo(context.Background(), &proto.Empty{})
res, err := c.grpcClient.AppliesTo(context.Background(), &proto.AppliesToRequest{Plugin: c.plugin})
if err != nil {
return restore.ResourceSelector{}, err
}
@@ -98,6 +97,7 @@ func (c *RestoreItemActionGRPCClient) Execute(item runtime.Unstructured, restore
}
req := &proto.RestoreExecuteRequest{
Plugin: c.plugin,
Item: itemJSON,
Restore: restoreJSON,
}
@@ -120,18 +120,43 @@ func (c *RestoreItemActionGRPCClient) Execute(item runtime.Unstructured, restore
return &updatedItem, warning, nil
}
func (c *RestoreItemActionGRPCClient) SetLog(log logrus.FieldLogger) {
c.log.impl = log
//////////////////////////////////////////////////////////////////////////////
// server code
//////////////////////////////////////////////////////////////////////////////
// GRPCServer registers a RestoreItemAction gRPC server.
func (p *RestoreItemActionPlugin) GRPCServer(s *grpc.Server) error {
proto.RegisterRestoreItemActionServer(s, &RestoreItemActionGRPCServer{mux: p.serverMux})
return nil
}
// RestoreItemActionGRPCServer implements the proto-generated RestoreItemActionServer interface, and accepts
// gRPC calls and forwards them to an implementation of the pluggable interface.
type RestoreItemActionGRPCServer struct {
impl restore.ItemAction
mux *serverMux
}
func (s *RestoreItemActionGRPCServer) AppliesTo(ctx context.Context, req *proto.Empty) (*proto.AppliesToResponse, error) {
appliesTo, err := s.impl.AppliesTo()
func (s *RestoreItemActionGRPCServer) getImpl(name string) (restore.ItemAction, error) {
impl, err := s.mux.getHandler(name)
if err != nil {
return nil, err
}
itemAction, ok := impl.(restore.ItemAction)
if !ok {
return nil, errors.Errorf("%T is not a restore item action", impl)
}
return itemAction, nil
}
func (s *RestoreItemActionGRPCServer) AppliesTo(ctx context.Context, req *proto.AppliesToRequest) (*proto.AppliesToResponse, error) {
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
appliesTo, err := impl.AppliesTo()
if err != nil {
return nil, err
}
@@ -146,6 +171,11 @@ func (s *RestoreItemActionGRPCServer) AppliesTo(ctx context.Context, req *proto.
}
func (s *RestoreItemActionGRPCServer) Execute(ctx context.Context, req *proto.RestoreExecuteRequest) (*proto.RestoreExecuteResponse, error) {
impl, err := s.getImpl(req.Plugin)
if err != nil {
return nil, err
}
var (
item unstructured.Unstructured
restore api.Restore
@@ -159,7 +189,7 @@ func (s *RestoreItemActionGRPCServer) Execute(ctx context.Context, req *proto.Re
return nil, err
}
res, warning, err := s.impl.Execute(&item, &restore)
res, warning, err := impl.Execute(&item, &restore)
if err != nil {
return nil, err
}
+166
View File
@@ -0,0 +1,166 @@
/*
Copyright 2017 the Heptio Ark 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 plugin
import (
"os"
plugin "github.com/hashicorp/go-plugin"
"github.com/sirupsen/logrus"
)
// Handshake is configuration information that allows go-plugin clients and servers to perform a handshake.
//
// TODO(ncdc): this should probably be a function so it can't be mutated, and we should probably move it to
// handshake.go.
var Handshake = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "ARK_PLUGIN",
MagicCookieValue: "hello",
}
// Server serves registered plugin implementations.
type Server interface {
// RegisterBackupItemAction registers a backup item action.
RegisterBackupItemAction(name string, initializer HandlerInitializer) Server
// RegisterBackupItemActions registers multiple backup item actions.
RegisterBackupItemActions(map[string]HandlerInitializer) Server
// RegisterBlockStore registers a block store.
RegisterBlockStore(name string, initializer HandlerInitializer) Server
// RegisterBlockStores registers multiple block stores.
RegisterBlockStores(map[string]HandlerInitializer) Server
// RegisterObjectStore registers an object store.
RegisterObjectStore(name string, initializer HandlerInitializer) Server
// RegisterObjectStores registers multiple object stores.
RegisterObjectStores(map[string]HandlerInitializer) Server
// RegisterRestoreItemAction registers a restore item action.
RegisterRestoreItemAction(name string, initializer HandlerInitializer) Server
// RegisterRestoreItemActions registers multiple restore item actions.
RegisterRestoreItemActions(map[string]HandlerInitializer) Server
// Server runs the plugin server.
Serve()
}
// server implements Server.
type server struct {
backupItemAction *BackupItemActionPlugin
blockStore *BlockStorePlugin
objectStore *ObjectStorePlugin
restoreItemAction *RestoreItemActionPlugin
}
// NewServer returns a new Server
func NewServer(log logrus.FieldLogger) Server {
return &server{
backupItemAction: NewBackupItemActionPlugin(serverLogger(log)),
blockStore: NewBlockStorePlugin(serverLogger(log)),
objectStore: NewObjectStorePlugin(serverLogger(log)),
restoreItemAction: NewRestoreItemActionPlugin(serverLogger(log)),
}
}
func (s *server) RegisterBackupItemAction(name string, initializer HandlerInitializer) Server {
s.backupItemAction.register(name, initializer)
return s
}
func (s *server) RegisterBackupItemActions(m map[string]HandlerInitializer) Server {
for name := range m {
s.RegisterBackupItemAction(name, m[name])
}
return s
}
func (s *server) RegisterBlockStore(name string, initializer HandlerInitializer) Server {
s.blockStore.register(name, initializer)
return s
}
func (s *server) RegisterBlockStores(m map[string]HandlerInitializer) Server {
for name := range m {
s.RegisterBlockStore(name, m[name])
}
return s
}
func (s *server) RegisterObjectStore(name string, initializer HandlerInitializer) Server {
s.objectStore.register(name, initializer)
return s
}
func (s *server) RegisterObjectStores(m map[string]HandlerInitializer) Server {
for name := range m {
s.RegisterObjectStore(name, m[name])
}
return s
}
func (s *server) RegisterRestoreItemAction(name string, initializer HandlerInitializer) Server {
s.restoreItemAction.register(name, initializer)
return s
}
func (s *server) RegisterRestoreItemActions(m map[string]HandlerInitializer) Server {
for name := range m {
s.RegisterRestoreItemAction(name, m[name])
}
return s
}
// getNames returns a list of PluginIdentifiers registered with plugin.
func getNames(command string, kind PluginKind, plugin Interface) []PluginIdentifier {
var pluginIdentifiers []PluginIdentifier
for _, name := range plugin.names() {
id := PluginIdentifier{Command: command, Kind: kind, Name: name}
pluginIdentifiers = append(pluginIdentifiers, id)
}
return pluginIdentifiers
}
func (s *server) Serve() {
command := os.Args[0]
var pluginIdentifiers []PluginIdentifier
pluginIdentifiers = append(pluginIdentifiers, getNames(command, PluginKindBackupItemAction, s.backupItemAction)...)
pluginIdentifiers = append(pluginIdentifiers, getNames(command, PluginKindBlockStore, s.blockStore)...)
pluginIdentifiers = append(pluginIdentifiers, getNames(command, PluginKindObjectStore, s.objectStore)...)
pluginIdentifiers = append(pluginIdentifiers, getNames(command, PluginKindRestoreItemAction, s.restoreItemAction)...)
pluginLister := NewPluginLister(pluginIdentifiers...)
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: Handshake,
Plugins: map[string]plugin.Plugin{
string(PluginKindBackupItemAction): s.backupItemAction,
string(PluginKindBlockStore): s.blockStore,
string(PluginKindObjectStore): s.objectStore,
string(PluginKindPluginLister): NewPluginListerPlugin(pluginLister),
string(PluginKindRestoreItemAction): s.restoreItemAction,
},
GRPCServer: plugin.DefaultGRPCServer,
})
}
+76
View File
@@ -0,0 +1,76 @@
/*
Copyright 2018 the Heptio Ark 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 plugin
import (
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/util/sets"
)
// HandlerInitializer is a function that initializes and returns a new instance of one of Ark's plugin interfaces
// (ObjectStore, BlockStore, BackupItemAction, RestoreItemAction).
type HandlerInitializer func(logger logrus.FieldLogger) (interface{}, error)
// serverMux manages multiple implementations of a single plugin kind, such as pod and pvc BackupItemActions.
type serverMux struct {
kind PluginKind
initializers map[string]HandlerInitializer
handlers map[string]interface{}
serverLog logrus.FieldLogger
}
// newServerMux returns a new serverMux.
func newServerMux(logger logrus.FieldLogger) *serverMux {
return &serverMux{
initializers: make(map[string]HandlerInitializer),
handlers: make(map[string]interface{}),
serverLog: logger,
}
}
// register registers the initializer for name.
func (m *serverMux) register(name string, f HandlerInitializer) {
// TODO(ncdc): return an error on duplicate registrations for the same name.
m.initializers[name] = f
}
// names returns a list of all registered implementations.
func (m *serverMux) names() []string {
return sets.StringKeySet(m.initializers).List()
}
// getHandler returns the instance for a plugin with the given name. If an instance has already been initialized,
// that is returned. Otherwise, the instance is initialized by calling its initialization function.
func (m *serverMux) getHandler(name string) (interface{}, error) {
if instance, found := m.handlers[name]; found {
return instance, nil
}
initializer, found := m.initializers[name]
if !found {
return nil, errors.Errorf("unknown %v plugin: %s", m.kind, name)
}
instance, err := initializer(m.serverLog)
if err != nil {
return nil, err
}
m.handlers[name] = instance
return m.handlers[name], nil
}
-38
View File
@@ -1,38 +0,0 @@
/*
Copyright 2017 the Heptio Ark 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 plugin
import plugin "github.com/hashicorp/go-plugin"
// Handshake is configuration information that allows go-plugin
// clients and servers to perform a handshake.
var Handshake = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "ARK_PLUGIN",
MagicCookieValue: "hello",
}
// Serve serves the plugin p.
func Serve(p Interface) {
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: Handshake,
Plugins: map[string]plugin.Plugin{
string(p.Kind()): p,
},
GRPCServer: plugin.DefaultGRPCServer,
})
}
+15
View File
@@ -1,3 +1,18 @@
/*
Copyright 2018 the Heptio Ark contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package restic
import "sync"
+1 -3
View File
@@ -30,9 +30,7 @@ type jobAction struct {
}
func NewJobAction(logger logrus.FieldLogger) ItemAction {
return &jobAction{
logger: logger,
}
return &jobAction{logger: logger}
}
func (a *jobAction) AppliesTo() (ResourceSelector, error) {
+78
View File
@@ -0,0 +1,78 @@
/*
Copyright 2018 the Heptio Ark 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.
*/
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import mock "github.com/stretchr/testify/mock"
import restore "github.com/heptio/ark/pkg/restore"
import runtime "k8s.io/apimachinery/pkg/runtime"
import v1 "github.com/heptio/ark/pkg/apis/ark/v1"
// ItemAction is an autogenerated mock type for the ItemAction type
type ItemAction struct {
mock.Mock
}
// AppliesTo provides a mock function with given fields:
func (_m *ItemAction) AppliesTo() (restore.ResourceSelector, error) {
ret := _m.Called()
var r0 restore.ResourceSelector
if rf, ok := ret.Get(0).(func() restore.ResourceSelector); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(restore.ResourceSelector)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Execute provides a mock function with given fields: obj, _a1
func (_m *ItemAction) Execute(obj runtime.Unstructured, _a1 *v1.Restore) (runtime.Unstructured, error, error) {
ret := _m.Called(obj, _a1)
var r0 runtime.Unstructured
if rf, ok := ret.Get(0).(func(runtime.Unstructured, *v1.Restore) runtime.Unstructured); ok {
r0 = rf(obj, _a1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(runtime.Unstructured)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(runtime.Unstructured, *v1.Restore) error); ok {
r1 = rf(obj, _a1)
} else {
r1 = ret.Error(1)
}
var r2 error
if rf, ok := ret.Get(2).(func(runtime.Unstructured, *v1.Restore) error); ok {
r2 = rf(obj, _a1)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
+1 -3
View File
@@ -32,9 +32,7 @@ type podAction struct {
}
func NewPodAction(logger logrus.FieldLogger) ItemAction {
return &podAction{
logger: logger,
}
return &podAction{logger: logger}
}
func (a *podAction) AppliesTo() (ResourceSelector, error) {
+2 -18
View File
@@ -57,14 +57,13 @@ import (
"github.com/heptio/ark/pkg/util/collections"
"github.com/heptio/ark/pkg/util/filesystem"
"github.com/heptio/ark/pkg/util/kube"
"github.com/heptio/ark/pkg/util/logging"
arksync "github.com/heptio/ark/pkg/util/sync"
)
// Restorer knows how to restore a backup.
type Restorer interface {
// Restore restores the backup data from backupReader, returning warnings and errors.
Restore(restore *api.Restore, backup *api.Backup, backupReader io.Reader, logFile io.Writer, actions []ItemAction) (api.RestoreResult, api.RestoreResult)
Restore(log logrus.FieldLogger, restore *api.Restore, backup *api.Backup, backupReader io.Reader, actions []ItemAction) (api.RestoreResult, api.RestoreResult)
}
type gvString string
@@ -74,7 +73,6 @@ type kindString string
type kubernetesRestorer struct {
discoveryHelper discovery.Helper
dynamicFactory client.DynamicFactory
backupService cloudprovider.BackupService
snapshotService cloudprovider.SnapshotService
backupClient arkv1client.BackupsGetter
namespaceClient corev1.NamespaceInterface
@@ -147,7 +145,6 @@ func prioritizeResources(helper discovery.Helper, priorities []string, includedR
func NewKubernetesRestorer(
discoveryHelper discovery.Helper,
dynamicFactory client.DynamicFactory,
backupService cloudprovider.BackupService,
snapshotService cloudprovider.SnapshotService,
resourcePriorities []string,
backupClient arkv1client.BackupsGetter,
@@ -159,7 +156,6 @@ func NewKubernetesRestorer(
return &kubernetesRestorer{
discoveryHelper: discoveryHelper,
dynamicFactory: dynamicFactory,
backupService: backupService,
snapshotService: snapshotService,
backupClient: backupClient,
namespaceClient: namespaceClient,
@@ -175,7 +171,7 @@ func NewKubernetesRestorer(
// Restore executes a restore into the target Kubernetes cluster according to the restore spec
// and using data from the provided backup/backup reader. Returns a warnings and errors RestoreResult,
// respectively, summarizing info about the restore.
func (kr *kubernetesRestorer) Restore(restore *api.Restore, backup *api.Backup, backupReader io.Reader, logFile io.Writer, actions []ItemAction) (api.RestoreResult, api.RestoreResult) {
func (kr *kubernetesRestorer) Restore(log logrus.FieldLogger, restore *api.Restore, backup *api.Backup, backupReader io.Reader, actions []ItemAction) (api.RestoreResult, api.RestoreResult) {
// metav1.LabelSelectorAsSelector converts a nil LabelSelector to a
// Nothing Selector, i.e. a selector that matches nothing. We want
// a selector that matches everything. This can be accomplished by
@@ -190,14 +186,6 @@ func (kr *kubernetesRestorer) Restore(restore *api.Restore, backup *api.Backup,
return api.RestoreResult{}, api.RestoreResult{Ark: []string{err.Error()}}
}
gzippedLog := gzip.NewWriter(logFile)
defer gzippedLog.Close()
log := logrus.New()
log.Out = gzippedLog
log.Hooks.Add(&logging.ErrorLocationHook{})
log.Hooks.Add(&logging.LogLocationHook{})
// get resource includes-excludes
resourceIncludesExcludes := getResourceIncludesExcludes(kr.discoveryHelper, restore.Spec.IncludedResources, restore.Spec.ExcludedResources)
prioritizedResources, err := prioritizeResources(kr.discoveryHelper, kr.resourcePriorities, resourceIncludesExcludes, log)
@@ -742,10 +730,6 @@ func (ctx *context) restoreResource(resource, namespace, resourcePath string) (a
ctx.infof("Executing item action for %v", &groupResource)
if logSetter, ok := action.ItemAction.(logging.LogSetter); ok {
logSetter.SetLog(ctx.logger)
}
updatedObj, warning, err := action.Execute(obj, ctx.restore)
if warning != nil {
addToResult(&warnings, namespace, fmt.Errorf("warning preparing %s: %v", fullPath, warning))
+2 -4
View File
@@ -29,10 +29,8 @@ type serviceAction struct {
log logrus.FieldLogger
}
func NewServiceAction(log logrus.FieldLogger) ItemAction {
return &serviceAction{
log: log,
}
func NewServiceAction(logger logrus.FieldLogger) ItemAction {
return &serviceAction{log: logger}
}
func (a *serviceAction) AppliesTo() (ResourceSelector, error) {
+5
View File
@@ -33,6 +33,7 @@ type Interface interface {
ReadFile(filename string) ([]byte, error)
DirExists(path string) (bool, error)
TempFile(dir, prefix string) (NameWriteCloser, error)
Stat(path string) (os.FileInfo, error)
}
type NameWriteCloser interface {
@@ -85,3 +86,7 @@ func (fs *osFileSystem) DirExists(path string) (bool, error) {
func (fs *osFileSystem) TempFile(dir, prefix string) (NameWriteCloser, error) {
return ioutil.TempFile(dir, prefix)
}
func (fs *osFileSystem) Stat(path string) (os.FileInfo, error) {
return os.Stat(path)
}
+15
View File
@@ -1,3 +1,18 @@
/*
Copyright 2018 the Heptio Ark 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 logging
import (
-9
View File
@@ -1,9 +0,0 @@
package logging
import "github.com/sirupsen/logrus"
// LogSetter is an interface for a type that allows a FieldLogger
// to be set on it.
type LogSetter interface {
SetLog(logrus.FieldLogger)
}
-174
View File
@@ -1,174 +0,0 @@
/*
Copyright 2017 the Heptio Ark 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.
*/
// Code generated by mockery v1.0.0
package test
import io "io"
import mock "github.com/stretchr/testify/mock"
import time "time"
import v1 "github.com/heptio/ark/pkg/apis/ark/v1"
// BackupService is an autogenerated mock type for the BackupService type
type BackupService struct {
mock.Mock
}
// CreateSignedURL provides a mock function with given fields: target, bucket, ttl
func (_m *BackupService) CreateSignedURL(target v1.DownloadTarget, bucket, directory string, ttl time.Duration) (string, error) {
ret := _m.Called(target, bucket, directory, ttl)
var r0 string
if rf, ok := ret.Get(0).(func(v1.DownloadTarget, string, string, time.Duration) string); ok {
r0 = rf(target, bucket, directory, ttl)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(v1.DownloadTarget, string, string, time.Duration) error); ok {
r1 = rf(target, bucket, directory, ttl)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteBackupDir provides a mock function with given fields: bucket, backupName
func (_m *BackupService) DeleteBackupDir(bucket string, backupName string) error {
ret := _m.Called(bucket, backupName)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(bucket, backupName)
} else {
r0 = ret.Error(0)
}
return r0
}
// DownloadBackup provides a mock function with given fields: bucket, name
func (_m *BackupService) DownloadBackup(bucket string, name string) (io.ReadCloser, error) {
ret := _m.Called(bucket, name)
var r0 io.ReadCloser
if rf, ok := ret.Get(0).(func(string, string) io.ReadCloser); ok {
r0 = rf(bucket, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(io.ReadCloser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(bucket, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetAllBackups provides a mock function with given fields: bucket
func (_m *BackupService) GetAllBackups(bucket string) ([]*v1.Backup, error) {
ret := _m.Called(bucket)
var r0 []*v1.Backup
if rf, ok := ret.Get(0).(func(string) []*v1.Backup); ok {
r0 = rf(bucket)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*v1.Backup)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(bucket)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetBackup provides a mock function with given fields: bucket, name
func (_m *BackupService) GetBackup(bucket string, name string) (*v1.Backup, error) {
ret := _m.Called(bucket, name)
var r0 *v1.Backup
if rf, ok := ret.Get(0).(func(string, string) *v1.Backup); ok {
r0 = rf(bucket, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1.Backup)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(bucket, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UploadBackup provides a mock function with given fields: bucket, name, metadata, backup, log
func (_m *BackupService) UploadBackup(bucket string, name string, metadata io.Reader, backup io.Reader, log io.Reader) error {
ret := _m.Called(bucket, name, metadata, backup, log)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, io.Reader, io.Reader, io.Reader) error); ok {
r0 = rf(bucket, name, metadata, backup, log)
} else {
r0 = ret.Error(0)
}
return r0
}
// UploadRestoreLog provides a mock function with given fields: bucket, backup, restore, log
func (_m *BackupService) UploadRestoreLog(bucket string, backup string, restore string, log io.Reader) error {
ret := _m.Called(bucket, backup, restore, log)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, io.Reader) error); ok {
r0 = rf(bucket, backup, restore, log)
} else {
r0 = ret.Error(0)
}
return r0
}
// UploadRestoreResults provides a mock function with given fields: bucket, backup, restore, results
func (_m *BackupService) UploadRestoreResults(bucket string, backup string, restore string, results io.Reader) error {
ret := _m.Called(bucket, backup, restore, results)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, io.Reader) error); ok {
r0 = rf(bucket, backup, restore, results)
} else {
r0 = ret.Error(0)
}
return r0
}
-71
View File
@@ -1,71 +0,0 @@
/*
Copyright 2017 the Heptio Ark 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 (
"io"
"github.com/stretchr/testify/mock"
"github.com/heptio/ark/pkg/apis/ark/v1"
)
type FakeBackupService struct {
mock.Mock
}
func (f *FakeBackupService) GetAllBackups(bucket string) ([]*v1.Backup, error) {
args := f.Called(bucket)
var backups []*v1.Backup
b := args.Get(0)
if b != nil {
backups = b.([]*v1.Backup)
}
return backups, args.Error(1)
}
func (f *FakeBackupService) UploadBackup(bucket, name string, metadata, backup io.Reader) error {
args := f.Called(bucket, name, metadata, backup)
return args.Error(0)
}
func (f *FakeBackupService) DownloadBackup(bucket, name string) (io.ReadCloser, error) {
args := f.Called(bucket, name)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
func (f *FakeBackupService) DeleteBackup(bucket, backupName string) error {
args := f.Called(bucket, backupName)
return args.Error(0)
}
func (f *FakeBackupService) GetBackup(bucket, name string) (*v1.Backup, error) {
var (
args = f.Called(bucket, name)
b = args.Get(0)
backup *v1.Backup
)
if b != nil {
backup = b.(*v1.Backup)
}
return backup, args.Error(1)
}
+27
View File
@@ -1,3 +1,18 @@
/*
Copyright 2018 the Heptio Ark 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 (
@@ -50,6 +65,10 @@ func (fs *FakeFileSystem) DirExists(path string) (bool, error) {
return afero.DirExists(fs.fs, path)
}
func (fs *FakeFileSystem) Stat(path string) (os.FileInfo, error) {
return fs.fs.Stat(path)
}
func (fs *FakeFileSystem) WithFile(path string, data []byte) *FakeFileSystem {
file, _ := fs.fs.Create(path)
file.Write(data)
@@ -58,6 +77,14 @@ func (fs *FakeFileSystem) WithFile(path string, data []byte) *FakeFileSystem {
return fs
}
func (fs *FakeFileSystem) WithFileAndMode(path string, data []byte, mode os.FileMode) *FakeFileSystem {
file, _ := fs.fs.OpenFile(path, os.O_CREATE|os.O_RDWR, mode)
file.Write(data)
file.Close()
return fs
}
func (fs *FakeFileSystem) WithDirectory(path string) *FakeFileSystem {
fs.fs.MkdirAll(path, 0755)
return fs
+15
View File
@@ -1,3 +1,18 @@
/*
Copyright 2018 the Heptio Ark 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 (

Some files were not shown because too many files have changed in this diff Show More