diff --git a/pkg/backup/backup.go b/pkg/backup/backup.go index c2812656e..f3cac18b0 100644 --- a/pkg/backup/backup.go +++ b/pkg/backup/backup.go @@ -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") diff --git a/pkg/backup/backup_pv_action.go b/pkg/backup/backup_pv_action.go index 684e2bfa6..4b9cceaa0 100644 --- a/pkg/backup/backup_pv_action.go +++ b/pkg/backup/backup_pv_action.go @@ -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) { diff --git a/pkg/backup/backup_test.go b/pkg/backup/backup_test.go index f52cbbc0d..d53a0a9b7 100644 --- a/pkg/backup/backup_test.go +++ b/pkg/backup/backup_test.go @@ -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) diff --git a/pkg/backup/item_backupper.go b/pkg/backup/item_backupper.go index 2e4d5010e..ea36a8291 100644 --- a/pkg/backup/item_backupper.go +++ b/pkg/backup/item_backupper.go @@ -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 diff --git a/pkg/backup/mocks/item_action.go b/pkg/backup/mocks/item_action.go index 6bd01e10e..a0046d556 100644 --- a/pkg/backup/mocks/item_action.go +++ b/pkg/backup/mocks/item_action.go @@ -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 diff --git a/pkg/backup/pod_action.go b/pkg/backup/pod_action.go index e940bc437..e4ce43087 100644 --- a/pkg/backup/pod_action.go +++ b/pkg/backup/pod_action.go @@ -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. diff --git a/pkg/backup/service_account_action.go b/pkg/backup/service_account_action.go index 28c92ee9b..badfafbd7 100644 --- a/pkg/backup/service_account_action.go +++ b/pkg/backup/service_account_action.go @@ -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 } diff --git a/pkg/buildinfo/version_test.go b/pkg/buildinfo/version_test.go index 5f4479991..c7e419127 100644 --- a/pkg/buildinfo/version_test.go +++ b/pkg/buildinfo/version_test.go @@ -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 ( diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 4ab5107d0..7aaf4cadf 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -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 ( diff --git a/pkg/cloudprovider/aws/block_store.go b/pkg/cloudprovider/aws/block_store.go index 8960e7c9b..1b1075c99 100644 --- a/pkg/cloudprovider/aws/block_store.go +++ b/pkg/cloudprovider/aws/block_store.go @@ -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 { diff --git a/pkg/cloudprovider/aws/object_store.go b/pkg/cloudprovider/aws/object_store.go index 7784c205c..5df9714f6 100644 --- a/pkg/cloudprovider/aws/object_store.go +++ b/pkg/cloudprovider/aws/object_store.go @@ -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 { diff --git a/pkg/cloudprovider/azure/block_store.go b/pkg/cloudprovider/azure/block_store.go index ca42f80a4..55ba4501a 100644 --- a/pkg/cloudprovider/azure/block_store.go +++ b/pkg/cloudprovider/azure/block_store.go @@ -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 { diff --git a/pkg/cloudprovider/azure/object_store.go b/pkg/cloudprovider/azure/object_store.go index 5c9c528f5..392030b2a 100644 --- a/pkg/cloudprovider/azure/object_store.go +++ b/pkg/cloudprovider/azure/object_store.go @@ -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 { diff --git a/pkg/cloudprovider/backup_cache.go b/pkg/cloudprovider/backup_cache.go index b5dd006b5..06abc99fb 100644 --- a/pkg/cloudprovider/backup_cache.go +++ b/pkg/cloudprovider/backup_cache.go @@ -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() diff --git a/pkg/cloudprovider/backup_cache_test.go b/pkg/cloudprovider/backup_cache_test.go index cc2ab424a..4eec9481b 100644 --- a/pkg/cloudprovider/backup_cache_test.go +++ b/pkg/cloudprovider/backup_cache_test.go @@ -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) } diff --git a/pkg/cloudprovider/backup_service.go b/pkg/cloudprovider/backup_service.go index 5b4f1e427..49aaebefc 100644 --- a/pkg/cloudprovider/backup_service.go +++ b/pkg/cloudprovider/backup_service.go @@ -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) } diff --git a/pkg/cloudprovider/backup_service_test.go b/pkg/cloudprovider/backup_service_test.go index 4d09006e8..17bd8adc3 100644 --- a/pkg/cloudprovider/backup_service_test.go +++ b/pkg/cloudprovider/backup_service_test.go @@ -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) }) } } diff --git a/pkg/cloudprovider/gcp/block_store.go b/pkg/cloudprovider/gcp/block_store.go index 0fe257131..764edd7c5 100644 --- a/pkg/cloudprovider/gcp/block_store.go +++ b/pkg/cloudprovider/gcp/block_store.go @@ -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 { diff --git a/pkg/cloudprovider/gcp/object_store.go b/pkg/cloudprovider/gcp/object_store.go index ac8510a3e..777a7563c 100644 --- a/pkg/cloudprovider/gcp/object_store.go +++ b/pkg/cloudprovider/gcp/object_store.go @@ -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 { diff --git a/pkg/cloudprovider/gcp/object_store_test.go b/pkg/cloudprovider/gcp/object_store_test.go index f2730d87f..419a8d2ec 100644 --- a/pkg/cloudprovider/gcp/object_store_test.go +++ b/pkg/cloudprovider/gcp/object_store_test.go @@ -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")) diff --git a/pkg/cloudprovider/mocks/backup_lister.go b/pkg/cloudprovider/mocks/backup_lister.go new file mode 100644 index 000000000..f7d901c68 --- /dev/null +++ b/pkg/cloudprovider/mocks/backup_lister.go @@ -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 +} diff --git a/pkg/cloudprovider/mocks/block_store.go b/pkg/cloudprovider/mocks/block_store.go new file mode 100644 index 000000000..baa8aa480 --- /dev/null +++ b/pkg/cloudprovider/mocks/block_store.go @@ -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 +} diff --git a/pkg/cmd/cli/plugin/add_test.go b/pkg/cmd/cli/plugin/add_test.go index d244a3623..706b175b1 100644 --- a/pkg/cmd/cli/plugin/add_test.go +++ b/pkg/cmd/cli/plugin/add_test.go @@ -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 ( diff --git a/pkg/cmd/cli/restic/server.go b/pkg/cmd/cli/restic/server.go index c0ab1297c..6767616b6 100644 --- a/pkg/cmd/cli/restic/server.go +++ b/pkg/cmd/cli/restic/server.go @@ -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 ( diff --git a/pkg/cmd/server/plugin/plugin.go b/pkg/cmd/server/plugin/plugin.go index 48ee1da30..c289ee4b7 100644 --- a/pkg/cmd/server/plugin/plugin.go +++ b/pkg/cmd/server/plugin/plugin.go @@ -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 +} diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index 4f2eec767..4c662cb43 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -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, ) diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 6cc1a558d..36613191b 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -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") diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index 94230fafd..27a000f1f 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -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 -} diff --git a/pkg/controller/backup_deletion_controller.go b/pkg/controller/backup_deletion_controller.go index fcc4d8908..c5e4286ce 100644 --- a/pkg/controller/backup_deletion_controller.go +++ b/pkg/controller/backup_deletion_controller.go @@ -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 diff --git a/pkg/controller/backup_deletion_controller_test.go b/pkg/controller/backup_deletion_controller_test.go index 51eb68df4..e3722ef39 100644 --- a/pkg/controller/backup_deletion_controller_test.go +++ b/pkg/controller/backup_deletion_controller_test.go @@ -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) diff --git a/pkg/controller/backup_sync_controller.go b/pkg/controller/backup_sync_controller.go index a97ff1c2a..1f30e528e 100644 --- a/pkg/controller/backup_sync_controller.go +++ b/pkg/controller/backup_sync_controller.go @@ -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 diff --git a/pkg/controller/backup_sync_controller_test.go b/pkg/controller/backup_sync_controller_test.go index de2623d83..e990eb629 100644 --- a/pkg/controller/backup_sync_controller_test.go +++ b/pkg/controller/backup_sync_controller_test.go @@ -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, diff --git a/pkg/controller/download_request_controller.go b/pkg/controller/download_request_controller.go index ae32a6a2b..a497421cc 100644 --- a/pkg/controller/download_request_controller.go +++ b/pkg/controller/download_request_controller.go @@ -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 } diff --git a/pkg/controller/download_request_controller_test.go b/pkg/controller/download_request_controller_test.go index f1165b479..25505e36a 100644 --- a/pkg/controller/download_request_controller_test.go +++ b/pkg/controller/download_request_controller_test.go @@ -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 diff --git a/pkg/controller/restore_controller.go b/pkg/controller/restore_controller.go index 6eae4ee55..7746b378e 100644 --- a/pkg/controller/restore_controller.go +++ b/pkg/controller/restore_controller.go @@ -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 } diff --git a/pkg/controller/restore_controller_test.go b/pkg/controller/restore_controller_test.go index b1b5aa53c..a618e2ee8 100644 --- a/pkg/controller/restore_controller_test.go +++ b/pkg/controller/restore_controller_test.go @@ -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 diff --git a/pkg/plugin/backup_item_action.go b/pkg/plugin/backup_item_action.go index 0e4cd54d6..3c789a06f 100644 --- a/pkg/plugin/backup_item_action.go +++ b/pkg/plugin/backup_item_action.go @@ -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 } diff --git a/pkg/plugin/backup_item_action_test.go b/pkg/plugin/backup_item_action_test.go index 00179b3ae..1cfee94ce 100644 --- a/pkg/plugin/backup_item_action_test.go +++ b/pkg/plugin/backup_item_action_test.go @@ -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 diff --git a/pkg/plugin/block_store.go b/pkg/plugin/block_store.go index 7d1a5e058..7fd01ecb4 100644 --- a/pkg/plugin/block_store.go +++ b/pkg/plugin/block_store.go @@ -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 } diff --git a/pkg/plugin/client_builder.go b/pkg/plugin/client_builder.go index cd98c4bd6..4f5ec9ae2 100644 --- a/pkg/plugin/client_builder.go +++ b/pkg/plugin/client_builder.go @@ -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()) } diff --git a/pkg/plugin/client_builder_test.go b/pkg/plugin/client_builder_test.go new file mode 100644 index 000000000..6cd179742 --- /dev/null +++ b/pkg/plugin/client_builder_test.go @@ -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) +} diff --git a/pkg/plugin/client_dispenser.go b/pkg/plugin/client_dispenser.go new file mode 100644 index 000000000..2efed9ed4 --- /dev/null +++ b/pkg/plugin/client_dispenser.go @@ -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 +} diff --git a/pkg/plugin/client_dispenser_test.go b/pkg/plugin/client_dispenser_test.go new file mode 100644 index 000000000..67a8dbda1 --- /dev/null +++ b/pkg/plugin/client_dispenser_test.go @@ -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) +} diff --git a/pkg/plugin/client_store.go b/pkg/plugin/client_store.go deleted file mode 100644 index 8656d8ac1..000000000 --- a/pkg/plugin/client_store.go +++ /dev/null @@ -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}) -} diff --git a/pkg/plugin/generated/BackupItemAction.pb.go b/pkg/plugin/generated/BackupItemAction.pb.go index a5a7a7d7c..73bfcd6c3 100644 --- a/pkg/plugin/generated/BackupItemAction.pb.go +++ b/pkg/plugin/generated/BackupItemAction.pb.go @@ -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, } diff --git a/pkg/plugin/generated/BlockStore.pb.go b/pkg/plugin/generated/BlockStore.pb.go index 81de53471..09382ee5d 100644 --- a/pkg/plugin/generated/BlockStore.pb.go +++ b/pkg/plugin/generated/BlockStore.pb.go @@ -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, } diff --git a/pkg/plugin/generated/ObjectStore.pb.go b/pkg/plugin/generated/ObjectStore.pb.go index 10f0a4f8d..38f2e9904 100644 --- a/pkg/plugin/generated/ObjectStore.pb.go +++ b/pkg/plugin/generated/ObjectStore.pb.go @@ -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, } diff --git a/pkg/plugin/generated/PluginLister.pb.go b/pkg/plugin/generated/PluginLister.pb.go new file mode 100644 index 000000000..0f4fe7c87 --- /dev/null +++ b/pkg/plugin/generated/PluginLister.pb.go @@ -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, +} diff --git a/pkg/plugin/generated/RestoreItemAction.pb.go b/pkg/plugin/generated/RestoreItemAction.pb.go index 8cc5d44e5..e048be154 100644 --- a/pkg/plugin/generated/RestoreItemAction.pb.go +++ b/pkg/plugin/generated/RestoreItemAction.pb.go @@ -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, } diff --git a/pkg/plugin/generated/Shared.pb.go b/pkg/plugin/generated/Shared.pb.go index 8f2de7153..619a2153e 100644 --- a/pkg/plugin/generated/Shared.pb.go +++ b/pkg/plugin/generated/Shared.pb.go @@ -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, } diff --git a/pkg/plugin/interface.go b/pkg/plugin/interface.go index 961bfc034..3b66a4f51 100644 --- a/pkg/plugin/interface.go +++ b/pkg/plugin/interface.go @@ -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 } diff --git a/pkg/plugin/logger_test.go b/pkg/plugin/logger_test.go new file mode 100644 index 000000000..51193d6ce --- /dev/null +++ b/pkg/plugin/logger_test.go @@ -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]) + } +} diff --git a/pkg/plugin/logrus_adapter_test.go b/pkg/plugin/logrus_adapter_test.go index 98e2872ef..6e394f8ee 100644 --- a/pkg/plugin/logrus_adapter_test.go +++ b/pkg/plugin/logrus_adapter_test.go @@ -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 ( diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 2e7b776a9..698c1c1b7 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -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 } diff --git a/pkg/plugin/manager_test.go b/pkg/plugin/manager_test.go new file mode 100644 index 000000000..6c9aa2fa5 --- /dev/null +++ b/pkg/plugin/manager_test.go @@ -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) + } + }) + } +} diff --git a/pkg/plugin/mocks/manager.go b/pkg/plugin/mocks/manager.go new file mode 100644 index 000000000..0d0a26182 --- /dev/null +++ b/pkg/plugin/mocks/manager.go @@ -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 +} diff --git a/pkg/plugin/mocks/process_factory.go b/pkg/plugin/mocks/process_factory.go new file mode 100644 index 000000000..8ec4af37f --- /dev/null +++ b/pkg/plugin/mocks/process_factory.go @@ -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 +} diff --git a/pkg/plugin/object_store.go b/pkg/plugin/object_store.go index 8896881d4..918fac2ef 100644 --- a/pkg/plugin/object_store.go +++ b/pkg/plugin/object_store.go @@ -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 } diff --git a/pkg/plugin/plugin_base.go b/pkg/plugin/plugin_base.go new file mode 100644 index 000000000..51acd3e95 --- /dev/null +++ b/pkg/plugin/plugin_base.go @@ -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) + } +} diff --git a/pkg/plugin/plugin_base_test.go b/pkg/plugin/plugin_base_test.go new file mode 100644 index 000000000..76a4b0860 --- /dev/null +++ b/pkg/plugin/plugin_base_test.go @@ -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) +} diff --git a/pkg/plugin/plugin_kinds.go b/pkg/plugin/plugin_kinds.go new file mode 100644 index 000000000..3891fce83 --- /dev/null +++ b/pkg/plugin/plugin_kinds.go @@ -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(), +) diff --git a/pkg/plugin/plugin_kinds_test.go b/pkg/plugin/plugin_kinds_test.go new file mode 100644 index 000000000..ef614a8a2 --- /dev/null +++ b/pkg/plugin/plugin_kinds_test.go @@ -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)) +} diff --git a/pkg/plugin/plugin_lister.go b/pkg/plugin/plugin_lister.go new file mode 100644 index 000000000..460a418c4 --- /dev/null +++ b/pkg/plugin/plugin_lister.go @@ -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 +} diff --git a/pkg/plugin/process.go b/pkg/plugin/process.go new file mode 100644 index 000000000..090d1ad23 --- /dev/null +++ b/pkg/plugin/process.go @@ -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() +} diff --git a/pkg/plugin/process_test.go b/pkg/plugin/process_test.go new file mode 100644 index 000000000..4785da5c8 --- /dev/null +++ b/pkg/plugin/process_test.go @@ -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) + }) + } +} diff --git a/pkg/plugin/proto/BackupItemAction.proto b/pkg/plugin/proto/BackupItemAction.proto index 900cb850f..a3d58a47a 100644 --- a/pkg/plugin/proto/BackupItemAction.proto +++ b/pkg/plugin/proto/BackupItemAction.proto @@ -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); } diff --git a/pkg/plugin/proto/BlockStore.proto b/pkg/plugin/proto/BlockStore.proto index 9827b1f45..4a943089c 100644 --- a/pkg/plugin/proto/BlockStore.proto +++ b/pkg/plugin/proto/BlockStore.proto @@ -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 tags = 3; + string plugin = 1; + string volumeID = 2; + string volumeAZ = 3; + map 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 { diff --git a/pkg/plugin/proto/ObjectStore.proto b/pkg/plugin/proto/ObjectStore.proto index 072d17b6d..ef5461fb7 100644 --- a/pkg/plugin/proto/ObjectStore.proto +++ b/pkg/plugin/proto/ObjectStore.proto @@ -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 { diff --git a/pkg/plugin/proto/PluginLister.proto b/pkg/plugin/proto/PluginLister.proto new file mode 100644 index 000000000..caa8b02aa --- /dev/null +++ b/pkg/plugin/proto/PluginLister.proto @@ -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); +} diff --git a/pkg/plugin/proto/RestoreItemAction.proto b/pkg/plugin/proto/RestoreItemAction.proto index 5e9ecd865..c5dea92bd 100644 --- a/pkg/plugin/proto/RestoreItemAction.proto +++ b/pkg/plugin/proto/RestoreItemAction.proto @@ -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); } diff --git a/pkg/plugin/proto/Shared.proto b/pkg/plugin/proto/Shared.proto index 6773ef1f1..e7c5d8a94 100644 --- a/pkg/plugin/proto/Shared.proto +++ b/pkg/plugin/proto/Shared.proto @@ -4,7 +4,12 @@ package generated; message Empty {} message InitRequest { - map config = 1; + string plugin = 1; + map config = 2; +} + +message AppliesToRequest { + string plugin = 1; } message AppliesToResponse { diff --git a/pkg/plugin/registry.go b/pkg/plugin/registry.go index fc5e11694..817e0d814 100644 --- a/pkg/plugin/registry.go +++ b/pkg/plugin/registry.go @@ -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 ®istry{ - 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, + ) } diff --git a/pkg/plugin/registry_test.go b/pkg/plugin/registry_test.go new file mode 100644 index 000000000..acbad1e43 --- /dev/null +++ b/pkg/plugin/registry_test.go @@ -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) +} diff --git a/pkg/plugin/restartable_backup_item_action.go b/pkg/plugin/restartable_backup_item_action.go new file mode 100644 index 000000000..c0b1fca99 --- /dev/null +++ b/pkg/plugin/restartable_backup_item_action.go @@ -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) +} diff --git a/pkg/plugin/restartable_backup_item_action_test.go b/pkg/plugin/restartable_backup_item_action_test.go new file mode 100644 index 000000000..20854191e --- /dev/null +++ b/pkg/plugin/restartable_backup_item_action_test.go @@ -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")}, + }, + ) +} diff --git a/pkg/plugin/restartable_block_store.go b/pkg/plugin/restartable_block_store.go new file mode 100644 index 000000000..fe132957c --- /dev/null +++ b/pkg/plugin/restartable_block_store.go @@ -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) +} diff --git a/pkg/plugin/restartable_block_store_test.go b/pkg/plugin/restartable_block_store_test.go new file mode 100644 index 000000000..d03f77718 --- /dev/null +++ b/pkg/plugin/restartable_block_store_test.go @@ -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")}, + }, + ) +} diff --git a/pkg/plugin/restartable_delegate_test.go b/pkg/plugin/restartable_delegate_test.go new file mode 100644 index 000000000..a87b2f15a --- /dev/null +++ b/pkg/plugin/restartable_delegate_test.go @@ -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) + }) + } +} diff --git a/pkg/plugin/restartable_object_store.go b/pkg/plugin/restartable_object_store.go new file mode 100644 index 000000000..34b8388f4 --- /dev/null +++ b/pkg/plugin/restartable_object_store.go @@ -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) +} diff --git a/pkg/plugin/restartable_object_store_test.go b/pkg/plugin/restartable_object_store_test.go new file mode 100644 index 000000000..76811a905 --- /dev/null +++ b/pkg/plugin/restartable_object_store_test.go @@ -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")}, + }, + ) +} diff --git a/pkg/plugin/restartable_process.go b/pkg/plugin/restartable_process.go new file mode 100644 index 000000000..e63fe0bcd --- /dev/null +++ b/pkg/plugin/restartable_process.go @@ -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() +} diff --git a/pkg/plugin/restartable_restore_item_action.go b/pkg/plugin/restartable_restore_item_action.go new file mode 100644 index 000000000..aa3fdb85d --- /dev/null +++ b/pkg/plugin/restartable_restore_item_action.go @@ -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) +} diff --git a/pkg/plugin/restartable_restore_item_action_test.go b/pkg/plugin/restartable_restore_item_action_test.go new file mode 100644 index 000000000..618faae44 --- /dev/null +++ b/pkg/plugin/restartable_restore_item_action_test.go @@ -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")}, + }, + ) +} diff --git a/pkg/plugin/restore_item_action.go b/pkg/plugin/restore_item_action.go index 53953ba80..dbf91eacf 100644 --- a/pkg/plugin/restore_item_action.go +++ b/pkg/plugin/restore_item_action.go @@ -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 } diff --git a/pkg/plugin/server.go b/pkg/plugin/server.go new file mode 100644 index 000000000..c22b6f486 --- /dev/null +++ b/pkg/plugin/server.go @@ -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, + }) +} diff --git a/pkg/plugin/server_mux.go b/pkg/plugin/server_mux.go new file mode 100644 index 000000000..1aaeb72bb --- /dev/null +++ b/pkg/plugin/server_mux.go @@ -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 +} diff --git a/pkg/plugin/shared.go b/pkg/plugin/shared.go deleted file mode 100644 index 969490aa0..000000000 --- a/pkg/plugin/shared.go +++ /dev/null @@ -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, - }) -} diff --git a/pkg/restic/repo_locker.go b/pkg/restic/repo_locker.go index 5bfbd6901..1732d63ea 100644 --- a/pkg/restic/repo_locker.go +++ b/pkg/restic/repo_locker.go @@ -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" diff --git a/pkg/restore/job_action.go b/pkg/restore/job_action.go index e81219b54..22056cb27 100644 --- a/pkg/restore/job_action.go +++ b/pkg/restore/job_action.go @@ -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) { diff --git a/pkg/restore/mocks/item_action.go b/pkg/restore/mocks/item_action.go new file mode 100644 index 000000000..39d8a5126 --- /dev/null +++ b/pkg/restore/mocks/item_action.go @@ -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 +} diff --git a/pkg/restore/pod_action.go b/pkg/restore/pod_action.go index 6d0b8b5cd..2dd7e883c 100644 --- a/pkg/restore/pod_action.go +++ b/pkg/restore/pod_action.go @@ -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) { diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 39e2992d0..caaad0700 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -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)) diff --git a/pkg/restore/service_action.go b/pkg/restore/service_action.go index 3f48b91de..3bb99165e 100644 --- a/pkg/restore/service_action.go +++ b/pkg/restore/service_action.go @@ -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) { diff --git a/pkg/util/filesystem/file_system.go b/pkg/util/filesystem/file_system.go index d8fbc472b..e99d17846 100644 --- a/pkg/util/filesystem/file_system.go +++ b/pkg/util/filesystem/file_system.go @@ -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) +} diff --git a/pkg/util/logging/log_level_flag.go b/pkg/util/logging/log_level_flag.go index d3b0e150a..88a0410a8 100644 --- a/pkg/util/logging/log_level_flag.go +++ b/pkg/util/logging/log_level_flag.go @@ -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 ( diff --git a/pkg/util/logging/log_setter.go b/pkg/util/logging/log_setter.go deleted file mode 100644 index 25c71fb42..000000000 --- a/pkg/util/logging/log_setter.go +++ /dev/null @@ -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) -} diff --git a/pkg/util/test/backup_service.go b/pkg/util/test/backup_service.go deleted file mode 100644 index 6ceaba87a..000000000 --- a/pkg/util/test/backup_service.go +++ /dev/null @@ -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 -} diff --git a/pkg/util/test/fake_backup_service.go b/pkg/util/test/fake_backup_service.go deleted file mode 100644 index 9099edd72..000000000 --- a/pkg/util/test/fake_backup_service.go +++ /dev/null @@ -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) -} diff --git a/pkg/util/test/fake_file_system.go b/pkg/util/test/fake_file_system.go index 9717ef65b..176415fc6 100644 --- a/pkg/util/test/fake_file_system.go +++ b/pkg/util/test/fake_file_system.go @@ -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 diff --git a/pkg/util/test/helpers.go b/pkg/util/test/helpers.go index ad27b2351..eb2ccc9f2 100644 --- a/pkg/util/test/helpers.go +++ b/pkg/util/test/helpers.go @@ -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 ( diff --git a/pkg/util/test/mock_pod_command_executor.go b/pkg/util/test/mock_pod_command_executor.go index 975661097..97de9c832 100644 --- a/pkg/util/test/mock_pod_command_executor.go +++ b/pkg/util/test/mock_pod_command_executor.go @@ -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 (