mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-20 06:54:32 +00:00
migrate backup actions to plugins
Signed-off-by: Steve Kriss <steve@heptio.com>
This commit is contained in:
+58
-43
@@ -26,12 +26,13 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"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/util/collections"
|
||||
kubeutil "github.com/heptio/ark/pkg/util/kube"
|
||||
@@ -42,32 +43,16 @@ import (
|
||||
type Backupper interface {
|
||||
// Backup takes a backup using the specification in the api.Backup and writes backup and log data
|
||||
// to the given writers.
|
||||
Backup(backup *api.Backup, backupFile, logFile io.Writer) error
|
||||
Backup(backup *api.Backup, backupFile, logFile io.Writer, actions []ItemAction) error
|
||||
}
|
||||
|
||||
// kubernetesBackupper implements Backupper.
|
||||
type kubernetesBackupper struct {
|
||||
dynamicFactory client.DynamicFactory
|
||||
discoveryHelper discovery.Helper
|
||||
actions map[schema.GroupResource]Action
|
||||
podCommandExecutor podCommandExecutor
|
||||
|
||||
dynamicFactory client.DynamicFactory
|
||||
discoveryHelper discovery.Helper
|
||||
podCommandExecutor podCommandExecutor
|
||||
groupBackupperFactory groupBackupperFactory
|
||||
}
|
||||
|
||||
// ResourceIdentifier describes a single item by its group, resource, namespace, and name.
|
||||
type ResourceIdentifier struct {
|
||||
schema.GroupResource
|
||||
Namespace string
|
||||
Name string
|
||||
}
|
||||
|
||||
// Action is an actor that performs an operation on an individual item being backed up.
|
||||
type Action interface {
|
||||
// Execute allows the Action to perform arbitrary logic with the item being backed up and the
|
||||
// backup itself. Implementations may return additional ResourceIdentifiers that indicate specific
|
||||
// items that also need to be backed up.
|
||||
Execute(log *logrus.Entry, item runtime.Unstructured, backup *api.Backup) ([]ResourceIdentifier, error)
|
||||
snapshotService cloudprovider.SnapshotService
|
||||
}
|
||||
|
||||
type itemKey struct {
|
||||
@@ -76,6 +61,20 @@ type itemKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
type resolvedAction struct {
|
||||
ItemAction
|
||||
|
||||
resourceIncludesExcludes *collections.IncludesExcludes
|
||||
namespaceIncludesExcludes *collections.IncludesExcludes
|
||||
selector labels.Selector
|
||||
}
|
||||
|
||||
// LogSetter is an interface for a type that allows a FieldLogger
|
||||
// to be set on it.
|
||||
type LogSetter interface {
|
||||
SetLog(logrus.FieldLogger)
|
||||
}
|
||||
|
||||
func (i *itemKey) String() string {
|
||||
return fmt.Sprintf("resource=%s,namespace=%s,name=%s", i.resource, i.namespace, i.name)
|
||||
}
|
||||
@@ -84,38 +83,48 @@ func (i *itemKey) String() string {
|
||||
func NewKubernetesBackupper(
|
||||
discoveryHelper discovery.Helper,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
actions map[string]Action,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
) (Backupper, error) {
|
||||
resolvedActions, err := resolveActions(discoveryHelper, actions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &kubernetesBackupper{
|
||||
discoveryHelper: discoveryHelper,
|
||||
dynamicFactory: dynamicFactory,
|
||||
actions: resolvedActions,
|
||||
podCommandExecutor: podCommandExecutor,
|
||||
|
||||
discoveryHelper: discoveryHelper,
|
||||
dynamicFactory: dynamicFactory,
|
||||
podCommandExecutor: podCommandExecutor,
|
||||
groupBackupperFactory: &defaultGroupBackupperFactory{},
|
||||
snapshotService: snapshotService,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolveActions resolves the string-based map of group-resources to actions and returns a map of
|
||||
// schema.GroupResources to actions.
|
||||
func resolveActions(helper discovery.Helper, actions map[string]Action) (map[schema.GroupResource]Action, error) {
|
||||
ret := make(map[schema.GroupResource]Action)
|
||||
func resolveActions(actions []ItemAction, helper discovery.Helper) ([]resolvedAction, error) {
|
||||
var resolved []resolvedAction
|
||||
|
||||
for resource, action := range actions {
|
||||
gvr, _, err := helper.ResourceFor(schema.ParseGroupResource(resource).WithVersion(""))
|
||||
for _, action := range actions {
|
||||
resourceSelector, err := action.AppliesTo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[gvr.GroupResource()] = action
|
||||
|
||||
resources := getResourceIncludesExcludes(helper, resourceSelector.IncludedResources, resourceSelector.ExcludedResources)
|
||||
namespaces := collections.NewIncludesExcludes().Includes(resourceSelector.IncludedNamespaces...).Excludes(resourceSelector.ExcludedNamespaces...)
|
||||
|
||||
selector := labels.Everything()
|
||||
if resourceSelector.LabelSelector != "" {
|
||||
if selector, err = labels.Parse(resourceSelector.LabelSelector); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
res := resolvedAction{
|
||||
ItemAction: action,
|
||||
resourceIncludesExcludes: resources,
|
||||
namespaceIncludesExcludes: namespaces,
|
||||
selector: selector,
|
||||
}
|
||||
|
||||
resolved = append(resolved, res)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// getResourceIncludesExcludes takes the lists of resources to include and exclude, uses the
|
||||
@@ -172,7 +181,7 @@ func getResourceHooks(hookSpecs []api.BackupResourceHookSpec, discoveryHelper di
|
||||
|
||||
// Backup backs up the items specified in the Backup, placing them in a gzip-compressed tar file
|
||||
// written to backupFile. The finalized api.Backup is written to metadata.
|
||||
func (kb *kubernetesBackupper) Backup(backup *api.Backup, backupFile, logFile io.Writer) error {
|
||||
func (kb *kubernetesBackupper) Backup(backup *api.Backup, backupFile, logFile io.Writer, actions []ItemAction) error {
|
||||
gzippedData := gzip.NewWriter(backupFile)
|
||||
defer gzippedData.Close()
|
||||
|
||||
@@ -215,6 +224,11 @@ func (kb *kubernetesBackupper) Backup(backup *api.Backup, backupFile, logFile io
|
||||
"networkpolicies": newCohabitatingResource("networkpolicies", "extensions", "networking.k8s.io"),
|
||||
}
|
||||
|
||||
resolvedActions, err := resolveActions(actions, kb.discoveryHelper)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gb := kb.groupBackupperFactory.newGroupBackupper(
|
||||
log,
|
||||
backup,
|
||||
@@ -225,10 +239,11 @@ func (kb *kubernetesBackupper) Backup(backup *api.Backup, backupFile, logFile io
|
||||
kb.discoveryHelper,
|
||||
backedUpItems,
|
||||
cohabitatingResources,
|
||||
kb.actions,
|
||||
resolvedActions,
|
||||
kb.podCommandExecutor,
|
||||
tw,
|
||||
resourceHooks,
|
||||
kb.snapshotService,
|
||||
)
|
||||
|
||||
for _, group := range kb.discoveryHelper.Resources() {
|
||||
|
||||
@@ -30,25 +30,33 @@ import (
|
||||
// backupPVAction inspects a PersistentVolumeClaim for the PersistentVolume
|
||||
// that it references and backs it up
|
||||
type backupPVAction struct {
|
||||
log logrus.FieldLogger
|
||||
}
|
||||
|
||||
func NewBackupPVAction() Action {
|
||||
return &backupPVAction{}
|
||||
func NewBackupPVAction(log logrus.FieldLogger) ItemAction {
|
||||
return &backupPVAction{log: log}
|
||||
}
|
||||
|
||||
var pvGroupResource = schema.GroupResource{Group: "", Resource: "persistentvolumes"}
|
||||
|
||||
func (a *backupPVAction) AppliesTo() (ResourceSelector, error) {
|
||||
return ResourceSelector{
|
||||
IncludedResources: []string{"persistentvolumeclaims"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Execute finds the PersistentVolume referenced by the provided
|
||||
// PersistentVolumeClaim and backs it up
|
||||
func (a *backupPVAction) Execute(log *logrus.Entry, item runtime.Unstructured, backup *v1.Backup) ([]ResourceIdentifier, error) {
|
||||
log.Info("Executing backupPVAction")
|
||||
func (a *backupPVAction) Execute(item runtime.Unstructured, backup *v1.Backup) (runtime.Unstructured, []ResourceIdentifier, error) {
|
||||
a.log.Info("Executing backupPVAction")
|
||||
|
||||
var additionalItems []ResourceIdentifier
|
||||
|
||||
pvc := item.UnstructuredContent()
|
||||
|
||||
volumeName, err := collections.GetString(pvc, "spec.volumeName")
|
||||
if err != nil {
|
||||
return additionalItems, errors.WithMessage(err, "unable to get spec.volumeName")
|
||||
return nil, nil, errors.WithMessage(err, "unable to get spec.volumeName")
|
||||
}
|
||||
|
||||
additionalItems = append(additionalItems, ResourceIdentifier{
|
||||
@@ -56,5 +64,5 @@ func (a *backupPVAction) Execute(log *logrus.Entry, item runtime.Unstructured, b
|
||||
Name: volumeName,
|
||||
})
|
||||
|
||||
return additionalItems, nil
|
||||
return item, additionalItems, nil
|
||||
}
|
||||
|
||||
@@ -35,13 +35,13 @@ func TestBackupPVAction(t *testing.T) {
|
||||
|
||||
backup := &v1.Backup{}
|
||||
|
||||
a := NewBackupPVAction()
|
||||
a := NewBackupPVAction(arktest.NewLogger())
|
||||
|
||||
additional, err := a.Execute(arktest.NewLogger(), pvc, backup)
|
||||
_, additional, err := a.Execute(pvc, backup)
|
||||
assert.EqualError(t, err, "unable to get spec.volumeName: key volumeName not found")
|
||||
|
||||
pvc.Object["spec"].(map[string]interface{})["volumeName"] = "myVolume"
|
||||
additional, err = a.Execute(arktest.NewLogger(), pvc, backup)
|
||||
_, additional, err = a.Execute(pvc, backup)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, additional, 1)
|
||||
assert.Equal(t, ResourceIdentifier{GroupResource: pvGroupResource, Name: "myVolume"}, additional[0])
|
||||
|
||||
+51
-28
@@ -41,6 +41,7 @@ import (
|
||||
|
||||
"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/util/collections"
|
||||
kubeutil "github.com/heptio/ark/pkg/util/kube"
|
||||
@@ -55,49 +56,73 @@ var (
|
||||
)
|
||||
|
||||
type fakeAction struct {
|
||||
selector ResourceSelector
|
||||
ids []string
|
||||
backups []*v1.Backup
|
||||
backups []v1.Backup
|
||||
additionalItems []ResourceIdentifier
|
||||
}
|
||||
|
||||
var _ Action = &fakeAction{}
|
||||
var _ ItemAction = &fakeAction{}
|
||||
|
||||
func (a *fakeAction) Execute(log *logrus.Entry, item runtime.Unstructured, backup *v1.Backup) ([]ResourceIdentifier, error) {
|
||||
func newFakeAction(resource string) *fakeAction {
|
||||
return (&fakeAction{}).ForResource(resource)
|
||||
}
|
||||
|
||||
func (a *fakeAction) Execute(item runtime.Unstructured, backup *v1.Backup) (runtime.Unstructured, []ResourceIdentifier, error) {
|
||||
metadata, err := meta.Accessor(item)
|
||||
if err != nil {
|
||||
return a.additionalItems, err
|
||||
return item, a.additionalItems, err
|
||||
}
|
||||
a.ids = append(a.ids, kubeutil.NamespaceAndName(metadata))
|
||||
a.backups = append(a.backups, backup)
|
||||
a.backups = append(a.backups, *backup)
|
||||
|
||||
return a.additionalItems, nil
|
||||
return item, a.additionalItems, nil
|
||||
}
|
||||
|
||||
func (a *fakeAction) AppliesTo() (ResourceSelector, error) {
|
||||
return a.selector, nil
|
||||
}
|
||||
|
||||
func (a *fakeAction) ForResource(resource string) *fakeAction {
|
||||
a.selector.IncludedResources = []string{resource}
|
||||
return a
|
||||
}
|
||||
|
||||
func TestResolveActions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input map[string]Action
|
||||
expected map[schema.GroupResource]Action
|
||||
input []ItemAction
|
||||
expected []resolvedAction
|
||||
resourcesWithErrors []string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "empty input",
|
||||
input: map[string]Action{},
|
||||
expected: map[schema.GroupResource]Action{},
|
||||
input: []ItemAction{},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "mapper error",
|
||||
input: map[string]Action{"badresource": &fakeAction{}},
|
||||
expected: map[schema.GroupResource]Action{},
|
||||
name: "resolve error",
|
||||
input: []ItemAction{&fakeAction{selector: ResourceSelector{LabelSelector: "=invalid-selector"}}},
|
||||
expected: nil,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "resolved",
|
||||
input: map[string]Action{"foo": &fakeAction{}, "bar": &fakeAction{}},
|
||||
expected: map[schema.GroupResource]Action{
|
||||
{Group: "somegroup", Resource: "foodies"}: &fakeAction{},
|
||||
{Group: "anothergroup", Resource: "barnacles"}: &fakeAction{},
|
||||
input: []ItemAction{newFakeAction("foo"), newFakeAction("bar")},
|
||||
expected: []resolvedAction{
|
||||
{
|
||||
ItemAction: newFakeAction("foo"),
|
||||
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("foodies.somegroup"),
|
||||
namespaceIncludesExcludes: collections.NewIncludesExcludes(),
|
||||
selector: labels.Everything(),
|
||||
},
|
||||
{
|
||||
ItemAction: newFakeAction("bar"),
|
||||
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("barnacles.anothergroup"),
|
||||
namespaceIncludesExcludes: collections.NewIncludesExcludes(),
|
||||
selector: labels.Everything(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -112,7 +137,7 @@ func TestResolveActions(t *testing.T) {
|
||||
}
|
||||
discoveryHelper := arktest.NewFakeDiscoveryHelper(false, resources)
|
||||
|
||||
actual, err := resolveActions(discoveryHelper, test.input)
|
||||
actual, err := resolveActions(test.input, discoveryHelper)
|
||||
gotError := err != nil
|
||||
|
||||
if e, a := test.expectError, gotError; e != a {
|
||||
@@ -349,7 +374,6 @@ func TestBackup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
backup *v1.Backup
|
||||
actions map[string]Action
|
||||
expectedNamespaces *collections.IncludesExcludes
|
||||
expectedResources *collections.IncludesExcludes
|
||||
expectedLabelSelector string
|
||||
@@ -369,7 +393,6 @@ func TestBackup(t *testing.T) {
|
||||
ExcludedNamespaces: []string{"c", "d"},
|
||||
},
|
||||
},
|
||||
actions: map[string]Action{},
|
||||
expectedNamespaces: collections.NewIncludesExcludes().Includes("a", "b").Excludes("c", "d"),
|
||||
expectedResources: collections.NewIncludesExcludes().Includes("configmaps", "certificatesigningrequests.certificates.k8s.io", "roles.rbac.authorization.k8s.io"),
|
||||
expectedHooks: []resourceHook{},
|
||||
@@ -388,7 +411,6 @@ func TestBackup(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
actions: map[string]Action{},
|
||||
expectedNamespaces: collections.NewIncludesExcludes(),
|
||||
expectedResources: collections.NewIncludesExcludes(),
|
||||
expectedHooks: []resourceHook{},
|
||||
@@ -402,7 +424,6 @@ func TestBackup(t *testing.T) {
|
||||
{
|
||||
name: "backupGroup errors",
|
||||
backup: &v1.Backup{},
|
||||
actions: map[string]Action{},
|
||||
expectedNamespaces: collections.NewIncludesExcludes(),
|
||||
expectedResources: collections.NewIncludesExcludes(),
|
||||
expectedHooks: []resourceHook{},
|
||||
@@ -440,7 +461,6 @@ func TestBackup(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
actions: map[string]Action{},
|
||||
expectedNamespaces: collections.NewIncludesExcludes(),
|
||||
expectedResources: collections.NewIncludesExcludes(),
|
||||
expectedHooks: []resourceHook{
|
||||
@@ -491,8 +511,8 @@ func TestBackup(t *testing.T) {
|
||||
b, err := NewKubernetesBackupper(
|
||||
discoveryHelper,
|
||||
dynamicFactory,
|
||||
test.actions,
|
||||
podCommandExecutor,
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
kb := b.(*kubernetesBackupper)
|
||||
@@ -519,10 +539,11 @@ func TestBackup(t *testing.T) {
|
||||
discoveryHelper,
|
||||
map[itemKey]struct{}{}, // backedUpItems
|
||||
cohabitatingResources,
|
||||
kb.actions,
|
||||
mock.Anything,
|
||||
kb.podCommandExecutor,
|
||||
mock.Anything, // tarWriter
|
||||
test.expectedHooks,
|
||||
mock.Anything,
|
||||
).Return(groupBackupper)
|
||||
|
||||
for group, err := range test.backupGroupErrors {
|
||||
@@ -531,7 +552,7 @@ func TestBackup(t *testing.T) {
|
||||
|
||||
var backupFile, logFile bytes.Buffer
|
||||
|
||||
err = b.Backup(test.backup, &backupFile, &logFile)
|
||||
err = b.Backup(test.backup, &backupFile, &logFile, nil)
|
||||
defer func() {
|
||||
// print log if anything failed
|
||||
if t.Failed() {
|
||||
@@ -560,7 +581,7 @@ type mockGroupBackupperFactory struct {
|
||||
}
|
||||
|
||||
func (f *mockGroupBackupperFactory) newGroupBackupper(
|
||||
log *logrus.Entry,
|
||||
log logrus.FieldLogger,
|
||||
backup *v1.Backup,
|
||||
namespaces, resources *collections.IncludesExcludes,
|
||||
labelSelector string,
|
||||
@@ -568,10 +589,11 @@ func (f *mockGroupBackupperFactory) newGroupBackupper(
|
||||
discoveryHelper discovery.Helper,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
cohabitatingResources map[string]*cohabitatingResource,
|
||||
actions map[schema.GroupResource]Action,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
) groupBackupper {
|
||||
args := f.Called(
|
||||
log,
|
||||
@@ -587,6 +609,7 @@ func (f *mockGroupBackupperFactory) newGroupBackupper(
|
||||
podCommandExecutor,
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
snapshotService,
|
||||
)
|
||||
return args.Get(0).(groupBackupper)
|
||||
}
|
||||
|
||||
@@ -19,19 +19,21 @@ package backup
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kuberrs "k8s.io/apimachinery/pkg/util/errors"
|
||||
|
||||
"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/util/collections"
|
||||
"github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
kuberrs "k8s.io/apimachinery/pkg/util/errors"
|
||||
)
|
||||
|
||||
type groupBackupperFactory interface {
|
||||
newGroupBackupper(
|
||||
log *logrus.Entry,
|
||||
log logrus.FieldLogger,
|
||||
backup *v1.Backup,
|
||||
namespaces, resources *collections.IncludesExcludes,
|
||||
labelSelector string,
|
||||
@@ -39,17 +41,18 @@ type groupBackupperFactory interface {
|
||||
discoveryHelper discovery.Helper,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
cohabitatingResources map[string]*cohabitatingResource,
|
||||
actions map[schema.GroupResource]Action,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
) groupBackupper
|
||||
}
|
||||
|
||||
type defaultGroupBackupperFactory struct{}
|
||||
|
||||
func (f *defaultGroupBackupperFactory) newGroupBackupper(
|
||||
log *logrus.Entry,
|
||||
log logrus.FieldLogger,
|
||||
backup *v1.Backup,
|
||||
namespaces, resources *collections.IncludesExcludes,
|
||||
labelSelector string,
|
||||
@@ -57,26 +60,27 @@ func (f *defaultGroupBackupperFactory) newGroupBackupper(
|
||||
discoveryHelper discovery.Helper,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
cohabitatingResources map[string]*cohabitatingResource,
|
||||
actions map[schema.GroupResource]Action,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
) groupBackupper {
|
||||
return &defaultGroupBackupper{
|
||||
log: log,
|
||||
backup: backup,
|
||||
namespaces: namespaces,
|
||||
resources: resources,
|
||||
labelSelector: labelSelector,
|
||||
dynamicFactory: dynamicFactory,
|
||||
discoveryHelper: discoveryHelper,
|
||||
backedUpItems: backedUpItems,
|
||||
cohabitatingResources: cohabitatingResources,
|
||||
actions: actions,
|
||||
podCommandExecutor: podCommandExecutor,
|
||||
tarWriter: tarWriter,
|
||||
resourceHooks: resourceHooks,
|
||||
|
||||
log: log,
|
||||
backup: backup,
|
||||
namespaces: namespaces,
|
||||
resources: resources,
|
||||
labelSelector: labelSelector,
|
||||
dynamicFactory: dynamicFactory,
|
||||
discoveryHelper: discoveryHelper,
|
||||
backedUpItems: backedUpItems,
|
||||
cohabitatingResources: cohabitatingResources,
|
||||
actions: actions,
|
||||
podCommandExecutor: podCommandExecutor,
|
||||
tarWriter: tarWriter,
|
||||
resourceHooks: resourceHooks,
|
||||
snapshotService: snapshotService,
|
||||
resourceBackupperFactory: &defaultResourceBackupperFactory{},
|
||||
}
|
||||
}
|
||||
@@ -86,7 +90,7 @@ type groupBackupper interface {
|
||||
}
|
||||
|
||||
type defaultGroupBackupper struct {
|
||||
log *logrus.Entry
|
||||
log logrus.FieldLogger
|
||||
backup *v1.Backup
|
||||
namespaces, resources *collections.IncludesExcludes
|
||||
labelSelector string
|
||||
@@ -94,10 +98,11 @@ type defaultGroupBackupper struct {
|
||||
discoveryHelper discovery.Helper
|
||||
backedUpItems map[itemKey]struct{}
|
||||
cohabitatingResources map[string]*cohabitatingResource
|
||||
actions map[schema.GroupResource]Action
|
||||
actions []resolvedAction
|
||||
podCommandExecutor podCommandExecutor
|
||||
tarWriter tarWriter
|
||||
resourceHooks []resourceHook
|
||||
snapshotService cloudprovider.SnapshotService
|
||||
resourceBackupperFactory resourceBackupperFactory
|
||||
}
|
||||
|
||||
@@ -121,6 +126,7 @@ func (gb *defaultGroupBackupper) backupGroup(group *metav1.APIResourceList) erro
|
||||
gb.podCommandExecutor,
|
||||
gb.tarWriter,
|
||||
gb.resourceHooks,
|
||||
gb.snapshotService,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"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/util/collections"
|
||||
arktest "github.com/heptio/ark/pkg/util/test"
|
||||
@@ -56,8 +57,11 @@ func TestBackupGroup(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
actions := map[schema.GroupResource]Action{
|
||||
{Group: "", Resource: "pods"}: &fakeAction{},
|
||||
actions := []resolvedAction{
|
||||
{
|
||||
ItemAction: newFakeAction("pods"),
|
||||
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("pods"),
|
||||
},
|
||||
}
|
||||
|
||||
podCommandExecutor := &mockPodCommandExecutor{}
|
||||
@@ -83,6 +87,7 @@ func TestBackupGroup(t *testing.T) {
|
||||
podCommandExecutor,
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
nil,
|
||||
).(*defaultGroupBackupper)
|
||||
|
||||
resourceBackupperFactory := &mockResourceBackupperFactory{}
|
||||
@@ -106,6 +111,7 @@ func TestBackupGroup(t *testing.T) {
|
||||
podCommandExecutor,
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
nil,
|
||||
).Return(resourceBackupper)
|
||||
|
||||
group := &metav1.APIResourceList{
|
||||
@@ -140,7 +146,7 @@ type mockResourceBackupperFactory struct {
|
||||
}
|
||||
|
||||
func (rbf *mockResourceBackupperFactory) newResourceBackupper(
|
||||
log *logrus.Entry,
|
||||
log logrus.FieldLogger,
|
||||
backup *v1.Backup,
|
||||
namespaces *collections.IncludesExcludes,
|
||||
resources *collections.IncludesExcludes,
|
||||
@@ -149,10 +155,11 @@ func (rbf *mockResourceBackupperFactory) newResourceBackupper(
|
||||
discoveryHelper discovery.Helper,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
cohabitatingResources map[string]*cohabitatingResource,
|
||||
actions map[schema.GroupResource]Action,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
) resourceBackupper {
|
||||
args := rbf.Called(
|
||||
log,
|
||||
@@ -168,6 +175,7 @@ func (rbf *mockResourceBackupperFactory) newResourceBackupper(
|
||||
podCommandExecutor,
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
snapshotService,
|
||||
)
|
||||
return args.Get(0).(resourceBackupper)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
)
|
||||
|
||||
// ItemAction is an actor that performs an operation on an individual item being backed up.
|
||||
type ItemAction interface {
|
||||
// AppliesTo returns information about which resources this action should be invoked for.
|
||||
AppliesTo() (ResourceSelector, error)
|
||||
|
||||
// Execute allows the ItemAction to perform arbitrary logic with the item being backed up and the
|
||||
// backup itself. Implementations may return additional ResourceIdentifiers that indicate specific
|
||||
// items that also need to be backed up.
|
||||
Execute(item runtime.Unstructured, backup *api.Backup) (runtime.Unstructured, []ResourceIdentifier, error)
|
||||
}
|
||||
|
||||
// ResourceIdentifier describes a single item by its group, resource, namespace, and name.
|
||||
type ResourceIdentifier struct {
|
||||
schema.GroupResource
|
||||
Namespace string
|
||||
Name string
|
||||
}
|
||||
|
||||
// ResourceSelector is a collection of included/excluded namespaces,
|
||||
// included/excluded resources, and a label-selector that can be used
|
||||
// to match a set of items from a cluster.
|
||||
type ResourceSelector struct {
|
||||
IncludedNamespaces []string
|
||||
ExcludedNamespaces []string
|
||||
IncludedResources []string
|
||||
ExcludedResources []string
|
||||
LabelSelector string
|
||||
}
|
||||
+124
-15
@@ -22,16 +22,21 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/client"
|
||||
"github.com/heptio/ark/pkg/discovery"
|
||||
"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/client"
|
||||
"github.com/heptio/ark/pkg/cloudprovider"
|
||||
"github.com/heptio/ark/pkg/discovery"
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
kubeutil "github.com/heptio/ark/pkg/util/kube"
|
||||
)
|
||||
|
||||
type itemBackupperFactory interface {
|
||||
@@ -39,12 +44,13 @@ type itemBackupperFactory interface {
|
||||
backup *api.Backup,
|
||||
namespaces, resources *collections.IncludesExcludes,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
actions map[schema.GroupResource]Action,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
discoveryHelper discovery.Helper,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
) ItemBackupper
|
||||
}
|
||||
|
||||
@@ -54,12 +60,13 @@ func (f *defaultItemBackupperFactory) newItemBackupper(
|
||||
backup *api.Backup,
|
||||
namespaces, resources *collections.IncludesExcludes,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
actions map[schema.GroupResource]Action,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
discoveryHelper discovery.Helper,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
) ItemBackupper {
|
||||
ib := &defaultItemBackupper{
|
||||
backup: backup,
|
||||
@@ -71,7 +78,7 @@ func (f *defaultItemBackupperFactory) newItemBackupper(
|
||||
resourceHooks: resourceHooks,
|
||||
dynamicFactory: dynamicFactory,
|
||||
discoveryHelper: discoveryHelper,
|
||||
|
||||
snapshotService: snapshotService,
|
||||
itemHookHandler: &defaultItemHookHandler{
|
||||
podCommandExecutor: podCommandExecutor,
|
||||
},
|
||||
@@ -84,7 +91,7 @@ func (f *defaultItemBackupperFactory) newItemBackupper(
|
||||
}
|
||||
|
||||
type ItemBackupper interface {
|
||||
backupItem(logger *logrus.Entry, obj runtime.Unstructured, groupResource schema.GroupResource) error
|
||||
backupItem(logger logrus.FieldLogger, obj runtime.Unstructured, groupResource schema.GroupResource) error
|
||||
}
|
||||
|
||||
type defaultItemBackupper struct {
|
||||
@@ -92,11 +99,12 @@ type defaultItemBackupper struct {
|
||||
namespaces *collections.IncludesExcludes
|
||||
resources *collections.IncludesExcludes
|
||||
backedUpItems map[itemKey]struct{}
|
||||
actions map[schema.GroupResource]Action
|
||||
actions []resolvedAction
|
||||
tarWriter tarWriter
|
||||
resourceHooks []resourceHook
|
||||
dynamicFactory client.DynamicFactory
|
||||
discoveryHelper discovery.Helper
|
||||
snapshotService cloudprovider.SnapshotService
|
||||
|
||||
itemHookHandler itemHookHandler
|
||||
additionalItemBackupper ItemBackupper
|
||||
@@ -107,7 +115,7 @@ var namespacesGroupResource = schema.GroupResource{Group: "", Resource: "namespa
|
||||
|
||||
// backupItem backs up an individual item to tarWriter. The item may be excluded based on the
|
||||
// namespaces IncludesExcludes list.
|
||||
func (ib *defaultItemBackupper) backupItem(logger *logrus.Entry, obj runtime.Unstructured, groupResource schema.GroupResource) error {
|
||||
func (ib *defaultItemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstructured, groupResource schema.GroupResource) error {
|
||||
metadata, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -154,18 +162,38 @@ func (ib *defaultItemBackupper) backupItem(logger *logrus.Entry, obj runtime.Uns
|
||||
|
||||
log.Info("Backing up resource")
|
||||
|
||||
item := obj.UnstructuredContent()
|
||||
// Never save status
|
||||
delete(item, "status")
|
||||
delete(obj.UnstructuredContent(), "status")
|
||||
|
||||
if err := ib.itemHookHandler.handleHooks(log, groupResource, obj, ib.resourceHooks); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if action, found := ib.actions[groupResource]; found {
|
||||
for _, action := range ib.actions {
|
||||
if !action.resourceIncludesExcludes.ShouldInclude(groupResource.String()) {
|
||||
log.Debug("Skipping action because it does not apply to this resource")
|
||||
continue
|
||||
}
|
||||
|
||||
if namespace != "" && !action.namespaceIncludesExcludes.ShouldInclude(namespace) {
|
||||
log.Debug("Skipping action because it does not apply to this namespace")
|
||||
continue
|
||||
}
|
||||
|
||||
if !action.selector.Matches(labels.Set(metadata.GetLabels())) {
|
||||
log.Debug("Skipping action because label selector does not match")
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info("Executing custom action")
|
||||
|
||||
if additionalItemIdentifiers, err := action.Execute(log, obj, ib.backup); err == nil {
|
||||
if logSetter, ok := action.ItemAction.(LogSetter); ok {
|
||||
logSetter.SetLog(log)
|
||||
}
|
||||
|
||||
if updatedItem, additionalItemIdentifiers, err := action.Execute(obj, ib.backup); err == nil {
|
||||
obj = updatedItem
|
||||
|
||||
for _, additionalItem := range additionalItemIdentifiers {
|
||||
gvr, resource, err := ib.discoveryHelper.ResourceFor(additionalItem.GroupResource.WithVersion(""))
|
||||
if err != nil {
|
||||
@@ -189,6 +217,16 @@ func (ib *defaultItemBackupper) backupItem(logger *logrus.Entry, obj runtime.Uns
|
||||
}
|
||||
}
|
||||
|
||||
if groupResource == pvGroupResource {
|
||||
if ib.snapshotService == nil {
|
||||
log.Debug("Skipping Persistent Volume snapshot because they're not enabled.")
|
||||
} else {
|
||||
if err := ib.takePVSnapshot(obj, ib.backup, log); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var filePath string
|
||||
if namespace != "" {
|
||||
filePath = filepath.Join(api.ResourcesDir, groupResource.String(), api.NamespaceScopedDir, namespace, name+".json")
|
||||
@@ -196,7 +234,7 @@ func (ib *defaultItemBackupper) backupItem(logger *logrus.Entry, obj runtime.Uns
|
||||
filePath = filepath.Join(api.ResourcesDir, groupResource.String(), api.ClusterScopedDir, name+".json")
|
||||
}
|
||||
|
||||
itemBytes, err := json.Marshal(item)
|
||||
itemBytes, err := json.Marshal(obj.UnstructuredContent())
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@@ -219,3 +257,74 @@ func (ib *defaultItemBackupper) backupItem(logger *logrus.Entry, obj runtime.Uns
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// zoneLabel is the label that stores availability-zone info
|
||||
// on PVs
|
||||
const zoneLabel = "failure-domain.beta.kubernetes.io/zone"
|
||||
|
||||
// takePVSnapshot triggers a snapshot for the volume/disk underlying a PersistentVolume if the provided
|
||||
// backup has volume snapshots enabled and the PV is of a compatible type. Also records cloud
|
||||
// disk type and IOPS (if applicable) to be able to restore to current state later.
|
||||
func (ib *defaultItemBackupper) takePVSnapshot(pv runtime.Unstructured, backup *api.Backup, log logrus.FieldLogger) error {
|
||||
log.Info("Executing takePVSnapshot")
|
||||
|
||||
if backup.Spec.SnapshotVolumes != nil && !*backup.Spec.SnapshotVolumes {
|
||||
log.Info("Backup has volume snapshots disabled; skipping volume snapshot action.")
|
||||
return nil
|
||||
}
|
||||
|
||||
metadata, err := meta.Accessor(pv)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
name := metadata.GetName()
|
||||
var pvFailureDomainZone string
|
||||
labels := metadata.GetLabels()
|
||||
|
||||
if labels[zoneLabel] != "" {
|
||||
pvFailureDomainZone = labels[zoneLabel]
|
||||
} else {
|
||||
log.Infof("label %q is not present on PersistentVolume", zoneLabel)
|
||||
}
|
||||
|
||||
volumeID, err := kubeutil.GetVolumeID(pv.UnstructuredContent())
|
||||
// non-nil error means it's a supported PV source but volume ID can't be found
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error getting volume ID for PersistentVolume")
|
||||
}
|
||||
// no volumeID / nil error means unsupported PV source
|
||||
if volumeID == "" {
|
||||
log.Info("PersistentVolume is not a supported volume type for snapshots, skipping.")
|
||||
return nil
|
||||
}
|
||||
|
||||
log = log.WithField("volumeID", volumeID)
|
||||
|
||||
log.Info("Snapshotting PersistentVolume")
|
||||
snapshotID, err := ib.snapshotService.CreateSnapshot(volumeID, pvFailureDomainZone)
|
||||
if err != nil {
|
||||
// log+error on purpose - log goes to the per-backup log file, error goes to the backup
|
||||
log.WithError(err).Error("error creating snapshot")
|
||||
return errors.WithMessage(err, "error creating snapshot")
|
||||
}
|
||||
|
||||
volumeType, iops, err := ib.snapshotService.GetVolumeInfo(volumeID, pvFailureDomainZone)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error getting volume info")
|
||||
return errors.WithMessage(err, "error getting volume info")
|
||||
}
|
||||
|
||||
if backup.Status.VolumeBackups == nil {
|
||||
backup.Status.VolumeBackups = make(map[string]*api.VolumeBackupInfo)
|
||||
}
|
||||
|
||||
backup.Status.VolumeBackups[name] = &api.VolumeBackupInfo{
|
||||
SnapshotID: snapshotID,
|
||||
Type: volumeType,
|
||||
Iops: iops,
|
||||
AvailabilityZone: pvFailureDomainZone,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,8 +22,10 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
arktest "github.com/heptio/ark/pkg/util/test"
|
||||
"github.com/pkg/errors"
|
||||
@@ -33,6 +35,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
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"
|
||||
)
|
||||
@@ -134,6 +137,8 @@ func TestBackupItemNoSkips(t *testing.T) {
|
||||
expectedActionID string
|
||||
customActionAdditionalItemIdentifiers []ResourceIdentifier
|
||||
customActionAdditionalItems []runtime.Unstructured
|
||||
groupResource string
|
||||
snapshottableVolumes map[string]api.VolumeBackupInfo
|
||||
}{
|
||||
{
|
||||
name: "explicit namespace include",
|
||||
@@ -223,12 +228,33 @@ func TestBackupItemNoSkips(t *testing.T) {
|
||||
unstructuredOrDie(`{"apiVersion":"g2/v1","kind":"r1","metadata":{"namespace":"ns2","name":"n2"}}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "takePVSnapshot is not invoked for PVs when snapshotService == nil",
|
||||
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
|
||||
item: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-east-1c"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectExcluded: false,
|
||||
expectedTarHeaderName: "resources/persistentvolumes/cluster/mypv.json",
|
||||
groupResource: "persistentvolumes",
|
||||
},
|
||||
{
|
||||
name: "takePVSnapshot is invoked for PVs when snapshotService != nil",
|
||||
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
|
||||
item: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-east-1c"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectExcluded: false,
|
||||
expectedTarHeaderName: "resources/persistentvolumes/cluster/mypv.json",
|
||||
groupResource: "persistentvolumes",
|
||||
snapshottableVolumes: map[string]api.VolumeBackupInfo{
|
||||
"vol-abc123": {SnapshotID: "snapshot-1", AvailabilityZone: "us-east-1c"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var (
|
||||
actions map[schema.GroupResource]Action
|
||||
actions []resolvedAction
|
||||
action *fakeAction
|
||||
backup = &v1.Backup{}
|
||||
groupResource = schema.ParseGroupResource("resource.group")
|
||||
@@ -237,6 +263,10 @@ func TestBackupItemNoSkips(t *testing.T) {
|
||||
w = &fakeTarWriter{}
|
||||
)
|
||||
|
||||
if test.groupResource != "" {
|
||||
groupResource = schema.ParseGroupResource(test.groupResource)
|
||||
}
|
||||
|
||||
item, err := getAsMap(test.item)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -258,8 +288,13 @@ func TestBackupItemNoSkips(t *testing.T) {
|
||||
action = &fakeAction{
|
||||
additionalItems: test.customActionAdditionalItemIdentifiers,
|
||||
}
|
||||
actions = map[schema.GroupResource]Action{
|
||||
groupResource: action,
|
||||
actions = []resolvedAction{
|
||||
{
|
||||
ItemAction: action,
|
||||
namespaceIncludesExcludes: collections.NewIncludesExcludes(),
|
||||
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes(groupResource.String()),
|
||||
selector: labels.Everything(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,8 +319,15 @@ func TestBackupItemNoSkips(t *testing.T) {
|
||||
resourceHooks,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
nil,
|
||||
).(*defaultItemBackupper)
|
||||
|
||||
var snapshotService *arktest.FakeSnapshotService
|
||||
if test.snapshottableVolumes != nil {
|
||||
snapshotService = &arktest.FakeSnapshotService{SnapshottableVolumes: test.snapshottableVolumes}
|
||||
b.snapshotService = snapshotService
|
||||
}
|
||||
|
||||
// make sure the podCommandExecutor was set correctly in the real hook handler
|
||||
assert.Equal(t, podCommandExecutor, b.itemHookHandler.(*defaultItemHookHandler).podCommandExecutor)
|
||||
|
||||
@@ -361,10 +403,231 @@ func TestBackupItemNoSkips(t *testing.T) {
|
||||
t.Errorf("action.ids[0]: expected %s, got %s", e, a)
|
||||
}
|
||||
|
||||
if len(action.backups) != 1 {
|
||||
t.Errorf("unexpected custom action backups: %#v", action.backups)
|
||||
} else if e, a := backup, action.backups[0]; e != a {
|
||||
t.Errorf("action.backups[0]: expected %#v, got %#v", e, a)
|
||||
require.Equal(t, 1, len(action.backups), "unexpected custom action backups: %#v", action.backups)
|
||||
assert.Equal(t, backup, &(action.backups[0]), "backup")
|
||||
}
|
||||
|
||||
if test.snapshottableVolumes != nil {
|
||||
require.Equal(t, 1, len(snapshotService.SnapshotsTaken))
|
||||
|
||||
var expectedBackups []api.VolumeBackupInfo
|
||||
for _, vbi := range test.snapshottableVolumes {
|
||||
expectedBackups = append(expectedBackups, vbi)
|
||||
}
|
||||
|
||||
var actualBackups []api.VolumeBackupInfo
|
||||
for _, vbi := range backup.Status.VolumeBackups {
|
||||
actualBackups = append(actualBackups, *vbi)
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedBackups, actualBackups)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTakePVSnapshot(t *testing.T) {
|
||||
iops := int64(1000)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
snapshotEnabled bool
|
||||
pv string
|
||||
ttl time.Duration
|
||||
expectError bool
|
||||
expectedVolumeID string
|
||||
expectedSnapshotsTaken int
|
||||
existingVolumeBackups map[string]*v1.VolumeBackupInfo
|
||||
volumeInfo map[string]v1.VolumeBackupInfo
|
||||
}{
|
||||
{
|
||||
name: "snapshot disabled",
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}}`,
|
||||
snapshotEnabled: false,
|
||||
},
|
||||
{
|
||||
name: "can't find volume id - missing spec",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}}`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "unsupported PV source type",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"unsupportedPVSource": {}}}`,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "can't find volume id - aws but no volume id",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"awsElasticBlockStore": {}}}`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "can't find volume id - gce but no volume id",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {}}}`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "aws - simple volume id",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-east-1c"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "vol-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"vol-abc123": {Type: "gp", SnapshotID: "snap-1", AvailabilityZone: "us-east-1c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws - simple volume id with provisioned IOPS",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-east-1c"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "vol-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"vol-abc123": {Type: "io1", Iops: &iops, SnapshotID: "snap-1", AvailabilityZone: "us-east-1c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws - dynamically provisioned volume id",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-west-2a"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-west-2a/vol-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "vol-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"vol-abc123": {Type: "gp", SnapshotID: "snap-1", AvailabilityZone: "us-west-2a"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gce",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "gcp-zone2"}}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "pd-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"pd-abc123": {Type: "gp", SnapshotID: "snap-1", AvailabilityZone: "gcp-zone2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "azure",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"azureDisk": {"diskName": "foo-disk"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "foo-disk",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"foo-disk": {Type: "gp", SnapshotID: "snap-1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preexisting volume backup info in backup status",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "pd-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
existingVolumeBackups: map[string]*v1.VolumeBackupInfo{
|
||||
"anotherpv": {SnapshotID: "anothersnap"},
|
||||
},
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"pd-abc123": {Type: "gp", SnapshotID: "snap-1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create snapshot error",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "PV with label metadata but no failureDomainZone",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/region": "us-east-1"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "vol-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"vol-abc123": {Type: "gp", SnapshotID: "snap-1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
backup := &v1.Backup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: v1.DefaultNamespace,
|
||||
Name: "mybackup",
|
||||
},
|
||||
Spec: v1.BackupSpec{
|
||||
SnapshotVolumes: &test.snapshotEnabled,
|
||||
TTL: metav1.Duration{Duration: test.ttl},
|
||||
},
|
||||
Status: v1.BackupStatus{
|
||||
VolumeBackups: test.existingVolumeBackups,
|
||||
},
|
||||
}
|
||||
|
||||
snapshotService := &arktest.FakeSnapshotService{SnapshottableVolumes: test.volumeInfo}
|
||||
|
||||
ib := &defaultItemBackupper{snapshotService: snapshotService}
|
||||
|
||||
pv, err := getAsMap(test.pv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// method under test
|
||||
err = ib.takePVSnapshot(&unstructured.Unstructured{Object: pv}, backup, arktest.NewLogger())
|
||||
|
||||
gotErr := err != nil
|
||||
|
||||
if e, a := test.expectError, gotErr; e != a {
|
||||
t.Errorf("error: expected %v, got %v", e, a)
|
||||
}
|
||||
if test.expectError {
|
||||
return
|
||||
}
|
||||
|
||||
if !test.snapshotEnabled {
|
||||
// don't need to check anything else if snapshots are disabled
|
||||
return
|
||||
}
|
||||
|
||||
expectedVolumeBackups := test.existingVolumeBackups
|
||||
if expectedVolumeBackups == nil {
|
||||
expectedVolumeBackups = make(map[string]*v1.VolumeBackupInfo)
|
||||
}
|
||||
|
||||
// we should have one snapshot taken exactly
|
||||
require.Equal(t, test.expectedSnapshotsTaken, snapshotService.SnapshotsTaken.Len())
|
||||
|
||||
if test.expectedSnapshotsTaken > 0 {
|
||||
// the snapshotID should be the one in the entry in snapshotService.SnapshottableVolumes
|
||||
// for the volume we ran the test for
|
||||
snapshotID, _ := snapshotService.SnapshotsTaken.PopAny()
|
||||
|
||||
expectedVolumeBackups["mypv"] = &v1.VolumeBackupInfo{
|
||||
SnapshotID: snapshotID,
|
||||
Type: test.volumeInfo[test.expectedVolumeID].Type,
|
||||
Iops: test.volumeInfo[test.expectedVolumeID].Iops,
|
||||
AvailabilityZone: test.volumeInfo[test.expectedVolumeID].AvailabilityZone,
|
||||
}
|
||||
|
||||
if e, a := expectedVolumeBackups, backup.Status.VolumeBackups; !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("backup.status.VolumeBackups: expected %v, got %v", e, a)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -395,7 +658,7 @@ type mockItemBackupper struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (ib *mockItemBackupper) backupItem(logger *logrus.Entry, obj runtime.Unstructured, groupResource schema.GroupResource) error {
|
||||
func (ib *mockItemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstructured, groupResource schema.GroupResource) error {
|
||||
args := ib.Called(logger, obj, groupResource)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ 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/util/collections"
|
||||
"github.com/pkg/errors"
|
||||
@@ -33,7 +34,7 @@ import (
|
||||
|
||||
type resourceBackupperFactory interface {
|
||||
newResourceBackupper(
|
||||
log *logrus.Entry,
|
||||
log logrus.FieldLogger,
|
||||
backup *api.Backup,
|
||||
namespaces *collections.IncludesExcludes,
|
||||
resources *collections.IncludesExcludes,
|
||||
@@ -42,17 +43,18 @@ type resourceBackupperFactory interface {
|
||||
discoveryHelper discovery.Helper,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
cohabitatingResources map[string]*cohabitatingResource,
|
||||
actions map[schema.GroupResource]Action,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
) resourceBackupper
|
||||
}
|
||||
|
||||
type defaultResourceBackupperFactory struct{}
|
||||
|
||||
func (f *defaultResourceBackupperFactory) newResourceBackupper(
|
||||
log *logrus.Entry,
|
||||
log logrus.FieldLogger,
|
||||
backup *api.Backup,
|
||||
namespaces *collections.IncludesExcludes,
|
||||
resources *collections.IncludesExcludes,
|
||||
@@ -61,10 +63,11 @@ func (f *defaultResourceBackupperFactory) newResourceBackupper(
|
||||
discoveryHelper discovery.Helper,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
cohabitatingResources map[string]*cohabitatingResource,
|
||||
actions map[schema.GroupResource]Action,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
) resourceBackupper {
|
||||
return &defaultResourceBackupper{
|
||||
log: log,
|
||||
@@ -80,8 +83,8 @@ func (f *defaultResourceBackupperFactory) newResourceBackupper(
|
||||
podCommandExecutor: podCommandExecutor,
|
||||
tarWriter: tarWriter,
|
||||
resourceHooks: resourceHooks,
|
||||
|
||||
itemBackupperFactory: &defaultItemBackupperFactory{},
|
||||
snapshotService: snapshotService,
|
||||
itemBackupperFactory: &defaultItemBackupperFactory{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +93,7 @@ type resourceBackupper interface {
|
||||
}
|
||||
|
||||
type defaultResourceBackupper struct {
|
||||
log *logrus.Entry
|
||||
log logrus.FieldLogger
|
||||
backup *api.Backup
|
||||
namespaces *collections.IncludesExcludes
|
||||
resources *collections.IncludesExcludes
|
||||
@@ -99,12 +102,12 @@ type defaultResourceBackupper struct {
|
||||
discoveryHelper discovery.Helper
|
||||
backedUpItems map[itemKey]struct{}
|
||||
cohabitatingResources map[string]*cohabitatingResource
|
||||
actions map[schema.GroupResource]Action
|
||||
actions []resolvedAction
|
||||
podCommandExecutor podCommandExecutor
|
||||
tarWriter tarWriter
|
||||
resourceHooks []resourceHook
|
||||
|
||||
itemBackupperFactory itemBackupperFactory
|
||||
snapshotService cloudprovider.SnapshotService
|
||||
itemBackupperFactory itemBackupperFactory
|
||||
}
|
||||
|
||||
// backupResource backs up all the objects for a given group-version-resource.
|
||||
@@ -177,6 +180,7 @@ func (rb *defaultResourceBackupper) backupResource(
|
||||
rb.resourceHooks,
|
||||
rb.dynamicFactory,
|
||||
rb.discoveryHelper,
|
||||
rb.snapshotService,
|
||||
)
|
||||
|
||||
namespacesToList := getNamespacesToList(rb.namespaces)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"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/util/collections"
|
||||
arktest "github.com/heptio/ark/pkg/util/test"
|
||||
@@ -238,8 +239,11 @@ func TestBackupResource(t *testing.T) {
|
||||
"networkpolicies": newCohabitatingResource("networkpolicies", "extensions", "networking.k8s.io"),
|
||||
}
|
||||
|
||||
actions := map[schema.GroupResource]Action{
|
||||
{Group: "", Resource: "pods"}: &fakeAction{},
|
||||
actions := []resolvedAction{
|
||||
{
|
||||
ItemAction: newFakeAction("pods"),
|
||||
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("pods"),
|
||||
},
|
||||
}
|
||||
|
||||
resourceHooks := []resourceHook{
|
||||
@@ -266,6 +270,7 @@ func TestBackupResource(t *testing.T) {
|
||||
podCommandExecutor,
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
nil,
|
||||
).(*defaultResourceBackupper)
|
||||
|
||||
itemBackupperFactory := &mockItemBackupperFactory{}
|
||||
@@ -287,6 +292,7 @@ func TestBackupResource(t *testing.T) {
|
||||
resourceHooks,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
mock.Anything,
|
||||
).Return(itemBackupper)
|
||||
|
||||
if len(test.listResponses) > 0 {
|
||||
@@ -393,8 +399,11 @@ func TestBackupResourceCohabitation(t *testing.T) {
|
||||
"networkpolicies": newCohabitatingResource("networkpolicies", "extensions", "networking.k8s.io"),
|
||||
}
|
||||
|
||||
actions := map[schema.GroupResource]Action{
|
||||
{Group: "", Resource: "pods"}: &fakeAction{},
|
||||
actions := []resolvedAction{
|
||||
{
|
||||
ItemAction: newFakeAction("pods"),
|
||||
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("pods"),
|
||||
},
|
||||
}
|
||||
|
||||
resourceHooks := []resourceHook{
|
||||
@@ -420,6 +429,7 @@ func TestBackupResourceCohabitation(t *testing.T) {
|
||||
podCommandExecutor,
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
nil,
|
||||
).(*defaultResourceBackupper)
|
||||
|
||||
itemBackupperFactory := &mockItemBackupperFactory{}
|
||||
@@ -440,6 +450,7 @@ func TestBackupResourceCohabitation(t *testing.T) {
|
||||
resourceHooks,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
mock.Anything,
|
||||
).Return(itemBackupper)
|
||||
|
||||
client := &arktest.FakeDynamicClient{}
|
||||
@@ -476,7 +487,7 @@ func TestBackupResourceOnlyIncludesSpecifiedNamespaces(t *testing.T) {
|
||||
|
||||
cohabitatingResources := map[string]*cohabitatingResource{}
|
||||
|
||||
actions := map[schema.GroupResource]Action{}
|
||||
actions := []resolvedAction{}
|
||||
|
||||
resourceHooks := []resourceHook{}
|
||||
|
||||
@@ -499,6 +510,7 @@ func TestBackupResourceOnlyIncludesSpecifiedNamespaces(t *testing.T) {
|
||||
podCommandExecutor,
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
nil,
|
||||
).(*defaultResourceBackupper)
|
||||
|
||||
itemBackupperFactory := &mockItemBackupperFactory{}
|
||||
@@ -519,6 +531,7 @@ func TestBackupResourceOnlyIncludesSpecifiedNamespaces(t *testing.T) {
|
||||
dynamicFactory: dynamicFactory,
|
||||
discoveryHelper: discoveryHelper,
|
||||
itemHookHandler: itemHookHandler,
|
||||
snapshotService: nil,
|
||||
}
|
||||
|
||||
itemBackupperFactory.On("newItemBackupper",
|
||||
@@ -532,6 +545,7 @@ func TestBackupResourceOnlyIncludesSpecifiedNamespaces(t *testing.T) {
|
||||
resourceHooks,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
mock.Anything,
|
||||
).Return(itemBackupper)
|
||||
|
||||
client := &arktest.FakeDynamicClient{}
|
||||
@@ -567,7 +581,7 @@ func TestBackupResourceListAllNamespacesExcludesCorrectly(t *testing.T) {
|
||||
|
||||
cohabitatingResources := map[string]*cohabitatingResource{}
|
||||
|
||||
actions := map[schema.GroupResource]Action{}
|
||||
actions := []resolvedAction{}
|
||||
|
||||
resourceHooks := []resourceHook{}
|
||||
|
||||
@@ -590,6 +604,7 @@ func TestBackupResourceListAllNamespacesExcludesCorrectly(t *testing.T) {
|
||||
podCommandExecutor,
|
||||
tarWriter,
|
||||
resourceHooks,
|
||||
nil,
|
||||
).(*defaultResourceBackupper)
|
||||
|
||||
itemBackupperFactory := &mockItemBackupperFactory{}
|
||||
@@ -613,6 +628,7 @@ func TestBackupResourceListAllNamespacesExcludesCorrectly(t *testing.T) {
|
||||
resourceHooks,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
mock.Anything,
|
||||
).Return(itemBackupper)
|
||||
|
||||
client := &arktest.FakeDynamicClient{}
|
||||
@@ -642,12 +658,13 @@ func (ibf *mockItemBackupperFactory) newItemBackupper(
|
||||
backup *v1.Backup,
|
||||
namespaces, resources *collections.IncludesExcludes,
|
||||
backedUpItems map[itemKey]struct{},
|
||||
actions map[schema.GroupResource]Action,
|
||||
actions []resolvedAction,
|
||||
podCommandExecutor podCommandExecutor,
|
||||
tarWriter tarWriter,
|
||||
resourceHooks []resourceHook,
|
||||
dynamicFactory client.DynamicFactory,
|
||||
discoveryHelper discovery.Helper,
|
||||
snapshotService cloudprovider.SnapshotService,
|
||||
) ItemBackupper {
|
||||
args := ibf.Called(
|
||||
backup,
|
||||
@@ -660,6 +677,7 @@ func (ibf *mockItemBackupperFactory) newItemBackupper(
|
||||
resourceHooks,
|
||||
dynamicFactory,
|
||||
discoveryHelper,
|
||||
snapshotService,
|
||||
)
|
||||
return args.Get(0).(ItemBackupper)
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 Heptio Inc.
|
||||
|
||||
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 (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/cloudprovider"
|
||||
kubeutil "github.com/heptio/ark/pkg/util/kube"
|
||||
)
|
||||
|
||||
// zoneLabel is the label that stores availability-zone info
|
||||
// on PVs
|
||||
const zoneLabel = "failure-domain.beta.kubernetes.io/zone"
|
||||
|
||||
// volumeSnapshotAction is a struct that knows how to take snapshots of PersistentVolumes
|
||||
// that are backed by compatible cloud volumes.
|
||||
type volumeSnapshotAction struct {
|
||||
snapshotService cloudprovider.SnapshotService
|
||||
}
|
||||
|
||||
func NewVolumeSnapshotAction(snapshotService cloudprovider.SnapshotService) (Action, error) {
|
||||
if snapshotService == nil {
|
||||
return nil, errors.New("snapshotService cannot be nil")
|
||||
}
|
||||
|
||||
return &volumeSnapshotAction{
|
||||
snapshotService: snapshotService,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Execute triggers a snapshot for the volume/disk underlying a PersistentVolume if the provided
|
||||
// backup has volume snapshots enabled and the PV is of a compatible type. Also records cloud
|
||||
// disk type and IOPS (if applicable) to be able to restore to current state later.
|
||||
func (a *volumeSnapshotAction) Execute(log *logrus.Entry, item runtime.Unstructured, backup *api.Backup) ([]ResourceIdentifier, error) {
|
||||
var noAdditionalItems []ResourceIdentifier
|
||||
|
||||
log.Info("Executing volumeSnapshotAction")
|
||||
|
||||
if backup.Spec.SnapshotVolumes != nil && !*backup.Spec.SnapshotVolumes {
|
||||
log.Info("Backup has volume snapshots disabled; skipping volume snapshot action.")
|
||||
return noAdditionalItems, nil
|
||||
}
|
||||
|
||||
metadata, err := meta.Accessor(item)
|
||||
if err != nil {
|
||||
return noAdditionalItems, errors.WithStack(err)
|
||||
}
|
||||
|
||||
name := metadata.GetName()
|
||||
var pvFailureDomainZone string
|
||||
labels := metadata.GetLabels()
|
||||
|
||||
if labels[zoneLabel] != "" {
|
||||
pvFailureDomainZone = labels[zoneLabel]
|
||||
} else {
|
||||
log.Infof("label %q is not present on PersistentVolume", zoneLabel)
|
||||
}
|
||||
|
||||
volumeID, err := kubeutil.GetVolumeID(item.UnstructuredContent())
|
||||
// non-nil error means it's a supported PV source but volume ID can't be found
|
||||
if err != nil {
|
||||
return noAdditionalItems, errors.Wrapf(err, "error getting volume ID for PersistentVolume")
|
||||
}
|
||||
// no volumeID / nil error means unsupported PV source
|
||||
if volumeID == "" {
|
||||
log.Info("PersistentVolume is not a supported volume type for snapshots, skipping.")
|
||||
return noAdditionalItems, nil
|
||||
}
|
||||
|
||||
log = log.WithField("volumeID", volumeID)
|
||||
|
||||
log.Info("Snapshotting PersistentVolume")
|
||||
snapshotID, err := a.snapshotService.CreateSnapshot(volumeID, pvFailureDomainZone)
|
||||
if err != nil {
|
||||
// log+error on purpose - log goes to the per-backup log file, error goes to the backup
|
||||
log.WithError(err).Error("error creating snapshot")
|
||||
return noAdditionalItems, errors.WithMessage(err, "error creating snapshot")
|
||||
}
|
||||
|
||||
volumeType, iops, err := a.snapshotService.GetVolumeInfo(volumeID, pvFailureDomainZone)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error getting volume info")
|
||||
return noAdditionalItems, errors.WithMessage(err, "error getting volume info")
|
||||
}
|
||||
|
||||
if backup.Status.VolumeBackups == nil {
|
||||
backup.Status.VolumeBackups = make(map[string]*api.VolumeBackupInfo)
|
||||
}
|
||||
|
||||
backup.Status.VolumeBackups[name] = &api.VolumeBackupInfo{
|
||||
SnapshotID: snapshotID,
|
||||
Type: volumeType,
|
||||
Iops: iops,
|
||||
AvailabilityZone: pvFailureDomainZone,
|
||||
}
|
||||
|
||||
return noAdditionalItems, nil
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 Heptio Inc.
|
||||
|
||||
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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
arktest "github.com/heptio/ark/pkg/util/test"
|
||||
)
|
||||
|
||||
func TestVolumeSnapshotAction(t *testing.T) {
|
||||
iops := int64(1000)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
snapshotEnabled bool
|
||||
pv string
|
||||
ttl time.Duration
|
||||
expectError bool
|
||||
expectedVolumeID string
|
||||
expectedSnapshotsTaken int
|
||||
existingVolumeBackups map[string]*v1.VolumeBackupInfo
|
||||
volumeInfo map[string]v1.VolumeBackupInfo
|
||||
}{
|
||||
{
|
||||
name: "snapshot disabled",
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}}`,
|
||||
snapshotEnabled: false,
|
||||
},
|
||||
{
|
||||
name: "can't find volume id - missing spec",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}}`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "unsupported PV source type",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"unsupportedPVSource": {}}}`,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "can't find volume id - aws but no volume id",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"awsElasticBlockStore": {}}}`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "can't find volume id - gce but no volume id",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {}}}`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "aws - simple volume id",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-east-1c"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "vol-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"vol-abc123": {Type: "gp", SnapshotID: "snap-1", AvailabilityZone: "us-east-1c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws - simple volume id with provisioned IOPS",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-east-1c"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "vol-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"vol-abc123": {Type: "io1", Iops: &iops, SnapshotID: "snap-1", AvailabilityZone: "us-east-1c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aws - dynamically provisioned volume id",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "us-west-2a"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-west-2a/vol-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "vol-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"vol-abc123": {Type: "gp", SnapshotID: "snap-1", AvailabilityZone: "us-west-2a"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "gce",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/zone": "gcp-zone2"}}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "pd-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"pd-abc123": {Type: "gp", SnapshotID: "snap-1", AvailabilityZone: "gcp-zone2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "azure",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"azureDisk": {"diskName": "foo-disk"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "foo-disk",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"foo-disk": {Type: "gp", SnapshotID: "snap-1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preexisting volume backup info in backup status",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "pd-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
existingVolumeBackups: map[string]*v1.VolumeBackupInfo{
|
||||
"anotherpv": {SnapshotID: "anothersnap"},
|
||||
},
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"pd-abc123": {Type: "gp", SnapshotID: "snap-1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create snapshot error",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv"}, "spec": {"gcePersistentDisk": {"pdName": "pd-abc123"}}}`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "PV with label metadata but no failureDomainZone",
|
||||
snapshotEnabled: true,
|
||||
pv: `{"apiVersion": "v1", "kind": "PersistentVolume", "metadata": {"name": "mypv", "labels": {"failure-domain.beta.kubernetes.io/region": "us-east-1"}}, "spec": {"awsElasticBlockStore": {"volumeID": "aws://us-east-1c/vol-abc123"}}}`,
|
||||
expectError: false,
|
||||
expectedSnapshotsTaken: 1,
|
||||
expectedVolumeID: "vol-abc123",
|
||||
ttl: 5 * time.Minute,
|
||||
volumeInfo: map[string]v1.VolumeBackupInfo{
|
||||
"vol-abc123": {Type: "gp", SnapshotID: "snap-1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
backup := &v1.Backup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: v1.DefaultNamespace,
|
||||
Name: "mybackup",
|
||||
},
|
||||
Spec: v1.BackupSpec{
|
||||
SnapshotVolumes: &test.snapshotEnabled,
|
||||
TTL: metav1.Duration{Duration: test.ttl},
|
||||
},
|
||||
Status: v1.BackupStatus{
|
||||
VolumeBackups: test.existingVolumeBackups,
|
||||
},
|
||||
}
|
||||
|
||||
snapshotService := &arktest.FakeSnapshotService{SnapshottableVolumes: test.volumeInfo}
|
||||
|
||||
vsa, _ := NewVolumeSnapshotAction(snapshotService)
|
||||
action := vsa.(*volumeSnapshotAction)
|
||||
|
||||
pv, err := getAsMap(test.pv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// method under test
|
||||
additionalItems, err := action.Execute(arktest.NewLogger(), &unstructured.Unstructured{Object: pv}, backup)
|
||||
assert.Len(t, additionalItems, 0)
|
||||
|
||||
gotErr := err != nil
|
||||
|
||||
if e, a := test.expectError, gotErr; e != a {
|
||||
t.Errorf("error: expected %v, got %v", e, a)
|
||||
}
|
||||
if test.expectError {
|
||||
return
|
||||
}
|
||||
|
||||
if !test.snapshotEnabled {
|
||||
// don't need to check anything else if snapshots are disabled
|
||||
return
|
||||
}
|
||||
|
||||
expectedVolumeBackups := test.existingVolumeBackups
|
||||
if expectedVolumeBackups == nil {
|
||||
expectedVolumeBackups = make(map[string]*v1.VolumeBackupInfo)
|
||||
}
|
||||
|
||||
// we should have one snapshot taken exactly
|
||||
require.Equal(t, test.expectedSnapshotsTaken, snapshotService.SnapshotsTaken.Len())
|
||||
|
||||
if test.expectedSnapshotsTaken > 0 {
|
||||
// the snapshotID should be the one in the entry in snapshotService.SnapshottableVolumes
|
||||
// for the volume we ran the test for
|
||||
snapshotID, _ := snapshotService.SnapshotsTaken.PopAny()
|
||||
|
||||
expectedVolumeBackups["mypv"] = &v1.VolumeBackupInfo{
|
||||
SnapshotID: snapshotID,
|
||||
Type: test.volumeInfo[test.expectedVolumeID].Type,
|
||||
Iops: test.volumeInfo[test.expectedVolumeID].Iops,
|
||||
AvailabilityZone: test.volumeInfo[test.expectedVolumeID].AvailabilityZone,
|
||||
}
|
||||
|
||||
if e, a := expectedVolumeBackups, backup.Status.VolumeBackups; !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("backup.status.VolumeBackups: expected %v, got %v", e, a)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user