mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-19 14:34:17 +00:00
Create hook package (#2734)
* Move pkg/backup/item_hook_handler to internal/hoo Signed-off-by: Nolan Brubaker <brubakern@vmware.com> * Add internal packages to test target Signed-off-by: Nolan Brubaker <brubakern@vmware.com>
This commit is contained in:
+15
-14
@@ -38,6 +38,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
kubeerrs "k8s.io/apimachinery/pkg/util/errors"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/hook"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/discovery"
|
||||
@@ -179,13 +180,13 @@ func getNamespaceIncludesExcludes(backup *velerov1api.Backup) *collections.Inclu
|
||||
return collections.NewIncludesExcludes().Includes(backup.Spec.IncludedNamespaces...).Excludes(backup.Spec.ExcludedNamespaces...)
|
||||
}
|
||||
|
||||
func getResourceHooks(hookSpecs []velerov1api.BackupResourceHookSpec, discoveryHelper discovery.Helper) ([]resourceHook, error) {
|
||||
resourceHooks := make([]resourceHook, 0, len(hookSpecs))
|
||||
func getResourceHooks(hookSpecs []velerov1api.BackupResourceHookSpec, discoveryHelper discovery.Helper) ([]hook.ResourceHook, error) {
|
||||
resourceHooks := make([]hook.ResourceHook, 0, len(hookSpecs))
|
||||
|
||||
for _, s := range hookSpecs {
|
||||
h, err := getResourceHook(s, discoveryHelper)
|
||||
if err != nil {
|
||||
return []resourceHook{}, err
|
||||
return []hook.ResourceHook{}, err
|
||||
}
|
||||
|
||||
resourceHooks = append(resourceHooks, h)
|
||||
@@ -194,21 +195,21 @@ func getResourceHooks(hookSpecs []velerov1api.BackupResourceHookSpec, discoveryH
|
||||
return resourceHooks, nil
|
||||
}
|
||||
|
||||
func getResourceHook(hookSpec velerov1api.BackupResourceHookSpec, discoveryHelper discovery.Helper) (resourceHook, error) {
|
||||
h := resourceHook{
|
||||
name: hookSpec.Name,
|
||||
namespaces: collections.NewIncludesExcludes().Includes(hookSpec.IncludedNamespaces...).Excludes(hookSpec.ExcludedNamespaces...),
|
||||
resources: getResourceIncludesExcludes(discoveryHelper, hookSpec.IncludedResources, hookSpec.ExcludedResources),
|
||||
pre: hookSpec.PreHooks,
|
||||
post: hookSpec.PostHooks,
|
||||
func getResourceHook(hookSpec velerov1api.BackupResourceHookSpec, discoveryHelper discovery.Helper) (hook.ResourceHook, error) {
|
||||
h := hook.ResourceHook{
|
||||
Name: hookSpec.Name,
|
||||
Namespaces: collections.NewIncludesExcludes().Includes(hookSpec.IncludedNamespaces...).Excludes(hookSpec.ExcludedNamespaces...),
|
||||
Resources: getResourceIncludesExcludes(discoveryHelper, hookSpec.IncludedResources, hookSpec.ExcludedResources),
|
||||
Pre: hookSpec.PreHooks,
|
||||
Post: hookSpec.PostHooks,
|
||||
}
|
||||
|
||||
if hookSpec.LabelSelector != nil {
|
||||
labelSelector, err := metav1.LabelSelectorAsSelector(hookSpec.LabelSelector)
|
||||
if err != nil {
|
||||
return resourceHook{}, errors.WithStack(err)
|
||||
return hook.ResourceHook{}, errors.WithStack(err)
|
||||
}
|
||||
h.labelSelector = labelSelector
|
||||
h.LabelSelector = labelSelector
|
||||
}
|
||||
|
||||
return h, nil
|
||||
@@ -312,8 +313,8 @@ func (kb *kubernetesBackupper) Backup(log logrus.FieldLogger, backupRequest *Req
|
||||
resticBackupper: resticBackupper,
|
||||
resticSnapshotTracker: newPVCSnapshotTracker(),
|
||||
volumeSnapshotterGetter: volumeSnapshotterGetter,
|
||||
itemHookHandler: &defaultItemHookHandler{
|
||||
podCommandExecutor: kb.podCommandExecutor,
|
||||
itemHookHandler: &hook.DefaultItemHookHandler{
|
||||
PodCommandExecutor: kb.podCommandExecutor,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
kubeerrs "k8s.io/apimachinery/pkg/util/errors"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/hook"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
"github.com/vmware-tanzu/velero/pkg/discovery"
|
||||
@@ -54,7 +55,7 @@ type itemBackupper struct {
|
||||
resticSnapshotTracker *pvcSnapshotTracker
|
||||
volumeSnapshotterGetter VolumeSnapshotterGetter
|
||||
|
||||
itemHookHandler itemHookHandler
|
||||
itemHookHandler hook.ItemHookHandler
|
||||
snapshotLocationVolumeSnapshotters map[string]velero.VolumeSnapshotter
|
||||
}
|
||||
|
||||
@@ -120,7 +121,7 @@ func (ib *itemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstr
|
||||
log.Info("Backing up item")
|
||||
|
||||
log.Debug("Executing pre hooks")
|
||||
if err := ib.itemHookHandler.handleHooks(log, groupResource, obj, ib.backupRequest.ResourceHooks, hookPhasePre); err != nil {
|
||||
if err := ib.itemHookHandler.HandleHooks(log, groupResource, obj, ib.backupRequest.ResourceHooks, hook.PhasePre); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -172,7 +173,7 @@ func (ib *itemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstr
|
||||
|
||||
// if there was an error running actions, execute post hooks and return
|
||||
log.Debug("Executing post hooks")
|
||||
if err := ib.itemHookHandler.handleHooks(log, groupResource, obj, ib.backupRequest.ResourceHooks, hookPhasePost); err != nil {
|
||||
if err := ib.itemHookHandler.HandleHooks(log, groupResource, obj, ib.backupRequest.ResourceHooks, hook.PhasePost); err != nil {
|
||||
backupErrs = append(backupErrs, err)
|
||||
}
|
||||
|
||||
@@ -202,7 +203,7 @@ func (ib *itemBackupper) backupItem(logger logrus.FieldLogger, obj runtime.Unstr
|
||||
}
|
||||
|
||||
log.Debug("Executing post hooks")
|
||||
if err := ib.itemHookHandler.handleHooks(log, groupResource, obj, ib.backupRequest.ResourceHooks, hookPhasePost); err != nil {
|
||||
if err := ib.itemHookHandler.HandleHooks(log, groupResource, obj, ib.backupRequest.ResourceHooks, hook.PhasePost); err != nil {
|
||||
backupErrs = append(backupErrs, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 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 backup
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/kuberesource"
|
||||
"github.com/vmware-tanzu/velero/pkg/podexec"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/collections"
|
||||
)
|
||||
|
||||
type hookPhase string
|
||||
|
||||
const (
|
||||
hookPhasePre hookPhase = "pre"
|
||||
hookPhasePost hookPhase = "post"
|
||||
)
|
||||
|
||||
// itemHookHandler invokes hooks for an item.
|
||||
type itemHookHandler interface {
|
||||
// handleHooks invokes hooks for an item. If the item is a pod and the appropriate annotations exist
|
||||
// to specify a hook, that is executed. Otherwise, this looks at the backup context's Backup to
|
||||
// determine if there are any hooks relevant to the item, taking into account the hook spec's
|
||||
// namespaces, resources, and label selector.
|
||||
handleHooks(
|
||||
log logrus.FieldLogger,
|
||||
groupResource schema.GroupResource,
|
||||
obj runtime.Unstructured,
|
||||
resourceHooks []resourceHook,
|
||||
phase hookPhase,
|
||||
) error
|
||||
}
|
||||
|
||||
// defaultItemHookHandler is the default itemHookHandler.
|
||||
type defaultItemHookHandler struct {
|
||||
podCommandExecutor podexec.PodCommandExecutor
|
||||
}
|
||||
|
||||
func (h *defaultItemHookHandler) handleHooks(
|
||||
log logrus.FieldLogger,
|
||||
groupResource schema.GroupResource,
|
||||
obj runtime.Unstructured,
|
||||
resourceHooks []resourceHook,
|
||||
phase hookPhase,
|
||||
) error {
|
||||
// We only support hooks on pods right now
|
||||
if groupResource != kuberesource.Pods {
|
||||
return nil
|
||||
}
|
||||
|
||||
metadata, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to get a metadata accessor")
|
||||
}
|
||||
|
||||
namespace := metadata.GetNamespace()
|
||||
name := metadata.GetName()
|
||||
|
||||
// If the pod has the hook specified via annotations, that takes priority.
|
||||
hookFromAnnotations := getPodExecHookFromAnnotations(metadata.GetAnnotations(), phase, log)
|
||||
if phase == hookPhasePre && hookFromAnnotations == nil {
|
||||
// See if the pod has the legacy hook annotation keys (i.e. without a phase specified)
|
||||
hookFromAnnotations = getPodExecHookFromAnnotations(metadata.GetAnnotations(), "", log)
|
||||
}
|
||||
if hookFromAnnotations != nil {
|
||||
hookLog := log.WithFields(
|
||||
logrus.Fields{
|
||||
"hookSource": "annotation",
|
||||
"hookType": "exec",
|
||||
"hookPhase": phase,
|
||||
},
|
||||
)
|
||||
if err := h.podCommandExecutor.ExecutePodCommand(hookLog, obj.UnstructuredContent(), namespace, name, "<from-annotation>", hookFromAnnotations); err != nil {
|
||||
hookLog.WithError(err).Error("Error executing hook")
|
||||
if hookFromAnnotations.OnError == api.HookErrorModeFail {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
labels := labels.Set(metadata.GetLabels())
|
||||
// Otherwise, check for hooks defined in the backup spec.
|
||||
for _, resourceHook := range resourceHooks {
|
||||
if !resourceHook.applicableTo(groupResource, namespace, labels) {
|
||||
continue
|
||||
}
|
||||
|
||||
var hooks []api.BackupResourceHook
|
||||
if phase == hookPhasePre {
|
||||
hooks = resourceHook.pre
|
||||
} else {
|
||||
hooks = resourceHook.post
|
||||
}
|
||||
for _, hook := range hooks {
|
||||
if groupResource == kuberesource.Pods {
|
||||
if hook.Exec != nil {
|
||||
hookLog := log.WithFields(
|
||||
logrus.Fields{
|
||||
"hookSource": "backupSpec",
|
||||
"hookType": "exec",
|
||||
"hookPhase": phase,
|
||||
},
|
||||
)
|
||||
err := h.podCommandExecutor.ExecutePodCommand(hookLog, obj.UnstructuredContent(), namespace, name, resourceHook.name, hook.Exec)
|
||||
if err != nil {
|
||||
hookLog.WithError(err).Error("Error executing hook")
|
||||
if hook.Exec.OnError == api.HookErrorModeFail {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
podBackupHookContainerAnnotationKey = "hook.backup.velero.io/container"
|
||||
podBackupHookCommandAnnotationKey = "hook.backup.velero.io/command"
|
||||
podBackupHookOnErrorAnnotationKey = "hook.backup.velero.io/on-error"
|
||||
podBackupHookTimeoutAnnotationKey = "hook.backup.velero.io/timeout"
|
||||
)
|
||||
|
||||
func phasedKey(phase hookPhase, key string) string {
|
||||
if phase != "" {
|
||||
return fmt.Sprintf("%v.%v", phase, key)
|
||||
}
|
||||
return string(key)
|
||||
}
|
||||
|
||||
func getHookAnnotation(annotations map[string]string, key string, phase hookPhase) string {
|
||||
return annotations[phasedKey(phase, key)]
|
||||
}
|
||||
|
||||
// getPodExecHookFromAnnotations returns an ExecHook based on the annotations, as long as the
|
||||
// 'command' annotation is present. If it is absent, this returns nil.
|
||||
// If there is an error in parsing a supplied timeout, it is logged.
|
||||
func getPodExecHookFromAnnotations(annotations map[string]string, phase hookPhase, log logrus.FieldLogger) *api.ExecHook {
|
||||
commandValue := getHookAnnotation(annotations, podBackupHookCommandAnnotationKey, phase)
|
||||
if commandValue == "" {
|
||||
return nil
|
||||
}
|
||||
var command []string
|
||||
// check for json array
|
||||
if commandValue[0] == '[' {
|
||||
if err := json.Unmarshal([]byte(commandValue), &command); err != nil {
|
||||
command = []string{commandValue}
|
||||
}
|
||||
} else {
|
||||
command = append(command, commandValue)
|
||||
}
|
||||
|
||||
container := getHookAnnotation(annotations, podBackupHookContainerAnnotationKey, phase)
|
||||
|
||||
onError := api.HookErrorMode(getHookAnnotation(annotations, podBackupHookOnErrorAnnotationKey, phase))
|
||||
if onError != api.HookErrorModeContinue && onError != api.HookErrorModeFail {
|
||||
onError = ""
|
||||
}
|
||||
|
||||
var timeout time.Duration
|
||||
timeoutString := getHookAnnotation(annotations, podBackupHookTimeoutAnnotationKey, phase)
|
||||
if timeoutString != "" {
|
||||
if temp, err := time.ParseDuration(timeoutString); err == nil {
|
||||
timeout = temp
|
||||
} else {
|
||||
log.Warn(errors.Wrapf(err, "Unable to parse provided timeout %s, using default", timeoutString))
|
||||
}
|
||||
}
|
||||
|
||||
return &api.ExecHook{
|
||||
Container: container,
|
||||
Command: command,
|
||||
OnError: onError,
|
||||
Timeout: metav1.Duration{Duration: timeout},
|
||||
}
|
||||
}
|
||||
|
||||
type resourceHook struct {
|
||||
name string
|
||||
namespaces *collections.IncludesExcludes
|
||||
resources *collections.IncludesExcludes
|
||||
labelSelector labels.Selector
|
||||
pre []api.BackupResourceHook
|
||||
post []api.BackupResourceHook
|
||||
}
|
||||
|
||||
func (r resourceHook) applicableTo(groupResource schema.GroupResource, namespace string, labels labels.Set) bool {
|
||||
if r.namespaces != nil && !r.namespaces.ShouldInclude(namespace) {
|
||||
return false
|
||||
}
|
||||
if r.resources != nil && !r.resources.ShouldInclude(groupResource.String()) {
|
||||
return false
|
||||
}
|
||||
if r.labelSelector != nil && !r.labelSelector.Matches(labels) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,705 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 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 backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
velerotest "github.com/vmware-tanzu/velero/pkg/test"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/collections"
|
||||
)
|
||||
|
||||
type mockItemHookHandler struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (h *mockItemHookHandler) handleHooks(log logrus.FieldLogger, groupResource schema.GroupResource, obj runtime.Unstructured, resourceHooks []resourceHook, phase hookPhase) error {
|
||||
args := h.Called(log, groupResource, obj, resourceHooks, phase)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func TestHandleHooksSkips(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
groupResource string
|
||||
item runtime.Unstructured
|
||||
hooks []resourceHook
|
||||
}{
|
||||
{
|
||||
name: "not a pod",
|
||||
groupResource: "widget.group",
|
||||
},
|
||||
{
|
||||
name: "pod without annotation / no spec hooks",
|
||||
item: velerotest.UnstructuredOrDie(
|
||||
`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "ns",
|
||||
"name": "foo"
|
||||
}
|
||||
}
|
||||
`,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "spec hooks not applicable",
|
||||
groupResource: "pods",
|
||||
item: velerotest.UnstructuredOrDie(
|
||||
`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "ns",
|
||||
"name": "foo",
|
||||
"labels": {
|
||||
"color": "blue"
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
),
|
||||
hooks: []resourceHook{
|
||||
{
|
||||
name: "ns exclude",
|
||||
namespaces: collections.NewIncludesExcludes().Excludes("ns"),
|
||||
},
|
||||
{
|
||||
name: "resource exclude",
|
||||
resources: collections.NewIncludesExcludes().Includes("widgets.group"),
|
||||
},
|
||||
{
|
||||
name: "label selector mismatch",
|
||||
labelSelector: parseLabelSelectorOrDie("color=green"),
|
||||
},
|
||||
{
|
||||
name: "missing exec hook",
|
||||
pre: []v1.BackupResourceHook{
|
||||
{},
|
||||
{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
podCommandExecutor := &velerotest.MockPodCommandExecutor{}
|
||||
defer podCommandExecutor.AssertExpectations(t)
|
||||
|
||||
h := &defaultItemHookHandler{
|
||||
podCommandExecutor: podCommandExecutor,
|
||||
}
|
||||
|
||||
groupResource := schema.ParseGroupResource(test.groupResource)
|
||||
err := h.handleHooks(velerotest.NewLogger(), groupResource, test.item, test.hooks, hookPhasePre)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleHooks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
phase hookPhase
|
||||
groupResource string
|
||||
item runtime.Unstructured
|
||||
hooks []resourceHook
|
||||
hookErrorsByContainer map[string]error
|
||||
expectedError error
|
||||
expectedPodHook *v1.ExecHook
|
||||
expectedPodHookError error
|
||||
}{
|
||||
{
|
||||
name: "pod, no annotation, spec (multiple pre hooks) = run spec",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: velerotest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "ns",
|
||||
"name": "name"
|
||||
}
|
||||
}`),
|
||||
hooks: []resourceHook{
|
||||
{
|
||||
name: "hook1",
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "1a",
|
||||
Command: []string{"pre-1a"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "1b",
|
||||
Command: []string{"pre-1b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hook2",
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "2a",
|
||||
Command: []string{"2a"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "2b",
|
||||
Command: []string{"2b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pod, no annotation, spec (multiple post hooks) = run spec",
|
||||
phase: hookPhasePost,
|
||||
groupResource: "pods",
|
||||
item: velerotest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "ns",
|
||||
"name": "name"
|
||||
}
|
||||
}`),
|
||||
hooks: []resourceHook{
|
||||
{
|
||||
name: "hook1",
|
||||
post: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "1a",
|
||||
Command: []string{"pre-1a"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "1b",
|
||||
Command: []string{"pre-1b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hook2",
|
||||
post: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "2a",
|
||||
Command: []string{"2a"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "2b",
|
||||
Command: []string{"2b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pod, annotation (legacy), no spec = run annotation",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: velerotest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "ns",
|
||||
"name": "name",
|
||||
"annotations": {
|
||||
"hook.backup.velero.io/container": "c",
|
||||
"hook.backup.velero.io/command": "/bin/ls"
|
||||
}
|
||||
}
|
||||
}`),
|
||||
expectedPodHook: &v1.ExecHook{
|
||||
Container: "c",
|
||||
Command: []string{"/bin/ls"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pod, annotation (pre), no spec = run annotation",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: velerotest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "ns",
|
||||
"name": "name",
|
||||
"annotations": {
|
||||
"pre.hook.backup.velero.io/container": "c",
|
||||
"pre.hook.backup.velero.io/command": "/bin/ls"
|
||||
}
|
||||
}
|
||||
}`),
|
||||
expectedPodHook: &v1.ExecHook{
|
||||
Container: "c",
|
||||
Command: []string{"/bin/ls"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pod, annotation (post), no spec = run annotation",
|
||||
phase: hookPhasePost,
|
||||
groupResource: "pods",
|
||||
item: velerotest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "ns",
|
||||
"name": "name",
|
||||
"annotations": {
|
||||
"post.hook.backup.velero.io/container": "c",
|
||||
"post.hook.backup.velero.io/command": "/bin/ls"
|
||||
}
|
||||
}
|
||||
}`),
|
||||
expectedPodHook: &v1.ExecHook{
|
||||
Container: "c",
|
||||
Command: []string{"/bin/ls"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pod, annotation & spec = run annotation",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: velerotest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "ns",
|
||||
"name": "name",
|
||||
"annotations": {
|
||||
"hook.backup.velero.io/container": "c",
|
||||
"hook.backup.velero.io/command": "/bin/ls"
|
||||
}
|
||||
}
|
||||
}`),
|
||||
expectedPodHook: &v1.ExecHook{
|
||||
Container: "c",
|
||||
Command: []string{"/bin/ls"},
|
||||
},
|
||||
hooks: []resourceHook{
|
||||
{
|
||||
name: "hook1",
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "1a",
|
||||
Command: []string{"1a"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pod, annotation, onError=fail = return error",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: velerotest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "ns",
|
||||
"name": "name",
|
||||
"annotations": {
|
||||
"hook.backup.velero.io/container": "c",
|
||||
"hook.backup.velero.io/command": "/bin/ls",
|
||||
"hook.backup.velero.io/on-error": "Fail"
|
||||
}
|
||||
}
|
||||
}`),
|
||||
expectedPodHook: &v1.ExecHook{
|
||||
Container: "c",
|
||||
Command: []string{"/bin/ls"},
|
||||
OnError: v1.HookErrorModeFail,
|
||||
},
|
||||
expectedPodHookError: errors.New("pod hook error"),
|
||||
expectedError: errors.New("pod hook error"),
|
||||
},
|
||||
{
|
||||
name: "pod, annotation, onError=continue = return nil",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: velerotest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "ns",
|
||||
"name": "name",
|
||||
"annotations": {
|
||||
"hook.backup.velero.io/container": "c",
|
||||
"hook.backup.velero.io/command": "/bin/ls",
|
||||
"hook.backup.velero.io/on-error": "Continue"
|
||||
}
|
||||
}
|
||||
}`),
|
||||
expectedPodHook: &v1.ExecHook{
|
||||
Container: "c",
|
||||
Command: []string{"/bin/ls"},
|
||||
OnError: v1.HookErrorModeContinue,
|
||||
},
|
||||
expectedPodHookError: errors.New("pod hook error"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "pod, spec, onError=fail = don't run other hooks",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: velerotest.UnstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "ns",
|
||||
"name": "name"
|
||||
}
|
||||
}`),
|
||||
hooks: []resourceHook{
|
||||
{
|
||||
name: "hook1",
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "1a",
|
||||
Command: []string{"1a"},
|
||||
OnError: v1.HookErrorModeContinue,
|
||||
},
|
||||
},
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "1b",
|
||||
Command: []string{"1b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hook2",
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "2",
|
||||
Command: []string{"2"},
|
||||
OnError: v1.HookErrorModeFail,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hook3",
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "3",
|
||||
Command: []string{"3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
hookErrorsByContainer: map[string]error{
|
||||
"1a": errors.New("1a error, but continue"),
|
||||
"2": errors.New("2 error, fail"),
|
||||
},
|
||||
expectedError: errors.New("2 error, fail"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
podCommandExecutor := &velerotest.MockPodCommandExecutor{}
|
||||
defer podCommandExecutor.AssertExpectations(t)
|
||||
|
||||
h := &defaultItemHookHandler{
|
||||
podCommandExecutor: podCommandExecutor,
|
||||
}
|
||||
|
||||
if test.expectedPodHook != nil {
|
||||
podCommandExecutor.On("ExecutePodCommand", mock.Anything, test.item.UnstructuredContent(), "ns", "name", "<from-annotation>", test.expectedPodHook).Return(test.expectedPodHookError)
|
||||
} else {
|
||||
hookLoop:
|
||||
for _, resourceHook := range test.hooks {
|
||||
for _, hook := range resourceHook.pre {
|
||||
hookError := test.hookErrorsByContainer[hook.Exec.Container]
|
||||
podCommandExecutor.On("ExecutePodCommand", mock.Anything, test.item.UnstructuredContent(), "ns", "name", resourceHook.name, hook.Exec).Return(hookError)
|
||||
if hookError != nil && hook.Exec.OnError == v1.HookErrorModeFail {
|
||||
break hookLoop
|
||||
}
|
||||
}
|
||||
for _, hook := range resourceHook.post {
|
||||
hookError := test.hookErrorsByContainer[hook.Exec.Container]
|
||||
podCommandExecutor.On("ExecutePodCommand", mock.Anything, test.item.UnstructuredContent(), "ns", "name", resourceHook.name, hook.Exec).Return(hookError)
|
||||
if hookError != nil && hook.Exec.OnError == v1.HookErrorModeFail {
|
||||
break hookLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groupResource := schema.ParseGroupResource(test.groupResource)
|
||||
err := h.handleHooks(velerotest.NewLogger(), groupResource, test.item, test.hooks, test.phase)
|
||||
|
||||
if test.expectedError != nil {
|
||||
assert.EqualError(t, err, test.expectedError.Error())
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPodExecHookFromAnnotations(t *testing.T) {
|
||||
phases := []hookPhase{"", hookPhasePre, hookPhasePost}
|
||||
for _, phase := range phases {
|
||||
tests := []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
expectedHook *v1.ExecHook
|
||||
}{
|
||||
{
|
||||
name: "missing command annotation",
|
||||
expectedHook: nil,
|
||||
},
|
||||
{
|
||||
name: "malformed command json array",
|
||||
annotations: map[string]string{
|
||||
phasedKey(phase, podBackupHookCommandAnnotationKey): "[blarg",
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"[blarg"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid command json array",
|
||||
annotations: map[string]string{
|
||||
phasedKey(phase, podBackupHookCommandAnnotationKey): `["a","b","c"]`,
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"a", "b", "c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "command as a string",
|
||||
annotations: map[string]string{
|
||||
phasedKey(phase, podBackupHookCommandAnnotationKey): "/usr/bin/foo",
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"/usr/bin/foo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hook mode set to continue",
|
||||
annotations: map[string]string{
|
||||
phasedKey(phase, podBackupHookCommandAnnotationKey): "/usr/bin/foo",
|
||||
phasedKey(phase, podBackupHookOnErrorAnnotationKey): string(v1.HookErrorModeContinue),
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"/usr/bin/foo"},
|
||||
OnError: v1.HookErrorModeContinue,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hook mode set to fail",
|
||||
annotations: map[string]string{
|
||||
phasedKey(phase, podBackupHookCommandAnnotationKey): "/usr/bin/foo",
|
||||
phasedKey(phase, podBackupHookOnErrorAnnotationKey): string(v1.HookErrorModeFail),
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"/usr/bin/foo"},
|
||||
OnError: v1.HookErrorModeFail,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "use the specified timeout",
|
||||
annotations: map[string]string{
|
||||
phasedKey(phase, podBackupHookCommandAnnotationKey): "/usr/bin/foo",
|
||||
phasedKey(phase, podBackupHookTimeoutAnnotationKey): "5m3s",
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"/usr/bin/foo"},
|
||||
Timeout: metav1.Duration{Duration: 5*time.Minute + 3*time.Second},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid timeout is logged",
|
||||
annotations: map[string]string{
|
||||
phasedKey(phase, podBackupHookCommandAnnotationKey): "/usr/bin/foo",
|
||||
phasedKey(phase, podBackupHookTimeoutAnnotationKey): "invalid",
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"/usr/bin/foo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "use the specified container",
|
||||
annotations: map[string]string{
|
||||
phasedKey(phase, podBackupHookContainerAnnotationKey): "some-container",
|
||||
phasedKey(phase, podBackupHookCommandAnnotationKey): "/usr/bin/foo",
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Container: "some-container",
|
||||
Command: []string{"/usr/bin/foo"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%s (phase=%q)", test.name, phase), func(t *testing.T) {
|
||||
l := velerotest.NewLogger()
|
||||
hook := getPodExecHookFromAnnotations(test.annotations, phase, l)
|
||||
assert.Equal(t, test.expectedHook, hook)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceHookApplicableTo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
includedNamespaces []string
|
||||
excludedNamespaces []string
|
||||
includedResources []string
|
||||
excludedResources []string
|
||||
labelSelector string
|
||||
namespace string
|
||||
resource schema.GroupResource
|
||||
labels labels.Set
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "allow anything",
|
||||
namespace: "foo",
|
||||
resource: schema.GroupResource{Group: "foo", Resource: "bar"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "namespace in included list",
|
||||
includedNamespaces: []string{"a", "b"},
|
||||
excludedNamespaces: []string{"c", "d"},
|
||||
namespace: "b",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "namespace not in included list",
|
||||
includedNamespaces: []string{"a", "b"},
|
||||
namespace: "c",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "namespace excluded",
|
||||
excludedNamespaces: []string{"a", "b"},
|
||||
namespace: "a",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "resource in included list",
|
||||
includedResources: []string{"foo.a", "bar.b"},
|
||||
excludedResources: []string{"baz.c"},
|
||||
resource: schema.GroupResource{Group: "a", Resource: "foo"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "resource not in included list",
|
||||
includedResources: []string{"foo.a", "bar.b"},
|
||||
resource: schema.GroupResource{Group: "c", Resource: "baz"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "resource excluded",
|
||||
excludedResources: []string{"foo.a", "bar.b"},
|
||||
resource: schema.GroupResource{Group: "b", Resource: "bar"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "label selector matches",
|
||||
labelSelector: "a=b",
|
||||
labels: labels.Set{"a": "b"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "label selector doesn't match",
|
||||
labelSelector: "a=b",
|
||||
labels: labels.Set{"a": "c"},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
h := resourceHook{
|
||||
namespaces: collections.NewIncludesExcludes().Includes(test.includedNamespaces...).Excludes(test.excludedNamespaces...),
|
||||
resources: collections.NewIncludesExcludes().Includes(test.includedResources...).Excludes(test.excludedResources...),
|
||||
}
|
||||
if test.labelSelector != "" {
|
||||
selector, err := labels.Parse(test.labelSelector)
|
||||
require.NoError(t, err)
|
||||
h.labelSelector = selector
|
||||
}
|
||||
|
||||
result := h.applicableTo(test.resource, test.namespace, test.labels)
|
||||
assert.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func parseLabelSelectorOrDie(s string) labels.Selector {
|
||||
ret, err := labels.Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 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.
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/vmware-tanzu/velero/internal/hook"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
"github.com/vmware-tanzu/velero/pkg/util/collections"
|
||||
"github.com/vmware-tanzu/velero/pkg/volume"
|
||||
@@ -40,7 +41,7 @@ type Request struct {
|
||||
SnapshotLocations []*velerov1api.VolumeSnapshotLocation
|
||||
NamespaceIncludesExcludes *collections.IncludesExcludes
|
||||
ResourceIncludesExcludes *collections.IncludesExcludes
|
||||
ResourceHooks []resourceHook
|
||||
ResourceHooks []hook.ResourceHook
|
||||
ResolvedActions []resolvedAction
|
||||
|
||||
VolumeSnapshots []*volume.Snapshot
|
||||
|
||||
Reference in New Issue
Block a user