From 718a94ad0565731780c904d27d09f27936f6b7d9 Mon Sep 17 00:00:00 2001 From: Nolan Brubaker Date: Thu, 20 Aug 2020 20:24:29 -0400 Subject: [PATCH] Invoke DeleteItemActions on backup deletion (#2815) * Add serving and listing support Signed-off-by: Nolan Brubaker --- changelogs/unreleased/2815-nrb | 1 + internal/delete/delete_item_action_handler.go | 199 +++++++ .../delete/delete_item_action_handler_test.go | 268 +++++++++ pkg/archive/filesystem.go | 55 ++ pkg/archive/filesystem_test.go | 31 ++ pkg/cmd/server/server.go | 1 + pkg/controller/backup_deletion_controller.go | 38 +- .../backup_deletion_controller_test.go | 11 +- pkg/plugin/clientmgmt/client_builder.go | 3 +- pkg/plugin/clientmgmt/client_builder_test.go | 3 +- .../framework/delete_item_action_server.go | 2 +- pkg/plugin/framework/server.go | 4 +- pkg/restore/restore.go | 68 +-- pkg/restore/restore_test.go | 508 ++++++++---------- pkg/test/tar_writer.go | 105 ++++ pkg/util/collections/includes_excludes.go | 26 +- site/content/docs/main/custom-plugins.md | 1 + 17 files changed, 963 insertions(+), 361 deletions(-) create mode 100644 changelogs/unreleased/2815-nrb create mode 100644 internal/delete/delete_item_action_handler.go create mode 100644 internal/delete/delete_item_action_handler_test.go create mode 100644 pkg/archive/filesystem.go create mode 100644 pkg/archive/filesystem_test.go create mode 100644 pkg/test/tar_writer.go diff --git a/changelogs/unreleased/2815-nrb b/changelogs/unreleased/2815-nrb new file mode 100644 index 000000000..55ac6afdc --- /dev/null +++ b/changelogs/unreleased/2815-nrb @@ -0,0 +1 @@ +Feature: Invoke DeleteItemAction plugins based on backup contents when a backup is deleted. diff --git a/internal/delete/delete_item_action_handler.go b/internal/delete/delete_item_action_handler.go new file mode 100644 index 000000000..bab1254d0 --- /dev/null +++ b/internal/delete/delete_item_action_handler.go @@ -0,0 +1,199 @@ +/* +Copyright 2020 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 delete + +import ( + "io" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/archive" + "github.com/vmware-tanzu/velero/pkg/discovery" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" + "github.com/vmware-tanzu/velero/pkg/util/collections" + "github.com/vmware-tanzu/velero/pkg/util/filesystem" +) + +// Context provides the necessary environment to run DeleteItemAction plugins +type Context struct { + Backup *velerov1api.Backup + BackupReader io.Reader + Actions []velero.DeleteItemAction + Filesystem filesystem.Interface + Log logrus.FieldLogger + DiscoveryHelper discovery.Helper + + resolvedActions []resolvedAction +} + +func InvokeDeleteActions(ctx *Context) error { + var err error + ctx.resolvedActions, err = resolveActions(ctx.Actions, ctx.DiscoveryHelper) + + // No actions installed and no error means we don't have to continue; + // just do the backup deletion without worrying about plugins. + if len(ctx.resolvedActions) == 0 && err == nil { + ctx.Log.Debug("No delete item actions present, proceeding with rest of backup deletion process") + return nil + } else if err != nil { + return errors.Wrapf(err, "error resolving actions") + } + + // get items out of backup tarball into a temp directory + dir, err := archive.NewExtractor(ctx.Log, ctx.Filesystem).UnzipAndExtractBackup(ctx.BackupReader) + if err != nil { + return errors.Wrapf(err, "error extracting backup") + + } + defer ctx.Filesystem.RemoveAll(dir) + ctx.Log.Debugf("Downloaded and extracted the backup file to: %s", dir) + + backupResources, err := archive.NewParser(ctx.Log, ctx.Filesystem).Parse(dir) + processdResources := sets.NewString() + + ctx.Log.Debugf("Trying to reconcile resource names with Kube API server.") + // Transform resource names based on what's canonical in the API server. + for resource := range backupResources { + gvr, _, err := ctx.DiscoveryHelper.ResourceFor(schema.ParseGroupResource(resource).WithVersion("")) + if err != nil { + return errors.Wrapf(err, "failed to resolve resource into complete group/version/resource: %v", resource) + } + + groupResource := gvr.GroupResource() + + // We've already seen this group/resource, so don't process it again. + if processdResources.Has(groupResource.String()) { + continue + } + + // Get a list of all items that exist for this resource + resourceList := backupResources[groupResource.String()] + if resourceList == nil { + // After canonicalization from the API server, the resources may not exist in the tarball + // Skip them if that's the case. + continue + } + + // Iterate over all items, grouped by namespace. + for namespace, items := range resourceList.ItemsByNamespace { + nsLog := ctx.Log.WithField("namespace", namespace) + nsLog.Info("Starting to check for items in namespace") + + // Filter applicable actions based on namespace only once per namespace. + actions := ctx.getApplicableActions(groupResource, namespace) + + // Process individual items from the backup + for _, item := range items { + itemPath := archive.GetItemFilePath(dir, resource, namespace, item) + + // obj is the Unstructured item from the backup + obj, err := archive.Unmarshal(ctx.Filesystem, itemPath) + if err != nil { + return errors.Wrapf(err, "Could not unmarshal item: %v", item) + } + + itemLog := nsLog.WithField("item", obj.GetName()) + itemLog.Infof("invoking DeleteItemAction plugins") + + for _, action := range actions { + if !action.selector.Matches(labels.Set(obj.GetLabels())) { + continue + } + err = action.Execute(&velero.DeleteItemActionExecuteInput{ + Item: obj, + Backup: ctx.Backup, + }) + // Since we want to keep looping even on errors, log them instead of just returning. + if err != nil { + itemLog.WithError(err).Error("plugin error") + + } + } + } + } + } + return nil +} + +// getApplicableActions takes resolved DeleteItemActions and filters them for a given group/resource and namespace. +func (ctx *Context) getApplicableActions(groupResource schema.GroupResource, namespace string) []resolvedAction { + var actions []resolvedAction + + for _, action := range ctx.resolvedActions { + if !action.resourceIncludesExcludes.ShouldInclude(groupResource.String()) { + continue + } + + if namespace != "" && !action.namespaceIncludesExcludes.ShouldInclude(namespace) { + continue + } + + if namespace == "" && !action.namespaceIncludesExcludes.IncludeEverything() { + continue + } + + actions = append(actions, action) + } + + return actions +} + +// resolvedActions are DeleteItemActions decorated with resource/namespace include/exclude collections, as well as label selectors for easy comparison. +type resolvedAction struct { + velero.DeleteItemAction + + resourceIncludesExcludes *collections.IncludesExcludes + namespaceIncludesExcludes *collections.IncludesExcludes + selector labels.Selector +} + +// resolveActions resolves the AppliesTo ResourceSelectors of DeleteItemActions plugins against the Kubernetes discovery API for fully-qualified names. +func resolveActions(actions []velero.DeleteItemAction, helper discovery.Helper) ([]resolvedAction, error) { + var resolved []resolvedAction + + for _, action := range actions { + resourceSelector, err := action.AppliesTo() + if err != nil { + return nil, err + } + + resources := collections.GetResourceIncludesExcludes(helper, resourceSelector.IncludedResources, resourceSelector.ExcludedResources) + namespaces := collections.NewIncludesExcludes().Includes(resourceSelector.IncludedNamespaces...).Excludes(resourceSelector.ExcludedNamespaces...) + + selector := labels.Everything() + if resourceSelector.LabelSelector != "" { + if selector, err = labels.Parse(resourceSelector.LabelSelector); err != nil { + return nil, err + } + } + + res := resolvedAction{ + DeleteItemAction: action, + resourceIncludesExcludes: resources, + namespaceIncludesExcludes: namespaces, + selector: selector, + } + resolved = append(resolved, res) + } + + return resolved, nil +} diff --git a/internal/delete/delete_item_action_handler_test.go b/internal/delete/delete_item_action_handler_test.go new file mode 100644 index 000000000..d7e5d153d --- /dev/null +++ b/internal/delete/delete_item_action_handler_test.go @@ -0,0 +1,268 @@ +/* +Copyright 2020 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 delete + +import ( + "context" + "io" + "sort" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "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" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/pkg/discovery" + "github.com/vmware-tanzu/velero/pkg/plugin/velero" + "github.com/vmware-tanzu/velero/pkg/test" + kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" +) + +func TestInvokeDeleteItemActionsRunForCorrectItems(t *testing.T) { + // Declare test-singleton objects. + fs := test.NewFakeFileSystem() + log := logrus.StandardLogger() + + tests := []struct { + name string + backup *velerov1api.Backup + apiResources []*test.APIResource + tarball io.Reader + actions map[*recordResourcesAction][]string // recordResourceActions are the plugins that will capture item ids, the []string values are the ids we'll test against. + }{ + { + name: "single action with no selector runs for all items", + backup: builder.ForBackup("velero", "velero").Result(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). + 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", + backup: builder.ForBackup("velero", "velero").Result(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). + 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", + backup: builder.ForBackup("velero", "velero").Result(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). + Done(), + apiResources: []*test.APIResource{test.Pods(), test.PVs()}, + actions: map[*recordResourcesAction][]string{ + new(recordResourcesAction).ForResource("persistentvolumes"): {"pv-1", "pv-2"}, + }, + }, + { + name: "single action with a namespace selector runs only for resources in that namespace", + backup: builder.ForBackup("velero", "velero").Result(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). + AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1").Result(), builder.ForPersistentVolumeClaim("ns-2", "pvc-2").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). + Done(), + apiResources: []*test.APIResource{test.Pods(), test.PVCs(), test.PVs()}, + actions: map[*recordResourcesAction][]string{ + new(recordResourcesAction).ForNamespace("ns-1"): {"ns-1/pod-1", "ns-1/pvc-1"}, + }, + }, + { + name: "multiple actions, each with a different resource selector using short name, run for matching resources", + backup: builder.ForBackup("velero", "velero").Result(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). + AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1").Result(), builder.ForPersistentVolumeClaim("ns-2", "pvc-2").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). + 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", + backup: builder.ForBackup("velero", "velero").Result(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-2", "pvc-2").Result()). + 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, + }, + }, + { + name: "single action with label selector runs only for those items", + backup: builder.ForBackup("velero", "velero").Result(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").ObjectMeta(builder.WithLabels("app", "app1")).Result(), builder.ForPod("ns-2", "pod-2").Result()). + AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1").Result(), builder.ForPersistentVolumeClaim("ns-2", "pvc-2").ObjectMeta(builder.WithLabels("app", "app1")).Result()). + Done(), + apiResources: []*test.APIResource{test.Pods(), test.PVCs()}, + actions: map[*recordResourcesAction][]string{ + new(recordResourcesAction).ForLabelSelector("app=app1"): {"ns-1/pod-1", "ns-2/pvc-2"}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // test harness contains the fake API server/discovery client + h := newHarness(t) + for _, r := range tc.apiResources { + h.addResource(t, r) + } + + // Get the plugins out of the map in order to use them. + actions := []velero.DeleteItemAction{} + for action := range tc.actions { + actions = append(actions, action) + } + + c := &Context{ + Backup: tc.backup, + BackupReader: tc.tarball, + Filesystem: fs, + DiscoveryHelper: h.discoveryHelper, + Actions: actions, + Log: log, + } + + err := InvokeDeleteActions(c) + require.NoError(t, err) + + // Compare the plugins against the ids that we wanted. + for action, want := range tc.actions { + sort.Strings(want) + sort.Strings(action.ids) + assert.Equal(t, want, action.ids) + } + }) + } +} + +// TODO: unify this with the test harness in pkg/restore/restore_test.go +type harness struct { + *test.APIServer + discoveryHelper discovery.Helper +} + +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, + discoveryHelper: discoveryHelper, + } +} + +// addResource adds an APIResource and it's items to a faked API server for testing. +func (h *harness) addResource(t *testing.T, resource *test.APIResource) { + t.Helper() + + h.DiscoveryClient.WithAPIResource(resource) + require.NoError(t, h.discoveryHelper.Refresh()) + + for _, item := range resource.Items { + obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(item) + require.NoError(t, err) + + unstructuredObj := &unstructured.Unstructured{Object: obj} + if resource.Namespaced { + _, err = h.DynamicClient.Resource(resource.GVR()).Namespace(item.GetNamespace()).Create(context.TODO(), unstructuredObj, metav1.CreateOptions{}) + } else { + _, err = h.DynamicClient.Resource(resource.GVR()).Create(context.TODO(), unstructuredObj, metav1.CreateOptions{}) + } + require.NoError(t, err) + } +} + +// recordResourcesAction is a delete item action that can be configured to run +// for specific resources/namespaces and simply record the items that is is +// executed for. +type recordResourcesAction struct { + selector velero.ResourceSelector + ids []string +} + +func (a *recordResourcesAction) AppliesTo() (velero.ResourceSelector, error) { + return a.selector, nil +} + +func (a *recordResourcesAction) Execute(input *velero.DeleteItemActionExecuteInput) error { + metadata, err := meta.Accessor(input.Item) + if err != nil { + return err + } + + a.ids = append(a.ids, kubeutil.NamespaceAndName(metadata)) + + return 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 TestInvokeDeleteItemActionsWithNoPlugins(t *testing.T) { + c := &Context{ + Backup: builder.ForBackup("velero", "velero").Result(), + Log: logrus.StandardLogger(), + // No other fields are set on the assumption that if 0 actions are present, + // the backup tarball and file system being empty will produce no errors. + } + err := InvokeDeleteActions(c) + require.NoError(t, err) +} diff --git a/pkg/archive/filesystem.go b/pkg/archive/filesystem.go new file mode 100644 index 000000000..32eed2bd2 --- /dev/null +++ b/pkg/archive/filesystem.go @@ -0,0 +1,55 @@ +/* +Copyright 2020 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 archive + +import ( + "encoding/json" + "path/filepath" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + "github.com/vmware-tanzu/velero/pkg/util/filesystem" +) + +// GetItemFilePath returns an item's file path once extracted from a Velero backup archive. +func GetItemFilePath(rootDir, groupResource, namespace, name string) string { + switch namespace { + case "": + return filepath.Join(rootDir, velerov1api.ResourcesDir, groupResource, velerov1api.ClusterScopedDir, name+".json") + default: + return filepath.Join(rootDir, velerov1api.ResourcesDir, groupResource, velerov1api.NamespaceScopedDir, namespace, name+".json") + } +} + +// Unmarshal reads the specified file, unmarshals the JSON contained within it +// and returns an Unstructured object. +func Unmarshal(fs filesystem.Interface, filePath string) (*unstructured.Unstructured, error) { + var obj unstructured.Unstructured + + bytes, err := fs.ReadFile(filePath) + if err != nil { + return nil, err + } + + err = json.Unmarshal(bytes, &obj) + if err != nil { + return nil, err + } + + return &obj, nil +} diff --git a/pkg/archive/filesystem_test.go b/pkg/archive/filesystem_test.go new file mode 100644 index 000000000..bb07af928 --- /dev/null +++ b/pkg/archive/filesystem_test.go @@ -0,0 +1,31 @@ +/* +Copyright 2020 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 archive + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetItemFilePath(t *testing.T) { + res := GetItemFilePath("root", "resource", "", "item") + assert.Equal(t, "root/resources/resource/cluster/item.json", res) + + res = GetItemFilePath("root", "resource", "namespace", "item") + assert.Equal(t, "root/resources/resource/namespaces/namespace/item.json", res) +} diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index 5fad93003..34d71b242 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -699,6 +699,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string s.csiSnapshotClient, newPluginManager, s.metrics, + s.discoveryHelper, ) return controllerRunInfo{ diff --git a/pkg/controller/backup_deletion_controller.go b/pkg/controller/backup_deletion_controller.go index 2138b1746..9b5c2ecad 100644 --- a/pkg/controller/backup_deletion_controller.go +++ b/pkg/controller/backup_deletion_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2018 the Velero contributors. +Copyright 2020 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. @@ -36,8 +36,10 @@ import ( kubeerrs "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/tools/cache" + "github.com/vmware-tanzu/velero/internal/delete" velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" pkgbackup "github.com/vmware-tanzu/velero/pkg/backup" + "github.com/vmware-tanzu/velero/pkg/discovery" "github.com/vmware-tanzu/velero/pkg/features" velerov1client "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/typed/velero/v1" velerov1informers "github.com/vmware-tanzu/velero/pkg/generated/informers/externalversions/velero/v1" @@ -48,6 +50,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt" "github.com/vmware-tanzu/velero/pkg/plugin/velero" "github.com/vmware-tanzu/velero/pkg/restic" + "github.com/vmware-tanzu/velero/pkg/util/filesystem" "github.com/vmware-tanzu/velero/pkg/util/kube" "sigs.k8s.io/controller-runtime/pkg/client" @@ -76,6 +79,7 @@ type backupDeletionController struct { newPluginManager func(logrus.FieldLogger) clientmgmt.Manager newBackupStore func(*velerov1api.BackupStorageLocation, persistence.ObjectStoreGetter, logrus.FieldLogger) (persistence.BackupStore, error) metrics *metrics.ServerMetrics + helper discovery.Helper } // NewBackupDeletionController creates a new backup deletion controller. @@ -96,6 +100,7 @@ func NewBackupDeletionController( csiSnapshotClient *snapshotterClientSet.Clientset, newPluginManager func(logrus.FieldLogger) clientmgmt.Manager, metrics *metrics.ServerMetrics, + helper discovery.Helper, ) Interface { c := &backupDeletionController{ genericController: newGenericController("backup-deletion", logger), @@ -113,6 +118,7 @@ func NewBackupDeletionController( csiSnapshotContentLister: csiSnapshotContentLister, csiSnapshotClient: csiSnapshotClient, metrics: metrics, + helper: helper, // use variables to refer to these functions so they can be // replaced with fakes for testing. newPluginManager: newPluginManager, @@ -289,6 +295,36 @@ func (c *backupDeletionController) processRequest(req *velerov1api.DeleteBackupR errs = append(errs, err.Error()) } + // Download the tarball + backupFile, err := downloadToTempFile(backup.Name, backupStore, log) + if err != nil { + return errors.Wrap(err, "error downloading backup") + } + defer closeAndRemoveFile(backupFile, c.logger) + + actions, err := pluginManager.GetDeleteItemActions() + log.Debugf("%d actions before invoking actions", len(actions)) + if err != nil { + return errors.Wrap(err, "error getting delete item actions") + } + // don't defer CleanupClients here, since it was already called above. + + ctx := &delete.Context{ + Backup: backup, + BackupReader: backupFile, + Actions: actions, + Log: c.logger, + DiscoveryHelper: c.helper, + Filesystem: filesystem.NewFileSystem(), + } + + // Optimization: wrap in a gofunc? Would be useful for large backups with lots of objects. + // but what do we do with the error returned? We can't just swallow it as that may lead to dangling resources. + err = delete.InvokeDeleteActions(ctx) + if err != nil { + return errors.Wrap(err, "error invoking delete item actions") + } + if backupStore != nil { log.Info("Removing PV snapshots") diff --git a/pkg/controller/backup_deletion_controller_test.go b/pkg/controller/backup_deletion_controller_test.go index 02d8c81db..212cc6e28 100644 --- a/pkg/controller/backup_deletion_controller_test.go +++ b/pkg/controller/backup_deletion_controller_test.go @@ -1,5 +1,5 @@ /* -Copyright 2018, 2019 the Velero contributors. +Copyright 2020 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. @@ -17,8 +17,10 @@ limitations under the License. package controller import ( + "bytes" "context" "fmt" + "io/ioutil" "testing" "time" @@ -72,6 +74,7 @@ func TestBackupDeletionControllerProcessQueueItem(t *testing.T) { nil, // csiSnapshotClient nil, // new plugin manager func metrics.NewServerMetrics(), + nil, // discovery helper ).(*backupDeletionController) // Error splitting key @@ -168,6 +171,7 @@ func setupBackupDeletionControllerTest(t *testing.T, objects ...runtime.Object) nil, // csiSnapshotClient func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager }, metrics.NewServerMetrics(), + nil, // discovery helper ).(*backupDeletionController), req: req, @@ -500,10 +504,12 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { pluginManager := &pluginmocks.Manager{} pluginManager.On("GetVolumeSnapshotter", "provider-1").Return(td.volumeSnapshotter, nil) + pluginManager.On("GetDeleteItemActions").Return(nil, nil) pluginManager.On("CleanupClients") td.controller.newPluginManager = func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager } td.backupStore.On("GetBackupVolumeSnapshots", td.req.Spec.BackupName).Return(snapshots, nil) + td.backupStore.On("GetBackupContents", td.req.Spec.BackupName).Return(ioutil.NopCloser(bytes.NewReader([]byte("hello world"))), nil) td.backupStore.On("DeleteBackup", td.req.Spec.BackupName).Return(nil) td.backupStore.On("DeleteRestore", "restore-1").Return(nil) td.backupStore.On("DeleteRestore", "restore-2").Return(nil) @@ -659,10 +665,12 @@ func TestBackupDeletionControllerProcessRequest(t *testing.T) { pluginManager := &pluginmocks.Manager{} pluginManager.On("GetVolumeSnapshotter", "provider-1").Return(td.volumeSnapshotter, nil) + pluginManager.On("GetDeleteItemActions").Return(nil, nil) pluginManager.On("CleanupClients") td.controller.newPluginManager = func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager } td.backupStore.On("GetBackupVolumeSnapshots", td.req.Spec.BackupName).Return(snapshots, nil) + td.backupStore.On("GetBackupContents", td.req.Spec.BackupName).Return(ioutil.NopCloser(bytes.NewReader([]byte("hello world"))), nil) td.backupStore.On("DeleteBackup", td.req.Spec.BackupName).Return(nil) td.backupStore.On("DeleteRestore", "restore-1").Return(nil) td.backupStore.On("DeleteRestore", "restore-2").Return(nil) @@ -865,6 +873,7 @@ func TestBackupDeletionControllerDeleteExpiredRequests(t *testing.T) { nil, // csiSnapshotClient nil, // new plugin manager func metrics.NewServerMetrics(), + nil, // discovery helper, ).(*backupDeletionController) fakeClock := &clock.FakeClock{} diff --git a/pkg/plugin/clientmgmt/client_builder.go b/pkg/plugin/clientmgmt/client_builder.go index 95b7ce629..60d278727 100644 --- a/pkg/plugin/clientmgmt/client_builder.go +++ b/pkg/plugin/clientmgmt/client_builder.go @@ -1,5 +1,5 @@ /* -Copyright 2018, 2019 the Velero contributors. +Copyright 2020 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. @@ -72,6 +72,7 @@ func (b *clientBuilder) clientConfig() *hcplugin.ClientConfig { string(framework.PluginKindObjectStore): framework.NewObjectStorePlugin(framework.ClientLogger(b.clientLogger)), string(framework.PluginKindPluginLister): &framework.PluginListerPlugin{}, string(framework.PluginKindRestoreItemAction): framework.NewRestoreItemActionPlugin(framework.ClientLogger(b.clientLogger)), + string(framework.PluginKindDeleteItemAction): framework.NewDeleteItemActionPlugin(framework.ClientLogger(b.clientLogger)), }, Logger: b.pluginLogger, Cmd: exec.Command(b.commandName, b.commandArgs...), diff --git a/pkg/plugin/clientmgmt/client_builder_test.go b/pkg/plugin/clientmgmt/client_builder_test.go index 510b9e53c..e7fff7bef 100644 --- a/pkg/plugin/clientmgmt/client_builder_test.go +++ b/pkg/plugin/clientmgmt/client_builder_test.go @@ -1,5 +1,5 @@ /* -Copyright 2018, 2019 the Velero contributors. +Copyright 2020 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. @@ -65,6 +65,7 @@ func TestClientConfig(t *testing.T) { string(framework.PluginKindObjectStore): framework.NewObjectStorePlugin(framework.ClientLogger(logger)), string(framework.PluginKindPluginLister): &framework.PluginListerPlugin{}, string(framework.PluginKindRestoreItemAction): framework.NewRestoreItemActionPlugin(framework.ClientLogger(logger)), + string(framework.PluginKindDeleteItemAction): framework.NewDeleteItemActionPlugin(framework.ClientLogger(logger)), }, Logger: cb.pluginLogger, Cmd: exec.Command(cb.commandName, cb.commandArgs...), diff --git a/pkg/plugin/framework/delete_item_action_server.go b/pkg/plugin/framework/delete_item_action_server.go index 4bdaa06ae..3c6be3b54 100644 --- a/pkg/plugin/framework/delete_item_action_server.go +++ b/pkg/plugin/framework/delete_item_action_server.go @@ -108,5 +108,5 @@ func (s *DeleteItemActionGRPCServer) Execute(ctx context.Context, req *proto.Del return nil, newGRPCError(err) } - return nil, nil + return &proto.Empty{}, nil } diff --git a/pkg/plugin/framework/server.go b/pkg/plugin/framework/server.go index 20893f921..83f740a53 100644 --- a/pkg/plugin/framework/server.go +++ b/pkg/plugin/framework/server.go @@ -1,5 +1,5 @@ /* -Copyright 2017, 2019 the Velero contributors. +Copyright 2020 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. @@ -205,6 +205,7 @@ func (s *server) Serve() { pluginIdentifiers = append(pluginIdentifiers, getNames(command, PluginKindVolumeSnapshotter, s.volumeSnapshotter)...) pluginIdentifiers = append(pluginIdentifiers, getNames(command, PluginKindObjectStore, s.objectStore)...) pluginIdentifiers = append(pluginIdentifiers, getNames(command, PluginKindRestoreItemAction, s.restoreItemAction)...) + pluginIdentifiers = append(pluginIdentifiers, getNames(command, PluginKindDeleteItemAction, s.deleteItemAction)...) pluginLister := NewPluginLister(pluginIdentifiers...) @@ -216,6 +217,7 @@ func (s *server) Serve() { string(PluginKindObjectStore): s.objectStore, string(PluginKindPluginLister): NewPluginListerPlugin(pluginLister), string(PluginKindRestoreItemAction): s.restoreItemAction, + string(PluginKindDeleteItemAction): s.deleteItemAction, }, GRPCServer: plugin.DefaultGRPCServer, }) diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 1a675ec70..13a1185ab 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -1,5 +1,5 @@ /* -Copyright 2017, 2019, 2020 the Velero contributors. +Copyright 2020 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. @@ -22,7 +22,6 @@ import ( "fmt" "io" "io/ioutil" - "path/filepath" "sort" "strings" "sync" @@ -154,7 +153,7 @@ func (kr *kubernetesRestorer) Restore( } // get resource includes-excludes - resourceIncludesExcludes := getResourceIncludesExcludes(kr.discoveryHelper, req.Restore.Spec.IncludedResources, req.Restore.Spec.ExcludedResources) + resourceIncludesExcludes := collections.GetResourceIncludesExcludes(kr.discoveryHelper, req.Restore.Spec.IncludedResources, req.Restore.Spec.ExcludedResources) // get namespace includes-excludes namespaceIncludesExcludes := collections.NewIncludesExcludes(). @@ -228,27 +227,6 @@ func (kr *kubernetesRestorer) Restore( return restoreCtx.execute() } -// getResourceIncludesExcludes takes the lists of resources to include and exclude, uses the -// discovery helper to resolve them to fully-qualified group-resource names, and returns an -// IncludesExcludes list. -func getResourceIncludesExcludes(helper discovery.Helper, includes, excludes []string) *collections.IncludesExcludes { - resources := collections.GenerateIncludesExcludes( - includes, - excludes, - func(item string) string { - gvr, _, err := helper.ResourceFor(schema.ParseGroupResource(item).WithVersion("")) - if err != nil { - return "" - } - - gr := gvr.GroupResource() - return gr.String() - }, - ) - - return resources -} - type resolvedAction struct { velero.RestoreItemAction @@ -266,7 +244,7 @@ func resolveActions(actions []velero.RestoreItemAction, helper discovery.Helper) return nil, err } - resources := getResourceIncludesExcludes(helper, resourceSelector.IncludedResources, resourceSelector.ExcludedResources) + resources := collections.GetResourceIncludesExcludes(helper, resourceSelector.IncludedResources, resourceSelector.ExcludedResources) namespaces := collections.NewIncludesExcludes().Includes(resourceSelector.IncludedNamespaces...).Excludes(resourceSelector.ExcludedNamespaces...) selector := labels.Everything() @@ -435,7 +413,7 @@ func (ctx *restoreContext) execute() (Result, Result) { // create a blank one. if namespace != "" && !existingNamespaces.Has(targetNamespace) { logger := ctx.log.WithField("namespace", namespace) - ns := getNamespace(logger, getItemFilePath(ctx.restoreDir, "namespaces", "", namespace), targetNamespace) + ns := getNamespace(logger, archive.GetItemFilePath(ctx.restoreDir, "namespaces", "", namespace), targetNamespace) if _, err := kube.EnsureNamespaceExistsAndIsReady(ns, ctx.namespaceClient, ctx.resourceTerminatingTimeout); err != nil { errs.AddVeleroError(err) continue @@ -490,15 +468,6 @@ func (ctx *restoreContext) execute() (Result, Result) { return warnings, errs } -func getItemFilePath(rootDir, groupResource, namespace, name string) string { - switch namespace { - case "": - return filepath.Join(rootDir, velerov1api.ResourcesDir, groupResource, velerov1api.ClusterScopedDir, name+".json") - default: - return filepath.Join(rootDir, velerov1api.ResourcesDir, groupResource, velerov1api.NamespaceScopedDir, namespace, name+".json") - } -} - // getNamespace returns a namespace API object that we should attempt to // create before restoring anything into it. It will come from the backup // tarball if it exists, else will be a new one. If from the tarball, it @@ -535,6 +504,7 @@ func getNamespace(logger logrus.FieldLogger, path, remappedName string) *v1.Name } } +// TODO: this should be combined with DeleteItemActions at some point. func (ctx *restoreContext) getApplicableActions(groupResource schema.GroupResource, namespace string) []resolvedAction { var actions []resolvedAction for _, action := range ctx.actions { @@ -713,9 +683,9 @@ func (ctx *restoreContext) restoreResource(resource, targetNamespace, originalNa groupResource := schema.ParseGroupResource(resource) for _, item := range items { - itemPath := getItemFilePath(ctx.restoreDir, resource, originalNamespace, item) + itemPath := archive.GetItemFilePath(ctx.restoreDir, resource, originalNamespace, item) - obj, err := ctx.unmarshal(itemPath) + obj, err := archive.Unmarshal(ctx.fileSystem, itemPath) if err != nil { errs.Add(targetNamespace, fmt.Errorf("error decoding %q: %v", strings.Replace(itemPath, ctx.restoreDir+"/", "", -1), err)) continue @@ -804,7 +774,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso // if the namespace scoped resource should be restored, ensure that the namespace into // which the resource is being restored into exists. // This is the *remapped* namespace that we are ensuring exists. - nsToEnsure := getNamespace(ctx.log, getItemFilePath(ctx.restoreDir, "namespaces", "", obj.GetNamespace()), namespace) + nsToEnsure := getNamespace(ctx.log, archive.GetItemFilePath(ctx.restoreDir, "namespaces", "", obj.GetNamespace()), namespace) if _, err := kube.EnsureNamespaceExistsAndIsReady(nsToEnsure, ctx.namespaceClient, ctx.resourceTerminatingTimeout); err != nil { errs.AddVeleroError(err) return warnings, errs @@ -988,7 +958,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso obj = unstructuredObj for _, additionalItem := range executeOutput.AdditionalItems { - itemPath := getItemFilePath(ctx.restoreDir, additionalItem.GroupResource.String(), additionalItem.Namespace, additionalItem.Name) + itemPath := archive.GetItemFilePath(ctx.restoreDir, additionalItem.GroupResource.String(), additionalItem.Namespace, additionalItem.Name) if _, err := ctx.fileSystem.Stat(itemPath); err != nil { ctx.log.WithError(err).WithFields(logrus.Fields{ @@ -1002,7 +972,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso } additionalResourceID := getResourceID(additionalItem.GroupResource, additionalItem.Namespace, additionalItem.Name) - additionalObj, err := ctx.unmarshal(itemPath) + additionalObj, err := archive.Unmarshal(ctx.fileSystem, itemPath) if err != nil { errs.Add(namespace, errors.Wrapf(err, "error restoring additional item %s", additionalResourceID)) } @@ -1335,21 +1305,3 @@ func isCompleted(obj *unstructured.Unstructured, groupResource schema.GroupResou // Assume any other resource isn't complete and can be restored return false, nil } - -// unmarshal reads the specified file, unmarshals the JSON contained within it -// and returns an Unstructured object. -func (ctx *restoreContext) unmarshal(filePath string) (*unstructured.Unstructured, error) { - var obj unstructured.Unstructured - - bytes, err := ctx.fileSystem.ReadFile(filePath) - if err != nil { - return nil, err - } - - err = json.Unmarshal(bytes, &obj) - if err != nil { - return nil, err - } - - return &obj, nil -} diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index 8fd3112d0..0c53cdbe4 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -17,9 +17,6 @@ limitations under the License. package restore import ( - "archive/tar" - "bytes" - "compress/gzip" "context" "encoding/json" "fmt" @@ -54,7 +51,6 @@ import ( resticmocks "github.com/vmware-tanzu/velero/pkg/restic/mocks" "github.com/vmware-tanzu/velero/pkg/test" testutil "github.com/vmware-tanzu/velero/pkg/test" - "github.com/vmware-tanzu/velero/pkg/util/encode" kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube" "github.com/vmware-tanzu/velero/pkg/volume" ) @@ -78,16 +74,16 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "no filters restores everything", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.PVs(), @@ -101,16 +97,16 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "included resources filter only restores resources of those types", restore: defaultRestore().IncludedResources("pods").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.PVs(), @@ -123,16 +119,16 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "excluded resources filter only restores resources not of those types", restore: defaultRestore().ExcludedResources("pvs").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.PVs(), @@ -145,20 +141,20 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "included namespaces filter only restores resources in those namespaces", restore: defaultRestore().IncludedNamespaces("ns-1").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("deployments.apps", + AddItems("deployments.apps", builder.ForDeployment("ns-1", "deploy-1").Result(), builder.ForDeployment("ns-2", "deploy-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.Deployments(), @@ -173,20 +169,20 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "excluded namespaces filter only restores resources not in those namespaces", restore: defaultRestore().ExcludedNamespaces("ns-2").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("deployments.apps", + AddItems("deployments.apps", builder.ForDeployment("ns-1", "deploy-1").Result(), builder.ForDeployment("ns-2", "deploy-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.Deployments(), @@ -201,20 +197,20 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "IncludeClusterResources=false only restores namespaced resources", restore: defaultRestore().IncludeClusterResources(false).Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("deployments.apps", + AddItems("deployments.apps", builder.ForDeployment("ns-1", "deploy-1").Result(), builder.ForDeployment("ns-2", "deploy-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.Deployments(), @@ -229,20 +225,20 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "label selector only restores matching resources", restore: defaultRestore().LabelSelector(&metav1.LabelSelector{MatchLabels: map[string]string{"a": "b"}}).Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").ObjectMeta(builder.WithLabels("a", "b")).Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("deployments.apps", + AddItems("deployments.apps", builder.ForDeployment("ns-1", "deploy-1").Result(), builder.ForDeployment("ns-2", "deploy-2").ObjectMeta(builder.WithLabels("a", "b")).Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").ObjectMeta(builder.WithLabels("a", "b")).Result(), builder.ForPersistentVolume("pv-2").ObjectMeta(builder.WithLabels("a", "c")).Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.Deployments(), @@ -258,20 +254,20 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "should include cluster-scoped resources if restoring subset of namespaces and IncludeClusterResources=true", restore: defaultRestore().IncludedNamespaces("ns-1").IncludeClusterResources(true).Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("deployments.apps", + AddItems("deployments.apps", builder.ForDeployment("ns-1", "deploy-1").Result(), builder.ForDeployment("ns-2", "deploy-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.Deployments(), @@ -287,20 +283,20 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "should not include cluster-scoped resources if restoring subset of namespaces and IncludeClusterResources=false", restore: defaultRestore().IncludedNamespaces("ns-1").IncludeClusterResources(false).Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("deployments.apps", + AddItems("deployments.apps", builder.ForDeployment("ns-1", "deploy-1").Result(), builder.ForDeployment("ns-2", "deploy-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.Deployments(), @@ -316,20 +312,20 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "should not include cluster-scoped resources if restoring subset of namespaces and IncludeClusterResources=nil", restore: defaultRestore().IncludedNamespaces("ns-1").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("deployments.apps", + AddItems("deployments.apps", builder.ForDeployment("ns-1", "deploy-1").Result(), builder.ForDeployment("ns-2", "deploy-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.Deployments(), @@ -345,20 +341,20 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "should include cluster-scoped resources if restoring all namespaces and IncludeClusterResources=true", restore: defaultRestore().IncludeClusterResources(true).Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("deployments.apps", + AddItems("deployments.apps", builder.ForDeployment("ns-1", "deploy-1").Result(), builder.ForDeployment("ns-2", "deploy-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.Deployments(), @@ -374,20 +370,20 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "should not include cluster-scoped resources if restoring all namespaces and IncludeClusterResources=false", restore: defaultRestore().IncludeClusterResources(false).Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("deployments.apps", + AddItems("deployments.apps", builder.ForDeployment("ns-1", "deploy-1").Result(), builder.ForDeployment("ns-2", "deploy-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.Deployments(), @@ -402,20 +398,20 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "when a wildcard and a specific resource are included, the wildcard takes precedence", restore: defaultRestore().IncludedResources("*", "pods").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("deployments.apps", + AddItems("deployments.apps", builder.ForDeployment("ns-1", "deploy-1").Result(), builder.ForDeployment("ns-2", "deploy-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.Deployments(), @@ -431,20 +427,20 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "wildcard excludes are ignored", restore: defaultRestore().ExcludedResources("*").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("deployments.apps", + AddItems("deployments.apps", builder.ForDeployment("ns-1", "deploy-1").Result(), builder.ForDeployment("ns-2", "deploy-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.Deployments(), @@ -460,20 +456,20 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "unresolvable included resources are ignored", restore: defaultRestore().IncludedResources("pods", "unresolvable").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("deployments.apps", + AddItems("deployments.apps", builder.ForDeployment("ns-1", "deploy-1").Result(), builder.ForDeployment("ns-2", "deploy-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.Deployments(), @@ -487,20 +483,20 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "unresolvable excluded resources are ignored", restore: defaultRestore().ExcludedResources("deployments", "unresolvable").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("deployments.apps", + AddItems("deployments.apps", builder.ForDeployment("ns-1", "deploy-1").Result(), builder.ForDeployment("ns-2", "deploy-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.Deployments(), @@ -515,7 +511,7 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "mirror pods are not restored", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t).addItems("pods", builder.ForPod("ns-1", "pod-1").ObjectMeta(builder.WithAnnotations(corev1api.MirrorPodAnnotationKey, "foo")).Result()).done(), + tarball: test.NewTarWriter(t).AddItems("pods", builder.ForPod("ns-1", "pod-1").ObjectMeta(builder.WithAnnotations(corev1api.MirrorPodAnnotationKey, "foo")).Result()).Done(), apiResources: []*test.APIResource{test.Pods()}, want: map[*test.APIResource][]string{test.Pods(): {}}, }, @@ -523,7 +519,7 @@ func TestRestoreResourceFiltering(t *testing.T) { name: "service accounts are restored", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t).addItems("serviceaccounts", builder.ForServiceAccount("ns-1", "sa-1").Result()).done(), + tarball: test.NewTarWriter(t).AddItems("serviceaccounts", builder.ForServiceAccount("ns-1", "sa-1").Result()).Done(), apiResources: []*test.APIResource{test.ServiceAccounts()}, want: map[*test.APIResource][]string{test.ServiceAccounts(): {"ns-1/sa-1"}}, }, @@ -579,13 +575,13 @@ func TestRestoreNamespaceMapping(t *testing.T) { apiResources: []*test.APIResource{ test.Pods(), }, - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), builder.ForPod("ns-3", "pod-3").Result(), ). - done(), + Done(), want: map[*test.APIResource][]string{ test.Pods(): {"mapped-ns-1/pod-1", "mapped-ns-2/pod-2", "ns-3/pod-3"}, }, @@ -597,13 +593,13 @@ func TestRestoreNamespaceMapping(t *testing.T) { apiResources: []*test.APIResource{ test.Pods(), }, - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), builder.ForPod("ns-3", "pod-3").Result(), ). - done(), + Done(), want: map[*test.APIResource][]string{ test.Pods(): {"mapped-ns-1/pod-1", "mapped-ns-2/pod-2"}, }, @@ -657,28 +653,28 @@ func TestRestoreResourcePriorities(t *testing.T) { name: "resources are restored according to the specified resource priorities", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result(), ). - addItems("persistentvolumes", + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result(), ). - addItems("deployments.apps", + AddItems("deployments.apps", builder.ForDeployment("ns-1", "deploy-1").Result(), builder.ForDeployment("ns-2", "deploy-2").Result(), ). - addItems("serviceaccounts", + AddItems("serviceaccounts", builder.ForServiceAccount("ns-1", "sa-1").Result(), builder.ForServiceAccount("ns-2", "sa-2").Result(), ). - addItems("persistentvolumeclaims", + AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1").Result(), builder.ForPersistentVolumeClaim("ns-2", "pvc-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), test.PVs(), @@ -739,8 +735,8 @@ func TestInvalidTarballContents(t *testing.T) { name: "empty tarball returns an error", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - done(), + tarball: test.NewTarWriter(t). + Done(), wantErrs: Result{ Velero: []string{"error parsing backup contents: directory \"resources\" does not exist"}, }, @@ -749,12 +745,12 @@ func TestInvalidTarballContents(t *testing.T) { name: "invalid JSON is reported as an error and restore continues", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - add("resources/pods/namespaces/ns-1/pod-1.json", []byte("invalid JSON")). - addItems("pods", + tarball: test.NewTarWriter(t). + Add("resources/pods/namespaces/ns-1/pod-1.json", []byte("invalid JSON")). + AddItems("pods", builder.ForPod("ns-1", "pod-2").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), }, @@ -815,8 +811,8 @@ func TestRestoreItems(t *testing.T) { name: "metadata other than namespace/name/labels/annotations gets removed", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1"). ObjectMeta( builder.WithLabels("key-1", "val-1"), @@ -826,7 +822,7 @@ func TestRestoreItems(t *testing.T) { ). Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), }, @@ -845,8 +841,8 @@ func TestRestoreItems(t *testing.T) { name: "status gets removed", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", + tarball: test.NewTarWriter(t). + AddItems("pods", &corev1api.Pod{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", @@ -861,7 +857,7 @@ func TestRestoreItems(t *testing.T) { }, }, ). - done(), + Done(), apiResources: []*test.APIResource{ test.Pods(), }, @@ -875,9 +871,9 @@ func TestRestoreItems(t *testing.T) { name: "object gets labeled with full backup and restore names when they're both shorter than 63 characters", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", builder.ForPod("ns-1", "pod-1").Result()). - done(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + Done(), apiResources: []*test.APIResource{ test.Pods(), }, @@ -891,9 +887,9 @@ func TestRestoreItems(t *testing.T) { Backup("the-really-long-kube-service-name-that-is-exactly-63-characters"). Result(), backup: builder.ForBackup(velerov1api.DefaultNamespace, "the-really-long-kube-service-name-that-is-exactly-63-characters").Result(), - tarball: newTarWriter(t). - addItems("pods", builder.ForPod("ns-1", "pod-1").Result()). - done(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + Done(), apiResources: []*test.APIResource{ test.Pods(), }, @@ -915,9 +911,9 @@ func TestRestoreItems(t *testing.T) { Backup("the-really-long-kube-service-name-that-is-much-greater-than-63-characters"). Result(), backup: builder.ForBackup(velerov1api.DefaultNamespace, "the-really-long-kube-service-name-that-is-much-greater-than-63-characters").Result(), - tarball: newTarWriter(t). - addItems("pods", builder.ForPod("ns-1", "pod-1").Result()). - done(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + Done(), apiResources: []*test.APIResource{ test.Pods(), }, @@ -937,9 +933,9 @@ func TestRestoreItems(t *testing.T) { name: "no error when service account already exists in cluster and is identical to the backed up one", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("serviceaccounts", builder.ForServiceAccount("ns-1", "sa-1").Result()). - done(), + tarball: test.NewTarWriter(t). + AddItems("serviceaccounts", builder.ForServiceAccount("ns-1", "sa-1").Result()). + Done(), apiResources: []*test.APIResource{ test.ServiceAccounts(builder.ForServiceAccount("ns-1", "sa-1").Result()), }, @@ -951,8 +947,8 @@ func TestRestoreItems(t *testing.T) { name: "service account secrets and image pull secrets are restored when service account already exists in cluster", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("serviceaccounts", &corev1api.ServiceAccount{ + tarball: test.NewTarWriter(t). + AddItems("serviceaccounts", &corev1api.ServiceAccount{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "ServiceAccount", @@ -964,7 +960,7 @@ func TestRestoreItems(t *testing.T) { Secrets: []corev1api.ObjectReference{{Name: "secret-1"}}, ImagePullSecrets: []corev1api.LocalObjectReference{{Name: "pull-secret-1"}}, }). - done(), + Done(), apiResources: []*test.APIResource{ test.ServiceAccounts(builder.ForServiceAccount("ns-1", "sa-1").Result()), }, @@ -990,7 +986,7 @@ func TestRestoreItems(t *testing.T) { h := newHarness(t) for _, r := range tc.apiResources { - h.addItems(t, r) + h.AddItems(t, r) } data := Request{ @@ -1080,10 +1076,10 @@ func TestRestoreActionsRunForCorrectItems(t *testing.T) { name: "single action with no selector runs for all items", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). - addItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). - done(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). + 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"}, @@ -1093,10 +1089,10 @@ func TestRestoreActionsRunForCorrectItems(t *testing.T) { name: "single action with a resource selector for namespaced resources runs only for matching resources", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). - addItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). - done(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). + Done(), apiResources: []*test.APIResource{test.Pods(), test.PVs()}, actions: map[*recordResourcesAction][]string{ new(recordResourcesAction).ForResource("pods"): {"ns-1/pod-1", "ns-2/pod-2"}, @@ -1106,10 +1102,10 @@ func TestRestoreActionsRunForCorrectItems(t *testing.T) { name: "single action with a resource selector for cluster-scoped resources runs only for matching resources", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). - addItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). - done(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). + Done(), apiResources: []*test.APIResource{test.Pods(), test.PVs()}, actions: map[*recordResourcesAction][]string{ new(recordResourcesAction).ForResource("persistentvolumes"): {"pv-1", "pv-2"}, @@ -1119,11 +1115,11 @@ func TestRestoreActionsRunForCorrectItems(t *testing.T) { name: "single action with a namespace selector runs only for resources in that namespace", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). - addItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1").Result(), builder.ForPersistentVolumeClaim("ns-2", "pvc-2").Result()). - addItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). - done(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). + AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1").Result(), builder.ForPersistentVolumeClaim("ns-2", "pvc-2").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). + Done(), apiResources: []*test.APIResource{test.Pods(), test.PVCs(), test.PVs()}, actions: map[*recordResourcesAction][]string{ new(recordResourcesAction).ForNamespace("ns-1"): {"ns-1/pod-1", "ns-1/pvc-1"}, @@ -1133,11 +1129,11 @@ func TestRestoreActionsRunForCorrectItems(t *testing.T) { name: "single action with a resource and namespace selector runs only for matching resources in that namespace", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). - addItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1").Result(), builder.ForPersistentVolumeClaim("ns-2", "pvc-2").Result()). - addItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). - done(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). + AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1").Result(), builder.ForPersistentVolumeClaim("ns-2", "pvc-2").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). + Done(), apiResources: []*test.APIResource{test.Pods(), test.PVCs(), test.PVs()}, actions: map[*recordResourcesAction][]string{ new(recordResourcesAction).ForNamespace("ns-1").ForResource("pods"): {"ns-1/pod-1"}, @@ -1147,11 +1143,11 @@ func TestRestoreActionsRunForCorrectItems(t *testing.T) { name: "multiple actions, each with a different resource selector using short name, run for matching resources", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). - addItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1").Result(), builder.ForPersistentVolumeClaim("ns-2", "pvc-2").Result()). - addItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). - done(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()). + AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1").Result(), builder.ForPersistentVolumeClaim("ns-2", "pvc-2").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result(), builder.ForPersistentVolume("pv-2").Result()). + 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"}, @@ -1162,10 +1158,10 @@ func TestRestoreActionsRunForCorrectItems(t *testing.T) { name: "actions with selectors that don't match anything don't run for any resources", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", builder.ForPod("ns-1", "pod-1").Result()). - addItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-2", "pvc-2").Result()). - done(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-2", "pvc-2").Result()). + Done(), apiResources: []*test.APIResource{test.Pods(), test.PVCs(), test.PVs()}, actions: map[*recordResourcesAction][]string{ new(recordResourcesAction).ForNamespace("ns-1").ForResource("persistentvolumeclaims"): nil, @@ -1179,7 +1175,7 @@ func TestRestoreActionsRunForCorrectItems(t *testing.T) { h := newHarness(t) for _, r := range tc.apiResources { - h.addItems(t, r) + h.AddItems(t, r) } actions := []velero.RestoreItemAction{} @@ -1271,7 +1267,7 @@ func TestRestoreActionModifications(t *testing.T) { name: "action that adds a label to item gets restored", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t).addItems("pods", builder.ForPod("ns-1", "pod-1").Result()).done(), + tarball: test.NewTarWriter(t).AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()).Done(), apiResources: []*test.APIResource{test.Pods()}, actions: []velero.RestoreItemAction{ modifyingActionGetter(func(item *unstructured.Unstructured) { @@ -1288,7 +1284,7 @@ func TestRestoreActionModifications(t *testing.T) { name: "action that removes a label to item gets restored", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t).addItems("pods", builder.ForPod("ns-1", "pod-1").ObjectMeta(builder.WithLabels("should-be-removed", "true")).Result()).done(), + tarball: test.NewTarWriter(t).AddItems("pods", builder.ForPod("ns-1", "pod-1").ObjectMeta(builder.WithLabels("should-be-removed", "true")).Result()).Done(), apiResources: []*test.APIResource{test.Pods()}, actions: []velero.RestoreItemAction{ modifyingActionGetter(func(item *unstructured.Unstructured) { @@ -1307,7 +1303,7 @@ func TestRestoreActionModifications(t *testing.T) { h := newHarness(t) for _, r := range tc.apiResources { - h.addItems(t, r) + h.AddItems(t, r) } // every restored item should have the restore and backup name labels, set @@ -1364,7 +1360,7 @@ func TestRestoreActionAdditionalItems(t *testing.T) { name: "additional items that are already being restored are not restored twice", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t).addItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()).done(), + tarball: test.NewTarWriter(t).AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()).Done(), apiResources: []*test.APIResource{test.Pods()}, actions: []velero.RestoreItemAction{ &pluggableAction{ @@ -1387,7 +1383,7 @@ func TestRestoreActionAdditionalItems(t *testing.T) { name: "when using a restore namespace filter, additional items that are in a non-included namespace are not restored", restore: defaultRestore().IncludedNamespaces("ns-1").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t).addItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()).done(), + tarball: test.NewTarWriter(t).AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()).Done(), apiResources: []*test.APIResource{test.Pods()}, actions: []velero.RestoreItemAction{ &pluggableAction{ @@ -1409,10 +1405,10 @@ func TestRestoreActionAdditionalItems(t *testing.T) { name: "when using a restore namespace filter, additional items that are cluster-scoped are restored when IncludeClusterResources=nil", restore: defaultRestore().IncludedNamespaces("ns-1").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", builder.ForPod("ns-1", "pod-1").Result()). - addItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()). - done(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()). + Done(), apiResources: []*test.APIResource{test.Pods(), test.PVs()}, actions: []velero.RestoreItemAction{ &pluggableAction{ @@ -1435,10 +1431,10 @@ func TestRestoreActionAdditionalItems(t *testing.T) { name: "additional items that are cluster-scoped are not restored when IncludeClusterResources=false", restore: defaultRestore().IncludeClusterResources(false).Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", builder.ForPod("ns-1", "pod-1").Result()). - addItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()). - done(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()). + Done(), apiResources: []*test.APIResource{test.Pods(), test.PVs()}, actions: []velero.RestoreItemAction{ &pluggableAction{ @@ -1461,10 +1457,10 @@ func TestRestoreActionAdditionalItems(t *testing.T) { name: "when using a restore resource filter, additional items that are non-included resources are not restored", restore: defaultRestore().IncludedResources("pods").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("pods", builder.ForPod("ns-1", "pod-1").Result()). - addItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()). - done(), + tarball: test.NewTarWriter(t). + AddItems("pods", builder.ForPod("ns-1", "pod-1").Result()). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").Result()). + Done(), apiResources: []*test.APIResource{test.Pods(), test.PVs()}, actions: []velero.RestoreItemAction{ &pluggableAction{ @@ -1490,7 +1486,7 @@ func TestRestoreActionAdditionalItems(t *testing.T) { h := newHarness(t) for _, r := range tc.apiResources { - h.addItems(t, r) + h.AddItems(t, r) } data := Request{ @@ -1643,7 +1639,7 @@ func TestShouldRestore(t *testing.T) { } for _, resource := range tc.apiResources { - h.addItems(t, resource) + h.AddItems(t, resource) } for _, ns := range tc.namespaces { @@ -1804,11 +1800,11 @@ func TestRestorePersistentVolumes(t *testing.T) { name: "when a PV with a reclaim policy of delete has no snapshot and does not exist in-cluster, it does not get restored, and its PVC gets reset for dynamic provisioning", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("persistentvolumes", + tarball: test.NewTarWriter(t). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").ReclaimPolicy(corev1api.PersistentVolumeReclaimDelete).ClaimRef("ns-1", "pvc-1").Result(), ). - addItems("persistentvolumeclaims", + AddItems("persistentvolumeclaims", builder.ForPersistentVolumeClaim("ns-1", "pvc-1"). VolumeName("pv-1"). ObjectMeta( @@ -1816,7 +1812,7 @@ func TestRestorePersistentVolumes(t *testing.T) { ). Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.PVs(), test.PVCs(), @@ -1837,11 +1833,11 @@ func TestRestorePersistentVolumes(t *testing.T) { name: "when a PV with a reclaim policy of retain has no snapshot and does not exist in-cluster, it gets restored, without its claim ref", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("persistentvolumes", + tarball: test.NewTarWriter(t). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").ReclaimPolicy(corev1api.PersistentVolumeReclaimRetain).ClaimRef("ns-1", "pvc-1").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.PVs(), test.PVCs(), @@ -1861,11 +1857,11 @@ func TestRestorePersistentVolumes(t *testing.T) { name: "when a PV with a reclaim policy of delete has a snapshot and does not exist in-cluster, the snapshot and PV are restored", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("persistentvolumes", + tarball: test.NewTarWriter(t). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1").ReclaimPolicy(corev1api.PersistentVolumeReclaimDelete).AWSEBSVolumeID("old-volume").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.PVs(), test.PVCs(), @@ -1907,14 +1903,14 @@ func TestRestorePersistentVolumes(t *testing.T) { name: "when a PV with a reclaim policy of retain has a snapshot and does not exist in-cluster, the snapshot and PV are restored", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("persistentvolumes", + tarball: test.NewTarWriter(t). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1"). ReclaimPolicy(corev1api.PersistentVolumeReclaimRetain). AWSEBSVolumeID("old-volume"). Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.PVs(), test.PVCs(), @@ -1956,14 +1952,14 @@ func TestRestorePersistentVolumes(t *testing.T) { name: "when a PV with a reclaim policy of delete has a snapshot and exists in-cluster, neither the snapshot nor the PV are restored", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("persistentvolumes", + tarball: test.NewTarWriter(t). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1"). ReclaimPolicy(corev1api.PersistentVolumeReclaimDelete). AWSEBSVolumeID("old-volume"). Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.PVs( builder.ForPersistentVolume("pv-1"). @@ -2008,14 +2004,14 @@ func TestRestorePersistentVolumes(t *testing.T) { name: "when a PV with a reclaim policy of retain has a snapshot and exists in-cluster, neither the snapshot nor the PV are restored", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("persistentvolumes", + tarball: test.NewTarWriter(t). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1"). ReclaimPolicy(corev1api.PersistentVolumeReclaimRetain). AWSEBSVolumeID("old-volume"). Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.PVs( builder.ForPersistentVolume("pv-1"). @@ -2060,16 +2056,16 @@ func TestRestorePersistentVolumes(t *testing.T) { name: "when a PV with a snapshot is used by a PVC in a namespace that's being remapped, and the original PV exists in-cluster, the PV is renamed", restore: defaultRestore().NamespaceMappings("source-ns", "target-ns").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems( + tarball: test.NewTarWriter(t). + AddItems( "persistentvolumes", builder.ForPersistentVolume("source-pv").AWSEBSVolumeID("source-volume").ClaimRef("source-ns", "pvc-1").Result(), ). - addItems( + AddItems( "persistentvolumeclaims", builder.ForPersistentVolumeClaim("source-ns", "pvc-1").VolumeName("source-pv").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.PVs( builder.ForPersistentVolume("source-pv").AWSEBSVolumeID("source-volume").ClaimRef("source-ns", "pvc-1").Result(), @@ -2124,16 +2120,16 @@ func TestRestorePersistentVolumes(t *testing.T) { name: "when a PV with a snapshot is used by a PVC in a namespace that's being remapped, and the original PV does not exist in-cluster, the PV is not renamed", restore: defaultRestore().NamespaceMappings("source-ns", "target-ns").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems( + tarball: test.NewTarWriter(t). + AddItems( "persistentvolumes", builder.ForPersistentVolume("source-pv").AWSEBSVolumeID("source-volume").ClaimRef("source-ns", "pvc-1").Result(), ). - addItems( + AddItems( "persistentvolumeclaims", builder.ForPersistentVolumeClaim("source-ns", "pvc-1").VolumeName("source-pv").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.PVs(), test.PVCs(), @@ -2182,14 +2178,14 @@ func TestRestorePersistentVolumes(t *testing.T) { name: "when a PV with a reclaim policy of retain has a snapshot and exists in-cluster, neither the snapshot nor the PV are restored", restore: defaultRestore().Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems("persistentvolumes", + tarball: test.NewTarWriter(t). + AddItems("persistentvolumes", builder.ForPersistentVolume("pv-1"). ReclaimPolicy(corev1api.PersistentVolumeReclaimRetain). AWSEBSVolumeID("old-volume"). Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.PVs( builder.ForPersistentVolume("pv-1"). @@ -2243,16 +2239,16 @@ func TestRestorePersistentVolumes(t *testing.T) { name: "when a PV with a snapshot is used by a PVC in a namespace that's being remapped, and the original PV exists in-cluster, the PV is renamed by volumesnapshotter", restore: defaultRestore().NamespaceMappings("source-ns", "target-ns").Result(), backup: defaultBackup().Result(), - tarball: newTarWriter(t). - addItems( + tarball: test.NewTarWriter(t). + AddItems( "persistentvolumes", builder.ForPersistentVolume("source-pv").AWSEBSVolumeID("source-volume").ClaimRef("source-ns", "pvc-1").Result(), ). - addItems( + AddItems( "persistentvolumeclaims", builder.ForPersistentVolumeClaim("source-ns", "pvc-1").VolumeName("source-pv").Result(), ). - done(), + Done(), apiResources: []*test.APIResource{ test.PVs( builder.ForPersistentVolume("source-pv").AWSEBSVolumeID("source-volume").ClaimRef("source-ns", "pvc-1").Result(), @@ -2322,7 +2318,7 @@ func TestRestorePersistentVolumes(t *testing.T) { } for _, r := range tc.apiResources { - h.addItems(t, r) + h.AddItems(t, r) } // Collect the IDs of all of the wanted resources so we can ensure the @@ -2434,19 +2430,19 @@ func TestRestoreWithRestic(t *testing.T) { // needed only to indicate resource types that can be restored, in this case, pods for _, resource := range tc.apiResources { - h.addItems(t, resource) + h.AddItems(t, resource) } - tarball := newTarWriter(t) + tarball := test.NewTarWriter(t) // these backed up pods don't have any PVBs associated with them, so a call to RestorePodVolumes is not expected to be made for them for _, pod := range tc.podWithoutPVBs { - tarball.addItems("pods", pod) + tarball.AddItems("pods", pod) } // these backed up pods have PVBs associated with them, so a call to RestorePodVolumes will be made for each of them for _, pod := range tc.podWithPVBs { - tarball.addItems("pods", pod) + tarball.AddItems("pods", pod) // the restore process adds these labels before restoring, so we must add them here too otherwise they won't match pod.Labels = map[string]string{"velero.io/backup-name": tc.backup.Name, "velero.io/restore-name": tc.restore.Name} @@ -2467,7 +2463,7 @@ func TestRestoreWithRestic(t *testing.T) { Restore: tc.restore, Backup: tc.backup, PodVolumeBackups: tc.podVolumeBackups, - BackupReader: tarball.done(), + BackupReader: tarball.Done(), } warnings, errs := h.restorer.Restore( @@ -2589,14 +2585,6 @@ func TestIsCompleted(t *testing.T) { } } -func TestGetItemFilePath(t *testing.T) { - res := getItemFilePath("root", "resource", "", "item") - assert.Equal(t, "root/resources/resource/cluster/item.json", res) - - res = getItemFilePath("root", "resource", "namespace", "item") - assert.Equal(t, "root/resources/resource/namespaces/namespace/item.json", res) -} - func Test_getOrderedResources(t *testing.T) { tests := []struct { name string @@ -2739,78 +2727,6 @@ func assertEmptyResults(t *testing.T, res ...Result) { } } -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) addItems(groupResource string, items ...metav1.Object) *tarWriter { - tw.t.Helper() - - for _, obj := range items { - - var path string - if obj.GetNamespace() == "" { - path = fmt.Sprintf("resources/%s/cluster/%s.json", groupResource, obj.GetName()) - } else { - path = fmt.Sprintf("resources/%s/namespaces/%s/%s.json", groupResource, obj.GetNamespace(), obj.GetName()) - } - - tw.add(path, obj) - } - - 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 @@ -2845,7 +2761,7 @@ func newHarness(t *testing.T) *harness { } } -func (h *harness) addItems(t *testing.T, resource *test.APIResource) { +func (h *harness) AddItems(t *testing.T, resource *test.APIResource) { t.Helper() h.DiscoveryClient.WithAPIResource(resource) diff --git a/pkg/test/tar_writer.go b/pkg/test/tar_writer.go new file mode 100644 index 000000000..94cf6c814 --- /dev/null +++ b/pkg/test/tar_writer.go @@ -0,0 +1,105 @@ +/* +Copyright 2020 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 test + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/vmware-tanzu/velero/pkg/util/encode" +) + +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) AddItems(groupResource string, items ...metav1.Object) *TarWriter { + tw.t.Helper() + + for _, obj := range items { + + var path string + if obj.GetNamespace() == "" { + path = fmt.Sprintf("resources/%s/cluster/%s.json", groupResource, obj.GetName()) + } else { + path = fmt.Sprintf("resources/%s/namespaces/%s/%s.json", groupResource, obj.GetNamespace(), obj.GetName()) + } + + tw.Add(path, obj) + } + + 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 +} diff --git a/pkg/util/collections/includes_excludes.go b/pkg/util/collections/includes_excludes.go index 2fc9028eb..02f84d21f 100644 --- a/pkg/util/collections/includes_excludes.go +++ b/pkg/util/collections/includes_excludes.go @@ -1,5 +1,5 @@ /* -Copyright 2017, 2020 the Velero contributors. +Copyright 2020 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. @@ -21,7 +21,10 @@ import ( "github.com/gobwas/glob" "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" + + "github.com/vmware-tanzu/velero/pkg/discovery" ) type globStringSet struct { @@ -187,3 +190,24 @@ func GenerateIncludesExcludes(includes, excludes []string, mapFunc func(string) return res } + +// GetResourceIncludesExcludes takes the lists of resources to include and exclude, uses the +// discovery helper to resolve them to fully-qualified group-resource names, and returns an +// IncludesExcludes list. +func GetResourceIncludesExcludes(helper discovery.Helper, includes, excludes []string) *IncludesExcludes { + resources := GenerateIncludesExcludes( + includes, + excludes, + func(item string) string { + gvr, _, err := helper.ResourceFor(schema.ParseGroupResource(item).WithVersion("")) + if err != nil { + return "" + } + + gr := gvr.GroupResource() + return gr.String() + }, + ) + + return resources +} diff --git a/site/content/docs/main/custom-plugins.md b/site/content/docs/main/custom-plugins.md index 3059006d3..1d17f2ffe 100644 --- a/site/content/docs/main/custom-plugins.md +++ b/site/content/docs/main/custom-plugins.md @@ -35,6 +35,7 @@ Velero currently supports the following kinds of plugins: - **Volume Snapshotter** - creates volume snapshots (during backup) and restores volumes from snapshots (during restore) - **Backup Item Action** - executes arbitrary logic for individual items prior to storing them in a backup file - **Restore Item Action** - executes arbitrary logic for individual items prior to restoring them into a cluster +- **Delete Item Action** - executes arbitrary logic based on individual items within a backup prior to deleting the backup ## Plugin Logging