velero/pkg/backup/backup_test.go

1102 lines
32 KiB
Go

/*
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 (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"errors"
"io"
"io/ioutil"
"reflect"
"sort"
"testing"
"time"
testlogger "github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"github.com/heptio/ark/pkg/apis/ark/v1"
"github.com/heptio/ark/pkg/util/collections"
. "github.com/heptio/ark/pkg/util/test"
)
type fakeAction struct {
ids []string
backups []*v1.Backup
}
var _ Action = &fakeAction{}
func (a *fakeAction) Execute(ctx ActionContext, item map[string]interface{}, backup *v1.Backup) error {
metadata, err := collections.GetMap(item, "metadata")
if err != nil {
return err
}
var id string
if v, ok := metadata["namespace"]; ok {
id = v.(string) + "/"
}
if v, ok := metadata["name"]; ok {
id += v.(string)
}
a.ids = append(a.ids, id)
a.backups = append(a.backups, backup)
return nil
}
func TestResolveActions(t *testing.T) {
tests := []struct {
name string
input map[string]Action
expected map[schema.GroupResource]Action
resourcesWithErrors []string
expectError bool
}{
{
name: "empty input",
input: map[string]Action{},
expected: map[schema.GroupResource]Action{},
},
{
name: "mapper error",
input: map[string]Action{"badresource": &fakeAction{}},
expected: map[schema.GroupResource]Action{},
expectError: true,
},
{
name: "resolved",
input: map[string]Action{"foo": &fakeAction{}, "bar": &fakeAction{}},
expected: map[schema.GroupResource]Action{
schema.GroupResource{Group: "somegroup", Resource: "foodies"}: &fakeAction{},
schema.GroupResource{Group: "anothergroup", Resource: "barnacles"}: &fakeAction{},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dh := &FakeDiscoveryHelper{
RESTMapper: &FakeMapper{
Resources: map[schema.GroupVersionResource]schema.GroupVersionResource{
schema.GroupVersionResource{Resource: "foo"}: schema.GroupVersionResource{Group: "somegroup", Resource: "foodies"},
schema.GroupVersionResource{Resource: "fie"}: schema.GroupVersionResource{Group: "somegroup", Resource: "fields"},
schema.GroupVersionResource{Resource: "bar"}: schema.GroupVersionResource{Group: "anothergroup", Resource: "barnacles"},
schema.GroupVersionResource{Resource: "baz"}: schema.GroupVersionResource{Group: "anothergroup", Resource: "bazaars"},
},
},
}
actual, err := resolveActions(dh, test.input)
gotError := err != nil
if e, a := test.expectError, gotError; e != a {
t.Fatalf("error: expected %t, got %t", e, a)
}
if test.expectError {
return
}
if e, a := test.expected, actual; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
})
}
}
func TestGetResourceIncludesExcludes(t *testing.T) {
tests := []struct {
name string
includes []string
excludes []string
resourcesWithErrors []string
expectedIncludes []string
expectedExcludes []string
}{
{
name: "no input",
expectedIncludes: []string{},
expectedExcludes: []string{},
},
{
name: "wildcard includes",
includes: []string{"*", "asdf"},
excludes: []string{},
expectedIncludes: []string{"*"},
expectedExcludes: []string{},
},
{
name: "wildcard excludes aren't allowed or resolved",
includes: []string{},
excludes: []string{"*"},
expectedIncludes: []string{},
expectedExcludes: []string{},
},
{
name: "resolution works",
includes: []string{"foo", "fie"},
excludes: []string{"bar", "baz"},
expectedIncludes: []string{"foodies.somegroup", "fields.somegroup"},
expectedExcludes: []string{"barnacles.anothergroup", "bazaars.anothergroup"},
},
{
name: "some unresolvable",
includes: []string{"foo", "fie", "bad1"},
excludes: []string{"bar", "baz", "bad2"},
expectedIncludes: []string{"foodies.somegroup", "fields.somegroup"},
expectedExcludes: []string{"barnacles.anothergroup", "bazaars.anothergroup"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
dh := &FakeDiscoveryHelper{
RESTMapper: &FakeMapper{
Resources: map[schema.GroupVersionResource]schema.GroupVersionResource{
schema.GroupVersionResource{Resource: "foo"}: schema.GroupVersionResource{Group: "somegroup", Resource: "foodies"},
schema.GroupVersionResource{Resource: "fie"}: schema.GroupVersionResource{Group: "somegroup", Resource: "fields"},
schema.GroupVersionResource{Resource: "bar"}: schema.GroupVersionResource{Group: "anothergroup", Resource: "barnacles"},
schema.GroupVersionResource{Resource: "baz"}: schema.GroupVersionResource{Group: "anothergroup", Resource: "bazaars"},
},
},
}
log, _ := testlogger.NewNullLogger()
ctx := &backupContext{
logger: log,
}
actual := ctx.getResourceIncludesExcludes(dh, test.includes, test.excludes)
sort.Strings(test.expectedIncludes)
actualIncludes := actual.GetIncludes()
sort.Strings(actualIncludes)
if e, a := test.expectedIncludes, actualIncludes; !reflect.DeepEqual(e, a) {
t.Errorf("includes: expected %v, got %v", e, a)
}
sort.Strings(test.expectedExcludes)
actualExcludes := actual.GetExcludes()
sort.Strings(actualExcludes)
if e, a := test.expectedExcludes, actualExcludes; !reflect.DeepEqual(e, a) {
t.Errorf("excludes: expected %v, got %v", e, a)
t.Errorf("excludes: expected %v, got %v", len(e), len(a))
}
})
}
}
func TestGetNamespaceIncludesExcludes(t *testing.T) {
backup := &v1.Backup{
Spec: v1.BackupSpec{
IncludedResources: []string{"foo", "bar"},
ExcludedResources: []string{"fie", "baz"},
IncludedNamespaces: []string{"a", "b", "c"},
ExcludedNamespaces: []string{"d", "e", "f"},
TTL: metav1.Duration{Duration: 1 * time.Hour},
},
}
ns := getNamespaceIncludesExcludes(backup)
actualIncludes := ns.GetIncludes()
sort.Strings(actualIncludes)
if e, a := backup.Spec.IncludedNamespaces, actualIncludes; !reflect.DeepEqual(e, a) {
t.Errorf("includes: expected %v, got %v", e, a)
}
actualExcludes := ns.GetExcludes()
sort.Strings(actualExcludes)
if e, a := backup.Spec.ExcludedNamespaces, actualExcludes; !reflect.DeepEqual(e, a) {
t.Errorf("excludes: expected %v, got %v", e, a)
}
}
func TestBackupMethod(t *testing.T) {
// TODO ensure LabelSelector is passed through to the List() calls
backup := &v1.Backup{
Spec: v1.BackupSpec{
// cm - shortcut in legacy api group, namespaced
// csr - shortcut in certificates.k8s.io api group, cluster-scoped
// roles - fully qualified in rbac.authorization.k8s.io api group, namespaced
IncludedResources: []string{"cm", "csr", "roles"},
IncludedNamespaces: []string{"a", "b"},
ExcludedNamespaces: []string{"c", "d"},
},
}
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"},
}
rolesResource := metav1.APIResource{
Name: "roles",
SingularName: "role",
Namespaced: true,
Kind: "Role",
Verbs: metav1.Verbs([]string{"create", "update", "get", "list", "watch", "delete"}),
}
certificateSigningRequestsResource := metav1.APIResource{
Name: "certificatesigningrequests",
SingularName: "certificatesigningrequest",
Namespaced: false,
Kind: "CertificateSigningRequest",
Verbs: metav1.Verbs([]string{"create", "update", "get", "list", "watch", "delete"}),
ShortNames: []string{"csr"},
}
discoveryHelper := &FakeDiscoveryHelper{
RESTMapper: &FakeMapper{
Resources: map[schema.GroupVersionResource]schema.GroupVersionResource{
schema.GroupVersionResource{Resource: "cm"}: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"},
schema.GroupVersionResource{Resource: "csr"}: schema.GroupVersionResource{Group: "certificates.k8s.io", Version: "v1beta1", Resource: "certificatesigningrequests"},
schema.GroupVersionResource{Resource: "roles"}: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1beta1", Resource: "roles"},
},
},
ResourceList: []*metav1.APIResourceList{
{
GroupVersion: "v1",
APIResources: []metav1.APIResource{configMapsResource, podsResource},
},
{
GroupVersion: "certificates.k8s.io/v1beta1",
APIResources: []metav1.APIResource{certificateSigningRequestsResource},
},
{
GroupVersion: "rbac.authorization.k8s.io/v1beta1",
APIResources: []metav1.APIResource{rolesResource},
},
},
}
dynamicFactory := &FakeDynamicFactory{}
legacyGV := schema.GroupVersionResource{Version: "v1"}
configMapsClientA := &FakeDynamicClient{}
configMapsA := toRuntimeObject(t, `{
"apiVersion": "v1",
"kind": "ConfigMapList",
"items": [
{
"metadata": {
"namespace":"a",
"name":"configMap1"
},
"data": {
"a": "b"
}
}
]
}`)
configMapsClientA.On("List", metav1.ListOptions{}).Return(configMapsA, nil)
dynamicFactory.On("ClientForGroupVersionResource", legacyGV, configMapsResource, "a").Return(configMapsClientA, nil)
configMapsClientB := &FakeDynamicClient{}
configMapsB := toRuntimeObject(t, `{
"apiVersion": "v1",
"kind": "ConfigMapList",
"items": [
{
"metadata": {
"namespace":"b",
"name":"configMap2"
},
"data": {
"c": "d"
}
}
]
}`)
configMapsClientB.On("List", metav1.ListOptions{}).Return(configMapsB, nil)
dynamicFactory.On("ClientForGroupVersionResource", legacyGV, configMapsResource, "b").Return(configMapsClientB, nil)
certificatesGV := schema.GroupVersionResource{Group: "certificates.k8s.io", Version: "v1beta1"}
csrList := toRuntimeObject(t, `{
"apiVersion": "certificates.k8s.io/v1beta1",
"kind": "CertificateSigningRequestList",
"items": [
{
"metadata": {
"name": "csr1"
},
"spec": {
"request": "some request",
"username": "bob",
"uid": "12345",
"groups": [
"group1",
"group2"
]
},
"status": {
"certificate": "some cert"
}
}
]
}`)
csrClient := &FakeDynamicClient{}
csrClient.On("List", metav1.ListOptions{}).Return(csrList, nil)
dynamicFactory.On("ClientForGroupVersionResource", certificatesGV, certificateSigningRequestsResource, "").Return(csrClient, nil)
roleListA := toRuntimeObject(t, `{
"apiVersion": "rbac.authorization.k8s.io/v1beta1",
"kind": "RoleList",
"items": [
{
"metadata": {
"namespace": "a",
"name": "role1"
},
"rules": [
{
"verbs": ["get","list"],
"apiGroups": ["apps","extensions"],
"resources": ["deployments"]
}
]
}
]
}`)
roleListB := toRuntimeObject(t, `{
"apiVersion": "rbac.authorization.k8s.io/v1beta1",
"kind": "RoleList",
"items": []
}`)
rbacGV := schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1beta1"}
rolesClientA := &FakeDynamicClient{}
rolesClientA.On("List", metav1.ListOptions{}).Return(roleListA, nil)
dynamicFactory.On("ClientForGroupVersionResource", rbacGV, rolesResource, "a").Return(rolesClientA, nil)
rolesClientB := &FakeDynamicClient{}
rolesClientB.On("List", metav1.ListOptions{}).Return(roleListB, nil)
dynamicFactory.On("ClientForGroupVersionResource", rbacGV, rolesResource, "b").Return(rolesClientB, nil)
cmAction := &fakeAction{}
csrAction := &fakeAction{}
actions := map[string]Action{
"cm": cmAction,
"csr": csrAction,
}
backupper, err := NewKubernetesBackupper(discoveryHelper, dynamicFactory, actions)
require.NoError(t, err)
output := new(bytes.Buffer)
err = backupper.Backup(backup, output, ioutil.Discard)
require.NoError(t, err)
expectedFiles := sets.NewString(
"namespaces/a/configmaps/configMap1.json",
"namespaces/b/configmaps/configMap2.json",
"cluster/certificatesigningrequests.certificates.k8s.io/csr1.json",
"namespaces/a/roles.rbac.authorization.k8s.io/role1.json",
)
expectedData := map[string]string{
"namespaces/a/configmaps/configMap1.json": `
{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"namespace":"a",
"name":"configMap1"
},
"data": {
"a": "b"
}
}`,
"namespaces/b/configmaps/configMap2.json": `
{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"namespace":"b",
"name":"configMap2"
},
"data": {
"c": "d"
}
}
`,
"cluster/certificatesigningrequests.certificates.k8s.io/csr1.json": `
{
"apiVersion": "certificates.k8s.io/v1beta1",
"kind": "CertificateSigningRequest",
"metadata": {
"name": "csr1"
},
"spec": {
"request": "some request",
"username": "bob",
"uid": "12345",
"groups": [
"group1",
"group2"
]
}
}
`,
"namespaces/a/roles.rbac.authorization.k8s.io/role1.json": `
{
"apiVersion": "rbac.authorization.k8s.io/v1beta1",
"kind": "Role",
"metadata": {
"namespace":"a",
"name": "role1"
},
"rules": [
{
"verbs": ["get","list"],
"apiGroups": ["apps","extensions"],
"resources": ["deployments"]
}
]
}
`,
}
seenFiles := sets.NewString()
gzipReader, err := gzip.NewReader(output)
require.NoError(t, err)
defer gzipReader.Close()
tarReader := tar.NewReader(gzipReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
require.NoError(t, err)
switch header.Typeflag {
case tar.TypeReg:
seenFiles.Insert(header.Name)
expected, err := getAsMap(expectedData[header.Name])
if !assert.NoError(t, err, "%q: %v", header.Name, err) {
continue
}
buf := new(bytes.Buffer)
n, err := io.Copy(buf, tarReader)
if !assert.NoError(t, err) {
continue
}
if !assert.Equal(t, header.Size, n) {
continue
}
actual, err := getAsMap(string(buf.Bytes()))
if !assert.NoError(t, err) {
continue
}
assert.Equal(t, expected, actual)
default:
t.Errorf("unexpected header: %#v", header)
}
}
if !expectedFiles.Equal(seenFiles) {
t.Errorf("did not get expected files. expected-seen: %v. seen-expected: %v", expectedFiles.Difference(seenFiles), seenFiles.Difference(expectedFiles))
}
expectedCMActionIDs := []string{"a/configMap1", "b/configMap2"}
expectedCSRActionIDs := []string{"csr1"}
assert.Equal(t, expectedCMActionIDs, cmAction.ids)
assert.Equal(t, expectedCSRActionIDs, csrAction.ids)
}
func TestBackupResource(t *testing.T) {
tests := []struct {
name string
resourceIncludesExcludes *collections.IncludesExcludes
resourceGroup string
resourceVersion string
resourceGV string
resourceName string
resourceNamespaced bool
namespaceIncludesExcludes *collections.IncludesExcludes
expectedListedNamespaces []string
lists []string
labelSelector string
actions map[string]Action
expectedActionIDs map[string][]string
deploymentsBackedUp bool
expectedDeploymentsBackedUp bool
networkPoliciesBackedUp bool
expectedNetworkPoliciesBackedUp bool
}{
{
name: "should not include resource",
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("pods"),
resourceGV: "v1",
resourceName: "secrets",
resourceNamespaced: true,
},
{
name: "should skip deployments.extensions if we've seen deployments.apps",
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
resourceGV: "extensions/v1beta1",
resourceName: "deployments",
resourceNamespaced: true,
deploymentsBackedUp: true,
expectedDeploymentsBackedUp: true,
},
{
name: "should skip deployments.apps if we've seen deployments.extensions",
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
resourceGV: "apps/v1beta1",
resourceName: "deployments",
resourceNamespaced: true,
deploymentsBackedUp: true,
expectedDeploymentsBackedUp: true,
},
{
name: "should skip networkpolicies.extensions if we've seen networkpolicies.networking.k8s.io",
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
resourceGV: "extensions/v1beta1",
resourceName: "networkpolicies",
resourceNamespaced: true,
networkPoliciesBackedUp: true,
expectedNetworkPoliciesBackedUp: true,
},
{
name: "should skip networkpolicies.networking.k8s.io if we've seen networkpolicies.extensions",
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
resourceGV: "networking.k8s.io/v1",
resourceName: "networkpolicies",
resourceNamespaced: true,
networkPoliciesBackedUp: true,
expectedNetworkPoliciesBackedUp: true,
},
{
name: "list per namespace when not including *",
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
resourceGroup: "apps",
resourceVersion: "v1beta1",
resourceGV: "apps/v1beta1",
resourceName: "deployments",
resourceNamespaced: true,
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("a", "b"),
expectedListedNamespaces: []string{"a", "b"},
lists: []string{
`{
"apiVersion": "apps/v1beta1",
"kind": "DeploymentList",
"items": [
{
"metadata": {
"namespace": "a",
"name": "1"
}
}
]
}`,
`{
"apiVersion": "apps/v1beta1v1",
"kind": "DeploymentList",
"items": [
{
"metadata": {
"namespace": "b",
"name": "2"
}
}
]
}`,
},
expectedDeploymentsBackedUp: true,
},
{
name: "list all namespaces when including *",
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
resourceGroup: "networking.k8s.io",
resourceVersion: "v1",
resourceGV: "networking.k8s.io/v1",
resourceName: "networkpolicies",
resourceNamespaced: true,
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
expectedListedNamespaces: []string{""},
lists: []string{
`{
"apiVersion": "networking.k8s.io/v1",
"kind": "NetworkPolicyList",
"items": [
{
"metadata": {
"namespace": "a",
"name": "1"
}
}
]
}`,
},
expectedNetworkPoliciesBackedUp: true,
},
{
name: "list all namespaces when cluster-scoped, even with namespace includes",
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
resourceGroup: "certificates.k8s.io",
resourceVersion: "v1beta1",
resourceGV: "certificates.k8s.io/v1beta1",
resourceName: "certificatesigningrequests",
resourceNamespaced: false,
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("a"),
expectedListedNamespaces: []string{""},
labelSelector: "a=b",
lists: []string{
`{
"apiVersion": "certifiaces.k8s.io/v1beta1",
"kind": "CertificateSigningRequestList",
"items": [
{
"metadata": {
"name": "1",
"labels": {
"a": "b"
}
}
}
]
}`,
},
},
{
name: "use a custom action",
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
resourceGroup: "certificates.k8s.io",
resourceVersion: "v1beta1",
resourceGV: "certificates.k8s.io/v1beta1",
resourceName: "certificatesigningrequests",
resourceNamespaced: false,
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("a"),
expectedListedNamespaces: []string{""},
labelSelector: "a=b",
lists: []string{
`{
"apiVersion": "certifiaces.k8s.io/v1beta1",
"kind": "CertificateSigningRequestList",
"items": [
{
"metadata": {
"name": "1",
"labels": {
"a": "b"
}
}
}
]
}`,
},
actions: map[string]Action{
"certificatesigningrequests": &fakeAction{},
"other": &fakeAction{},
},
expectedActionIDs: map[string][]string{
"certificatesigningrequests": {"1"},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var labelSelector *metav1.LabelSelector
if test.labelSelector != "" {
s, err := metav1.ParseToLabelSelector(test.labelSelector)
require.NoError(t, err)
labelSelector = s
}
log, _ := testlogger.NewNullLogger()
ctx := &backupContext{
backup: &v1.Backup{
Spec: v1.BackupSpec{
LabelSelector: labelSelector,
},
},
resourceIncludesExcludes: test.resourceIncludesExcludes,
namespaceIncludesExcludes: test.namespaceIncludesExcludes,
deploymentsBackedUp: test.deploymentsBackedUp,
networkPoliciesBackedUp: test.networkPoliciesBackedUp,
logger: log,
}
group := &metav1.APIResourceList{
GroupVersion: test.resourceGV,
}
resource := metav1.APIResource{Name: test.resourceName, Namespaced: test.resourceNamespaced}
itemBackupper := &fakeItemBackupper{}
var actualActionIDs map[string][]string
dynamicFactory := &FakeDynamicFactory{}
gvr := schema.GroupVersionResource{Group: test.resourceGroup, Version: test.resourceVersion}
gr := schema.GroupResource{Group: test.resourceGroup, Resource: test.resourceName}
for i, namespace := range test.expectedListedNamespaces {
obj := toRuntimeObject(t, test.lists[i])
client := &FakeDynamicClient{}
client.On("List", metav1.ListOptions{LabelSelector: test.labelSelector}).Return(obj, nil)
dynamicFactory.On("ClientForGroupVersionResource", gvr, resource, namespace).Return(client, nil)
action := test.actions[test.resourceName]
list, err := meta.ExtractList(obj)
require.NoError(t, err)
for i := range list {
item := list[i].(*unstructured.Unstructured)
itemBackupper.On("backupItem", ctx, item.Object, gr.String(), action).Return(nil)
if action != nil {
a, err := meta.Accessor(item)
require.NoError(t, err)
ns := a.GetNamespace()
name := a.GetName()
id := ns
if id != "" {
id += "/"
}
id += name
if actualActionIDs == nil {
actualActionIDs = make(map[string][]string)
}
actualActionIDs[test.resourceName] = append(actualActionIDs[test.resourceName], id)
}
}
}
discoveryHelper := &FakeDiscoveryHelper{
RESTMapper: &FakeMapper{
Resources: map[schema.GroupVersionResource]schema.GroupVersionResource{
schema.GroupVersionResource{Resource: "certificatesigningrequests"}: schema.GroupVersionResource{Group: "certificates.k8s.io", Version: "v1beta1", Resource: "certificatesigningrequests"},
schema.GroupVersionResource{Resource: "other"}: schema.GroupVersionResource{Group: "somegroup", Version: "someversion", Resource: "otherthings"},
},
},
}
kb, err := NewKubernetesBackupper(discoveryHelper, dynamicFactory, test.actions)
require.NoError(t, err)
backupper := kb.(*kubernetesBackupper)
backupper.itemBackupper = itemBackupper
err = backupper.backupResource(ctx, group, resource)
assert.Equal(t, test.expectedDeploymentsBackedUp, ctx.deploymentsBackedUp)
assert.Equal(t, test.expectedNetworkPoliciesBackedUp, ctx.networkPoliciesBackedUp)
assert.Equal(t, test.expectedActionIDs, actualActionIDs)
})
}
}
type fakeItemBackupper struct {
mock.Mock
}
func (f *fakeItemBackupper) backupItem(ctx *backupContext, obj map[string]interface{}, groupResource string, action Action) error {
args := f.Called(ctx, obj, groupResource, action)
return args.Error(0)
}
type fakeTarWriter struct {
closeCalled bool
headers []*tar.Header
data [][]byte
writeHeaderError error
writeError error
}
func (w *fakeTarWriter) Close() error { return nil }
func (w *fakeTarWriter) Write(data []byte) (int, error) {
w.data = append(w.data, data)
return 0, w.writeError
}
func (w *fakeTarWriter) WriteHeader(header *tar.Header) error {
w.headers = append(w.headers, header)
return w.writeHeaderError
}
func TestBackupItem(t *testing.T) {
tests := []struct {
name string
item string
namespaceIncludesExcludes *collections.IncludesExcludes
expectError bool
expectExcluded bool
expectedTarHeaderName string
tarWriteError bool
tarHeaderWriteError bool
customAction bool
expectedActionID string
}{
{
name: "empty map",
item: "{}",
expectError: true,
},
{
name: "missing name",
item: `{"metadata":{}}`,
expectError: true,
},
{
name: "excluded by namespace",
item: `{"metadata":{"namespace":"foo","name":"bar"}}`,
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*").Excludes("foo"),
expectError: false,
expectExcluded: true,
},
{
name: "explicit namespace include",
item: `{"metadata":{"namespace":"foo","name":"bar"}}`,
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("foo"),
expectError: false,
expectExcluded: false,
expectedTarHeaderName: "namespaces/foo/resource.group/bar.json",
},
{
name: "* namespace include",
item: `{"metadata":{"namespace":"foo","name":"bar"}}`,
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
expectError: false,
expectExcluded: false,
expectedTarHeaderName: "namespaces/foo/resource.group/bar.json",
},
{
name: "cluster-scoped",
item: `{"metadata":{"name":"bar"}}`,
expectError: false,
expectExcluded: false,
expectedTarHeaderName: "cluster/resource.group/bar.json",
},
{
name: "make sure status is deleted",
item: `{"metadata":{"name":"bar"},"spec":{"color":"green"},"status":{"foo":"bar"}}`,
expectError: false,
expectExcluded: false,
expectedTarHeaderName: "cluster/resource.group/bar.json",
},
{
name: "tar header write error",
item: `{"metadata":{"name":"bar"},"spec":{"color":"green"},"status":{"foo":"bar"}}`,
expectError: true,
tarHeaderWriteError: true,
},
{
name: "tar write error",
item: `{"metadata":{"name":"bar"},"spec":{"color":"green"},"status":{"foo":"bar"}}`,
expectError: true,
tarWriteError: true,
},
{
name: "action invoked - cluster-scoped",
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
item: `{"metadata":{"name":"bar"}}`,
expectError: false,
expectExcluded: false,
expectedTarHeaderName: "cluster/resource.group/bar.json",
customAction: true,
expectedActionID: "bar",
},
{
name: "action invoked - namespaced",
namespaceIncludesExcludes: collections.NewIncludesExcludes().Includes("*"),
item: `{"metadata":{"namespace": "myns", "name":"bar"}}`,
expectError: false,
expectExcluded: false,
expectedTarHeaderName: "namespaces/myns/resource.group/bar.json",
customAction: true,
expectedActionID: "myns/bar",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
item, err := getAsMap(test.item)
if err != nil {
t.Fatal(err)
}
namespaces := test.namespaceIncludesExcludes
if namespaces == nil {
namespaces = collections.NewIncludesExcludes()
}
w := &fakeTarWriter{}
if test.tarHeaderWriteError {
w.writeHeaderError = errors.New("error")
}
if test.tarWriteError {
w.writeError = errors.New("error")
}
var (
actionParam Action
action *fakeAction
backup *v1.Backup
)
if test.customAction {
action = &fakeAction{}
actionParam = action
backup = &v1.Backup{}
}
log, _ := testlogger.NewNullLogger()
ctx := &backupContext{
backup: backup,
namespaceIncludesExcludes: namespaces,
w: w,
logger: log,
}
b := &realItemBackupper{}
err = b.backupItem(ctx, item, "resource.group", actionParam)
gotError := err != nil
if e, a := test.expectError, gotError; e != a {
t.Fatalf("error: expected %t, got %t", e, a)
}
if test.expectError {
return
}
if test.expectExcluded {
if len(w.headers) > 0 {
t.Errorf("unexpected header write")
}
if len(w.data) > 0 {
t.Errorf("unexpected data write")
}
return
}
// we have to delete status as that's what backupItem does,
// and this ensures that we're verifying the right data
delete(item, "status")
itemWithoutStatus, err := json.Marshal(&item)
if err != nil {
t.Fatal(err)
}
if e, a := 1, len(w.headers); e != a {
t.Errorf("headers: expected %d, got %d", e, a)
}
if e, a := test.expectedTarHeaderName, w.headers[0].Name; e != a {
t.Errorf("header.name: expected %s, got %s", e, a)
}
if e, a := int64(len(itemWithoutStatus)), w.headers[0].Size; e != a {
t.Errorf("header.size: expected %d, got %d", e, a)
}
if e, a := byte(tar.TypeReg), w.headers[0].Typeflag; e != a {
t.Errorf("header.typeflag: expected %v, got %v", e, a)
}
if e, a := int64(0755), w.headers[0].Mode; e != a {
t.Errorf("header.mode: expected %d, got %d", e, a)
}
if w.headers[0].ModTime.IsZero() {
t.Errorf("header.modTime: expected it to be set")
}
if e, a := 1, len(w.data); e != a {
t.Errorf("# of data: expected %d, got %d", e, a)
}
actual, err := getAsMap(string(w.data[0]))
if err != nil {
t.Fatal(err)
}
if e, a := item, actual; !reflect.DeepEqual(e, a) {
t.Errorf("data: expected %s, got %s", e, a)
}
if test.customAction {
if len(action.ids) != 1 {
t.Errorf("unexpected custom action ids: %v", action.ids)
} else if e, a := test.expectedActionID, action.ids[0]; e != a {
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)
}
}
})
}
}
func getAsMap(j string) (map[string]interface{}, error) {
m := make(map[string]interface{})
err := json.Unmarshal([]byte(j), &m)
return m, err
}
func toRuntimeObject(t *testing.T, data string) runtime.Object {
o, _, err := unstructured.UnstructuredJSONScheme.Decode([]byte(data), nil, nil)
require.NoError(t, err)
return o
}