diff --git a/pkg/restore/builder.go b/pkg/restore/builder.go new file mode 100644 index 000000000..2daf1afd6 --- /dev/null +++ b/pkg/restore/builder.go @@ -0,0 +1,78 @@ +/* +Copyright 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 restore + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + velerov1api "github.com/heptio/velero/pkg/apis/velero/v1" +) + +// Builder is a helper for concisely constructing Restore API objects. +type Builder struct { + restore velerov1api.Restore +} + +// NewBuilder returns a Builder for a Restore with no namespace/name. +func NewBuilder() *Builder { + return NewNamedBuilder("", "") +} + +// NewNamedBuilder returns a Builder for a Restore with the specified namespace +// and name. +func NewNamedBuilder(namespace, name string) *Builder { + return &Builder{ + restore: velerov1api.Restore{ + TypeMeta: metav1.TypeMeta{ + APIVersion: velerov1api.SchemeGroupVersion.String(), + Kind: "Restore", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + }, + } +} + +// Restore returns the built Restore API object. +func (b *Builder) Restore() *velerov1api.Restore { + return &b.restore +} + +// Backup sets the Restore's backup name. +func (b *Builder) Backup(name string) *Builder { + b.restore.Spec.BackupName = name + return b +} + +// NamespaceMappings sets the Restore's namespace mappings. +func (b *Builder) NamespaceMappings(mapping ...string) *Builder { + if b.restore.Spec.NamespaceMapping == nil { + b.restore.Spec.NamespaceMapping = make(map[string]string) + } + + if len(mapping)%2 != 0 { + panic("mapping must contain an even number of values") + } + + for i := 0; i < len(mapping); i += 2 { + b.restore.Spec.NamespaceMapping[mapping[i]] = mapping[i+1] + } + + return b +} diff --git a/pkg/restore/restore_new_test.go b/pkg/restore/restore_new_test.go new file mode 100644 index 000000000..8197671a7 --- /dev/null +++ b/pkg/restore/restore_new_test.go @@ -0,0 +1,232 @@ +/* +Copyright 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 restore + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + + 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/test" + "github.com/heptio/velero/pkg/util/encode" + testutil "github.com/heptio/velero/pkg/util/test" +) + +func TestRestoreNew(t *testing.T) { + tests := []struct { + name string + restore *velerov1api.Restore + backup *velerov1api.Backup + apiResources []*test.APIResource + tarball io.Reader + want map[*test.APIResource][]string + }{ + { + name: "base case - restore a single resource", + restore: defaultRestore().Backup("backup-1").Restore(), + backup: backup.NewNamedBuilder(velerov1api.DefaultNamespace, "backup-1").Backup(), + tarball: newTarWriter(t). + add("metadata/version", []byte("1")). + add("resources/pods/namespaces/ns-1/pod-1.json", test.NewPod("ns-1", "pod-1")). + done(), + apiResources: []*test.APIResource{ + test.Pods(), + }, + want: map[*test.APIResource][]string{ + test.Pods(): {"ns-1/pod-1"}, + }, + }, + { + name: "restore a resource to a remapped namespace", + restore: defaultRestore().Backup("backup-1").NamespaceMappings("ns-1", "ns-2").Restore(), + backup: backup.NewNamedBuilder(velerov1api.DefaultNamespace, "backup-1").Backup(), + tarball: newTarWriter(t). + add("metadata/version", []byte("1")). + add("resources/pods/namespaces/ns-1/pod-1.json", test.NewPod("ns-1", "pod-1")). + done(), + apiResources: []*test.APIResource{ + test.Pods(), + }, + want: map[*test.APIResource][]string{ + test.Pods(): {"ns-2/pod-1"}, + }, + }, + } + + 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, errs) + assertAPIContents(t, h, tc.want) + }) + } +} + +func defaultRestore() *Builder { + return NewNamedBuilder(velerov1api.DefaultNamespace, "restore-1") +} + +// assertAPIContents asserts that the dynamic client on the provided harness contains +// all of the items specified in 'want' (a map from an APIResource definition to a slice +// of resource identifiers, formatted as /). +func assertAPIContents(t *testing.T, h *harness, want map[*test.APIResource][]string) { + for r, want := range want { + res, err := h.DynamicClient.Resource(r.GVR()).List(metav1.ListOptions{}) + assert.NoError(t, err) + if err != nil { + continue + } + + got := sets.NewString() + for _, item := range res.Items { + got.Insert(fmt.Sprintf("%s/%s", item.GetNamespace(), item.GetName())) + } + + assert.Equal(t, sets.NewString(want...), got) + } +} + +func assertEmptyResults(t *testing.T, res ...Result) { + t.Helper() + + for _, r := range res { + assert.Empty(t, r.Cluster) + assert.Empty(t, r.Namespaces) + assert.Empty(t, r.Velero) + } +} + +type tarWriter struct { + t *testing.T + buf *bytes.Buffer + gzw *gzip.Writer + tw *tar.Writer +} + +func newTarWriter(t *testing.T) *tarWriter { + tw := new(tarWriter) + tw.t = t + tw.buf = new(bytes.Buffer) + tw.gzw = gzip.NewWriter(tw.buf) + tw.tw = tar.NewWriter(tw.gzw) + + return tw +} + +func (tw *tarWriter) add(name string, obj interface{}) *tarWriter { + tw.t.Helper() + + var data []byte + var err error + + switch obj.(type) { + case runtime.Object: + data, err = encode.Encode(obj.(runtime.Object), "json") + case []byte: + data = obj.([]byte) + default: + data, err = json.Marshal(obj) + } + require.NoError(tw.t, err) + + require.NoError(tw.t, tw.tw.WriteHeader(&tar.Header{ + Name: name, + Size: int64(len(data)), + Typeflag: tar.TypeReg, + Mode: 0755, + ModTime: time.Now(), + })) + + _, err = tw.tw.Write(data) + require.NoError(tw.t, err) + + return tw +} + +func (tw *tarWriter) done() *bytes.Buffer { + require.NoError(tw.t, tw.tw.Close()) + require.NoError(tw.t, tw.gzw.Close()) + + return tw.buf +} + +type harness struct { + *test.APIServer + + restorer *kubernetesRestorer + log logrus.FieldLogger +} + +func newHarness(t *testing.T) *harness { + t.Helper() + + apiServer := test.NewAPIServer(t) + log := logrus.StandardLogger() + + discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, log) + require.NoError(t, err) + + return &harness{ + APIServer: apiServer, + restorer: &kubernetesRestorer{ + discoveryHelper: discoveryHelper, + dynamicFactory: client.NewDynamicFactory(apiServer.DynamicClient), + namespaceClient: apiServer.KubeClient.CoreV1().Namespaces(), + resourceTerminatingTimeout: time.Minute, + logger: log, + fileSystem: testutil.NewFakeFileSystem(), + + // unsupported + resticRestorerFactory: nil, + resticTimeout: 0, + }, + log: log, + } +} diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index c7ff98346..4e7034b64 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -36,7 +36,6 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/kubernetes/scheme" - corev1 "k8s.io/client-go/kubernetes/typed/core/v1" api "github.com/heptio/velero/pkg/apis/velero/v1" pkgclient "github.com/heptio/velero/pkg/client" @@ -1962,14 +1961,3 @@ func (r *fakeAction) Execute(input *velero.RestoreItemActionExecuteInput) (*vele return velero.NewRestoreItemActionExecuteOutput(res), nil } - -type fakeNamespaceClient struct { - createdNamespaces []*v1.Namespace - - corev1.NamespaceInterface -} - -func (nsc *fakeNamespaceClient) Create(ns *v1.Namespace) (*v1.Namespace, error) { - nsc.createdNamespaces = append(nsc.createdNamespaces, ns) - return ns, nil -}