Invoke DeleteItemActions on backup deletion (#2815)
* Add serving and listing support Signed-off-by: Nolan Brubaker <brubakern@vmware.com>pull/2848/head
parent
71fd7cc5a7
commit
718a94ad05
|
@ -0,0 +1 @@
|
|||
Feature: Invoke DeleteItemAction plugins based on backup contents when a backup is deleted.
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -699,6 +699,7 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
|
|||
s.csiSnapshotClient,
|
||||
newPluginManager,
|
||||
s.metrics,
|
||||
s.discoveryHelper,
|
||||
)
|
||||
|
||||
return controllerRunInfo{
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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...),
|
||||
|
|
|
@ -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...),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue