add restic integration for doing pod volume backups/restores

Signed-off-by: Steve Kriss <steve@heptio.com>
This commit is contained in:
Steve Kriss
2018-06-06 09:48:10 -07:00
parent c2c5b9040c
commit 50d4084fac
86 changed files with 5421 additions and 485 deletions
+43 -17
View File
@@ -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
View File
@@ -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
+10 -8
View File
@@ -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,
)
)
+8 -8
View File
@@ -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,
+59 -5
View File
@@ -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()) {
+12 -11
View File
@@ -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)
}
+9 -8
View File
@@ -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 {
+16 -16
View File
@@ -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
}
+3 -3
View File
@@ -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",
-223
View File
@@ -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)
}
-276
View File
@@ -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)
}
+24 -14
View File
@@ -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)
}
+62 -44
View File
@@ -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)
}
+3 -3
View File
@@ -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",