mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-01-08 14:21:18 +00:00
Refactor pkg/restore tests (part 2) (#1606)
* update TestPrioritizeResources to use real discovery helper Signed-off-by: Steve Kriss <krisss@vmware.com> * migrate invalid tarball contents tests Signed-off-by: Steve Kriss <krisss@vmware.com> * migrate item restore tests Signed-off-by: Steve Kriss <krisss@vmware.com> * migrate restore item action tests Signed-off-by: Steve Kriss <krisss@vmware.com>
This commit is contained in:
committed by
Adnan Abdulhussein
parent
dd96aa76db
commit
81c2adc059
@@ -25,6 +25,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -733,7 +734,7 @@ func (ctx *context) restoreResource(resource, namespace, resourcePath string) (R
|
||||
fullPath := filepath.Join(resourcePath, file.Name())
|
||||
obj, err := ctx.unmarshal(fullPath)
|
||||
if err != nil {
|
||||
addToResult(&errs, namespace, fmt.Errorf("error decoding %q: %v", fullPath, err))
|
||||
addToResult(&errs, namespace, fmt.Errorf("error decoding %q: %v", strings.Replace(fullPath, ctx.restoreDir+"/", "", -1), err))
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -32,15 +33,20 @@ import (
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/client-go/dynamic"
|
||||
kubetesting "k8s.io/client-go/testing"
|
||||
|
||||
velerov1api "github.com/heptio/velero/pkg/apis/velero/v1"
|
||||
"github.com/heptio/velero/pkg/backup"
|
||||
"github.com/heptio/velero/pkg/client"
|
||||
"github.com/heptio/velero/pkg/discovery"
|
||||
"github.com/heptio/velero/pkg/plugin/velero"
|
||||
"github.com/heptio/velero/pkg/test"
|
||||
"github.com/heptio/velero/pkg/util/encode"
|
||||
kubeutil "github.com/heptio/velero/pkg/util/kube"
|
||||
testutil "github.com/heptio/velero/pkg/util/test"
|
||||
)
|
||||
|
||||
@@ -646,6 +652,479 @@ func TestRestoreResourcePriorities(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestInvalidTarballContents runs restores for tarballs that are invalid in some way, and
|
||||
// verifies that the set of items created in the API and the errors returned are correct.
|
||||
// Validation is done by looking at the namespaces/names of the items in the API and the
|
||||
// Result objects returned from the restorer.
|
||||
func TestInvalidTarballContents(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
restore *velerov1api.Restore
|
||||
backup *velerov1api.Backup
|
||||
apiResources []*test.APIResource
|
||||
tarball io.Reader
|
||||
want map[*test.APIResource][]string
|
||||
wantErrs Result
|
||||
}{
|
||||
{
|
||||
name: "empty tarball returns an error",
|
||||
restore: defaultRestore().Restore(),
|
||||
backup: defaultBackup().Backup(),
|
||||
tarball: newTarWriter(t).
|
||||
done(),
|
||||
wantErrs: Result{
|
||||
Velero: []string{"backup does not contain top level resources directory"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid JSON is reported as an error and restore continues",
|
||||
restore: defaultRestore().Restore(),
|
||||
backup: defaultBackup().Backup(),
|
||||
tarball: newTarWriter(t).
|
||||
add("resources/pods/namespaces/ns-1/pod-1.json", []byte("invalid JSON")).
|
||||
addItems("pods",
|
||||
test.NewPod("ns-1", "pod-2"),
|
||||
).
|
||||
done(),
|
||||
apiResources: []*test.APIResource{
|
||||
test.Pods(),
|
||||
},
|
||||
want: map[*test.APIResource][]string{
|
||||
test.Pods(): {"ns-1/pod-2"},
|
||||
},
|
||||
wantErrs: Result{
|
||||
Namespaces: map[string][]string{
|
||||
"ns-1": {"error decoding \"resources/pods/namespaces/ns-1/pod-1.json\": invalid character 'i' looking for beginning of value"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
|
||||
for _, r := range tc.apiResources {
|
||||
h.DiscoveryClient.WithAPIResource(r)
|
||||
}
|
||||
require.NoError(t, h.restorer.discoveryHelper.Refresh())
|
||||
|
||||
warnings, errs := h.restorer.Restore(
|
||||
h.log,
|
||||
tc.restore,
|
||||
tc.backup,
|
||||
nil, // volume snapshots
|
||||
tc.tarball,
|
||||
nil, // actions
|
||||
nil, // snapshot location lister
|
||||
nil, // volume snapshotter getter
|
||||
)
|
||||
|
||||
assertEmptyResults(t, warnings)
|
||||
assert.Equal(t, tc.wantErrs, errs)
|
||||
assertAPIContents(t, h, tc.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRestoreItems runs restores of specific items and validates that they are created
|
||||
// with the expected metadata/spec/status in the API.
|
||||
func TestRestoreItems(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
restore *velerov1api.Restore
|
||||
backup *velerov1api.Backup
|
||||
apiResources []*test.APIResource
|
||||
tarball io.Reader
|
||||
want []*test.APIResource
|
||||
}{
|
||||
{
|
||||
name: "metadata other than namespace/name/labels/annotations gets removed",
|
||||
restore: defaultRestore().Restore(),
|
||||
backup: defaultBackup().Backup(),
|
||||
tarball: newTarWriter(t).
|
||||
addItems("pods",
|
||||
test.NewPod("ns-1", "pod-1",
|
||||
test.WithLabels("key-1", "val-1"),
|
||||
test.WithAnnotations("key-1", "val-1"),
|
||||
test.WithClusterName("cluster-1"),
|
||||
test.WithFinalizers("finalizer-1")),
|
||||
).
|
||||
done(),
|
||||
apiResources: []*test.APIResource{
|
||||
test.Pods(),
|
||||
},
|
||||
want: []*test.APIResource{
|
||||
test.Pods(
|
||||
test.NewPod("ns-1", "pod-1",
|
||||
test.WithLabels("key-1", "val-1", "velero.io/backup-name", "backup-1", "velero.io/restore-name", "restore-1"),
|
||||
test.WithAnnotations("key-1", "val-1"),
|
||||
),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "status gets removed",
|
||||
restore: defaultRestore().Restore(),
|
||||
backup: defaultBackup().Backup(),
|
||||
tarball: newTarWriter(t).
|
||||
addItems("pods",
|
||||
&corev1api.Pod{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "pod-1",
|
||||
},
|
||||
Status: corev1api.PodStatus{
|
||||
Message: "a non-empty status",
|
||||
},
|
||||
},
|
||||
).
|
||||
done(),
|
||||
apiResources: []*test.APIResource{
|
||||
test.Pods(),
|
||||
},
|
||||
want: []*test.APIResource{
|
||||
test.Pods(
|
||||
test.NewPod("ns-1", "pod-1", test.WithLabels("velero.io/backup-name", "backup-1", "velero.io/restore-name", "restore-1")),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "object gets labeled with full backup and restore names when they're both shorter than 63 characters",
|
||||
restore: defaultRestore().Restore(),
|
||||
backup: defaultBackup().Backup(),
|
||||
tarball: newTarWriter(t).
|
||||
addItems("pods", test.NewPod("ns-1", "pod-1")).
|
||||
done(),
|
||||
apiResources: []*test.APIResource{
|
||||
test.Pods(),
|
||||
},
|
||||
want: []*test.APIResource{
|
||||
test.Pods(test.NewPod("ns-1", "pod-1", test.WithLabels("velero.io/backup-name", "backup-1", "velero.io/restore-name", "restore-1"))),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "object gets labeled with full backup and restore names when they're both equal to 63 characters",
|
||||
restore: NewNamedBuilder(velerov1api.DefaultNamespace, "the-really-long-kube-service-name-that-is-exactly-63-characters").
|
||||
Backup("the-really-long-kube-service-name-that-is-exactly-63-characters").
|
||||
Restore(),
|
||||
backup: backup.NewNamedBuilder(velerov1api.DefaultNamespace, "the-really-long-kube-service-name-that-is-exactly-63-characters").Backup(),
|
||||
tarball: newTarWriter(t).
|
||||
addItems("pods", test.NewPod("ns-1", "pod-1")).
|
||||
done(),
|
||||
apiResources: []*test.APIResource{
|
||||
test.Pods(),
|
||||
},
|
||||
want: []*test.APIResource{
|
||||
test.Pods(test.NewPod("ns-1", "pod-1", test.WithLabels(
|
||||
"velero.io/backup-name", "the-really-long-kube-service-name-that-is-exactly-63-characters",
|
||||
"velero.io/restore-name", "the-really-long-kube-service-name-that-is-exactly-63-characters",
|
||||
))),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "object gets labeled with shortened backup and restore names when they're both longer than 63 characters",
|
||||
restore: NewNamedBuilder(velerov1api.DefaultNamespace, "the-really-long-kube-service-name-that-is-much-greater-than-63-characters").
|
||||
Backup("the-really-long-kube-service-name-that-is-much-greater-than-63-characters").
|
||||
Restore(),
|
||||
backup: backup.NewNamedBuilder(velerov1api.DefaultNamespace, "the-really-long-kube-service-name-that-is-much-greater-than-63-characters").Backup(),
|
||||
tarball: newTarWriter(t).
|
||||
addItems("pods", test.NewPod("ns-1", "pod-1")).
|
||||
done(),
|
||||
apiResources: []*test.APIResource{
|
||||
test.Pods(),
|
||||
},
|
||||
want: []*test.APIResource{
|
||||
test.Pods(test.NewPod("ns-1", "pod-1", test.WithLabels(
|
||||
"velero.io/backup-name", "the-really-long-kube-service-name-that-is-much-greater-th8a11b3",
|
||||
"velero.io/restore-name", "the-really-long-kube-service-name-that-is-much-greater-th8a11b3",
|
||||
))),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no error when service account already exists in cluster and is identical to the backed up one",
|
||||
restore: defaultRestore().Restore(),
|
||||
backup: defaultBackup().Backup(),
|
||||
tarball: newTarWriter(t).
|
||||
addItems("serviceaccounts", test.NewServiceAccount("ns-1", "sa-1")).
|
||||
done(),
|
||||
apiResources: []*test.APIResource{
|
||||
test.ServiceAccounts(test.NewServiceAccount("ns-1", "sa-1")),
|
||||
},
|
||||
want: []*test.APIResource{
|
||||
test.ServiceAccounts(test.NewServiceAccount("ns-1", "sa-1")),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "service account secrets and image pull secrets are restored when service account already exists in cluster",
|
||||
restore: defaultRestore().Restore(),
|
||||
backup: defaultBackup().Backup(),
|
||||
tarball: newTarWriter(t).
|
||||
addItems("serviceaccounts", &corev1api.ServiceAccount{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ServiceAccount",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "sa-1",
|
||||
},
|
||||
Secrets: []corev1api.ObjectReference{{Name: "secret-1"}},
|
||||
ImagePullSecrets: []corev1api.LocalObjectReference{{Name: "pull-secret-1"}},
|
||||
}).
|
||||
done(),
|
||||
apiResources: []*test.APIResource{
|
||||
test.ServiceAccounts(test.NewServiceAccount("ns-1", "sa-1")),
|
||||
},
|
||||
want: []*test.APIResource{
|
||||
test.ServiceAccounts(&corev1api.ServiceAccount{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ServiceAccount",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "sa-1",
|
||||
},
|
||||
Secrets: []corev1api.ObjectReference{{Name: "secret-1"}},
|
||||
ImagePullSecrets: []corev1api.LocalObjectReference{{Name: "pull-secret-1"}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
|
||||
for _, r := range tc.apiResources {
|
||||
h.addItems(t, r)
|
||||
}
|
||||
|
||||
warnings, errs := h.restorer.Restore(
|
||||
h.log,
|
||||
tc.restore,
|
||||
tc.backup,
|
||||
nil, // volume snapshots
|
||||
tc.tarball,
|
||||
nil, // actions
|
||||
nil, // snapshot location lister
|
||||
nil, // volume snapshotter getter
|
||||
)
|
||||
|
||||
assertEmptyResults(t, warnings, errs)
|
||||
|
||||
for _, resource := range tc.want {
|
||||
resourceClient := h.DynamicClient.Resource(resource.GVR())
|
||||
for _, item := range resource.Items {
|
||||
var client dynamic.ResourceInterface
|
||||
if item.GetNamespace() != "" {
|
||||
client = resourceClient.Namespace(item.GetNamespace())
|
||||
} else {
|
||||
client = resourceClient
|
||||
}
|
||||
|
||||
res, err := client.Get(item.GetName(), metav1.GetOptions{})
|
||||
if !assert.NoError(t, err) {
|
||||
continue
|
||||
}
|
||||
|
||||
itemJSON, err := json.Marshal(item)
|
||||
if !assert.NoError(t, err) {
|
||||
continue
|
||||
}
|
||||
|
||||
t.Logf("%v", string(itemJSON))
|
||||
|
||||
u := make(map[string]interface{})
|
||||
if !assert.NoError(t, json.Unmarshal(itemJSON, &u)) {
|
||||
continue
|
||||
}
|
||||
want := &unstructured.Unstructured{Object: u}
|
||||
|
||||
// These fields get non-nil zero values in the unstructured objects if they're
|
||||
// empty in the structured objects. Remove them to make comparison easier.
|
||||
unstructured.RemoveNestedField(want.Object, "metadata", "creationTimestamp")
|
||||
unstructured.RemoveNestedField(want.Object, "status")
|
||||
|
||||
assert.Equal(t, want, res)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// recordResourcesAction is a restore item action that can be configured
|
||||
// to run for specific resources/namespaces and simply records the items
|
||||
// that it is executed for.
|
||||
type recordResourcesAction struct {
|
||||
selector velero.ResourceSelector
|
||||
ids []string
|
||||
additionalItems []velero.ResourceIdentifier
|
||||
}
|
||||
|
||||
func (a *recordResourcesAction) AppliesTo() (velero.ResourceSelector, error) {
|
||||
return a.selector, nil
|
||||
}
|
||||
|
||||
func (a *recordResourcesAction) Execute(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
metadata, err := meta.Accessor(input.Item)
|
||||
if err != nil {
|
||||
return &velero.RestoreItemActionExecuteOutput{
|
||||
UpdatedItem: input.Item,
|
||||
AdditionalItems: a.additionalItems,
|
||||
}, err
|
||||
}
|
||||
a.ids = append(a.ids, kubeutil.NamespaceAndName(metadata))
|
||||
|
||||
return &velero.RestoreItemActionExecuteOutput{
|
||||
UpdatedItem: input.Item,
|
||||
AdditionalItems: a.additionalItems,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *recordResourcesAction) ForResource(resource string) *recordResourcesAction {
|
||||
a.selector.IncludedResources = append(a.selector.IncludedResources, resource)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *recordResourcesAction) ForNamespace(namespace string) *recordResourcesAction {
|
||||
a.selector.IncludedNamespaces = append(a.selector.IncludedNamespaces, namespace)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *recordResourcesAction) ForLabelSelector(selector string) *recordResourcesAction {
|
||||
a.selector.LabelSelector = selector
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *recordResourcesAction) WithAdditionalItems(items []velero.ResourceIdentifier) *recordResourcesAction {
|
||||
a.additionalItems = items
|
||||
return a
|
||||
}
|
||||
|
||||
// TestRestoreActionsRunsForCorrectItems runs restores with restore item actions, and
|
||||
// verifies that each restore item action is run for the correct set of resources based on its
|
||||
// AppliesTo() resource selector. Verification is done by using the recordResourcesAction struct,
|
||||
// which records which resources it's executed for.
|
||||
func TestRestoreActionsRunForCorrectItems(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
restore *velerov1api.Restore
|
||||
backup *velerov1api.Backup
|
||||
apiResources []*test.APIResource
|
||||
tarball io.Reader
|
||||
actions map[*recordResourcesAction][]string
|
||||
}{
|
||||
{
|
||||
name: "single action with no selector runs for all items",
|
||||
restore: defaultRestore().Restore(),
|
||||
backup: defaultBackup().Backup(),
|
||||
tarball: newTarWriter(t).
|
||||
addItems("pods", test.NewPod("ns-1", "pod-1"), test.NewPod("ns-2", "pod-2")).
|
||||
addItems("persistentvolumes", test.NewPV("pv-1"), test.NewPV("pv-2")).
|
||||
done(),
|
||||
apiResources: []*test.APIResource{test.Pods(), test.PVs()},
|
||||
actions: map[*recordResourcesAction][]string{
|
||||
new(recordResourcesAction): {"ns-1/pod-1", "ns-2/pod-2", "pv-1", "pv-2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single action with a resource selector for namespaced resources runs only for matching resources",
|
||||
restore: defaultRestore().Restore(),
|
||||
backup: defaultBackup().Backup(),
|
||||
tarball: newTarWriter(t).
|
||||
addItems("pods", test.NewPod("ns-1", "pod-1"), test.NewPod("ns-2", "pod-2")).
|
||||
addItems("persistentvolumes", test.NewPV("pv-1"), test.NewPV("pv-2")).
|
||||
done(),
|
||||
apiResources: []*test.APIResource{test.Pods(), test.PVs()},
|
||||
actions: map[*recordResourcesAction][]string{
|
||||
new(recordResourcesAction).ForResource("pods"): {"ns-1/pod-1", "ns-2/pod-2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single action with a resource selector for cluster-scoped resources runs only for matching resources",
|
||||
restore: defaultRestore().Restore(),
|
||||
backup: defaultBackup().Backup(),
|
||||
tarball: newTarWriter(t).
|
||||
addItems("pods", test.NewPod("ns-1", "pod-1"), test.NewPod("ns-2", "pod-2")).
|
||||
addItems("persistentvolumes", test.NewPV("pv-1"), test.NewPV("pv-2")).
|
||||
done(),
|
||||
apiResources: []*test.APIResource{test.Pods(), test.PVs()},
|
||||
actions: map[*recordResourcesAction][]string{
|
||||
new(recordResourcesAction).ForResource("persistentvolumes"): {"pv-1", "pv-2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple actions, each with a different resource selector using short name, run for matching resources",
|
||||
restore: defaultRestore().Restore(),
|
||||
backup: defaultBackup().Backup(),
|
||||
tarball: newTarWriter(t).
|
||||
addItems("pods", test.NewPod("ns-1", "pod-1"), test.NewPod("ns-2", "pod-2")).
|
||||
addItems("persistentvolumeclaims", test.NewPVC("ns-1", "pvc-1"), test.NewPVC("ns-2", "pvc-2")).
|
||||
addItems("persistentvolumes", test.NewPV("pv-1"), test.NewPV("pv-2")).
|
||||
done(),
|
||||
apiResources: []*test.APIResource{test.Pods(), test.PVCs(), test.PVs()},
|
||||
actions: map[*recordResourcesAction][]string{
|
||||
new(recordResourcesAction).ForResource("po"): {"ns-1/pod-1", "ns-2/pod-2"},
|
||||
new(recordResourcesAction).ForResource("pv"): {"pv-1", "pv-2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "actions with selectors that don't match anything don't run for any resources",
|
||||
restore: defaultRestore().Restore(),
|
||||
backup: defaultBackup().Backup(),
|
||||
tarball: newTarWriter(t).
|
||||
addItems("pods", test.NewPod("ns-1", "pod-1")).
|
||||
addItems("persistentvolumeclaims", test.NewPVC("ns-2", "pvc-2")).
|
||||
done(),
|
||||
apiResources: []*test.APIResource{test.Pods(), test.PVCs(), test.PVs()},
|
||||
actions: map[*recordResourcesAction][]string{
|
||||
new(recordResourcesAction).ForNamespace("ns-1").ForResource("persistentvolumeclaims"): nil,
|
||||
new(recordResourcesAction).ForNamespace("ns-2").ForResource("pods"): nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
|
||||
for _, r := range tc.apiResources {
|
||||
h.addItems(t, r)
|
||||
}
|
||||
|
||||
actions := []velero.RestoreItemAction{}
|
||||
for action := range tc.actions {
|
||||
actions = append(actions, action)
|
||||
}
|
||||
|
||||
warnings, errs := h.restorer.Restore(
|
||||
h.log,
|
||||
tc.restore,
|
||||
tc.backup,
|
||||
nil, // volume snapshots
|
||||
tc.tarball,
|
||||
actions,
|
||||
nil, // snapshot location lister
|
||||
nil, // volume snapshotter getter
|
||||
)
|
||||
|
||||
assertEmptyResults(t, warnings, errs)
|
||||
|
||||
for action, want := range tc.actions {
|
||||
sort.Strings(want)
|
||||
sort.Strings(action.ids)
|
||||
assert.Equal(t, want, action.ids)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// assertResourceCreationOrder ensures that resources were created in the expected
|
||||
// order. Any resources *not* in resourcePriorities are required to come *after* all
|
||||
// resources in any order.
|
||||
@@ -717,6 +1196,8 @@ func defaultRestore() *Builder {
|
||||
// all of the items specified in 'want' (a map from an APIResource definition to a slice
|
||||
// of resource identifiers, formatted as <namespace>/<name>).
|
||||
func assertAPIContents(t *testing.T, h *harness, want map[*test.APIResource][]string) {
|
||||
t.Helper()
|
||||
|
||||
for r, want := range want {
|
||||
res, err := h.DynamicClient.Resource(r.GVR()).List(metav1.ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
@@ -848,3 +1329,29 @@ func newHarness(t *testing.T) *harness {
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *harness) addItems(t *testing.T, resource *test.APIResource) {
|
||||
t.Helper()
|
||||
|
||||
h.DiscoveryClient.WithAPIResource(resource)
|
||||
require.NoError(t, h.restorer.discoveryHelper.Refresh())
|
||||
|
||||
for _, item := range resource.Items {
|
||||
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(item)
|
||||
require.NoError(t, err)
|
||||
|
||||
unstructuredObj := &unstructured.Unstructured{Object: obj}
|
||||
|
||||
// These fields have non-nil zero values in the unstructured objects. We remove
|
||||
// them to make comparison easier in our tests.
|
||||
unstructured.RemoveNestedField(unstructuredObj.Object, "metadata", "creationTimestamp")
|
||||
unstructured.RemoveNestedField(unstructuredObj.Object, "status")
|
||||
|
||||
if resource.Namespaced {
|
||||
_, err = h.DynamicClient.Resource(resource.GVR()).Namespace(item.GetNamespace()).Create(unstructuredObj, metav1.CreateOptions{})
|
||||
} else {
|
||||
_, err = h.DynamicClient.Resource(resource.GVR()).Create(unstructuredObj, metav1.CreateOptions{})
|
||||
}
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -35,16 +33,17 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
discoveryfake "k8s.io/client-go/discovery/fake"
|
||||
kubefake "k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
|
||||
api "github.com/heptio/velero/pkg/apis/velero/v1"
|
||||
pkgclient "github.com/heptio/velero/pkg/client"
|
||||
"github.com/heptio/velero/pkg/generated/clientset/versioned/fake"
|
||||
informers "github.com/heptio/velero/pkg/generated/informers/externalversions"
|
||||
"github.com/heptio/velero/pkg/discovery"
|
||||
"github.com/heptio/velero/pkg/kuberesource"
|
||||
"github.com/heptio/velero/pkg/plugin/velero"
|
||||
"github.com/heptio/velero/pkg/test"
|
||||
"github.com/heptio/velero/pkg/util/collections"
|
||||
"github.com/heptio/velero/pkg/util/logging"
|
||||
velerotest "github.com/heptio/velero/pkg/util/test"
|
||||
"github.com/heptio/velero/pkg/volume"
|
||||
)
|
||||
@@ -90,32 +89,40 @@ func TestPrioritizeResources(t *testing.T) {
|
||||
|
||||
logger := velerotest.NewLogger()
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var helperResourceList []*metav1.APIResourceList
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
discoveryClient := &test.DiscoveryClient{
|
||||
FakeDiscovery: kubefake.NewSimpleClientset().Discovery().(*discoveryfake.FakeDiscovery),
|
||||
}
|
||||
|
||||
helper, err := discovery.NewHelper(discoveryClient, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
// add all the test case's API resources to the discovery client
|
||||
for gvString, resources := range tc.apiResources {
|
||||
gv, err := schema.ParseGroupVersion(gvString)
|
||||
require.NoError(t, err)
|
||||
|
||||
for gv, resources := range test.apiResources {
|
||||
resourceList := &metav1.APIResourceList{GroupVersion: gv}
|
||||
for _, resource := range resources {
|
||||
resourceList.APIResources = append(resourceList.APIResources, metav1.APIResource{Name: resource})
|
||||
discoveryClient.WithAPIResource(&test.APIResource{
|
||||
Group: gv.Group,
|
||||
Version: gv.Version,
|
||||
Name: resource,
|
||||
})
|
||||
}
|
||||
helperResourceList = append(helperResourceList, resourceList)
|
||||
}
|
||||
|
||||
helper := velerotest.NewFakeDiscoveryHelper(true, nil)
|
||||
helper.ResourceList = helperResourceList
|
||||
require.NoError(t, helper.Refresh())
|
||||
|
||||
includesExcludes := collections.NewIncludesExcludes().Includes(test.includes...).Excludes(test.excludes...)
|
||||
includesExcludes := collections.NewIncludesExcludes().Includes(tc.includes...).Excludes(tc.excludes...)
|
||||
|
||||
result, err := prioritizeResources(helper, test.priorities, includesExcludes, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
result, err := prioritizeResources(helper, tc.priorities, includesExcludes, logger)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(test.expected), len(result))
|
||||
require.Equal(t, len(tc.expected), len(result))
|
||||
|
||||
for i := range result {
|
||||
if e, a := test.expected[i], result[i].Resource; e != a {
|
||||
if e, a := tc.expected[i], result[i].Resource; e != a {
|
||||
t.Errorf("index %d, expected %s, got %s", i, e, a)
|
||||
}
|
||||
}
|
||||
@@ -123,430 +130,6 @@ func TestPrioritizeResources(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestorePriority(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fileSystem *velerotest.FakeFileSystem
|
||||
restore *api.Restore
|
||||
baseDir string
|
||||
prioritizedResources []schema.GroupResource
|
||||
expectedErrors Result
|
||||
expectedReadDirs []string
|
||||
}{
|
||||
{
|
||||
name: "error in a single resource doesn't terminate restore immediately, but is returned",
|
||||
fileSystem: velerotest.NewFakeFileSystem().
|
||||
WithFile("bak/resources/a/namespaces/ns-1/invalid-json.json", []byte("invalid json")).
|
||||
WithDirectory("bak/resources/c/namespaces/ns-1"),
|
||||
restore: &api.Restore{Spec: api.RestoreSpec{IncludedNamespaces: []string{"*"}}},
|
||||
baseDir: "bak",
|
||||
prioritizedResources: []schema.GroupResource{
|
||||
{Resource: "a"},
|
||||
{Resource: "b"},
|
||||
{Resource: "c"},
|
||||
},
|
||||
expectedErrors: Result{
|
||||
Namespaces: map[string][]string{
|
||||
"ns-1": {"error decoding \"bak/resources/a/namespaces/ns-1/invalid-json.json\": invalid character 'i' looking for beginning of value"},
|
||||
},
|
||||
},
|
||||
expectedReadDirs: []string{"bak/resources", "bak/resources/a/namespaces", "bak/resources/a/namespaces/ns-1", "bak/resources/c/namespaces", "bak/resources/c/namespaces/ns-1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
log := velerotest.NewLogger()
|
||||
|
||||
nsClient := &velerotest.FakeNamespaceClient{}
|
||||
|
||||
ctx := &context{
|
||||
restore: test.restore,
|
||||
namespaceClient: nsClient,
|
||||
fileSystem: test.fileSystem,
|
||||
prioritizedResources: test.prioritizedResources,
|
||||
log: log,
|
||||
restoreDir: test.baseDir,
|
||||
}
|
||||
|
||||
nsClient.On("Get", mock.Anything, metav1.GetOptions{}).Return(&v1.Namespace{}, nil)
|
||||
|
||||
warnings, errors := ctx.restoreFromDir()
|
||||
|
||||
assert.Empty(t, warnings.Velero)
|
||||
assert.Empty(t, warnings.Cluster)
|
||||
assert.Empty(t, warnings.Namespaces)
|
||||
assert.Equal(t, test.expectedErrors, errors)
|
||||
|
||||
assert.Equal(t, test.expectedReadDirs, test.fileSystem.ReadDirCalls)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreResourceForNamespace(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
resourcePath string
|
||||
labelSelector labels.Selector
|
||||
includeClusterResources *bool
|
||||
fileSystem *velerotest.FakeFileSystem
|
||||
actions []resolvedAction
|
||||
expectedErrors Result
|
||||
expectedObjs []unstructured.Unstructured
|
||||
}{
|
||||
{
|
||||
name: "no such directory causes error",
|
||||
namespace: "ns-1",
|
||||
resourcePath: "configmaps",
|
||||
fileSystem: velerotest.NewFakeFileSystem(),
|
||||
expectedErrors: Result{
|
||||
Namespaces: map[string][]string{
|
||||
"ns-1": {"error reading \"configmaps\" resource directory: open configmaps: file does not exist"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty directory is no-op",
|
||||
namespace: "ns-1",
|
||||
resourcePath: "configmaps",
|
||||
fileSystem: velerotest.NewFakeFileSystem().WithDirectory("configmaps"),
|
||||
},
|
||||
{
|
||||
name: "unmarshall failure does not cause immediate return",
|
||||
namespace: "ns-1",
|
||||
resourcePath: "configmaps",
|
||||
labelSelector: labels.NewSelector(),
|
||||
fileSystem: velerotest.NewFakeFileSystem().
|
||||
WithFile("configmaps/cm-1-invalid.json", []byte("this is not valid json")).
|
||||
WithFile("configmaps/cm-2.json", newNamedTestConfigMap("cm-2").ToJSON()),
|
||||
expectedErrors: Result{
|
||||
Namespaces: map[string][]string{
|
||||
"ns-1": {"error decoding \"configmaps/cm-1-invalid.json\": invalid character 'h' in literal true (expecting 'r')"},
|
||||
},
|
||||
},
|
||||
expectedObjs: toUnstructured(newNamedTestConfigMap("cm-2").ConfigMap),
|
||||
},
|
||||
{
|
||||
name: "custom restorer is correctly used",
|
||||
namespace: "ns-1",
|
||||
resourcePath: "configmaps",
|
||||
labelSelector: labels.NewSelector(),
|
||||
fileSystem: velerotest.NewFakeFileSystem().WithFile("configmaps/cm-1.json", newTestConfigMap().ToJSON()),
|
||||
actions: []resolvedAction{
|
||||
{
|
||||
RestoreItemAction: newFakeAction("configmaps"),
|
||||
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("configmaps"),
|
||||
namespaceIncludesExcludes: collections.NewIncludesExcludes(),
|
||||
selector: labels.Everything(),
|
||||
},
|
||||
},
|
||||
expectedObjs: toUnstructured(newTestConfigMap().WithLabels(map[string]string{"fake-restorer": "foo"}).ConfigMap),
|
||||
},
|
||||
{
|
||||
name: "custom restorer for different group/resource is not used",
|
||||
namespace: "ns-1",
|
||||
resourcePath: "configmaps",
|
||||
labelSelector: labels.NewSelector(),
|
||||
fileSystem: velerotest.NewFakeFileSystem().WithFile("configmaps/cm-1.json", newTestConfigMap().ToJSON()),
|
||||
actions: []resolvedAction{
|
||||
{
|
||||
RestoreItemAction: newFakeAction("foo-resource"),
|
||||
resourceIncludesExcludes: collections.NewIncludesExcludes().Includes("foo-resource"),
|
||||
namespaceIncludesExcludes: collections.NewIncludesExcludes(),
|
||||
selector: labels.Everything(),
|
||||
},
|
||||
},
|
||||
expectedObjs: toUnstructured(newTestConfigMap().ConfigMap),
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
client = fake.NewSimpleClientset()
|
||||
sharedInformers = informers.NewSharedInformerFactory(client, 0)
|
||||
snapshotLocationLister = sharedInformers.Velero().V1().VolumeSnapshotLocations().Lister()
|
||||
)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
resourceClient := &velerotest.FakeDynamicClient{}
|
||||
for i := range test.expectedObjs {
|
||||
addRestoreLabels(&test.expectedObjs[i], "my-restore", "my-backup")
|
||||
resourceClient.On("Create", &test.expectedObjs[i]).Return(&test.expectedObjs[i], nil)
|
||||
}
|
||||
|
||||
dynamicFactory := &velerotest.FakeDynamicFactory{}
|
||||
gv := schema.GroupVersion{Group: "", Version: "v1"}
|
||||
|
||||
configMapResource := metav1.APIResource{Name: "configmaps", Namespaced: true}
|
||||
dynamicFactory.On("ClientForGroupVersionResource", gv, configMapResource, test.namespace).Return(resourceClient, nil)
|
||||
|
||||
pvResource := metav1.APIResource{Name: "persistentvolumes", Namespaced: false}
|
||||
dynamicFactory.On("ClientForGroupVersionResource", gv, pvResource, test.namespace).Return(resourceClient, nil)
|
||||
resourceClient.On("Watch", metav1.ListOptions{}).Return(&fakeWatch{}, nil)
|
||||
if test.resourcePath == "persistentvolumes" {
|
||||
resourceClient.On("Get", mock.Anything, metav1.GetOptions{}).Return(&unstructured.Unstructured{}, k8serrors.NewNotFound(schema.GroupResource{Resource: "persistentvolumes"}, ""))
|
||||
}
|
||||
|
||||
// Assume the persistentvolume doesn't already exist in the cluster.
|
||||
saResource := metav1.APIResource{Name: "serviceaccounts", Namespaced: true}
|
||||
dynamicFactory.On("ClientForGroupVersionResource", gv, saResource, test.namespace).Return(resourceClient, nil)
|
||||
|
||||
podResource := metav1.APIResource{Name: "pods", Namespaced: true}
|
||||
dynamicFactory.On("ClientForGroupVersionResource", gv, podResource, test.namespace).Return(resourceClient, nil)
|
||||
|
||||
ctx := &context{
|
||||
dynamicFactory: dynamicFactory,
|
||||
actions: test.actions,
|
||||
fileSystem: test.fileSystem,
|
||||
selector: test.labelSelector,
|
||||
restore: &api.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: api.DefaultNamespace,
|
||||
Name: "my-restore",
|
||||
},
|
||||
Spec: api.RestoreSpec{
|
||||
IncludeClusterResources: test.includeClusterResources,
|
||||
BackupName: "my-backup",
|
||||
},
|
||||
},
|
||||
backup: &api.Backup{},
|
||||
log: velerotest.NewLogger(),
|
||||
pvRestorer: &pvRestorer{
|
||||
logger: logging.DefaultLogger(logrus.DebugLevel),
|
||||
volumeSnapshotterGetter: &fakeVolumeSnapshotterGetter{
|
||||
volumeMap: map[velerotest.VolumeBackupInfo]string{{SnapshotID: "snap-1"}: "volume-1"},
|
||||
volumeID: "volume-1",
|
||||
},
|
||||
snapshotLocationLister: snapshotLocationLister,
|
||||
backup: &api.Backup{},
|
||||
},
|
||||
applicableActions: make(map[schema.GroupResource][]resolvedAction),
|
||||
resourceClients: make(map[resourceClientKey]pkgclient.Dynamic),
|
||||
restoredItems: make(map[velero.ResourceIdentifier]struct{}),
|
||||
}
|
||||
|
||||
warnings, errors := ctx.restoreResource(test.resourcePath, test.namespace, test.resourcePath)
|
||||
|
||||
assert.Empty(t, warnings.Velero)
|
||||
assert.Empty(t, warnings.Cluster)
|
||||
assert.Empty(t, warnings.Namespaces)
|
||||
assert.Equal(t, test.expectedErrors, errors)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreLabels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
resourcePath string
|
||||
backupName string
|
||||
restoreName string
|
||||
labelSelector labels.Selector
|
||||
includeClusterResources *bool
|
||||
fileSystem *velerotest.FakeFileSystem
|
||||
actions []resolvedAction
|
||||
expectedErrors Result
|
||||
expectedObjs []unstructured.Unstructured
|
||||
}{
|
||||
{
|
||||
name: "backup name and restore name less than 63 characters",
|
||||
namespace: "ns-1",
|
||||
resourcePath: "configmaps",
|
||||
backupName: "less-than-63-characters",
|
||||
restoreName: "less-than-63-characters-12345",
|
||||
labelSelector: labels.NewSelector(),
|
||||
fileSystem: velerotest.NewFakeFileSystem().
|
||||
WithFile("configmaps/cm-1.json", newNamedTestConfigMap("cm-1").ToJSON()),
|
||||
expectedObjs: toUnstructured(
|
||||
newNamedTestConfigMap("cm-1").WithLabels(map[string]string{
|
||||
api.BackupNameLabel: "less-than-63-characters",
|
||||
api.RestoreNameLabel: "less-than-63-characters-12345",
|
||||
}).ConfigMap,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "backup name equal to 63 characters",
|
||||
namespace: "ns-1",
|
||||
resourcePath: "configmaps",
|
||||
backupName: "the-really-long-kube-service-name-that-is-exactly-63-characters",
|
||||
restoreName: "the-really-long-kube-service-name-that-is-exactly-63-characters-12345",
|
||||
labelSelector: labels.NewSelector(),
|
||||
fileSystem: velerotest.NewFakeFileSystem().
|
||||
WithFile("configmaps/cm-1.json", newNamedTestConfigMap("cm-1").ToJSON()),
|
||||
expectedObjs: toUnstructured(
|
||||
newNamedTestConfigMap("cm-1").WithLabels(map[string]string{
|
||||
api.BackupNameLabel: "the-really-long-kube-service-name-that-is-exactly-63-characters",
|
||||
api.RestoreNameLabel: "the-really-long-kube-service-name-that-is-exactly-63-char0871f3",
|
||||
}).ConfigMap,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "backup name greter than 63 characters",
|
||||
namespace: "ns-1",
|
||||
resourcePath: "configmaps",
|
||||
backupName: "the-really-long-kube-service-name-that-is-much-greater-than-63-characters",
|
||||
restoreName: "the-really-long-kube-service-name-that-is-much-greater-than-63-characters-12345",
|
||||
labelSelector: labels.NewSelector(),
|
||||
fileSystem: velerotest.NewFakeFileSystem().
|
||||
WithFile("configmaps/cm-1.json", newNamedTestConfigMap("cm-1").ToJSON()),
|
||||
expectedObjs: toUnstructured(
|
||||
newNamedTestConfigMap("cm-1").WithLabels(map[string]string{
|
||||
api.BackupNameLabel: "the-really-long-kube-service-name-that-is-much-greater-th8a11b3",
|
||||
api.RestoreNameLabel: "the-really-long-kube-service-name-that-is-much-greater-th1bf26f",
|
||||
}).ConfigMap,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
client = fake.NewSimpleClientset()
|
||||
sharedInformers = informers.NewSharedInformerFactory(client, 0)
|
||||
snapshotLocationLister = sharedInformers.Velero().V1().VolumeSnapshotLocations().Lister()
|
||||
)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
resourceClient := &velerotest.FakeDynamicClient{}
|
||||
for i := range test.expectedObjs {
|
||||
resourceClient.On("Create", &test.expectedObjs[i]).Return(&test.expectedObjs[i], nil)
|
||||
}
|
||||
|
||||
dynamicFactory := &velerotest.FakeDynamicFactory{}
|
||||
gv := schema.GroupVersion{Group: "", Version: "v1"}
|
||||
|
||||
configMapResource := metav1.APIResource{Name: "configmaps", Namespaced: true}
|
||||
dynamicFactory.On("ClientForGroupVersionResource", gv, configMapResource, test.namespace).Return(resourceClient, nil)
|
||||
|
||||
ctx := &context{
|
||||
dynamicFactory: dynamicFactory,
|
||||
actions: test.actions,
|
||||
fileSystem: test.fileSystem,
|
||||
selector: test.labelSelector,
|
||||
restore: &api.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: api.DefaultNamespace,
|
||||
Name: test.restoreName,
|
||||
},
|
||||
Spec: api.RestoreSpec{
|
||||
IncludeClusterResources: test.includeClusterResources,
|
||||
BackupName: test.backupName,
|
||||
},
|
||||
},
|
||||
backup: &api.Backup{},
|
||||
log: velerotest.NewLogger(),
|
||||
pvRestorer: &pvRestorer{
|
||||
logger: logging.DefaultLogger(logrus.DebugLevel),
|
||||
volumeSnapshotterGetter: &fakeVolumeSnapshotterGetter{
|
||||
volumeMap: map[velerotest.VolumeBackupInfo]string{{SnapshotID: "snap-1"}: "volume-1"},
|
||||
volumeID: "volume-1",
|
||||
},
|
||||
snapshotLocationLister: snapshotLocationLister,
|
||||
backup: &api.Backup{},
|
||||
},
|
||||
applicableActions: make(map[schema.GroupResource][]resolvedAction),
|
||||
resourceClients: make(map[resourceClientKey]pkgclient.Dynamic),
|
||||
restoredItems: make(map[velero.ResourceIdentifier]struct{}),
|
||||
}
|
||||
|
||||
warnings, errors := ctx.restoreResource(test.resourcePath, test.namespace, test.resourcePath)
|
||||
|
||||
assert.Empty(t, warnings.Velero)
|
||||
assert.Empty(t, warnings.Cluster)
|
||||
assert.Empty(t, warnings.Namespaces)
|
||||
assert.Equal(t, test.expectedErrors, errors)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoringExistingServiceAccount(t *testing.T) {
|
||||
fromCluster := newTestServiceAccount()
|
||||
fromClusterUnstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fromCluster.ServiceAccount)
|
||||
require.NoError(t, err)
|
||||
|
||||
different := newTestServiceAccount().WithImagePullSecret("image-secret").WithSecret("secret")
|
||||
differentUnstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(different.ServiceAccount)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expectedPatch []byte
|
||||
fromBackup *unstructured.Unstructured
|
||||
}{
|
||||
{
|
||||
name: "fromCluster and fromBackup are exactly the same",
|
||||
fromBackup: &unstructured.Unstructured{Object: fromClusterUnstructured},
|
||||
},
|
||||
{
|
||||
name: "fromCluster and fromBackup are different",
|
||||
fromBackup: &unstructured.Unstructured{Object: differentUnstructured},
|
||||
expectedPatch: []byte(`{"imagePullSecrets":[{"name":"image-secret"}],"secrets":[{"name":"secret"}]}`),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
resourceClient := &velerotest.FakeDynamicClient{}
|
||||
defer resourceClient.AssertExpectations(t)
|
||||
name := fromCluster.GetName()
|
||||
|
||||
// restoreResource will add the restore label to object provided to create, so we need to make a copy to provide to our expected call
|
||||
m := make(map[string]interface{})
|
||||
for k, v := range test.fromBackup.Object {
|
||||
m[k] = v
|
||||
}
|
||||
fromBackupWithLabel := &unstructured.Unstructured{Object: m}
|
||||
addRestoreLabels(fromBackupWithLabel, "my-restore", "my-backup")
|
||||
// resetMetadataAndStatus will strip the creationTimestamp before calling Create
|
||||
fromBackupWithLabel.SetCreationTimestamp(metav1.Time{Time: time.Time{}})
|
||||
|
||||
resourceClient.On("Create", fromBackupWithLabel).Return(new(unstructured.Unstructured), k8serrors.NewAlreadyExists(kuberesource.ServiceAccounts, name))
|
||||
resourceClient.On("Get", name, metav1.GetOptions{}).Return(&unstructured.Unstructured{Object: fromClusterUnstructured}, nil)
|
||||
|
||||
if len(test.expectedPatch) > 0 {
|
||||
resourceClient.On("Patch", name, test.expectedPatch).Return(test.fromBackup, nil)
|
||||
}
|
||||
|
||||
dynamicFactory := &velerotest.FakeDynamicFactory{}
|
||||
gv := schema.GroupVersion{Group: "", Version: "v1"}
|
||||
|
||||
resource := metav1.APIResource{Name: "serviceaccounts", Namespaced: true}
|
||||
dynamicFactory.On("ClientForGroupVersionResource", gv, resource, "ns-1").Return(resourceClient, nil)
|
||||
fromBackupJSON, err := json.Marshal(test.fromBackup)
|
||||
require.NoError(t, err)
|
||||
ctx := &context{
|
||||
dynamicFactory: dynamicFactory,
|
||||
actions: []resolvedAction{},
|
||||
fileSystem: velerotest.NewFakeFileSystem().
|
||||
WithFile("foo/resources/serviceaccounts/namespaces/ns-1/sa-1.json", fromBackupJSON),
|
||||
selector: labels.NewSelector(),
|
||||
restore: &api.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: api.DefaultNamespace,
|
||||
Name: "my-restore",
|
||||
},
|
||||
Spec: api.RestoreSpec{
|
||||
IncludeClusterResources: nil,
|
||||
BackupName: "my-backup",
|
||||
},
|
||||
},
|
||||
backup: &api.Backup{},
|
||||
log: velerotest.NewLogger(),
|
||||
applicableActions: make(map[schema.GroupResource][]resolvedAction),
|
||||
resourceClients: make(map[resourceClientKey]pkgclient.Dynamic),
|
||||
restoredItems: make(map[velero.ResourceIdentifier]struct{}),
|
||||
}
|
||||
warnings, errors := ctx.restoreResource("serviceaccounts", "ns-1", "foo/resources/serviceaccounts/namespaces/ns-1/")
|
||||
|
||||
assert.Empty(t, warnings.Velero)
|
||||
assert.Empty(t, warnings.Cluster)
|
||||
assert.Empty(t, warnings.Namespaces)
|
||||
assert.Equal(t, Result{}, errors)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoringPVsWithoutSnapshots(t *testing.T) {
|
||||
pv := `apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
@@ -1372,67 +955,6 @@ func toUnstructured(objs ...runtime.Object) []unstructured.Unstructured {
|
||||
return res
|
||||
}
|
||||
|
||||
type testServiceAccount struct {
|
||||
*v1.ServiceAccount
|
||||
}
|
||||
|
||||
func newTestServiceAccount() *testServiceAccount {
|
||||
return &testServiceAccount{
|
||||
ServiceAccount: &v1.ServiceAccount{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ServiceAccount",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: "test-sa",
|
||||
CreationTimestamp: metav1.Time{Time: time.Now()},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (sa *testServiceAccount) WithImagePullSecret(name string) *testServiceAccount {
|
||||
secret := v1.LocalObjectReference{Name: name}
|
||||
sa.ImagePullSecrets = append(sa.ImagePullSecrets, secret)
|
||||
return sa
|
||||
}
|
||||
|
||||
func (sa *testServiceAccount) WithSecret(name string) *testServiceAccount {
|
||||
secret := v1.ObjectReference{Name: name}
|
||||
sa.Secrets = append(sa.Secrets, secret)
|
||||
return sa
|
||||
}
|
||||
|
||||
func (sa *testServiceAccount) ToJSON() []byte {
|
||||
bytes, _ := json.Marshal(sa.ServiceAccount)
|
||||
return bytes
|
||||
}
|
||||
|
||||
type testPersistentVolume struct {
|
||||
*v1.PersistentVolume
|
||||
}
|
||||
|
||||
func newTestPV() *testPersistentVolume {
|
||||
return &testPersistentVolume{
|
||||
PersistentVolume: &v1.PersistentVolume{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "PersistentVolume",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pv",
|
||||
},
|
||||
Status: v1.PersistentVolumeStatus{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (pv *testPersistentVolume) ToJSON() []byte {
|
||||
bytes, _ := json.Marshal(pv.PersistentVolume)
|
||||
return bytes
|
||||
}
|
||||
|
||||
type testNamespace struct {
|
||||
*v1.Namespace
|
||||
}
|
||||
@@ -1452,60 +974,6 @@ func (ns *testNamespace) ToJSON() []byte {
|
||||
return bytes
|
||||
}
|
||||
|
||||
type testConfigMap struct {
|
||||
*v1.ConfigMap
|
||||
}
|
||||
|
||||
func newTestConfigMap() *testConfigMap {
|
||||
return newNamedTestConfigMap("cm-1")
|
||||
}
|
||||
|
||||
func newNamedTestConfigMap(name string) *testConfigMap {
|
||||
return &testConfigMap{
|
||||
ConfigMap: &v1.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ConfigMap",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "ns-1",
|
||||
Name: name,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *testConfigMap) WithNamespace(name string) *testConfigMap {
|
||||
cm.Namespace = name
|
||||
return cm
|
||||
}
|
||||
|
||||
func (cm *testConfigMap) WithLabels(labels map[string]string) *testConfigMap {
|
||||
cm.Labels = labels
|
||||
return cm
|
||||
}
|
||||
|
||||
func (cm *testConfigMap) WithControllerOwner() *testConfigMap {
|
||||
t := true
|
||||
ownerRef := metav1.OwnerReference{
|
||||
Controller: &t,
|
||||
}
|
||||
cm.ConfigMap.OwnerReferences = append(cm.ConfigMap.OwnerReferences, ownerRef)
|
||||
return cm
|
||||
}
|
||||
|
||||
func (cm *testConfigMap) ToJSON() []byte {
|
||||
bytes, _ := json.Marshal(cm.ConfigMap)
|
||||
return bytes
|
||||
}
|
||||
|
||||
type fakeAction struct {
|
||||
resource string
|
||||
}
|
||||
|
||||
type fakeVolumeSnapshotterGetter struct {
|
||||
fakeVolumeSnapshotter *velerotest.FakeVolumeSnapshotter
|
||||
volumeMap map[velerotest.VolumeBackupInfo]string
|
||||
@@ -1521,42 +989,3 @@ func (r *fakeVolumeSnapshotterGetter) GetVolumeSnapshotter(provider string) (vel
|
||||
}
|
||||
return r.fakeVolumeSnapshotter, nil
|
||||
}
|
||||
|
||||
func newFakeAction(resource string) *fakeAction {
|
||||
return &fakeAction{resource}
|
||||
}
|
||||
|
||||
func (r *fakeAction) AppliesTo() (velero.ResourceSelector, error) {
|
||||
return velero.ResourceSelector{
|
||||
IncludedResources: []string{r.resource},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *fakeAction) Execute(input *velero.RestoreItemActionExecuteInput) (*velero.RestoreItemActionExecuteOutput, error) {
|
||||
labels, found, err := unstructured.NestedMap(input.Item.UnstructuredContent(), "metadata", "labels")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
labels = make(map[string]interface{})
|
||||
}
|
||||
|
||||
labels["fake-restorer"] = "foo"
|
||||
|
||||
if err := unstructured.SetNestedField(input.Item.UnstructuredContent(), labels, "metadata", "labels"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unstructuredObj, ok := input.Item.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return nil, errors.New("Unexpected type")
|
||||
}
|
||||
|
||||
// want the baseline functionality too
|
||||
res, err := resetMetadataAndStatus(unstructuredObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return velero.NewRestoreItemActionExecuteOutput(res), nil
|
||||
}
|
||||
|
||||
@@ -294,3 +294,19 @@ func WithAnnotations(vals ...string) func(obj metav1.Object) {
|
||||
obj.SetAnnotations(objAnnotations)
|
||||
}
|
||||
}
|
||||
|
||||
// WithClusterName is a functional option that applies the specified
|
||||
// cluster name to an object.
|
||||
func WithClusterName(val string) func(obj metav1.Object) {
|
||||
return func(obj metav1.Object) {
|
||||
obj.SetClusterName(val)
|
||||
}
|
||||
}
|
||||
|
||||
// WithFinalizers is a functional option that applies the specified
|
||||
// finalizers to an object.
|
||||
func WithFinalizers(vals ...string) func(obj metav1.Object) {
|
||||
return func(obj metav1.Object) {
|
||||
obj.SetFinalizers(vals)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user