migrate backup hooks tests (#1591)

* migrate hooks tests

Signed-off-by: Steve Kriss <krisss@vmware.com>

* add more test cases

Signed-off-by: Steve Kriss <krisss@vmware.com>

* refactor to strongly typed expectation, add pre+post hook test case

Signed-off-by: Steve Kriss <krisss@vmware.com>
This commit is contained in:
Steve Kriss
2019-06-24 12:23:17 -07:00
committed by Adnan Abdulhussein
parent 19052994ed
commit 5634a4f463
4 changed files with 330 additions and 463 deletions
+316
View File
@@ -31,6 +31,7 @@ import (
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
@@ -51,6 +52,7 @@ import (
"github.com/heptio/velero/pkg/plugin/velero"
"github.com/heptio/velero/pkg/test"
kubeutil "github.com/heptio/velero/pkg/util/kube"
testutil "github.com/heptio/velero/pkg/util/test"
"github.com/heptio/velero/pkg/volume"
)
@@ -1704,6 +1706,320 @@ func TestBackupWithSnapshots(t *testing.T) {
}
}
// TestBackupWithInvalidHooks runs backups with invalid hook specifications and verifies
// that an error is returned.
func TestBackupWithInvalidHooks(t *testing.T) {
tests := []struct {
name string
backup *velerov1.Backup
apiResources []*apiResource
want error
}{
{
name: "hook with invalid label selector causes backup to fail",
backup: defaultBackup().
Hooks(velerov1.BackupHooks{
Resources: []velerov1.BackupResourceHookSpec{
{
Name: "hook-with-invalid-label-selector",
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "foo",
Operator: metav1.LabelSelectorOperator("nonexistent-operator"),
Values: []string{"bar"},
},
},
},
},
},
}).
Backup(),
apiResources: []*apiResource{
pods(
newPod("foo", "bar"),
),
},
want: errors.New("\"nonexistent-operator\" is not a valid pod selector operator"),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var (
h = newHarness(t)
req = &Request{Backup: tc.backup}
backupFile = bytes.NewBuffer([]byte{})
)
for _, resource := range tc.apiResources {
h.addItems(t, resource.group, resource.version, resource.name, resource.shortName, resource.namespaced, resource.items...)
}
assert.EqualError(t, h.backupper.Backup(h.log, req, backupFile, nil, nil), tc.want.Error())
})
}
}
// TestBackupWithHooks runs backups with valid hook specifications and verifies that the
// hooks are run. It uses a MockPodCommandExecutor since hooks can't actually be executed
// in running pods during the unit test. Verification is done by asserting expected method
// calls on the mock object.
func TestBackupWithHooks(t *testing.T) {
type expectedCall struct {
podNamespace string
podName string
hookName string
hook *velerov1.ExecHook
err error
}
tests := []struct {
name string
backup *velerov1.Backup
apiResources []*apiResource
wantExecutePodCommandCalls []*expectedCall
wantBackedUp []string
}{
{
name: "pre hook with no resource filters runs for all pods",
backup: defaultBackup().
Hooks(velerov1.BackupHooks{
Resources: []velerov1.BackupResourceHookSpec{
{
Name: "hook-1",
PreHooks: []velerov1.BackupResourceHook{
{
Exec: &velerov1.ExecHook{
Command: []string{"ls", "/tmp"},
},
},
},
},
},
}).
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-2"),
),
},
wantExecutePodCommandCalls: []*expectedCall{
{
podNamespace: "ns-1",
podName: "pod-1",
hookName: "hook-1",
hook: &velerov1.ExecHook{
Command: []string{"ls", "/tmp"},
},
err: nil,
},
{
podNamespace: "ns-2",
podName: "pod-2",
hookName: "hook-1",
hook: &velerov1.ExecHook{
Command: []string{"ls", "/tmp"},
},
err: nil,
},
},
wantBackedUp: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
"resources/pods/namespaces/ns-2/pod-2.json",
},
},
{
name: "post hook with no resource filters runs for all pods",
backup: defaultBackup().
Hooks(velerov1.BackupHooks{
Resources: []velerov1.BackupResourceHookSpec{
{
Name: "hook-1",
PostHooks: []velerov1.BackupResourceHook{
{
Exec: &velerov1.ExecHook{
Command: []string{"ls", "/tmp"},
},
},
},
},
},
}).
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-2"),
),
},
wantExecutePodCommandCalls: []*expectedCall{
{
podNamespace: "ns-1",
podName: "pod-1",
hookName: "hook-1",
hook: &velerov1.ExecHook{
Command: []string{"ls", "/tmp"},
},
err: nil,
},
{
podNamespace: "ns-2",
podName: "pod-2",
hookName: "hook-1",
hook: &velerov1.ExecHook{
Command: []string{"ls", "/tmp"},
},
err: nil,
},
},
wantBackedUp: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
"resources/pods/namespaces/ns-2/pod-2.json",
},
},
{
name: "pre and post hooks run for a pod",
backup: defaultBackup().
Hooks(velerov1.BackupHooks{
Resources: []velerov1.BackupResourceHookSpec{
{
Name: "hook-1",
PreHooks: []velerov1.BackupResourceHook{
{
Exec: &velerov1.ExecHook{
Command: []string{"pre"},
},
},
},
PostHooks: []velerov1.BackupResourceHook{
{
Exec: &velerov1.ExecHook{
Command: []string{"post"},
},
},
},
},
},
}).
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
),
},
wantExecutePodCommandCalls: []*expectedCall{
{
podNamespace: "ns-1",
podName: "pod-1",
hookName: "hook-1",
hook: &velerov1.ExecHook{
Command: []string{"pre"},
},
err: nil,
},
{
podNamespace: "ns-1",
podName: "pod-1",
hookName: "hook-1",
hook: &velerov1.ExecHook{
Command: []string{"post"},
},
err: nil,
},
},
wantBackedUp: []string{
"resources/pods/namespaces/ns-1/pod-1.json",
},
},
{
name: "item is not backed up if hook returns an error when OnError=Fail",
backup: defaultBackup().
Hooks(velerov1.BackupHooks{
Resources: []velerov1.BackupResourceHookSpec{
{
Name: "hook-1",
PreHooks: []velerov1.BackupResourceHook{
{
Exec: &velerov1.ExecHook{
Command: []string{"ls", "/tmp"},
OnError: velerov1.HookErrorModeFail,
},
},
},
},
},
}).
Backup(),
apiResources: []*apiResource{
pods(
newPod("ns-1", "pod-1"),
newPod("ns-2", "pod-2"),
),
},
wantExecutePodCommandCalls: []*expectedCall{
{
podNamespace: "ns-1",
podName: "pod-1",
hookName: "hook-1",
hook: &velerov1.ExecHook{
Command: []string{"ls", "/tmp"},
OnError: velerov1.HookErrorModeFail,
},
err: errors.New("exec hook error"),
},
{
podNamespace: "ns-2",
podName: "pod-2",
hookName: "hook-1",
hook: &velerov1.ExecHook{
Command: []string{"ls", "/tmp"},
OnError: velerov1.HookErrorModeFail,
},
err: nil,
},
},
wantBackedUp: []string{
"resources/pods/namespaces/ns-2/pod-2.json",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var (
h = newHarness(t)
req = &Request{Backup: tc.backup}
backupFile = bytes.NewBuffer([]byte{})
podCommandExecutor = new(testutil.MockPodCommandExecutor)
)
h.backupper.podCommandExecutor = podCommandExecutor
defer podCommandExecutor.AssertExpectations(t)
for _, expect := range tc.wantExecutePodCommandCalls {
podCommandExecutor.On("ExecutePodCommand",
mock.Anything,
mock.Anything,
expect.podNamespace,
expect.podName,
expect.hookName,
expect.hook,
).Return(expect.err)
}
for _, resource := range tc.apiResources {
h.addItems(t, resource.group, resource.version, resource.name, resource.shortName, resource.namespaced, resource.items...)
}
require.NoError(t, h.backupper.Backup(h.log, req, backupFile, nil, nil))
assertTarballContents(t, backupFile, append(tc.wantBackedUp, "metadata/version")...)
})
}
}
// pluggableAction is a backup item action that can be plugged with an Execute
// function body at runtime.
type pluggableAction struct {
-463
View File
@@ -1,463 +0,0 @@
/*
Copyright 2017, 2019 the Velero 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"
"testing"
"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/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
v1 "github.com/heptio/velero/pkg/apis/velero/v1"
"github.com/heptio/velero/pkg/client"
"github.com/heptio/velero/pkg/discovery"
"github.com/heptio/velero/pkg/plugin/velero"
"github.com/heptio/velero/pkg/podexec"
"github.com/heptio/velero/pkg/restic"
"github.com/heptio/velero/pkg/util/collections"
"github.com/heptio/velero/pkg/util/logging"
velerotest "github.com/heptio/velero/pkg/util/test"
)
var (
v1Group = &metav1.APIResourceList{
GroupVersion: "v1",
APIResources: []metav1.APIResource{configMapsResource, podsResource, namespacesResource},
}
configMapsResource = metav1.APIResource{
Name: "configmaps",
SingularName: "configmap",
Namespaced: true,
Kind: "ConfigMap",
Verbs: metav1.Verbs([]string{"create", "update", "get", "list", "watch", "delete"}),
ShortNames: []string{"cm"},
Categories: []string{"all"},
}
podsResource = metav1.APIResource{
Name: "pods",
SingularName: "pod",
Namespaced: true,
Kind: "Pod",
Verbs: metav1.Verbs([]string{"create", "update", "get", "list", "watch", "delete"}),
ShortNames: []string{"po"},
Categories: []string{"all"},
}
rbacGroup = &metav1.APIResourceList{
GroupVersion: "rbac.authorization.k8s.io/v1beta1",
APIResources: []metav1.APIResource{rolesResource},
}
rolesResource = metav1.APIResource{
Name: "roles",
SingularName: "role",
Namespaced: true,
Kind: "Role",
Verbs: metav1.Verbs([]string{"create", "update", "get", "list", "watch", "delete"}),
}
namespacesResource = metav1.APIResource{
Name: "namespaces",
SingularName: "namespace",
Namespaced: false,
Kind: "Namespace",
Verbs: metav1.Verbs([]string{"create", "update", "get", "list", "watch", "delete"}),
}
certificatesGroup = &metav1.APIResourceList{
GroupVersion: "certificates.k8s.io/v1beta1",
APIResources: []metav1.APIResource{certificateSigningRequestsResource},
}
certificateSigningRequestsResource = metav1.APIResource{
Name: "certificatesigningrequests",
SingularName: "certificatesigningrequest",
Namespaced: false,
Kind: "CertificateSigningRequest",
Verbs: metav1.Verbs([]string{"create", "update", "get", "list", "watch", "delete"}),
ShortNames: []string{"csr"},
}
extensionsGroup = &metav1.APIResourceList{
GroupVersion: "extensions/v1beta1",
APIResources: []metav1.APIResource{deploymentsResource, networkPoliciesResource},
}
extensionsGroupVersion = schema.GroupVersion{
Group: "extensions",
Version: "v1beta1",
}
appsGroup = &metav1.APIResourceList{
GroupVersion: "apps/v1beta1",
APIResources: []metav1.APIResource{deploymentsResource},
}
appsGroupVersion = schema.GroupVersion{
Group: "apps",
Version: "v1beta1",
}
deploymentsResource = metav1.APIResource{
Name: "deployments",
SingularName: "deployment",
Namespaced: true,
Kind: "Deployment",
Verbs: metav1.Verbs([]string{"create", "update", "get", "list", "watch", "delete"}),
ShortNames: []string{"deploy"},
Categories: []string{"all"},
}
networkingGroup = &metav1.APIResourceList{
GroupVersion: "networking.k8s.io/v1",
APIResources: []metav1.APIResource{networkPoliciesResource},
}
networkingGroupVersion = schema.GroupVersion{
Group: "networking.k8s.io",
Version: "v1",
}
networkPoliciesResource = metav1.APIResource{
Name: "networkpolicies",
SingularName: "networkpolicy",
Namespaced: true,
Kind: "Deployment",
Verbs: metav1.Verbs([]string{"create", "update", "get", "list", "watch", "delete"}),
}
)
func parseLabelSelectorOrDie(s string) labels.Selector {
ret, err := labels.Parse(s)
if err != nil {
panic(err)
}
return ret
}
func TestBackup(t *testing.T) {
tests := []struct {
name string
backup *v1.Backup
actions []velero.BackupItemAction
expectedNamespaces *collections.IncludesExcludes
expectedResources *collections.IncludesExcludes
expectedHooks []resourceHook
backupGroupErrors map[*metav1.APIResourceList]error
expectedError error
}{
{
name: "error resolving hooks returns an error",
backup: &v1.Backup{
Spec: v1.BackupSpec{
// cm - shortcut in legacy api group
// csr - shortcut in certificates.k8s.io api group
// roles - fully qualified in rbac.authorization.k8s.io api group
IncludedResources: []string{"cm", "csr", "roles"},
IncludedNamespaces: []string{"a", "b"},
ExcludedNamespaces: []string{"c", "d"},
Hooks: v1.BackupHooks{
Resources: []v1.BackupResourceHookSpec{
{
Name: "hook-with-invalid-label-selector",
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "foo",
Operator: metav1.LabelSelectorOperator("nonexistent-operator"),
Values: []string{"bar"},
},
},
},
},
},
},
},
},
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{},
expectedError: errors.New("\"nonexistent-operator\" is not a valid pod selector operator"),
},
{
name: "backupGroup errors",
backup: &v1.Backup{},
expectedNamespaces: collections.NewIncludesExcludes(),
expectedResources: collections.NewIncludesExcludes(),
expectedHooks: []resourceHook{},
backupGroupErrors: map[*metav1.APIResourceList]error{
v1Group: errors.New("v1 error"),
certificatesGroup: nil,
rbacGroup: errors.New("rbac error"),
},
expectedError: nil,
},
{
name: "hooks",
backup: &v1.Backup{
Spec: v1.BackupSpec{
Hooks: v1.BackupHooks{
Resources: []v1.BackupResourceHookSpec{
{
Name: "hook1",
IncludedNamespaces: []string{"a"},
ExcludedNamespaces: []string{"b"},
IncludedResources: []string{"cm"},
ExcludedResources: []string{"roles"},
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"1": "2"},
},
PreHooks: []v1.BackupResourceHook{
{
Exec: &v1.ExecHook{
Command: []string{"ls", "/tmp"},
},
},
},
},
},
},
},
},
expectedNamespaces: collections.NewIncludesExcludes(),
expectedResources: collections.NewIncludesExcludes(),
expectedHooks: []resourceHook{
{
name: "hook1",
namespaces: collections.NewIncludesExcludes().Includes("a").Excludes("b"),
resources: collections.NewIncludesExcludes().Includes("configmaps").Excludes("roles.rbac.authorization.k8s.io"),
labelSelector: parseLabelSelectorOrDie("1=2"),
pre: []v1.BackupResourceHook{
{
Exec: &v1.ExecHook{
Command: []string{"ls", "/tmp"},
},
},
},
},
},
backupGroupErrors: map[*metav1.APIResourceList]error{
v1Group: nil,
certificatesGroup: nil,
rbacGroup: nil,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := &Request{
Backup: test.backup,
}
discoveryHelper := &velerotest.FakeDiscoveryHelper{
Mapper: &velerotest.FakeMapper{
Resources: map[schema.GroupVersionResource]schema.GroupVersionResource{
{Resource: "cm"}: {Group: "", Version: "v1", Resource: "configmaps"},
{Resource: "csr"}: {Group: "certificates.k8s.io", Version: "v1beta1", Resource: "certificatesigningrequests"},
{Resource: "roles"}: {Group: "rbac.authorization.k8s.io", Version: "v1beta1", Resource: "roles"},
},
},
ResourceList: []*metav1.APIResourceList{
v1Group,
certificatesGroup,
rbacGroup,
},
}
dynamicFactory := new(velerotest.FakeDynamicFactory)
podCommandExecutor := &velerotest.MockPodCommandExecutor{}
defer podCommandExecutor.AssertExpectations(t)
groupBackupperFactory := &mockGroupBackupperFactory{}
defer groupBackupperFactory.AssertExpectations(t)
groupBackupper := &mockGroupBackupper{}
defer groupBackupper.AssertExpectations(t)
groupBackupperFactory.On("newGroupBackupper",
mock.Anything, // log
req,
dynamicFactory,
discoveryHelper,
map[itemKey]struct{}{}, // backedUpItems
cohabitatingResources(),
podCommandExecutor,
mock.Anything, // tarWriter
mock.Anything, // restic backupper
mock.Anything, // pvc snapshot tracker
mock.Anything, // volume snapshotter getter
).Maybe().Return(groupBackupper)
for group, err := range test.backupGroupErrors {
groupBackupper.On("backupGroup", group).Return(err)
}
kb := &kubernetesBackupper{
discoveryHelper: discoveryHelper,
dynamicFactory: dynamicFactory,
podCommandExecutor: podCommandExecutor,
groupBackupperFactory: groupBackupperFactory,
}
err := kb.Backup(logging.DefaultLogger(logrus.DebugLevel), req, new(bytes.Buffer), test.actions, nil)
assert.Equal(t, test.expectedNamespaces, req.NamespaceIncludesExcludes)
assert.Equal(t, test.expectedResources, req.ResourceIncludesExcludes)
assert.Equal(t, test.expectedHooks, req.ResourceHooks)
if test.expectedError != nil {
assert.EqualError(t, err, test.expectedError.Error())
return
}
assert.NoError(t, err)
})
}
}
type mockGroupBackupperFactory struct {
mock.Mock
}
func (f *mockGroupBackupperFactory) newGroupBackupper(
log logrus.FieldLogger,
backup *Request,
dynamicFactory client.DynamicFactory,
discoveryHelper discovery.Helper,
backedUpItems map[itemKey]struct{},
cohabitatingResources map[string]*cohabitatingResource,
podCommandExecutor podexec.PodCommandExecutor,
tarWriter tarWriter,
resticBackupper restic.Backupper,
resticSnapshotTracker *pvcSnapshotTracker,
volumeSnapshotterGetter VolumeSnapshotterGetter,
) groupBackupper {
args := f.Called(
log,
backup,
dynamicFactory,
discoveryHelper,
backedUpItems,
cohabitatingResources,
podCommandExecutor,
tarWriter,
resticBackupper,
resticSnapshotTracker,
volumeSnapshotterGetter,
)
return args.Get(0).(groupBackupper)
}
type mockGroupBackupper struct {
mock.Mock
}
func (gb *mockGroupBackupper) backupGroup(group *metav1.APIResourceList) error {
args := gb.Called(group)
return args.Error(0)
}
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 TestGetResourceHook(t *testing.T) {
tests := []struct {
name string
hookSpec v1.BackupResourceHookSpec
expected resourceHook
}{
{
name: "Full test",
hookSpec: v1.BackupResourceHookSpec{
Name: "spec1",
IncludedNamespaces: []string{"ns1", "ns2"},
ExcludedNamespaces: []string{"ns3", "ns4"},
IncludedResources: []string{"foo", "fie"},
ExcludedResources: []string{"bar", "baz"},
PreHooks: []v1.BackupResourceHook{
{
Exec: &v1.ExecHook{
Container: "a",
Command: []string{"b"},
},
},
},
PostHooks: []v1.BackupResourceHook{
{
Exec: &v1.ExecHook{
Container: "c",
Command: []string{"d"},
},
},
},
},
expected: resourceHook{
name: "spec1",
namespaces: collections.NewIncludesExcludes().Includes("ns1", "ns2").Excludes("ns3", "ns4"),
resources: collections.NewIncludesExcludes().Includes("foodies.somegroup", "fields.somegroup").Excludes("barnacles.anothergroup", "bazaars.anothergroup"),
pre: []v1.BackupResourceHook{
{
Exec: &v1.ExecHook{
Container: "a",
Command: []string{"b"},
},
},
},
post: []v1.BackupResourceHook{
{
Exec: &v1.ExecHook{
Container: "c",
Command: []string{"d"},
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resources := map[schema.GroupVersionResource]schema.GroupVersionResource{
{Resource: "foo"}: {Group: "somegroup", Resource: "foodies"},
{Resource: "fie"}: {Group: "somegroup", Resource: "fields"},
{Resource: "bar"}: {Group: "anothergroup", Resource: "barnacles"},
{Resource: "baz"}: {Group: "anothergroup", Resource: "bazaars"},
}
discoveryHelper := velerotest.NewFakeDiscoveryHelper(false, resources)
actual, err := getResourceHook(test.hookSpec, discoveryHelper)
require.NoError(t, err)
assert.Equal(t, test.expected, actual)
})
}
}
+6
View File
@@ -170,3 +170,9 @@ func (b *Builder) NoTypeMeta() *Builder {
b.backup.TypeMeta = metav1.TypeMeta{}
return b
}
// Hooks sets the Backup's hooks.
func (b *Builder) Hooks(hooks velerov1api.BackupHooks) *Builder {
b.backup.Spec.Hooks = hooks
return b
}
+8
View File
@@ -694,3 +694,11 @@ func TestResourceHookApplicableTo(t *testing.T) {
})
}
}
func parseLabelSelectorOrDie(s string) labels.Selector {
ret, err := labels.Parse(s)
if err != nil {
panic(err)
}
return ret
}