mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-19 14:34:17 +00:00
add restic integration for doing pod volume backups/restores
Signed-off-by: Steve Kriss <steve@heptio.com>
This commit is contained in:
+43
-17
@@ -19,8 +19,10 @@ package backup
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -34,6 +36,8 @@ import (
|
||||
"github.com/heptio/ark/pkg/client"
|
||||
"github.com/heptio/ark/pkg/cloudprovider"
|
||||
"github.com/heptio/ark/pkg/discovery"
|
||||
"github.com/heptio/ark/pkg/podexec"
|
||||
"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"
|
||||
@@ -48,11 +52,13 @@ type Backupper interface {
|
||||
|
||||
// kubernetesBackupper implements Backupper.
|
||||
type kubernetesBackupper struct {
|
||||
dynamicFactory client.DynamicFactory
|
||||
discoveryHelper discovery.Helper
|
||||
podCommandExecutor podCommandExecutor
|
||||
groupBackupperFactory groupBackupperFactory
|
||||
snapshotService cloudprovider.SnapshotService
|
||||
dynamicFactory client.DynamicFactory
|
||||
discoveryHelper discovery.Helper
|
||||
podCommandExecutor podexec.PodCommandExecutor
|
||||
groupBackupperFactory groupBackupperFactory
|
||||
snapshotService cloudprovider.SnapshotService
|
||||
resticBackupperFactory restic.BackupperFactory
|
||||
resticTimeout time.Duration
|
||||
}
|
||||
|
||||
type itemKey struct {
|
||||
@@ -87,15 +93,19 @@ func cohabitatingResources() map[string]*cohabitatingResource {
|
||||
func NewKubernetesBackupper(
|
||||
discoveryHelper discovery.Helper,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
podCommandExecutor podexec.PodCommandExecutor,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
resticBackupperFactory restic.BackupperFactory,
|
||||
resticTimeout time.Duration,
|
||||
) (Backupper, error) {
|
||||
return &kubernetesBackupper{
|
||||
discoveryHelper: discoveryHelper,
|
||||
dynamicFactory: dynamicFactory,
|
||||
podCommandExecutor: podCommandExecutor,
|
||||
groupBackupperFactory: &defaultGroupBackupperFactory{},
|
||||
snapshotService: snapshotService,
|
||||
discoveryHelper: discoveryHelper,
|
||||
dynamicFactory: dynamicFactory,
|
||||
podCommandExecutor: podCommandExecutor,
|
||||
groupBackupperFactory: &defaultGroupBackupperFactory{},
|
||||
snapshotService: snapshotService,
|
||||
resticBackupperFactory: resticBackupperFactory,
|
||||
resticTimeout: resticTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -232,11 +242,6 @@ func (kb *kubernetesBackupper) Backup(backup *api.Backup, backupFile, logFile io
|
||||
return err
|
||||
}
|
||||
|
||||
var labelSelector string
|
||||
if backup.Spec.LabelSelector != nil {
|
||||
labelSelector = metav1.FormatLabelSelector(backup.Spec.LabelSelector)
|
||||
}
|
||||
|
||||
backedUpItems := make(map[itemKey]struct{})
|
||||
var errs []error
|
||||
|
||||
@@ -245,12 +250,32 @@ func (kb *kubernetesBackupper) Backup(backup *api.Backup, backupFile, logFile io
|
||||
return err
|
||||
}
|
||||
|
||||
podVolumeTimeout := kb.resticTimeout
|
||||
if val := backup.Annotations[api.PodVolumeOperationTimeoutAnnotation]; val != "" {
|
||||
parsed, err := time.ParseDuration(val)
|
||||
if err != nil {
|
||||
log.WithError(errors.WithStack(err)).Errorf("Unable to parse pod volume timeout annotation %s, using server value.", val)
|
||||
} else {
|
||||
podVolumeTimeout = parsed
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), podVolumeTimeout)
|
||||
defer cancelFunc()
|
||||
|
||||
var resticBackupper restic.Backupper
|
||||
if kb.resticBackupperFactory != nil {
|
||||
resticBackupper, err = kb.resticBackupperFactory.NewBackupper(ctx, backup)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
gb := kb.groupBackupperFactory.newGroupBackupper(
|
||||
log,
|
||||
backup,
|
||||
namespaceIncludesExcludes,
|
||||
resourceIncludesExcludes,
|
||||
labelSelector,
|
||||
kb.dynamicFactory,
|
||||
kb.discoveryHelper,
|
||||
backedUpItems,
|
||||
@@ -260,6 +285,7 @@ func (kb *kubernetesBackupper) Backup(backup *api.Backup, backupFile, logFile io
|
||||
tw,
|
||||
resourceHooks,
|
||||
kb.snapshotService,
|
||||
resticBackupper,
|
||||
)
|
||||
|
||||
for _, group := range kb.discoveryHelper.Resources() {
|
||||
|
||||
+12
-23
@@ -19,7 +19,6 @@ package backup
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
@@ -43,6 +42,8 @@ import (
|
||||
"github.com/heptio/ark/pkg/client"
|
||||
"github.com/heptio/ark/pkg/cloudprovider"
|
||||
"github.com/heptio/ark/pkg/discovery"
|
||||
"github.com/heptio/ark/pkg/podexec"
|
||||
"github.com/heptio/ark/pkg/restic"
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
kubeutil "github.com/heptio/ark/pkg/util/kube"
|
||||
arktest "github.com/heptio/ark/pkg/util/test"
|
||||
@@ -505,7 +506,7 @@ func TestBackup(t *testing.T) {
|
||||
|
||||
dynamicFactory := &arktest.FakeDynamicFactory{}
|
||||
|
||||
podCommandExecutor := &mockPodCommandExecutor{}
|
||||
podCommandExecutor := &arktest.MockPodCommandExecutor{}
|
||||
defer podCommandExecutor.AssertExpectations(t)
|
||||
|
||||
b, err := NewKubernetesBackupper(
|
||||
@@ -513,6 +514,8 @@ func TestBackup(t *testing.T) {
|
||||
dynamicFactory,
|
||||
podCommandExecutor,
|
||||
nil,
|
||||
nil, // restic backupper factory
|
||||
0, // restic timeout
|
||||
)
|
||||
require.NoError(t, err)
|
||||
kb := b.(*kubernetesBackupper)
|
||||
@@ -529,7 +532,6 @@ func TestBackup(t *testing.T) {
|
||||
test.backup,
|
||||
test.expectedNamespaces,
|
||||
test.expectedResources,
|
||||
test.expectedLabelSelector,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
map[itemKey]struct{}{}, // backedUpItems
|
||||
@@ -539,6 +541,7 @@ func TestBackup(t *testing.T) {
|
||||
mock.Anything, // tarWriter
|
||||
test.expectedHooks,
|
||||
mock.Anything,
|
||||
mock.Anything, // restic backupper
|
||||
).Return(groupBackupper)
|
||||
|
||||
for group, err := range test.backupGroupErrors {
|
||||
@@ -578,7 +581,7 @@ func TestBackupUsesNewCohabitatingResourcesForEachBackup(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
b, err := NewKubernetesBackupper(discoveryHelper, nil, nil, nil)
|
||||
b, err := NewKubernetesBackupper(discoveryHelper, nil, nil, nil, nil, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
kb := b.(*kubernetesBackupper)
|
||||
@@ -594,7 +597,6 @@ func TestBackupUsesNewCohabitatingResourcesForEachBackup(t *testing.T) {
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
discoveryHelper,
|
||||
mock.Anything,
|
||||
firstCohabitatingResources,
|
||||
@@ -603,6 +605,7 @@ func TestBackupUsesNewCohabitatingResourcesForEachBackup(t *testing.T) {
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
).Return(&mockGroupBackupper{})
|
||||
|
||||
assert.NoError(t, b.Backup(&v1.Backup{}, &bytes.Buffer{}, &bytes.Buffer{}, nil))
|
||||
@@ -625,7 +628,6 @@ func TestBackupUsesNewCohabitatingResourcesForEachBackup(t *testing.T) {
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
discoveryHelper,
|
||||
mock.Anything,
|
||||
secondCohabitatingResources,
|
||||
@@ -634,6 +636,7 @@ func TestBackupUsesNewCohabitatingResourcesForEachBackup(t *testing.T) {
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
).Return(&mockGroupBackupper{})
|
||||
|
||||
assert.NoError(t, b.Backup(&v1.Backup{}, &bytes.Buffer{}, &bytes.Buffer{}, nil))
|
||||
@@ -652,23 +655,22 @@ func (f *mockGroupBackupperFactory) newGroupBackupper(
|
||||
log logrus.FieldLogger,
|
||||
backup *v1.Backup,
|
||||
namespaces, resources *collections.IncludesExcludes,
|
||||
labelSelector string,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
discoveryHelper discovery.Helper,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
cohabitatingResources map[string]*cohabitatingResource,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
podCommandExecutor podexec.PodCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
resticBackupper restic.Backupper,
|
||||
) groupBackupper {
|
||||
args := f.Called(
|
||||
log,
|
||||
backup,
|
||||
namespaces,
|
||||
resources,
|
||||
labelSelector,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
backedUpItems,
|
||||
@@ -678,6 +680,7 @@ func (f *mockGroupBackupperFactory) newGroupBackupper(
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
snapshotService,
|
||||
resticBackupper,
|
||||
)
|
||||
return args.Get(0).(groupBackupper)
|
||||
}
|
||||
@@ -691,26 +694,12 @@ func (gb *mockGroupBackupper) backupGroup(group *metav1.APIResourceList) error {
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func getAsMap(j string) (map[string]interface{}, error) {
|
||||
m := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(j), &m)
|
||||
return m, err
|
||||
}
|
||||
|
||||
func toRuntimeObject(t *testing.T, data string) runtime.Object {
|
||||
o, _, err := unstructured.UnstructuredJSONScheme.Decode([]byte(data), nil, nil)
|
||||
require.NoError(t, err)
|
||||
return o
|
||||
}
|
||||
|
||||
func unstructuredOrDie(data string) *unstructured.Unstructured {
|
||||
o, _, err := unstructured.UnstructuredJSONScheme.Decode([]byte(data), nil, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return o.(*unstructured.Unstructured)
|
||||
}
|
||||
|
||||
func TestGetResourceHook(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -31,6 +31,8 @@ import (
|
||||
"github.com/heptio/ark/pkg/client"
|
||||
"github.com/heptio/ark/pkg/cloudprovider"
|
||||
"github.com/heptio/ark/pkg/discovery"
|
||||
"github.com/heptio/ark/pkg/podexec"
|
||||
"github.com/heptio/ark/pkg/restic"
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
)
|
||||
|
||||
@@ -39,16 +41,16 @@ type groupBackupperFactory interface {
|
||||
log logrus.FieldLogger,
|
||||
backup *v1.Backup,
|
||||
namespaces, resources *collections.IncludesExcludes,
|
||||
labelSelector string,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
discoveryHelper discovery.Helper,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
cohabitatingResources map[string]*cohabitatingResource,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
podCommandExecutor podexec.PodCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
resticBackupper restic.Backupper,
|
||||
) groupBackupper
|
||||
}
|
||||
|
||||
@@ -58,23 +60,22 @@ func (f *defaultGroupBackupperFactory) newGroupBackupper(
|
||||
log logrus.FieldLogger,
|
||||
backup *v1.Backup,
|
||||
namespaces, resources *collections.IncludesExcludes,
|
||||
labelSelector string,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
discoveryHelper discovery.Helper,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
cohabitatingResources map[string]*cohabitatingResource,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
podCommandExecutor podexec.PodCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
resticBackupper restic.Backupper,
|
||||
) groupBackupper {
|
||||
return &defaultGroupBackupper{
|
||||
log: log,
|
||||
backup: backup,
|
||||
namespaces: namespaces,
|
||||
resources: resources,
|
||||
labelSelector: labelSelector,
|
||||
dynamicFactory: dynamicFactory,
|
||||
discoveryHelper: discoveryHelper,
|
||||
backedUpItems: backedUpItems,
|
||||
@@ -84,6 +85,7 @@ func (f *defaultGroupBackupperFactory) newGroupBackupper(
|
||||
tarWriter: tarWriter,
|
||||
resourceHooks: resourceHooks,
|
||||
snapshotService: snapshotService,
|
||||
resticBackupper: resticBackupper,
|
||||
resourceBackupperFactory: &defaultResourceBackupperFactory{},
|
||||
}
|
||||
}
|
||||
@@ -96,16 +98,16 @@ type defaultGroupBackupper struct {
|
||||
log logrus.FieldLogger
|
||||
backup *v1.Backup
|
||||
namespaces, resources *collections.IncludesExcludes
|
||||
labelSelector string
|
||||
dynamicFactory client.DynamicFactory
|
||||
discoveryHelper discovery.Helper
|
||||
backedUpItems map[itemKey]struct{}
|
||||
cohabitatingResources map[string]*cohabitatingResource
|
||||
actions []resolvedAction
|
||||
podCommandExecutor podCommandExecutor
|
||||
podCommandExecutor podexec.PodCommandExecutor
|
||||
tarWriter tarWriter
|
||||
resourceHooks []resourceHook
|
||||
snapshotService cloudprovider.SnapshotService
|
||||
resticBackupper restic.Backupper
|
||||
resourceBackupperFactory resourceBackupperFactory
|
||||
}
|
||||
|
||||
@@ -119,7 +121,6 @@ func (gb *defaultGroupBackupper) backupGroup(group *metav1.APIResourceList) erro
|
||||
gb.backup,
|
||||
gb.namespaces,
|
||||
gb.resources,
|
||||
gb.labelSelector,
|
||||
gb.dynamicFactory,
|
||||
gb.discoveryHelper,
|
||||
gb.backedUpItems,
|
||||
@@ -129,6 +130,7 @@ func (gb *defaultGroupBackupper) backupGroup(group *metav1.APIResourceList) erro
|
||||
gb.tarWriter,
|
||||
gb.resourceHooks,
|
||||
gb.snapshotService,
|
||||
gb.resticBackupper,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
"github.com/heptio/ark/pkg/client"
|
||||
"github.com/heptio/ark/pkg/cloudprovider"
|
||||
"github.com/heptio/ark/pkg/discovery"
|
||||
"github.com/heptio/ark/pkg/podexec"
|
||||
"github.com/heptio/ark/pkg/restic"
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
arktest "github.com/heptio/ark/pkg/util/test"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -38,7 +40,6 @@ func TestBackupGroup(t *testing.T) {
|
||||
|
||||
namespaces := collections.NewIncludesExcludes().Includes("a")
|
||||
resources := collections.NewIncludesExcludes().Includes("b")
|
||||
labelSelector := "foo=bar"
|
||||
|
||||
dynamicFactory := &arktest.FakeDynamicFactory{}
|
||||
defer dynamicFactory.AssertExpectations(t)
|
||||
@@ -64,7 +65,7 @@ func TestBackupGroup(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
podCommandExecutor := &mockPodCommandExecutor{}
|
||||
podCommandExecutor := &arktest.MockPodCommandExecutor{}
|
||||
defer podCommandExecutor.AssertExpectations(t)
|
||||
|
||||
tarWriter := &fakeTarWriter{}
|
||||
@@ -78,7 +79,6 @@ func TestBackupGroup(t *testing.T) {
|
||||
backup,
|
||||
namespaces,
|
||||
resources,
|
||||
labelSelector,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
backedUpItems,
|
||||
@@ -87,7 +87,8 @@ func TestBackupGroup(t *testing.T) {
|
||||
podCommandExecutor,
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
nil,
|
||||
nil, // snapshot service
|
||||
nil, // restic backupper
|
||||
).(*defaultGroupBackupper)
|
||||
|
||||
resourceBackupperFactory := &mockResourceBackupperFactory{}
|
||||
@@ -102,7 +103,6 @@ func TestBackupGroup(t *testing.T) {
|
||||
backup,
|
||||
namespaces,
|
||||
resources,
|
||||
labelSelector,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
backedUpItems,
|
||||
@@ -112,6 +112,7 @@ func TestBackupGroup(t *testing.T) {
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
nil,
|
||||
mock.Anything, // restic backupper
|
||||
).Return(resourceBackupper)
|
||||
|
||||
group := &metav1.APIResourceList{
|
||||
@@ -150,23 +151,22 @@ func (rbf *mockResourceBackupperFactory) newResourceBackupper(
|
||||
backup *v1.Backup,
|
||||
namespaces *collections.IncludesExcludes,
|
||||
resources *collections.IncludesExcludes,
|
||||
labelSelector string,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
discoveryHelper discovery.Helper,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
cohabitatingResources map[string]*cohabitatingResource,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
podCommandExecutor podexec.PodCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
resticBackupper restic.Backupper,
|
||||
) resourceBackupper {
|
||||
args := rbf.Called(
|
||||
log,
|
||||
backup,
|
||||
namespaces,
|
||||
resources,
|
||||
labelSelector,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
backedUpItems,
|
||||
|
||||
@@ -25,18 +25,22 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/heptio/ark/pkg/kuberesource"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
kuberrs "k8s.io/apimachinery/pkg/util/errors"
|
||||
kubeerrs "k8s.io/apimachinery/pkg/util/errors"
|
||||
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/client"
|
||||
"github.com/heptio/ark/pkg/cloudprovider"
|
||||
"github.com/heptio/ark/pkg/discovery"
|
||||
"github.com/heptio/ark/pkg/kuberesource"
|
||||
"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"
|
||||
)
|
||||
@@ -47,12 +51,13 @@ type itemBackupperFactory interface {
|
||||
namespaces, resources *collections.IncludesExcludes,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
podCommandExecutor podexec.PodCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
discoveryHelper discovery.Helper,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
resticBackupper restic.Backupper,
|
||||
) ItemBackupper
|
||||
}
|
||||
|
||||
@@ -63,12 +68,13 @@ func (f *defaultItemBackupperFactory) newItemBackupper(
|
||||
namespaces, resources *collections.IncludesExcludes,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
podCommandExecutor podexec.PodCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
discoveryHelper discovery.Helper,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
resticBackupper restic.Backupper,
|
||||
) ItemBackupper {
|
||||
ib := &defaultItemBackupper{
|
||||
backup: backup,
|
||||
@@ -84,6 +90,7 @@ func (f *defaultItemBackupperFactory) newItemBackupper(
|
||||
itemHookHandler: &defaultItemHookHandler{
|
||||
podCommandExecutor: podCommandExecutor,
|
||||
},
|
||||
resticBackupper: resticBackupper,
|
||||
}
|
||||
|
||||
// this is for testing purposes
|
||||
@@ -107,6 +114,7 @@ type defaultItemBackupper struct {
|
||||
dynamicFactory client.DynamicFactory
|
||||
discoveryHelper discovery.Helper
|
||||
snapshotService cloudprovider.SnapshotService
|
||||
resticBackupper restic.Backupper
|
||||
|
||||
itemHookHandler itemHookHandler
|
||||
additionalItemBackupper ItemBackupper
|
||||
@@ -183,13 +191,26 @@ func (ib *defaultItemBackupper) backupItem(logger logrus.FieldLogger, obj runtim
|
||||
}
|
||||
}
|
||||
|
||||
if groupResource == kuberesource.Pods && len(restic.GetVolumesToBackup(metadata)) > 0 {
|
||||
var (
|
||||
updatedObj runtime.Unstructured
|
||||
errs []error
|
||||
)
|
||||
|
||||
if updatedObj, errs = backupPodVolumes(log, ib.backup, obj, ib.resticBackupper); len(errs) > 0 {
|
||||
backupErrs = append(backupErrs, errs...)
|
||||
} else {
|
||||
obj = updatedObj
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("Executing post hooks")
|
||||
if err := ib.itemHookHandler.handleHooks(log, groupResource, obj, ib.resourceHooks, hookPhasePost); err != nil {
|
||||
backupErrs = append(backupErrs, err)
|
||||
}
|
||||
|
||||
if len(backupErrs) != 0 {
|
||||
return kuberrs.NewAggregate(backupErrs)
|
||||
return kubeerrs.NewAggregate(backupErrs)
|
||||
}
|
||||
|
||||
var filePath string
|
||||
@@ -223,6 +244,39 @@ func (ib *defaultItemBackupper) backupItem(logger logrus.FieldLogger, obj runtim
|
||||
return nil
|
||||
}
|
||||
|
||||
func backupPodVolumes(log logrus.FieldLogger, backup *api.Backup, obj runtime.Unstructured, backupper restic.Backupper) (runtime.Unstructured, []error) {
|
||||
if backupper == nil {
|
||||
log.Warn("No restic backupper, not backing up pod's volumes")
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
pod := new(corev1api.Pod)
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), pod); err != nil {
|
||||
return nil, []error{errors.WithStack(err)}
|
||||
}
|
||||
|
||||
volumeSnapshots, errs := backupper.BackupPodVolumes(backup, pod, log)
|
||||
if len(errs) > 0 {
|
||||
return nil, errs
|
||||
}
|
||||
if len(volumeSnapshots) == 0 {
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// annotate the pod with the successful volume snapshots
|
||||
for volume, snapshot := range volumeSnapshots {
|
||||
restic.SetPodSnapshotAnnotation(pod, volume, snapshot)
|
||||
}
|
||||
|
||||
// convert annotated pod back to unstructured to return
|
||||
unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pod)
|
||||
if err != nil {
|
||||
return nil, []error{errors.WithStack(err)}
|
||||
}
|
||||
|
||||
return &unstructured.Unstructured{Object: unstructuredObj}, nil
|
||||
}
|
||||
|
||||
func (ib *defaultItemBackupper) executeActions(log logrus.FieldLogger, obj runtime.Unstructured, groupResource schema.GroupResource, name, namespace string, metadata metav1.Object) error {
|
||||
for _, action := range ib.actions {
|
||||
if !action.resourceIncludesExcludes.ShouldInclude(groupResource.String()) {
|
||||
|
||||
@@ -99,7 +99,7 @@ func TestBackupItemSkips(t *testing.T) {
|
||||
backedUpItems: test.backedUpItems,
|
||||
}
|
||||
|
||||
u := unstructuredOrDie(fmt.Sprintf(`{"apiVersion":"v1","kind":"Pod","metadata":{"namespace":"%s","name":"%s"}}`, test.namespace, test.name))
|
||||
u := arktest.UnstructuredOrDie(fmt.Sprintf(`{"apiVersion":"v1","kind":"Pod","metadata":{"namespace":"%s","name":"%s"}}`, test.namespace, test.name))
|
||||
err := ib.backupItem(arktest.NewLogger(), u, test.groupResource)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
@@ -118,7 +118,7 @@ func TestBackupItemSkipsClusterScopedResourceWhenIncludeClusterResourcesFalse(t
|
||||
resources: collections.NewIncludesExcludes(),
|
||||
}
|
||||
|
||||
u := unstructuredOrDie(`{"apiVersion":"v1","kind":"Foo","metadata":{"name":"bar"}}`)
|
||||
u := arktest.UnstructuredOrDie(`{"apiVersion":"v1","kind":"Foo","metadata":{"name":"bar"}}`)
|
||||
err := ib.backupItem(arktest.NewLogger(), u, schema.GroupResource{Group: "foo", Resource: "bar"})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -219,8 +219,8 @@ func TestBackupItemNoSkips(t *testing.T) {
|
||||
},
|
||||
},
|
||||
customActionAdditionalItems: []runtime.Unstructured{
|
||||
unstructuredOrDie(`{"apiVersion":"g1/v1","kind":"r1","metadata":{"namespace":"ns1","name":"n1"}}`),
|
||||
unstructuredOrDie(`{"apiVersion":"g2/v1","kind":"r1","metadata":{"namespace":"ns2","name":"n2"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"g1/v1","kind":"r1","metadata":{"namespace":"ns1","name":"n1"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"g2/v1","kind":"r1","metadata":{"namespace":"ns2","name":"n2"}}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -245,8 +245,8 @@ func TestBackupItemNoSkips(t *testing.T) {
|
||||
},
|
||||
},
|
||||
customActionAdditionalItems: []runtime.Unstructured{
|
||||
unstructuredOrDie(`{"apiVersion":"g1/v1","kind":"r1","metadata":{"namespace":"ns1","name":"n1"}}`),
|
||||
unstructuredOrDie(`{"apiVersion":"g2/v1","kind":"r1","metadata":{"namespace":"ns2","name":"n2"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"g1/v1","kind":"r1","metadata":{"namespace":"ns1","name":"n1"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"g2/v1","kind":"r1","metadata":{"namespace":"ns2","name":"n2"}}`),
|
||||
},
|
||||
additionalItemError: errors.New("foo"),
|
||||
},
|
||||
@@ -300,7 +300,7 @@ func TestBackupItemNoSkips(t *testing.T) {
|
||||
groupResource = schema.ParseGroupResource(test.groupResource)
|
||||
}
|
||||
|
||||
item, err := getAsMap(test.item)
|
||||
item, err := arktest.GetAsMap(test.item)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -333,7 +333,7 @@ func TestBackupItemNoSkips(t *testing.T) {
|
||||
|
||||
resourceHooks := []resourceHook{}
|
||||
|
||||
podCommandExecutor := &mockPodCommandExecutor{}
|
||||
podCommandExecutor := &arktest.MockPodCommandExecutor{}
|
||||
defer podCommandExecutor.AssertExpectations(t)
|
||||
|
||||
dynamicFactory := &arktest.FakeDynamicFactory{}
|
||||
@@ -352,7 +352,8 @@ func TestBackupItemNoSkips(t *testing.T) {
|
||||
resourceHooks,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
nil,
|
||||
nil, // snapshot service
|
||||
nil, // restic backupper
|
||||
).(*defaultItemBackupper)
|
||||
|
||||
var snapshotService *arktest.FakeSnapshotService
|
||||
@@ -426,7 +427,7 @@ func TestBackupItemNoSkips(t *testing.T) {
|
||||
assert.False(t, w.headers[0].ModTime.IsZero(), "header.modTime set")
|
||||
assert.Equal(t, 1, len(w.data), "# of data")
|
||||
|
||||
actual, err := getAsMap(string(w.data[0]))
|
||||
actual, err := arktest.GetAsMap(string(w.data[0]))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -572,7 +573,7 @@ func TestTakePVSnapshot(t *testing.T) {
|
||||
|
||||
ib := &defaultItemBackupper{snapshotService: snapshotService}
|
||||
|
||||
pv, err := getAsMap(test.pv)
|
||||
pv, err := arktest.GetAsMap(test.pv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -21,16 +21,19 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/kuberesource"
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/kuberesource"
|
||||
"github.com/heptio/ark/pkg/podexec"
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
)
|
||||
|
||||
type hookPhase string
|
||||
@@ -57,7 +60,7 @@ type itemHookHandler interface {
|
||||
|
||||
// defaultItemHookHandler is the default itemHookHandler.
|
||||
type defaultItemHookHandler struct {
|
||||
podCommandExecutor podCommandExecutor
|
||||
podCommandExecutor podexec.PodCommandExecutor
|
||||
}
|
||||
|
||||
func (h *defaultItemHookHandler) handleHooks(
|
||||
@@ -94,7 +97,7 @@ func (h *defaultItemHookHandler) handleHooks(
|
||||
"hookPhase": phase,
|
||||
},
|
||||
)
|
||||
if err := h.podCommandExecutor.executePodCommand(hookLog, obj.UnstructuredContent(), namespace, name, "<from-annotation>", hookFromAnnotations); err != nil {
|
||||
if err := h.podCommandExecutor.ExecutePodCommand(hookLog, obj.UnstructuredContent(), namespace, name, "<from-annotation>", hookFromAnnotations); err != nil {
|
||||
hookLog.WithError(err).Error("Error executing hook")
|
||||
if hookFromAnnotations.OnError == api.HookErrorModeFail {
|
||||
return err
|
||||
@@ -127,7 +130,7 @@ func (h *defaultItemHookHandler) handleHooks(
|
||||
"hookPhase": phase,
|
||||
},
|
||||
)
|
||||
err := h.podCommandExecutor.executePodCommand(hookLog, obj.UnstructuredContent(), namespace, name, resourceHook.name, hook.Exec)
|
||||
err := h.podCommandExecutor.ExecutePodCommand(hookLog, obj.UnstructuredContent(), namespace, name, resourceHook.name, hook.Exec)
|
||||
if err != nil {
|
||||
hookLog.WithError(err).Error("Error executing hook")
|
||||
if hook.Exec.OnError == api.HookErrorModeFail {
|
||||
@@ -147,8 +150,6 @@ const (
|
||||
podBackupHookCommandAnnotationKey = "hook.backup.ark.heptio.com/command"
|
||||
podBackupHookOnErrorAnnotationKey = "hook.backup.ark.heptio.com/on-error"
|
||||
podBackupHookTimeoutAnnotationKey = "hook.backup.ark.heptio.com/timeout"
|
||||
defaultHookOnError = api.HookErrorModeFail
|
||||
defaultHookTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
func phasedKey(phase hookPhase, key string) string {
|
||||
|
||||
@@ -57,7 +57,7 @@ func TestHandleHooksSkips(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "pod without annotation / no spec hooks",
|
||||
item: unstructuredOrDie(
|
||||
item: arktest.UnstructuredOrDie(
|
||||
`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
@@ -73,7 +73,7 @@ func TestHandleHooksSkips(t *testing.T) {
|
||||
{
|
||||
name: "spec hooks not applicable",
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(
|
||||
item: arktest.UnstructuredOrDie(
|
||||
`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
@@ -114,7 +114,7 @@ func TestHandleHooksSkips(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
podCommandExecutor := &mockPodCommandExecutor{}
|
||||
podCommandExecutor := &arktest.MockPodCommandExecutor{}
|
||||
defer podCommandExecutor.AssertExpectations(t)
|
||||
|
||||
h := &defaultItemHookHandler{
|
||||
@@ -144,7 +144,7 @@ func TestHandleHooks(t *testing.T) {
|
||||
name: "pod, no annotation, spec (multiple pre hooks) = run spec",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
item: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
@@ -194,7 +194,7 @@ func TestHandleHooks(t *testing.T) {
|
||||
name: "pod, no annotation, spec (multiple post hooks) = run spec",
|
||||
phase: hookPhasePost,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
item: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
@@ -244,7 +244,7 @@ func TestHandleHooks(t *testing.T) {
|
||||
name: "pod, annotation (legacy), no spec = run annotation",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
item: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
@@ -266,7 +266,7 @@ func TestHandleHooks(t *testing.T) {
|
||||
name: "pod, annotation (pre), no spec = run annotation",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
item: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
@@ -288,7 +288,7 @@ func TestHandleHooks(t *testing.T) {
|
||||
name: "pod, annotation (post), no spec = run annotation",
|
||||
phase: hookPhasePost,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
item: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
@@ -310,7 +310,7 @@ func TestHandleHooks(t *testing.T) {
|
||||
name: "pod, annotation & spec = run annotation",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
item: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
@@ -345,7 +345,7 @@ func TestHandleHooks(t *testing.T) {
|
||||
name: "pod, annotation, onError=fail = return error",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
item: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
@@ -371,7 +371,7 @@ func TestHandleHooks(t *testing.T) {
|
||||
name: "pod, annotation, onError=continue = return nil",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
item: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
@@ -397,7 +397,7 @@ func TestHandleHooks(t *testing.T) {
|
||||
name: "pod, spec, onError=fail = don't run other hooks",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
item: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
@@ -459,7 +459,7 @@ func TestHandleHooks(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
podCommandExecutor := &mockPodCommandExecutor{}
|
||||
podCommandExecutor := &arktest.MockPodCommandExecutor{}
|
||||
defer podCommandExecutor.AssertExpectations(t)
|
||||
|
||||
h := &defaultItemHookHandler{
|
||||
@@ -467,20 +467,20 @@ func TestHandleHooks(t *testing.T) {
|
||||
}
|
||||
|
||||
if test.expectedPodHook != nil {
|
||||
podCommandExecutor.On("executePodCommand", mock.Anything, test.item.UnstructuredContent(), "ns", "name", "<from-annotation>", test.expectedPodHook).Return(test.expectedPodHookError)
|
||||
podCommandExecutor.On("ExecutePodCommand", mock.Anything, test.item.UnstructuredContent(), "ns", "name", "<from-annotation>", test.expectedPodHook).Return(test.expectedPodHookError)
|
||||
} else {
|
||||
hookLoop:
|
||||
for _, resourceHook := range test.hooks {
|
||||
for _, hook := range resourceHook.pre {
|
||||
hookError := test.hookErrorsByContainer[hook.Exec.Container]
|
||||
podCommandExecutor.On("executePodCommand", mock.Anything, test.item.UnstructuredContent(), "ns", "name", resourceHook.name, hook.Exec).Return(hookError)
|
||||
podCommandExecutor.On("ExecutePodCommand", mock.Anything, test.item.UnstructuredContent(), "ns", "name", resourceHook.name, hook.Exec).Return(hookError)
|
||||
if hookError != nil && hook.Exec.OnError == v1.HookErrorModeFail {
|
||||
break hookLoop
|
||||
}
|
||||
}
|
||||
for _, hook := range resourceHook.post {
|
||||
hookError := test.hookErrorsByContainer[hook.Exec.Container]
|
||||
podCommandExecutor.On("executePodCommand", mock.Anything, test.item.UnstructuredContent(), "ns", "name", resourceHook.name, hook.Exec).Return(hookError)
|
||||
podCommandExecutor.On("ExecutePodCommand", mock.Anything, test.item.UnstructuredContent(), "ns", "name", resourceHook.name, hook.Exec).Return(hookError)
|
||||
if hookError != nil && hook.Exec.OnError == v1.HookErrorModeFail {
|
||||
break hookLoop
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestPodActionExecute(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "no spec.volumes",
|
||||
pod: unstructuredOrDie(`
|
||||
pod: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
@@ -59,7 +59,7 @@ func TestPodActionExecute(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "persistentVolumeClaim without claimName",
|
||||
pod: unstructuredOrDie(`
|
||||
pod: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
@@ -79,7 +79,7 @@ func TestPodActionExecute(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "full test, mix of volume types",
|
||||
pod: unstructuredOrDie(`
|
||||
pod: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
|
||||
@@ -1,223 +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 backup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
kapiv1 "k8s.io/api/core/v1"
|
||||
kscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
)
|
||||
|
||||
// podCommandExecutor is capable of executing a command in a container in a pod.
|
||||
type podCommandExecutor interface {
|
||||
// executePodCommand executes a command in a container in a pod. If the command takes longer than
|
||||
// the specified timeout, an error is returned.
|
||||
executePodCommand(log logrus.FieldLogger, item map[string]interface{}, namespace, name, hookName string, hook *api.ExecHook) error
|
||||
}
|
||||
|
||||
type poster interface {
|
||||
Post() *rest.Request
|
||||
}
|
||||
|
||||
type defaultPodCommandExecutor struct {
|
||||
restClientConfig *rest.Config
|
||||
restClient poster
|
||||
|
||||
streamExecutorFactory streamExecutorFactory
|
||||
}
|
||||
|
||||
// NewPodCommandExecutor creates a new podCommandExecutor.
|
||||
func NewPodCommandExecutor(restClientConfig *rest.Config, restClient poster) podCommandExecutor {
|
||||
return &defaultPodCommandExecutor{
|
||||
restClientConfig: restClientConfig,
|
||||
restClient: restClient,
|
||||
|
||||
streamExecutorFactory: &defaultStreamExecutorFactory{},
|
||||
}
|
||||
}
|
||||
|
||||
// executePodCommand uses the pod exec API to execute a command in a container in a pod. If the
|
||||
// command takes longer than the specified timeout, an error is returned (NOTE: it is not currently
|
||||
// possible to ensure the command is terminated when the timeout occurs, so it may continue to run
|
||||
// in the background).
|
||||
func (e *defaultPodCommandExecutor) executePodCommand(log logrus.FieldLogger, item map[string]interface{}, namespace, name, hookName string, hook *api.ExecHook) error {
|
||||
if item == nil {
|
||||
return errors.New("item is required")
|
||||
}
|
||||
if namespace == "" {
|
||||
return errors.New("namespace is required")
|
||||
}
|
||||
if name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if hookName == "" {
|
||||
return errors.New("hookName is required")
|
||||
}
|
||||
if hook == nil {
|
||||
return errors.New("hook is required")
|
||||
}
|
||||
|
||||
if hook.Container == "" {
|
||||
if err := setDefaultHookContainer(item, hook); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := ensureContainerExists(item, hook.Container); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(hook.Command) == 0 {
|
||||
return errors.New("command is required")
|
||||
}
|
||||
|
||||
switch hook.OnError {
|
||||
case api.HookErrorModeFail, api.HookErrorModeContinue:
|
||||
// use the specified value
|
||||
default:
|
||||
// default to fail
|
||||
hook.OnError = api.HookErrorModeFail
|
||||
}
|
||||
|
||||
if hook.Timeout.Duration == 0 {
|
||||
hook.Timeout.Duration = defaultHookTimeout
|
||||
}
|
||||
|
||||
hookLog := log.WithFields(
|
||||
logrus.Fields{
|
||||
"hookName": hookName,
|
||||
"hookContainer": hook.Container,
|
||||
"hookCommand": hook.Command,
|
||||
"hookOnError": hook.OnError,
|
||||
"hookTimeout": hook.Timeout,
|
||||
},
|
||||
)
|
||||
hookLog.Info("running exec hook")
|
||||
|
||||
req := e.restClient.Post().
|
||||
Resource("pods").
|
||||
Namespace(namespace).
|
||||
Name(name).
|
||||
SubResource("exec")
|
||||
|
||||
req.VersionedParams(&kapiv1.PodExecOptions{
|
||||
Container: hook.Container,
|
||||
Command: hook.Command,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
}, kscheme.ParameterCodec)
|
||||
|
||||
executor, err := e.streamExecutorFactory.NewSPDYExecutor(e.restClientConfig, "POST", req.URL())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
streamOptions := remotecommand.StreamOptions{
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
}
|
||||
|
||||
errCh := make(chan error)
|
||||
|
||||
go func() {
|
||||
err = executor.Stream(streamOptions)
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
var timeoutCh <-chan time.Time
|
||||
if hook.Timeout.Duration > 0 {
|
||||
timer := time.NewTimer(hook.Timeout.Duration)
|
||||
defer timer.Stop()
|
||||
timeoutCh = timer.C
|
||||
}
|
||||
|
||||
select {
|
||||
case err = <-errCh:
|
||||
case <-timeoutCh:
|
||||
return errors.Errorf("timed out after %v", hook.Timeout.Duration)
|
||||
}
|
||||
|
||||
hookLog.Infof("stdout: %s", stdout.String())
|
||||
hookLog.Infof("stderr: %s", stderr.String())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func ensureContainerExists(pod map[string]interface{}, container string) error {
|
||||
containers, err := collections.GetSlice(pod, "spec.containers")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, obj := range containers {
|
||||
c, ok := obj.(map[string]interface{})
|
||||
if !ok {
|
||||
return errors.Errorf("unexpected type for container %T", obj)
|
||||
}
|
||||
name, ok := c["name"].(string)
|
||||
if !ok {
|
||||
return errors.Errorf("unexpected type for container name %T", c["name"])
|
||||
}
|
||||
if name == container {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Errorf("no such container: %q", container)
|
||||
}
|
||||
|
||||
func setDefaultHookContainer(pod map[string]interface{}, hook *api.ExecHook) error {
|
||||
containers, err := collections.GetSlice(pod, "spec.containers")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(containers) < 1 {
|
||||
return errors.New("need at least 1 container")
|
||||
}
|
||||
|
||||
container, ok := containers[0].(map[string]interface{})
|
||||
if !ok {
|
||||
return errors.Errorf("unexpected type for container %T", pod)
|
||||
}
|
||||
|
||||
name, ok := container["name"].(string)
|
||||
if !ok {
|
||||
return errors.Errorf("unexpected type for container name %T", container["name"])
|
||||
}
|
||||
hook.Container = name
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type streamExecutorFactory interface {
|
||||
NewSPDYExecutor(config *rest.Config, method string, url *url.URL) (remotecommand.Executor, error)
|
||||
}
|
||||
|
||||
type defaultStreamExecutorFactory struct{}
|
||||
|
||||
func (f *defaultStreamExecutorFactory) NewSPDYExecutor(config *rest.Config, method string, url *url.URL) (remotecommand.Executor, error) {
|
||||
return remotecommand.NewSPDYExecutor(config, method, url)
|
||||
}
|
||||
@@ -1,276 +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 backup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
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/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
)
|
||||
|
||||
func TestNewPodCommandExecutor(t *testing.T) {
|
||||
restClientConfig := &rest.Config{Host: "foo"}
|
||||
poster := &mockPoster{}
|
||||
pce := NewPodCommandExecutor(restClientConfig, poster).(*defaultPodCommandExecutor)
|
||||
assert.Equal(t, restClientConfig, pce.restClientConfig)
|
||||
assert.Equal(t, poster, pce.restClient)
|
||||
assert.Equal(t, &defaultStreamExecutorFactory{}, pce.streamExecutorFactory)
|
||||
}
|
||||
|
||||
func TestExecutePodCommandMissingInputs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
item map[string]interface{}
|
||||
podNamespace string
|
||||
podName string
|
||||
hookName string
|
||||
hook *v1.ExecHook
|
||||
}{
|
||||
{
|
||||
name: "missing item",
|
||||
},
|
||||
{
|
||||
name: "missing pod namespace",
|
||||
item: map[string]interface{}{},
|
||||
},
|
||||
{
|
||||
name: "missing pod name",
|
||||
item: map[string]interface{}{},
|
||||
podNamespace: "ns",
|
||||
},
|
||||
{
|
||||
name: "missing hookName",
|
||||
item: map[string]interface{}{},
|
||||
podNamespace: "ns",
|
||||
podName: "pod",
|
||||
},
|
||||
{
|
||||
name: "missing hook",
|
||||
item: map[string]interface{}{},
|
||||
podNamespace: "ns",
|
||||
podName: "pod",
|
||||
hookName: "hook",
|
||||
},
|
||||
{
|
||||
name: "container not found",
|
||||
item: unstructuredOrDie(`{"kind":"Pod","spec":{"containers":[{"name":"foo"}]}}`).Object,
|
||||
podNamespace: "ns",
|
||||
podName: "pod",
|
||||
hookName: "hook",
|
||||
hook: &v1.ExecHook{
|
||||
Container: "missing",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "command missing",
|
||||
item: unstructuredOrDie(`{"kind":"Pod","spec":{"containers":[{"name":"foo"}]}}`).Object,
|
||||
podNamespace: "ns",
|
||||
podName: "pod",
|
||||
hookName: "hook",
|
||||
hook: &v1.ExecHook{
|
||||
Container: "foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
e := &defaultPodCommandExecutor{}
|
||||
err := e.executePodCommand(arktest.NewLogger(), test.item, test.podNamespace, test.podName, test.hookName, test.hook)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecutePodCommand(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
containerName string
|
||||
expectedContainerName string
|
||||
command []string
|
||||
errorMode v1.HookErrorMode
|
||||
expectedErrorMode v1.HookErrorMode
|
||||
timeout time.Duration
|
||||
expectedTimeout time.Duration
|
||||
hookError error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "validate defaults",
|
||||
command: []string{"some", "command"},
|
||||
expectedContainerName: "foo",
|
||||
expectedErrorMode: v1.HookErrorModeFail,
|
||||
expectedTimeout: 30 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "use specified values",
|
||||
command: []string{"some", "command"},
|
||||
containerName: "bar",
|
||||
expectedContainerName: "bar",
|
||||
errorMode: v1.HookErrorModeContinue,
|
||||
expectedErrorMode: v1.HookErrorModeContinue,
|
||||
timeout: 10 * time.Second,
|
||||
expectedTimeout: 10 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "hook error",
|
||||
command: []string{"some", "command"},
|
||||
expectedContainerName: "foo",
|
||||
expectedErrorMode: v1.HookErrorModeFail,
|
||||
expectedTimeout: 30 * time.Second,
|
||||
hookError: errors.New("hook error"),
|
||||
expectedError: "hook error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
hook := v1.ExecHook{
|
||||
Container: test.containerName,
|
||||
Command: test.command,
|
||||
OnError: test.errorMode,
|
||||
Timeout: metav1.Duration{Duration: test.timeout},
|
||||
}
|
||||
|
||||
pod, err := getAsMap(`
|
||||
{
|
||||
"metadata": {
|
||||
"namespace": "namespace",
|
||||
"name": "name"
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{"name": "foo"},
|
||||
{"name": "bar"}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
clientConfig := &rest.Config{}
|
||||
poster := &mockPoster{}
|
||||
defer poster.AssertExpectations(t)
|
||||
podCommandExecutor := NewPodCommandExecutor(clientConfig, poster).(*defaultPodCommandExecutor)
|
||||
|
||||
streamExecutorFactory := &mockStreamExecutorFactory{}
|
||||
defer streamExecutorFactory.AssertExpectations(t)
|
||||
podCommandExecutor.streamExecutorFactory = streamExecutorFactory
|
||||
|
||||
baseUrl, _ := url.Parse("https://some.server")
|
||||
contentConfig := rest.ContentConfig{
|
||||
GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"},
|
||||
}
|
||||
postRequest := rest.NewRequest(nil, "POST", baseUrl, "/api/v1", contentConfig, rest.Serializers{}, nil, nil, 0)
|
||||
poster.On("Post").Return(postRequest)
|
||||
|
||||
streamExecutor := &mockStreamExecutor{}
|
||||
defer streamExecutor.AssertExpectations(t)
|
||||
|
||||
expectedCommand := strings.Join(test.command, "&command=")
|
||||
expectedURL, _ := url.Parse(
|
||||
fmt.Sprintf("https://some.server/api/v1/namespaces/namespace/pods/name/exec?command=%s&container=%s&stderr=true&stdout=true", expectedCommand, test.expectedContainerName),
|
||||
)
|
||||
streamExecutorFactory.On("NewSPDYExecutor", clientConfig, "POST", expectedURL).Return(streamExecutor, nil)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
expectedStreamOptions := remotecommand.StreamOptions{
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
}
|
||||
streamExecutor.On("Stream", expectedStreamOptions).Return(test.hookError)
|
||||
|
||||
err = podCommandExecutor.executePodCommand(arktest.NewLogger(), pod, "namespace", "name", "hookName", &hook)
|
||||
if test.expectedError != "" {
|
||||
assert.EqualError(t, err, test.expectedError)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureContainerExists(t *testing.T) {
|
||||
pod := map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"containers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := ensureContainerExists(pod, "bar")
|
||||
assert.EqualError(t, err, `no such container: "bar"`)
|
||||
|
||||
err = ensureContainerExists(pod, "foo")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
type mockStreamExecutorFactory struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (f *mockStreamExecutorFactory) NewSPDYExecutor(config *rest.Config, method string, url *url.URL) (remotecommand.Executor, error) {
|
||||
args := f.Called(config, method, url)
|
||||
return args.Get(0).(remotecommand.Executor), args.Error(1)
|
||||
}
|
||||
|
||||
type mockStreamExecutor struct {
|
||||
mock.Mock
|
||||
remotecommand.Executor
|
||||
}
|
||||
|
||||
func (e *mockStreamExecutor) Stream(options remotecommand.StreamOptions) error {
|
||||
args := e.Called(options)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
type mockPoster struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (p *mockPoster) Post() *rest.Request {
|
||||
args := p.Called()
|
||||
return args.Get(0).(*rest.Request)
|
||||
}
|
||||
|
||||
type mockPodCommandExecutor struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (e *mockPodCommandExecutor) executePodCommand(log logrus.FieldLogger, item map[string]interface{}, namespace, name, hookName string, hook *v1.ExecHook) error {
|
||||
args := e.Called(log, item, namespace, name, hookName, hook)
|
||||
return args.Error(0)
|
||||
}
|
||||
@@ -17,20 +17,24 @@ limitations under the License.
|
||||
package backup
|
||||
|
||||
import (
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/client"
|
||||
"github.com/heptio/ark/pkg/cloudprovider"
|
||||
"github.com/heptio/ark/pkg/discovery"
|
||||
"github.com/heptio/ark/pkg/kuberesource"
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
kuberrs "k8s.io/apimachinery/pkg/util/errors"
|
||||
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/client"
|
||||
"github.com/heptio/ark/pkg/cloudprovider"
|
||||
"github.com/heptio/ark/pkg/discovery"
|
||||
"github.com/heptio/ark/pkg/kuberesource"
|
||||
"github.com/heptio/ark/pkg/podexec"
|
||||
"github.com/heptio/ark/pkg/restic"
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
)
|
||||
|
||||
type resourceBackupperFactory interface {
|
||||
@@ -39,16 +43,16 @@ type resourceBackupperFactory interface {
|
||||
backup *api.Backup,
|
||||
namespaces *collections.IncludesExcludes,
|
||||
resources *collections.IncludesExcludes,
|
||||
labelSelector string,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
discoveryHelper discovery.Helper,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
cohabitatingResources map[string]*cohabitatingResource,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
podCommandExecutor podexec.PodCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
resticBackupper restic.Backupper,
|
||||
) resourceBackupper
|
||||
}
|
||||
|
||||
@@ -59,23 +63,22 @@ func (f *defaultResourceBackupperFactory) newResourceBackupper(
|
||||
backup *api.Backup,
|
||||
namespaces *collections.IncludesExcludes,
|
||||
resources *collections.IncludesExcludes,
|
||||
labelSelector string,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
discoveryHelper discovery.Helper,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
cohabitatingResources map[string]*cohabitatingResource,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
podCommandExecutor podexec.PodCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
resticBackupper restic.Backupper,
|
||||
) resourceBackupper {
|
||||
return &defaultResourceBackupper{
|
||||
log: log,
|
||||
backup: backup,
|
||||
namespaces: namespaces,
|
||||
resources: resources,
|
||||
labelSelector: labelSelector,
|
||||
dynamicFactory: dynamicFactory,
|
||||
discoveryHelper: discoveryHelper,
|
||||
backedUpItems: backedUpItems,
|
||||
@@ -85,6 +88,7 @@ func (f *defaultResourceBackupperFactory) newResourceBackupper(
|
||||
tarWriter: tarWriter,
|
||||
resourceHooks: resourceHooks,
|
||||
snapshotService: snapshotService,
|
||||
resticBackupper: resticBackupper,
|
||||
itemBackupperFactory: &defaultItemBackupperFactory{},
|
||||
}
|
||||
}
|
||||
@@ -98,16 +102,16 @@ type defaultResourceBackupper struct {
|
||||
backup *api.Backup
|
||||
namespaces *collections.IncludesExcludes
|
||||
resources *collections.IncludesExcludes
|
||||
labelSelector string
|
||||
dynamicFactory client.DynamicFactory
|
||||
discoveryHelper discovery.Helper
|
||||
backedUpItems map[itemKey]struct{}
|
||||
cohabitatingResources map[string]*cohabitatingResource
|
||||
actions []resolvedAction
|
||||
podCommandExecutor podCommandExecutor
|
||||
podCommandExecutor podexec.PodCommandExecutor
|
||||
tarWriter tarWriter
|
||||
resourceHooks []resourceHook
|
||||
snapshotService cloudprovider.SnapshotService
|
||||
resticBackupper restic.Backupper
|
||||
itemBackupperFactory itemBackupperFactory
|
||||
}
|
||||
|
||||
@@ -182,6 +186,7 @@ func (rb *defaultResourceBackupper) backupResource(
|
||||
rb.dynamicFactory,
|
||||
rb.discoveryHelper,
|
||||
rb.snapshotService,
|
||||
rb.resticBackupper,
|
||||
)
|
||||
|
||||
namespacesToList := getNamespacesToList(rb.namespaces)
|
||||
@@ -235,8 +240,13 @@ func (rb *defaultResourceBackupper) backupResource(
|
||||
return err
|
||||
}
|
||||
|
||||
var labelSelector string
|
||||
if selector := rb.backup.Spec.LabelSelector; selector != nil {
|
||||
labelSelector = metav1.FormatLabelSelector(selector)
|
||||
}
|
||||
|
||||
log.WithField("namespace", namespace).Info("Listing items")
|
||||
unstructuredList, err := resourceClient.List(metav1.ListOptions{LabelSelector: rb.labelSelector})
|
||||
unstructuredList, err := resourceClient.List(metav1.ListOptions{LabelSelector: labelSelector})
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import (
|
||||
"github.com/heptio/ark/pkg/cloudprovider"
|
||||
"github.com/heptio/ark/pkg/discovery"
|
||||
"github.com/heptio/ark/pkg/kuberesource"
|
||||
"github.com/heptio/ark/pkg/podexec"
|
||||
"github.com/heptio/ark/pkg/restic"
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
arktest "github.com/heptio/ark/pkg/util/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -74,8 +76,8 @@ func TestBackupResource(t *testing.T) {
|
||||
groupResource: schema.GroupResource{Group: "", Resource: "pods"},
|
||||
listResponses: [][]*unstructured.Unstructured{
|
||||
{
|
||||
unstructuredOrDie(`{"apiVersion":"v1","kind":"Pod","metadata":{"namespace":"myns","name":"myname1"}}`),
|
||||
unstructuredOrDie(`{"apiVersion":"v1","kind":"Pod","metadata":{"namespace":"myns","name":"myname2"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"v1","kind":"Pod","metadata":{"namespace":"myns","name":"myname1"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"v1","kind":"Pod","metadata":{"namespace":"myns","name":"myname2"}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -90,12 +92,12 @@ func TestBackupResource(t *testing.T) {
|
||||
groupResource: schema.GroupResource{Group: "", Resource: "pods"},
|
||||
listResponses: [][]*unstructured.Unstructured{
|
||||
{
|
||||
unstructuredOrDie(`{"apiVersion":"v1","kind":"Pod","metadata":{"namespace":"a","name":"myname1"}}`),
|
||||
unstructuredOrDie(`{"apiVersion":"v1","kind":"Pod","metadata":{"namespace":"a","name":"myname2"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"v1","kind":"Pod","metadata":{"namespace":"a","name":"myname1"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"v1","kind":"Pod","metadata":{"namespace":"a","name":"myname2"}}`),
|
||||
},
|
||||
{
|
||||
unstructuredOrDie(`{"apiVersion":"v1","kind":"Pod","metadata":{"namespace":"b","name":"myname3"}}`),
|
||||
unstructuredOrDie(`{"apiVersion":"v1","kind":"Pod","metadata":{"namespace":"b","name":"myname4"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"v1","kind":"Pod","metadata":{"namespace":"b","name":"myname3"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"v1","kind":"Pod","metadata":{"namespace":"b","name":"myname4"}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -110,8 +112,8 @@ func TestBackupResource(t *testing.T) {
|
||||
groupResource: schema.GroupResource{Group: "certificates.k8s.io", Resource: "certificatesigningrequests"},
|
||||
listResponses: [][]*unstructured.Unstructured{
|
||||
{
|
||||
unstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname1"}}`),
|
||||
unstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname2"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname1"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname2"}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -127,8 +129,8 @@ func TestBackupResource(t *testing.T) {
|
||||
groupResource: schema.GroupResource{Group: "certificates.k8s.io", Resource: "certificatesigningrequests"},
|
||||
listResponses: [][]*unstructured.Unstructured{
|
||||
{
|
||||
unstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname1"}}`),
|
||||
unstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname2"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname1"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname2"}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -166,8 +168,8 @@ func TestBackupResource(t *testing.T) {
|
||||
groupResource: schema.GroupResource{Group: "certificates.k8s.io", Resource: "certificatesigningrequests"},
|
||||
listResponses: [][]*unstructured.Unstructured{
|
||||
{
|
||||
unstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname1"}}`),
|
||||
unstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname2"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname1"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname2"}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -194,8 +196,8 @@ func TestBackupResource(t *testing.T) {
|
||||
groupResource: schema.GroupResource{Group: "certificates.k8s.io", Resource: "certificatesigningrequests"},
|
||||
listResponses: [][]*unstructured.Unstructured{
|
||||
{
|
||||
unstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname1"}}`),
|
||||
unstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname2"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname1"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"certificates.k8s.io/v1beta1","kind":"CertificateSigningRequest","metadata":{"name":"myname2"}}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -211,8 +213,8 @@ func TestBackupResource(t *testing.T) {
|
||||
groupResource: schema.GroupResource{Group: "", Resource: "namespaces"},
|
||||
expectSkip: false,
|
||||
getResponses: []*unstructured.Unstructured{
|
||||
unstructuredOrDie(`{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"ns-1"}}`),
|
||||
unstructuredOrDie(`{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"ns-2"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"ns-1"}}`),
|
||||
arktest.UnstructuredOrDie(`{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"ns-2"}}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -224,8 +226,6 @@ func TestBackupResource(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
labelSelector := "foo=bar"
|
||||
|
||||
dynamicFactory := &arktest.FakeDynamicFactory{}
|
||||
defer dynamicFactory.AssertExpectations(t)
|
||||
|
||||
@@ -251,7 +251,7 @@ func TestBackupResource(t *testing.T) {
|
||||
{name: "myhook"},
|
||||
}
|
||||
|
||||
podCommandExecutor := &mockPodCommandExecutor{}
|
||||
podCommandExecutor := &arktest.MockPodCommandExecutor{}
|
||||
defer podCommandExecutor.AssertExpectations(t)
|
||||
|
||||
tarWriter := &fakeTarWriter{}
|
||||
@@ -262,7 +262,6 @@ func TestBackupResource(t *testing.T) {
|
||||
backup,
|
||||
test.namespaces,
|
||||
test.resources,
|
||||
labelSelector,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
backedUpItems,
|
||||
@@ -271,7 +270,8 @@ func TestBackupResource(t *testing.T) {
|
||||
podCommandExecutor,
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
nil,
|
||||
nil, // snapshot service
|
||||
nil, // restic backupper
|
||||
).(*defaultResourceBackupper)
|
||||
|
||||
itemBackupperFactory := &mockItemBackupperFactory{}
|
||||
@@ -294,6 +294,7 @@ func TestBackupResource(t *testing.T) {
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
).Return(itemBackupper)
|
||||
|
||||
if len(test.listResponses) > 0 {
|
||||
@@ -310,7 +311,7 @@ func TestBackupResource(t *testing.T) {
|
||||
list.Items = append(list.Items, *item)
|
||||
itemBackupper.On("backupItem", mock.AnythingOfType("*logrus.Entry"), item, test.groupResource).Return(nil)
|
||||
}
|
||||
client.On("List", metav1.ListOptions{LabelSelector: labelSelector}).Return(list, nil)
|
||||
client.On("List", metav1.ListOptions{}).Return(list, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,13 +380,19 @@ func TestBackupResourceCohabitation(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
backup := &v1.Backup{}
|
||||
backup := &v1.Backup{
|
||||
Spec: v1.BackupSpec{
|
||||
LabelSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
namespaces := collections.NewIncludesExcludes().Includes("*")
|
||||
resources := collections.NewIncludesExcludes().Includes("*")
|
||||
|
||||
labelSelector := "foo=bar"
|
||||
|
||||
dynamicFactory := &arktest.FakeDynamicFactory{}
|
||||
defer dynamicFactory.AssertExpectations(t)
|
||||
|
||||
@@ -411,7 +418,7 @@ func TestBackupResourceCohabitation(t *testing.T) {
|
||||
{name: "myhook"},
|
||||
}
|
||||
|
||||
podCommandExecutor := &mockPodCommandExecutor{}
|
||||
podCommandExecutor := &arktest.MockPodCommandExecutor{}
|
||||
defer podCommandExecutor.AssertExpectations(t)
|
||||
|
||||
tarWriter := &fakeTarWriter{}
|
||||
@@ -421,7 +428,6 @@ func TestBackupResourceCohabitation(t *testing.T) {
|
||||
backup,
|
||||
namespaces,
|
||||
resources,
|
||||
labelSelector,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
backedUpItems,
|
||||
@@ -430,7 +436,8 @@ func TestBackupResourceCohabitation(t *testing.T) {
|
||||
podCommandExecutor,
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
nil,
|
||||
nil, // snapshot service
|
||||
nil, // restic backupper
|
||||
).(*defaultResourceBackupper)
|
||||
|
||||
itemBackupperFactory := &mockItemBackupperFactory{}
|
||||
@@ -451,7 +458,8 @@ func TestBackupResourceCohabitation(t *testing.T) {
|
||||
resourceHooks,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
mock.Anything,
|
||||
mock.Anything, // snapshot service
|
||||
mock.Anything, // restic backupper
|
||||
).Return(itemBackupper)
|
||||
|
||||
client := &arktest.FakeDynamicClient{}
|
||||
@@ -459,7 +467,7 @@ func TestBackupResourceCohabitation(t *testing.T) {
|
||||
|
||||
// STEP 1: make sure the initial backup goes through
|
||||
dynamicFactory.On("ClientForGroupVersionResource", test.groupVersion1, test.apiResource, "").Return(client, nil)
|
||||
client.On("List", metav1.ListOptions{LabelSelector: labelSelector}).Return(&unstructured.UnstructuredList{}, nil)
|
||||
client.On("List", metav1.ListOptions{LabelSelector: metav1.FormatLabelSelector(backup.Spec.LabelSelector)}).Return(&unstructured.UnstructuredList{}, nil)
|
||||
|
||||
// STEP 2: do the backup
|
||||
err := rb.backupResource(test.apiGroup1, test.apiResource)
|
||||
@@ -478,7 +486,6 @@ func TestBackupResourceOnlyIncludesSpecifiedNamespaces(t *testing.T) {
|
||||
namespaces := collections.NewIncludesExcludes().Includes("ns-1")
|
||||
resources := collections.NewIncludesExcludes().Includes("*")
|
||||
|
||||
labelSelector := "foo=bar"
|
||||
backedUpItems := map[itemKey]struct{}{}
|
||||
|
||||
dynamicFactory := &arktest.FakeDynamicFactory{}
|
||||
@@ -492,7 +499,7 @@ func TestBackupResourceOnlyIncludesSpecifiedNamespaces(t *testing.T) {
|
||||
|
||||
resourceHooks := []resourceHook{}
|
||||
|
||||
podCommandExecutor := &mockPodCommandExecutor{}
|
||||
podCommandExecutor := &arktest.MockPodCommandExecutor{}
|
||||
defer podCommandExecutor.AssertExpectations(t)
|
||||
|
||||
tarWriter := &fakeTarWriter{}
|
||||
@@ -502,7 +509,6 @@ func TestBackupResourceOnlyIncludesSpecifiedNamespaces(t *testing.T) {
|
||||
backup,
|
||||
namespaces,
|
||||
resources,
|
||||
labelSelector,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
backedUpItems,
|
||||
@@ -511,7 +517,8 @@ func TestBackupResourceOnlyIncludesSpecifiedNamespaces(t *testing.T) {
|
||||
podCommandExecutor,
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
nil,
|
||||
nil, // snapshot service
|
||||
nil, // restic backupper
|
||||
).(*defaultResourceBackupper)
|
||||
|
||||
itemBackupperFactory := &mockItemBackupperFactory{}
|
||||
@@ -547,6 +554,7 @@ func TestBackupResourceOnlyIncludesSpecifiedNamespaces(t *testing.T) {
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
).Return(itemBackupper)
|
||||
|
||||
client := &arktest.FakeDynamicClient{}
|
||||
@@ -554,7 +562,7 @@ func TestBackupResourceOnlyIncludesSpecifiedNamespaces(t *testing.T) {
|
||||
|
||||
coreV1Group := schema.GroupVersion{Group: "", Version: "v1"}
|
||||
dynamicFactory.On("ClientForGroupVersionResource", coreV1Group, namespacesResource, "").Return(client, nil)
|
||||
ns1 := unstructuredOrDie(`{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"ns-1"}}`)
|
||||
ns1 := arktest.UnstructuredOrDie(`{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"ns-1"}}`)
|
||||
client.On("Get", "ns-1", metav1.GetOptions{}).Return(ns1, nil)
|
||||
|
||||
itemHookHandler.On("handleHooks", mock.Anything, schema.GroupResource{Group: "", Resource: "namespaces"}, ns1, resourceHooks, hookPhasePre).Return(nil)
|
||||
@@ -568,12 +576,19 @@ func TestBackupResourceOnlyIncludesSpecifiedNamespaces(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBackupResourceListAllNamespacesExcludesCorrectly(t *testing.T) {
|
||||
backup := &v1.Backup{}
|
||||
backup := &v1.Backup{
|
||||
Spec: v1.BackupSpec{
|
||||
LabelSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
namespaces := collections.NewIncludesExcludes().Excludes("ns-1")
|
||||
resources := collections.NewIncludesExcludes().Includes("*")
|
||||
|
||||
labelSelector := "foo=bar"
|
||||
backedUpItems := map[itemKey]struct{}{}
|
||||
|
||||
dynamicFactory := &arktest.FakeDynamicFactory{}
|
||||
@@ -587,7 +602,7 @@ func TestBackupResourceListAllNamespacesExcludesCorrectly(t *testing.T) {
|
||||
|
||||
resourceHooks := []resourceHook{}
|
||||
|
||||
podCommandExecutor := &mockPodCommandExecutor{}
|
||||
podCommandExecutor := &arktest.MockPodCommandExecutor{}
|
||||
defer podCommandExecutor.AssertExpectations(t)
|
||||
|
||||
tarWriter := &fakeTarWriter{}
|
||||
@@ -597,7 +612,6 @@ func TestBackupResourceListAllNamespacesExcludesCorrectly(t *testing.T) {
|
||||
backup,
|
||||
namespaces,
|
||||
resources,
|
||||
labelSelector,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
backedUpItems,
|
||||
@@ -606,7 +620,8 @@ func TestBackupResourceListAllNamespacesExcludesCorrectly(t *testing.T) {
|
||||
podCommandExecutor,
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
nil,
|
||||
nil, // snapshot service
|
||||
nil, // restic backupper
|
||||
).(*defaultResourceBackupper)
|
||||
|
||||
itemBackupperFactory := &mockItemBackupperFactory{}
|
||||
@@ -631,6 +646,7 @@ func TestBackupResourceListAllNamespacesExcludesCorrectly(t *testing.T) {
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
).Return(itemBackupper)
|
||||
|
||||
client := &arktest.FakeDynamicClient{}
|
||||
@@ -639,12 +655,12 @@ func TestBackupResourceListAllNamespacesExcludesCorrectly(t *testing.T) {
|
||||
coreV1Group := schema.GroupVersion{Group: "", Version: "v1"}
|
||||
dynamicFactory.On("ClientForGroupVersionResource", coreV1Group, namespacesResource, "").Return(client, nil)
|
||||
|
||||
ns1 := unstructuredOrDie(`{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"ns-1"}}`)
|
||||
ns2 := unstructuredOrDie(`{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"ns-2"}}`)
|
||||
ns1 := arktest.UnstructuredOrDie(`{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"ns-1"}}`)
|
||||
ns2 := arktest.UnstructuredOrDie(`{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"ns-2"}}`)
|
||||
list := &unstructured.UnstructuredList{
|
||||
Items: []unstructured.Unstructured{*ns1, *ns2},
|
||||
}
|
||||
client.On("List", metav1.ListOptions{LabelSelector: labelSelector}).Return(list, nil)
|
||||
client.On("List", metav1.ListOptions{LabelSelector: metav1.FormatLabelSelector(backup.Spec.LabelSelector)}).Return(list, nil)
|
||||
|
||||
itemBackupper.On("backupItem", mock.AnythingOfType("*logrus.Entry"), ns2, kuberesource.Namespaces).Return(nil)
|
||||
|
||||
@@ -661,12 +677,13 @@ func (ibf *mockItemBackupperFactory) newItemBackupper(
|
||||
namespaces, resources *collections.IncludesExcludes,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
podCommandExecutor podexec.PodCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
discoveryHelper discovery.Helper,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
resticBackupper restic.Backupper,
|
||||
) ItemBackupper {
|
||||
args := ibf.Called(
|
||||
backup,
|
||||
@@ -680,6 +697,7 @@ func (ibf *mockItemBackupperFactory) newItemBackupper(
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
snapshotService,
|
||||
resticBackupper,
|
||||
)
|
||||
return args.Get(0).(ItemBackupper)
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestServiceAccountActionExecute(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "no crbs",
|
||||
serviceAccount: unstructuredOrDie(`
|
||||
serviceAccount: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ServiceAccount",
|
||||
@@ -81,7 +81,7 @@ func TestServiceAccountActionExecute(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "no matching crbs",
|
||||
serviceAccount: unstructuredOrDie(`
|
||||
serviceAccount: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ServiceAccount",
|
||||
@@ -124,7 +124,7 @@ func TestServiceAccountActionExecute(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "some matching crbs",
|
||||
serviceAccount: unstructuredOrDie(`
|
||||
serviceAccount: arktest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ServiceAccount",
|
||||
|
||||
Reference in New Issue
Block a user