Support pre and post hooks.
Signed-off-by: Andy Goldstein <andy.goldstein@gmail.com>pull/243/head
parent
656428d0b9
commit
de785af89d
|
@ -88,8 +88,12 @@ spec:
|
|||
matchLabels:
|
||||
app: ark
|
||||
component: server
|
||||
# An array of hooks to run. Currently only "exec" hooks are supported.
|
||||
# An array of hooks to run before executing custom actions. Currently only "exec" hooks are supported.
|
||||
# DEPRECATED. Use pre instead.
|
||||
hooks:
|
||||
# Same content as pre below.
|
||||
# An array of hooks to run before executing custom actions. Currently only "exec" hooks are supported.
|
||||
pre:
|
||||
-
|
||||
# The type of hook. This must be "exec".
|
||||
exec:
|
||||
|
@ -105,6 +109,10 @@ spec:
|
|||
onError: Fail
|
||||
# How long to wait for the command to finish executing. Defaults to 30 seconds. Optional.
|
||||
timeout: 10s
|
||||
# An array of hooks to run after all custom actions and additional items have been
|
||||
# processed. Currently only "exec" hooks are supported.
|
||||
post:
|
||||
# Same content as pre above.
|
||||
# Status about the Backup. Users should not set any data here.
|
||||
status:
|
||||
# The date and time when the Backup is eligible for garbage collection.
|
||||
|
|
|
@ -5,19 +5,46 @@ Heptio Ark currently supports executing commands in containers in pods during a
|
|||
## Backup Hooks
|
||||
|
||||
When performing a backup, you can specify one or more commands to execute in a container in a pod
|
||||
when that pod is being backed up. There are two ways to specify hooks: annotations on the pod
|
||||
itself, and in the Backup spec.
|
||||
when that pod is being backed up.
|
||||
|
||||
Ark versions prior to v0.7.0 only support hooks that execute prior to any custom action processing
|
||||
("pre" hooks).
|
||||
|
||||
As of version v0.7.0, Ark also supports "post" hooks - these execute after all custom actions have
|
||||
completed, as well as after all the additional items specified by custom actions have been backed
|
||||
up.
|
||||
|
||||
An example of when you might use both pre and post hooks is freezing a file system. If you want to
|
||||
ensure that all pending disk I/O operations have completed prior to taking a snapshot, you could use
|
||||
a pre hook to run `fsfreeze --freeze`. Next, Ark would take a snapshot of the disk. Finally, you
|
||||
could use a post hook to run `fsfreeze --unfreeze`.
|
||||
|
||||
There are two ways to specify hooks: annotations on the pod itself, and in the Backup spec.
|
||||
|
||||
### Specifying Hooks As Pod Annotations
|
||||
|
||||
You can use the following annotations on a pod to make Ark execute a hook when backing up the pod:
|
||||
|
||||
#### Pre hooks
|
||||
|
||||
| Annotation Name | Description |
|
||||
| --- | --- |
|
||||
| `hook.backup.ark.heptio.com/container` | The container where the command should be executed. Defaults to the first container in the pod. Optional. |
|
||||
| `hook.backup.ark.heptio.com/command` | The command to execute. If you need multiple arguments, specify the command as a JSON array, such as `["/usr/bin/uname", "-a"]` |
|
||||
| `hook.backup.ark.heptio.com/on-error` | What to do if the command returns a non-zero exit code. Defaults to Fail. Valid values are Fail and Continue. Optional. |
|
||||
| `hook.backup.ark.heptio.com/timeout` | How long to wait for the command to execute. The hook is considered in error if the command exceeds the timeout. Defaults to 30s. Optional. |
|
||||
| `pre.hook.backup.ark.heptio.com/container` | The container where the command should be executed. Defaults to the first container in the pod. Optional. |
|
||||
| `pre.hook.backup.ark.heptio.com/command` | The command to execute. If you need multiple arguments, specify the command as a JSON array, such as `["/usr/bin/uname", "-a"]` |
|
||||
| `pre.hook.backup.ark.heptio.com/on-error` | What to do if the command returns a non-zero exit code. Defaults to Fail. Valid values are Fail and Continue. Optional. |
|
||||
| `pre.hook.backup.ark.heptio.com/timeout` | How long to wait for the command to execute. The hook is considered in error if the command exceeds the timeout. Defaults to 30s. Optional. |
|
||||
|
||||
Ark v0.7.0+ continues to support the original (deprecated) way to specify pre hooks - without the
|
||||
`pre.` prefix in the annotation names (e.g. `hook.backup.ark.heptio.com/container`).
|
||||
|
||||
#### Post hooks (v0.7.0+)
|
||||
|
||||
| Annotation Name | Description |
|
||||
| --- | --- |
|
||||
| `post.hook.backup.ark.heptio.com/container` | The container where the command should be executed. Defaults to the first container in the pod. Optional. |
|
||||
| `post.hook.backup.ark.heptio.com/command` | The command to execute. If you need multiple arguments, specify the command as a JSON array, such as `["/usr/bin/uname", "-a"]` |
|
||||
| `post.hook.backup.ark.heptio.com/on-error` | What to do if the command returns a non-zero exit code. Defaults to Fail. Valid values are Fail and Continue. Optional. |
|
||||
| `post.hook.backup.ark.heptio.com/timeout` | How long to wait for the command to execute. The hook is considered in error if the command exceeds the timeout. Defaults to 30s. Optional. |
|
||||
|
||||
### Specifying Hooks in the Backup Spec
|
||||
|
||||
|
|
|
@ -81,8 +81,14 @@ type BackupResourceHookSpec struct {
|
|||
ExcludedResources []string `json:"excludedResources"`
|
||||
// LabelSelector, if specified, filters the resources to which this hook spec applies.
|
||||
LabelSelector *metav1.LabelSelector `json:"labelSelector"`
|
||||
// Hooks is a list of BackupResourceHooks to execute.
|
||||
// Hooks is a list of BackupResourceHooks to execute. DEPRECATED. Replaced by PreHooks.
|
||||
Hooks []BackupResourceHook `json:"hooks"`
|
||||
// PreHooks is a list of BackupResourceHooks to execute prior to storing the item in the backup.
|
||||
// These are executed before any "additional items" from item actions are processed.
|
||||
PreHooks []BackupResourceHook `json:"pre,omitempty"`
|
||||
// PostHooks is a list of BackupResourceHooks to execute after storing the item in the backup.
|
||||
// These are executed after all "additional items" from item actions are processed.
|
||||
PostHooks []BackupResourceHook `json:"post,omitempty"`
|
||||
}
|
||||
|
||||
// BackupResourceHook defines a hook for a resource.
|
||||
|
|
|
@ -293,6 +293,20 @@ func (in *BackupResourceHookSpec) DeepCopyInto(out *BackupResourceHookSpec) {
|
|||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.PreHooks != nil {
|
||||
in, out := &in.PreHooks, &out.PreHooks
|
||||
*out = make([]BackupResourceHook, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.PostHooks != nil {
|
||||
in, out := &in.PostHooks, &out.PostHooks
|
||||
*out = make([]BackupResourceHook, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -151,20 +151,10 @@ func getNamespaceIncludesExcludes(backup *api.Backup) *collections.IncludesExclu
|
|||
func getResourceHooks(hookSpecs []api.BackupResourceHookSpec, discoveryHelper discovery.Helper) ([]resourceHook, error) {
|
||||
resourceHooks := make([]resourceHook, 0, len(hookSpecs))
|
||||
|
||||
for _, r := range hookSpecs {
|
||||
h := resourceHook{
|
||||
name: r.Name,
|
||||
namespaces: collections.NewIncludesExcludes().Includes(r.IncludedNamespaces...).Excludes(r.ExcludedNamespaces...),
|
||||
resources: getResourceIncludesExcludes(discoveryHelper, r.IncludedResources, r.ExcludedResources),
|
||||
hooks: r.Hooks,
|
||||
}
|
||||
|
||||
if r.LabelSelector != nil {
|
||||
labelSelector, err := metav1.LabelSelectorAsSelector(r.LabelSelector)
|
||||
if err != nil {
|
||||
return []resourceHook{}, errors.WithStack(err)
|
||||
}
|
||||
h.labelSelector = labelSelector
|
||||
for _, s := range hookSpecs {
|
||||
h, err := getResourceHook(s, discoveryHelper)
|
||||
if err != nil {
|
||||
return []resourceHook{}, err
|
||||
}
|
||||
|
||||
resourceHooks = append(resourceHooks, h)
|
||||
|
@ -173,6 +163,33 @@ func getResourceHooks(hookSpecs []api.BackupResourceHookSpec, discoveryHelper di
|
|||
return resourceHooks, nil
|
||||
}
|
||||
|
||||
func getResourceHook(hookSpec api.BackupResourceHookSpec, discoveryHelper discovery.Helper) (resourceHook, error) {
|
||||
// Use newer PreHooks if it's set
|
||||
preHooks := hookSpec.PreHooks
|
||||
if len(preHooks) == 0 {
|
||||
// Fall back to Hooks otherwise (DEPRECATED)
|
||||
preHooks = hookSpec.Hooks
|
||||
}
|
||||
|
||||
h := resourceHook{
|
||||
name: hookSpec.Name,
|
||||
namespaces: collections.NewIncludesExcludes().Includes(hookSpec.IncludedNamespaces...).Excludes(hookSpec.ExcludedNamespaces...),
|
||||
resources: getResourceIncludesExcludes(discoveryHelper, hookSpec.IncludedResources, hookSpec.ExcludedResources),
|
||||
pre: preHooks,
|
||||
post: hookSpec.PostHooks,
|
||||
}
|
||||
|
||||
if hookSpec.LabelSelector != nil {
|
||||
labelSelector, err := metav1.LabelSelectorAsSelector(hookSpec.LabelSelector)
|
||||
if err != nil {
|
||||
return resourceHook{}, errors.WithStack(err)
|
||||
}
|
||||
h.labelSelector = labelSelector
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// Backup backs up the items specified in the Backup, placing them in a gzip-compressed tar file
|
||||
// written to backupFile. The finalized api.Backup is written to metadata.
|
||||
func (kb *kubernetesBackupper) Backup(backup *api.Backup, backupFile, logFile io.Writer, actions []ItemAction) error {
|
||||
|
|
|
@ -469,7 +469,7 @@ func TestBackup(t *testing.T) {
|
|||
namespaces: collections.NewIncludesExcludes().Includes("a").Excludes("b"),
|
||||
resources: collections.NewIncludesExcludes().Includes("configmaps").Excludes("roles.rbac.authorization.k8s.io"),
|
||||
labelSelector: parseLabelSelectorOrDie("1=2"),
|
||||
hooks: []v1.BackupResourceHook{
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Command: []string{"ls", "/tmp"},
|
||||
|
@ -623,325 +623,6 @@ func (gb *mockGroupBackupper) backupGroup(group *metav1.APIResourceList) error {
|
|||
return args.Error(0)
|
||||
}
|
||||
|
||||
/*
|
||||
func TestBackupMethod(t *testing.T) {
|
||||
// TODO ensure LabelSelector is passed through to the List() calls
|
||||
backup := &v1.Backup{
|
||||
Spec: v1.BackupSpec{
|
||||
// cm - shortcut in legacy api group, namespaced
|
||||
// csr - shortcut in certificates.k8s.io api group, cluster-scoped
|
||||
// roles - fully qualified in rbac.authorization.k8s.io api group, namespaced
|
||||
IncludedResources: []string{"cm", "csr", "roles"},
|
||||
IncludedNamespaces: []string{"a", "b"},
|
||||
ExcludedNamespaces: []string{"c", "d"},
|
||||
},
|
||||
}
|
||||
|
||||
configMapsResource := metav1.APIResource{
|
||||
Name: "configmaps",
|
||||
SingularName: "configmap",
|
||||
Namespaced: true,
|
||||
Kind: "ConfigMap",
|
||||
Verbs: metav1.Verbs([]string{"create", "update", "get", "list", "watch", "delete"}),
|
||||
ShortNames: []string{"cm"},
|
||||
Categories: []string{"all"},
|
||||
}
|
||||
|
||||
podsResource := metav1.APIResource{
|
||||
Name: "pods",
|
||||
SingularName: "pod",
|
||||
Namespaced: true,
|
||||
Kind: "Pod",
|
||||
Verbs: metav1.Verbs([]string{"create", "update", "get", "list", "watch", "delete"}),
|
||||
ShortNames: []string{"po"},
|
||||
Categories: []string{"all"},
|
||||
}
|
||||
|
||||
rolesResource := metav1.APIResource{
|
||||
Name: "roles",
|
||||
SingularName: "role",
|
||||
Namespaced: true,
|
||||
Kind: "Role",
|
||||
Verbs: metav1.Verbs([]string{"create", "update", "get", "list", "watch", "delete"}),
|
||||
}
|
||||
|
||||
certificateSigningRequestsResource := metav1.APIResource{
|
||||
Name: "certificatesigningrequests",
|
||||
SingularName: "certificatesigningrequest",
|
||||
Namespaced: false,
|
||||
Kind: "CertificateSigningRequest",
|
||||
Verbs: metav1.Verbs([]string{"create", "update", "get", "list", "watch", "delete"}),
|
||||
ShortNames: []string{"csr"},
|
||||
}
|
||||
|
||||
discoveryHelper := &arktest.FakeDiscoveryHelper{
|
||||
Mapper: &arktest.FakeMapper{
|
||||
Resources: map[schema.GroupVersionResource]schema.GroupVersionResource{
|
||||
schema.GroupVersionResource{Resource: "cm"}: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"},
|
||||
schema.GroupVersionResource{Resource: "csr"}: schema.GroupVersionResource{Group: "certificates.k8s.io", Version: "v1beta1", Resource: "certificatesigningrequests"},
|
||||
schema.GroupVersionResource{Resource: "roles"}: schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1beta1", Resource: "roles"},
|
||||
},
|
||||
},
|
||||
ResourceList: []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []metav1.APIResource{configMapsResource, podsResource},
|
||||
},
|
||||
{
|
||||
GroupVersion: "certificates.k8s.io/v1beta1",
|
||||
APIResources: []metav1.APIResource{certificateSigningRequestsResource},
|
||||
},
|
||||
{
|
||||
GroupVersion: "rbac.authorization.k8s.io/v1beta1",
|
||||
APIResources: []metav1.APIResource{rolesResource},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dynamicFactory := &arktest.FakeDynamicFactory{}
|
||||
|
||||
legacyGV := schema.GroupVersionResource{Version: "v1"}
|
||||
|
||||
configMapsClientA := &arktest.FakeDynamicClient{}
|
||||
configMapsA := toRuntimeObject(t, `{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMapList",
|
||||
"items": [
|
||||
{
|
||||
"metadata": {
|
||||
"namespace":"a",
|
||||
"name":"configMap1"
|
||||
},
|
||||
"data": {
|
||||
"a": "b"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`)
|
||||
configMapsClientA.On("List", metav1.ListOptions{}).Return(configMapsA, nil)
|
||||
dynamicFactory.On("ClientForGroupVersionResource", legacyGV, configMapsResource, "a").Return(configMapsClientA, nil)
|
||||
|
||||
configMapsClientB := &arktest.FakeDynamicClient{}
|
||||
configMapsB := toRuntimeObject(t, `{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMapList",
|
||||
"items": [
|
||||
{
|
||||
"metadata": {
|
||||
"namespace":"b",
|
||||
"name":"configMap2"
|
||||
},
|
||||
"data": {
|
||||
"c": "d"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`)
|
||||
configMapsClientB.On("List", metav1.ListOptions{}).Return(configMapsB, nil)
|
||||
dynamicFactory.On("ClientForGroupVersionResource", legacyGV, configMapsResource, "b").Return(configMapsClientB, nil)
|
||||
|
||||
certificatesGV := schema.GroupVersionResource{Group: "certificates.k8s.io", Version: "v1beta1"}
|
||||
|
||||
csrList := toRuntimeObject(t, `{
|
||||
"apiVersion": "certificates.k8s.io/v1beta1",
|
||||
"kind": "CertificateSigningRequestList",
|
||||
"items": [
|
||||
{
|
||||
"metadata": {
|
||||
"name": "csr1"
|
||||
},
|
||||
"spec": {
|
||||
"request": "some request",
|
||||
"username": "bob",
|
||||
"uid": "12345",
|
||||
"groups": [
|
||||
"group1",
|
||||
"group2"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"certificate": "some cert"
|
||||
}
|
||||
}
|
||||
]
|
||||
}`)
|
||||
csrClient := &arktest.FakeDynamicClient{}
|
||||
csrClient.On("List", metav1.ListOptions{}).Return(csrList, nil)
|
||||
dynamicFactory.On("ClientForGroupVersionResource", certificatesGV, certificateSigningRequestsResource, "").Return(csrClient, nil)
|
||||
|
||||
roleListA := toRuntimeObject(t, `{
|
||||
"apiVersion": "rbac.authorization.k8s.io/v1beta1",
|
||||
"kind": "RoleList",
|
||||
"items": [
|
||||
{
|
||||
"metadata": {
|
||||
"namespace": "a",
|
||||
"name": "role1"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"verbs": ["get","list"],
|
||||
"apiGroups": ["apps","extensions"],
|
||||
"resources": ["deployments"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
roleListB := toRuntimeObject(t, `{
|
||||
"apiVersion": "rbac.authorization.k8s.io/v1beta1",
|
||||
"kind": "RoleList",
|
||||
"items": []
|
||||
}`)
|
||||
|
||||
rbacGV := schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1beta1"}
|
||||
|
||||
rolesClientA := &arktest.FakeDynamicClient{}
|
||||
rolesClientA.On("List", metav1.ListOptions{}).Return(roleListA, nil)
|
||||
dynamicFactory.On("ClientForGroupVersionResource", rbacGV, rolesResource, "a").Return(rolesClientA, nil)
|
||||
rolesClientB := &arktest.FakeDynamicClient{}
|
||||
rolesClientB.On("List", metav1.ListOptions{}).Return(roleListB, nil)
|
||||
dynamicFactory.On("ClientForGroupVersionResource", rbacGV, rolesResource, "b").Return(rolesClientB, nil)
|
||||
|
||||
cmAction := &fakeAction{}
|
||||
csrAction := &fakeAction{}
|
||||
|
||||
actions := map[string]Action{
|
||||
"cm": cmAction,
|
||||
"csr": csrAction,
|
||||
}
|
||||
|
||||
podCommandExecutor := &arktest.PodCommandExecutor{}
|
||||
defer podCommandExecutor.AssertExpectations(t)
|
||||
|
||||
backupper, err := NewKubernetesBackupper(discoveryHelper, dynamicFactory, actions, podCommandExecutor)
|
||||
require.NoError(t, err)
|
||||
|
||||
var output, log bytes.Buffer
|
||||
err = backupper.Backup(backup, &output, &log)
|
||||
defer func() {
|
||||
// print log if anything failed
|
||||
if t.Failed() {
|
||||
gzr, err := gzip.NewReader(&log)
|
||||
require.NoError(t, err)
|
||||
t.Log("Backup log contents:")
|
||||
var buf bytes.Buffer
|
||||
_, err = io.Copy(&buf, gzr)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, gzr.Close())
|
||||
t.Log(buf.String())
|
||||
}
|
||||
}()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedFiles := sets.NewString(
|
||||
"resources/configmaps/namespaces/a/configMap1.json",
|
||||
"resources/configmaps/namespaces/b/configMap2.json",
|
||||
"resources/roles.rbac.authorization.k8s.io/namespaces/a/role1.json",
|
||||
// CSRs are not expected because they're unrelated cluster-scoped resources
|
||||
)
|
||||
|
||||
expectedData := map[string]string{
|
||||
"resources/configmaps/namespaces/a/configMap1.json": `
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": {
|
||||
"namespace":"a",
|
||||
"name":"configMap1"
|
||||
},
|
||||
"data": {
|
||||
"a": "b"
|
||||
}
|
||||
}`,
|
||||
"resources/configmaps/namespaces/b/configMap2.json": `
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": {
|
||||
"namespace":"b",
|
||||
"name":"configMap2"
|
||||
},
|
||||
"data": {
|
||||
"c": "d"
|
||||
}
|
||||
}
|
||||
`,
|
||||
"resources/roles.rbac.authorization.k8s.io/namespaces/a/role1.json": `
|
||||
{
|
||||
"apiVersion": "rbac.authorization.k8s.io/v1beta1",
|
||||
"kind": "Role",
|
||||
"metadata": {
|
||||
"namespace":"a",
|
||||
"name": "role1"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"verbs": ["get","list"],
|
||||
"apiGroups": ["apps","extensions"],
|
||||
"resources": ["deployments"]
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
// CSRs are not expected because they're unrelated cluster-scoped resources
|
||||
}
|
||||
|
||||
seenFiles := sets.NewString()
|
||||
|
||||
gzipReader, err := gzip.NewReader(&output)
|
||||
require.NoError(t, err)
|
||||
defer gzipReader.Close()
|
||||
|
||||
tarReader := tar.NewReader(gzipReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeReg:
|
||||
seenFiles.Insert(header.Name)
|
||||
expected, err := getAsMap(expectedData[header.Name])
|
||||
if !assert.NoError(t, err, "%q: %v", header.Name, err) {
|
||||
continue
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
n, err := io.Copy(buf, tarReader)
|
||||
if !assert.NoError(t, err) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !assert.Equal(t, header.Size, n) {
|
||||
continue
|
||||
}
|
||||
|
||||
actual, err := getAsMap(string(buf.Bytes()))
|
||||
if !assert.NoError(t, err) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, expected, actual)
|
||||
default:
|
||||
t.Errorf("unexpected header: %#v", header)
|
||||
}
|
||||
}
|
||||
|
||||
if !expectedFiles.Equal(seenFiles) {
|
||||
t.Errorf("did not get expected files. expected-seen: %v. seen-expected: %v", expectedFiles.Difference(seenFiles), seenFiles.Difference(expectedFiles))
|
||||
}
|
||||
|
||||
expectedCMActionIDs := []string{"a/configMap1", "b/configMap2"}
|
||||
|
||||
assert.Equal(t, expectedCMActionIDs, cmAction.ids)
|
||||
// CSRs are not expected because they're unrelated cluster-scoped resources
|
||||
assert.Nil(t, csrAction.ids)
|
||||
}
|
||||
*/
|
||||
|
||||
func getAsMap(j string) (map[string]interface{}, error) {
|
||||
m := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(j), &m)
|
||||
|
@ -961,3 +642,137 @@ func unstructuredOrDie(data string) *unstructured.Unstructured {
|
|||
}
|
||||
return o.(*unstructured.Unstructured)
|
||||
}
|
||||
|
||||
func TestGetResourceHook(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hookSpec v1.BackupResourceHookSpec
|
||||
expected resourceHook
|
||||
}{
|
||||
{
|
||||
name: "PreHooks take priority over Hooks",
|
||||
hookSpec: v1.BackupResourceHookSpec{
|
||||
Name: "spec1",
|
||||
PreHooks: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "a",
|
||||
Command: []string{"b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Hooks: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "c",
|
||||
Command: []string{"d"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: resourceHook{
|
||||
name: "spec1",
|
||||
namespaces: collections.NewIncludesExcludes(),
|
||||
resources: collections.NewIncludesExcludes(),
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "a",
|
||||
Command: []string{"b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Use Hooks if PreHooks isn't set",
|
||||
hookSpec: v1.BackupResourceHookSpec{
|
||||
Name: "spec1",
|
||||
Hooks: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "a",
|
||||
Command: []string{"b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: resourceHook{
|
||||
name: "spec1",
|
||||
namespaces: collections.NewIncludesExcludes(),
|
||||
resources: collections.NewIncludesExcludes(),
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "a",
|
||||
Command: []string{"b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Full test",
|
||||
hookSpec: v1.BackupResourceHookSpec{
|
||||
Name: "spec1",
|
||||
IncludedNamespaces: []string{"ns1", "ns2"},
|
||||
ExcludedNamespaces: []string{"ns3", "ns4"},
|
||||
IncludedResources: []string{"foo", "fie"},
|
||||
ExcludedResources: []string{"bar", "baz"},
|
||||
PreHooks: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "a",
|
||||
Command: []string{"b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
PostHooks: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "c",
|
||||
Command: []string{"d"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: resourceHook{
|
||||
name: "spec1",
|
||||
namespaces: collections.NewIncludesExcludes().Includes("ns1", "ns2").Excludes("ns3", "ns4"),
|
||||
resources: collections.NewIncludesExcludes().Includes("foodies.somegroup", "fields.somegroup").Excludes("barnacles.anothergroup", "bazaars.anothergroup"),
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "a",
|
||||
Command: []string{"b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
post: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "c",
|
||||
Command: []string{"d"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
resources := map[schema.GroupVersionResource]schema.GroupVersionResource{
|
||||
{Resource: "foo"}: {Group: "somegroup", Resource: "foodies"},
|
||||
{Resource: "fie"}: {Group: "somegroup", Resource: "fields"},
|
||||
{Resource: "bar"}: {Group: "anothergroup", Resource: "barnacles"},
|
||||
{Resource: "baz"}: {Group: "anothergroup", Resource: "bazaars"},
|
||||
}
|
||||
discoveryHelper := arktest.NewFakeDiscoveryHelper(false, resources)
|
||||
|
||||
actual, err := getResourceHook(test.hookSpec, discoveryHelper)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,11 +17,14 @@ limitations under the License.
|
|||
package backup
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
kuberrs "k8s.io/apimachinery/pkg/util/errors"
|
||||
|
||||
"github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
|
@ -110,7 +113,6 @@ type defaultGroupBackupper struct {
|
|||
func (gb *defaultGroupBackupper) backupGroup(group *metav1.APIResourceList) error {
|
||||
var (
|
||||
errs []error
|
||||
pv *metav1.APIResource
|
||||
log = gb.log.WithField("group", group.GroupVersion)
|
||||
rb = gb.resourceBackupperFactory.newResourceBackupper(
|
||||
log,
|
||||
|
@ -132,26 +134,69 @@ func (gb *defaultGroupBackupper) backupGroup(group *metav1.APIResourceList) erro
|
|||
|
||||
log.Infof("Backing up group")
|
||||
|
||||
processResource := func(resource metav1.APIResource) {
|
||||
// Parse so we can check if this is the core group
|
||||
gv, err := schema.ParseGroupVersion(group.GroupVersion)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error parsing GroupVersion %q", group.GroupVersion)
|
||||
}
|
||||
if gv.Group == "" {
|
||||
// This is the core group, so make sure we process in the following order: pods, pvcs, pvs,
|
||||
// everything else.
|
||||
sortCoreGroup(group)
|
||||
}
|
||||
|
||||
for _, resource := range group.APIResources {
|
||||
if err := rb.backupResource(group, resource); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, resource := range group.APIResources {
|
||||
// do PVs last because if we're also backing up PVCs, we want to backup PVs within the scope of
|
||||
// the PVCs (within the PVC action) to allow for hooks to run
|
||||
if strings.ToLower(resource.Name) == "persistentvolumes" && strings.ToLower(group.GroupVersion) == "v1" {
|
||||
pvResource := resource
|
||||
pv = &pvResource
|
||||
continue
|
||||
}
|
||||
processResource(resource)
|
||||
}
|
||||
|
||||
if pv != nil {
|
||||
processResource(*pv)
|
||||
}
|
||||
|
||||
return kuberrs.NewAggregate(errs)
|
||||
}
|
||||
|
||||
// sortCoreGroup sorts group as a coreGroup.
|
||||
func sortCoreGroup(group *metav1.APIResourceList) {
|
||||
sort.Stable(coreGroup(group.APIResources))
|
||||
}
|
||||
|
||||
// coreGroup is used to sort APIResources in the core API group. The sort order is pods, pvcs, pvs,
|
||||
// then everything else.
|
||||
type coreGroup []metav1.APIResource
|
||||
|
||||
func (c coreGroup) Len() int {
|
||||
return len(c)
|
||||
}
|
||||
|
||||
func (c coreGroup) Less(i, j int) bool {
|
||||
return coreGroupResourcePriority(c[i].Name) < coreGroupResourcePriority(c[j].Name)
|
||||
}
|
||||
|
||||
func (c coreGroup) Swap(i, j int) {
|
||||
c[j], c[i] = c[i], c[j]
|
||||
}
|
||||
|
||||
// These constants represent the relative priorities for resources in the core API group. We want to
|
||||
// ensure that we process pods, then pvcs, then pvs, then anything else. This ensures that when a
|
||||
// pod is backed up, we can perform a pre hook, then process pvcs and pvs (including taking a
|
||||
// snapshot), then perform a post hook on the pod.
|
||||
const (
|
||||
pod = iota
|
||||
pvc
|
||||
pv
|
||||
other
|
||||
)
|
||||
|
||||
// coreGroupResourcePriority returns the relative priority of the resource, in the following order:
|
||||
// pods, pvcs, pvs, everything else.
|
||||
func coreGroupResourcePriority(resource string) int {
|
||||
switch strings.ToLower(resource) {
|
||||
case "pods":
|
||||
return pod
|
||||
case "persistentvolumeclaims":
|
||||
return pvc
|
||||
case "persistentvolumes":
|
||||
return pv
|
||||
}
|
||||
|
||||
return other
|
||||
}
|
||||
|
|
|
@ -188,3 +188,29 @@ func (rb *mockResourceBackupper) backupResource(group *metav1.APIResourceList, r
|
|||
args := rb.Called(group, resource)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func TestSortCoreGroup(t *testing.T) {
|
||||
group := &metav1.APIResourceList{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []metav1.APIResource{
|
||||
{Name: "persistentvolumes"},
|
||||
{Name: "configmaps"},
|
||||
{Name: "antelopes"},
|
||||
{Name: "persistentvolumeclaims"},
|
||||
{Name: "pods"},
|
||||
},
|
||||
}
|
||||
|
||||
sortCoreGroup(group)
|
||||
|
||||
expected := []string{
|
||||
"pods",
|
||||
"persistentvolumeclaims",
|
||||
"persistentvolumes",
|
||||
"configmaps",
|
||||
"antelopes",
|
||||
}
|
||||
for i, r := range group.APIResources {
|
||||
assert.Equal(t, expected[i], r.Name)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -165,7 +165,8 @@ func (ib *defaultItemBackupper) backupItem(logger logrus.FieldLogger, obj runtim
|
|||
// Never save status
|
||||
delete(obj.UnstructuredContent(), "status")
|
||||
|
||||
if err := ib.itemHookHandler.handleHooks(log, groupResource, obj, ib.resourceHooks); err != nil {
|
||||
log.Info("Executing pre hooks")
|
||||
if err := ib.itemHookHandler.handleHooks(log, groupResource, obj, ib.resourceHooks, hookPhasePre); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -232,6 +233,11 @@ func (ib *defaultItemBackupper) backupItem(logger logrus.FieldLogger, obj runtim
|
|||
}
|
||||
}
|
||||
|
||||
log.Info("Executing post hooks")
|
||||
if err := ib.itemHookHandler.handleHooks(log, groupResource, obj, ib.resourceHooks, hookPhasePost); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var filePath string
|
||||
if namespace != "" {
|
||||
filePath = filepath.Join(api.ResourcesDir, groupResource.String(), api.NamespaceScopedDir, namespace, name+".json")
|
||||
|
|
|
@ -343,7 +343,8 @@ func TestBackupItemNoSkips(t *testing.T) {
|
|||
b.additionalItemBackupper = additionalItemBackupper
|
||||
|
||||
obj := &unstructured.Unstructured{Object: item}
|
||||
itemHookHandler.On("handleHooks", mock.Anything, groupResource, obj, resourceHooks).Return(nil)
|
||||
itemHookHandler.On("handleHooks", mock.Anything, groupResource, obj, resourceHooks, hookPhasePre).Return(nil)
|
||||
itemHookHandler.On("handleHooks", mock.Anything, groupResource, obj, resourceHooks, hookPhasePost).Return(nil)
|
||||
|
||||
for i, item := range test.customActionAdditionalItemIdentifiers {
|
||||
itemClient := &arktest.FakeDynamicClient{}
|
||||
|
|
|
@ -18,6 +18,7 @@ package backup
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
api "github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
|
@ -31,13 +32,26 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
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) error
|
||||
handleHooks(
|
||||
log logrus.FieldLogger,
|
||||
groupResource schema.GroupResource,
|
||||
obj runtime.Unstructured,
|
||||
resourceHooks []resourceHook,
|
||||
phase hookPhase,
|
||||
) error
|
||||
}
|
||||
|
||||
// defaultItemHookHandler is the default itemHookHandler.
|
||||
|
@ -50,6 +64,7 @@ func (h *defaultItemHookHandler) handleHooks(
|
|||
groupResource schema.GroupResource,
|
||||
obj runtime.Unstructured,
|
||||
resourceHooks []resourceHook,
|
||||
phase hookPhase,
|
||||
) error {
|
||||
// We only support hooks on pods right now
|
||||
if groupResource != podsGroupResource {
|
||||
|
@ -65,7 +80,12 @@ func (h *defaultItemHookHandler) handleHooks(
|
|||
name := metadata.GetName()
|
||||
|
||||
// If the pod has the hook specified via annotations, that takes priority.
|
||||
if hookFromAnnotations := getPodExecHookFromAnnotations(metadata.GetAnnotations()); hookFromAnnotations != nil {
|
||||
hookFromAnnotations := getPodExecHookFromAnnotations(metadata.GetAnnotations(), phase)
|
||||
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(), "")
|
||||
}
|
||||
if hookFromAnnotations != nil {
|
||||
hookLog := log.WithFields(
|
||||
logrus.Fields{
|
||||
"hookSource": "annotation",
|
||||
|
@ -89,7 +109,13 @@ func (h *defaultItemHookHandler) handleHooks(
|
|||
continue
|
||||
}
|
||||
|
||||
for _, hook := range resourceHook.hooks {
|
||||
var hooks []api.BackupResourceHook
|
||||
if phase == hookPhasePre {
|
||||
hooks = resourceHook.pre
|
||||
} else {
|
||||
hooks = resourceHook.post
|
||||
}
|
||||
for _, hook := range hooks {
|
||||
if groupResource == podsGroupResource {
|
||||
if hook.Exec != nil {
|
||||
hookLog := log.WithFields(
|
||||
|
@ -122,13 +148,22 @@ const (
|
|||
defaultHookTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
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.
|
||||
func getPodExecHookFromAnnotations(annotations map[string]string) *api.ExecHook {
|
||||
container := annotations[podBackupHookContainerAnnotationKey]
|
||||
|
||||
commandValue, ok := annotations[podBackupHookCommandAnnotationKey]
|
||||
if !ok {
|
||||
func getPodExecHookFromAnnotations(annotations map[string]string, phase hookPhase) *api.ExecHook {
|
||||
commandValue := getHookAnnotation(annotations, podBackupHookCommandAnnotationKey, phase)
|
||||
if commandValue == "" {
|
||||
return nil
|
||||
}
|
||||
var command []string
|
||||
|
@ -141,13 +176,15 @@ func getPodExecHookFromAnnotations(annotations map[string]string) *api.ExecHook
|
|||
command = append(command, commandValue)
|
||||
}
|
||||
|
||||
onError := api.HookErrorMode(annotations[podBackupHookOnErrorAnnotationKey])
|
||||
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 := annotations[podBackupHookTimeoutAnnotationKey]
|
||||
timeoutString := getHookAnnotation(annotations, podBackupHookTimeoutAnnotationKey, phase)
|
||||
if timeoutString != "" {
|
||||
if temp, err := time.ParseDuration(timeoutString); err == nil {
|
||||
timeout = temp
|
||||
|
@ -169,7 +206,8 @@ type resourceHook struct {
|
|||
namespaces *collections.IncludesExcludes
|
||||
resources *collections.IncludesExcludes
|
||||
labelSelector labels.Selector
|
||||
hooks []api.BackupResourceHook
|
||||
pre []api.BackupResourceHook
|
||||
post []api.BackupResourceHook
|
||||
}
|
||||
|
||||
func (r resourceHook) applicableTo(groupResource schema.GroupResource, namespace string, labels labels.Set) bool {
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -38,8 +39,8 @@ type mockItemHookHandler struct {
|
|||
mock.Mock
|
||||
}
|
||||
|
||||
func (h *mockItemHookHandler) handleHooks(log logrus.FieldLogger, groupResource schema.GroupResource, obj runtime.Unstructured, resourceHooks []resourceHook) error {
|
||||
args := h.Called(log, groupResource, obj, resourceHooks)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -102,7 +103,7 @@ func TestHandleHooksSkips(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "missing exec hook",
|
||||
hooks: []v1.BackupResourceHook{
|
||||
pre: []v1.BackupResourceHook{
|
||||
{},
|
||||
{},
|
||||
},
|
||||
|
@ -121,15 +122,16 @@ func TestHandleHooksSkips(t *testing.T) {
|
|||
}
|
||||
|
||||
groupResource := schema.ParseGroupResource(test.groupResource)
|
||||
err := h.handleHooks(arktest.NewLogger(), groupResource, test.item, test.hooks)
|
||||
err := h.handleHooks(arktest.NewLogger(), groupResource, test.item, test.hooks, hookPhasePre)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
||||
func TestHandleHooks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
phase hookPhase
|
||||
groupResource string
|
||||
item runtime.Unstructured
|
||||
hooks []resourceHook
|
||||
|
@ -139,7 +141,8 @@ func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
|||
expectedPodHookError error
|
||||
}{
|
||||
{
|
||||
name: "pod, no annotation, spec (multiple hooks) = run spec",
|
||||
name: "pod, no annotation, spec (multiple pre hooks) = run spec",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
{
|
||||
|
@ -153,24 +156,24 @@ func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
|||
hooks: []resourceHook{
|
||||
{
|
||||
name: "hook1",
|
||||
hooks: []v1.BackupResourceHook{
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "1a",
|
||||
Command: []string{"1a"},
|
||||
Command: []string{"pre-1a"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "1b",
|
||||
Command: []string{"1b"},
|
||||
Command: []string{"pre-1b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hook2",
|
||||
hooks: []v1.BackupResourceHook{
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "2a",
|
||||
|
@ -188,7 +191,58 @@ func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "pod, annotation, no spec = run annotation",
|
||||
name: "pod, no annotation, spec (multiple post hooks) = run spec",
|
||||
phase: hookPhasePost,
|
||||
groupResource: "pods",
|
||||
item: 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: unstructuredOrDie(`
|
||||
{
|
||||
|
@ -208,8 +262,53 @@ func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
|||
Command: []string{"/bin/ls"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pod, annotation (pre), no spec = run annotation",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "ns",
|
||||
"name": "name",
|
||||
"annotations": {
|
||||
"pre.hook.backup.ark.heptio.com/container": "c",
|
||||
"pre.hook.backup.ark.heptio.com/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: unstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "ns",
|
||||
"name": "name",
|
||||
"annotations": {
|
||||
"post.hook.backup.ark.heptio.com/container": "c",
|
||||
"post.hook.backup.ark.heptio.com/command": "/bin/ls"
|
||||
}
|
||||
}
|
||||
}`),
|
||||
expectedPodHook: &v1.ExecHook{
|
||||
Container: "c",
|
||||
Command: []string{"/bin/ls"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pod, annotation & spec = run annotation",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
{
|
||||
|
@ -231,7 +330,7 @@ func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
|||
hooks: []resourceHook{
|
||||
{
|
||||
name: "hook1",
|
||||
hooks: []v1.BackupResourceHook{
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "1a",
|
||||
|
@ -244,6 +343,7 @@ func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "pod, annotation, onError=fail = return error",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
{
|
||||
|
@ -269,6 +369,7 @@ func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "pod, annotation, onError=continue = return nil",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
{
|
||||
|
@ -294,6 +395,7 @@ func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "pod, spec, onError=fail = don't run other hooks",
|
||||
phase: hookPhasePre,
|
||||
groupResource: "pods",
|
||||
item: unstructuredOrDie(`
|
||||
{
|
||||
|
@ -307,7 +409,7 @@ func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
|||
hooks: []resourceHook{
|
||||
{
|
||||
name: "hook1",
|
||||
hooks: []v1.BackupResourceHook{
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "1a",
|
||||
|
@ -325,7 +427,7 @@ func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "hook2",
|
||||
hooks: []v1.BackupResourceHook{
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "2",
|
||||
|
@ -337,7 +439,7 @@ func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "hook3",
|
||||
hooks: []v1.BackupResourceHook{
|
||||
pre: []v1.BackupResourceHook{
|
||||
{
|
||||
Exec: &v1.ExecHook{
|
||||
Container: "3",
|
||||
|
@ -369,7 +471,14 @@ func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
|||
} else {
|
||||
hookLoop:
|
||||
for _, resourceHook := range test.hooks {
|
||||
for _, hook := range resourceHook.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 {
|
||||
|
@ -380,7 +489,7 @@ func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
|||
}
|
||||
|
||||
groupResource := schema.ParseGroupResource(test.groupResource)
|
||||
err := h.handleHooks(arktest.NewLogger(), groupResource, test.item, test.hooks)
|
||||
err := h.handleHooks(arktest.NewLogger(), groupResource, test.item, test.hooks, test.phase)
|
||||
|
||||
if test.expectedError != nil {
|
||||
assert.EqualError(t, err, test.expectedError.Error())
|
||||
|
@ -393,103 +502,106 @@ func TestHandleHooksPodFromPodAnnotation(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetPodExecHookFromAnnotations(t *testing.T) {
|
||||
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{
|
||||
podBackupHookCommandAnnotationKey: "[blarg",
|
||||
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,
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"[blarg"},
|
||||
{
|
||||
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{
|
||||
podBackupHookCommandAnnotationKey: `["a","b","c"]`,
|
||||
{
|
||||
name: "valid command json array",
|
||||
annotations: map[string]string{
|
||||
phasedKey(phase, podBackupHookCommandAnnotationKey): `["a","b","c"]`,
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"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: "command as a string",
|
||||
annotations: map[string]string{
|
||||
podBackupHookCommandAnnotationKey: "/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,
|
||||
},
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"/usr/bin/foo"},
|
||||
{
|
||||
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: "hook mode set to continue",
|
||||
annotations: map[string]string{
|
||||
podBackupHookCommandAnnotationKey: "/usr/bin/foo",
|
||||
podBackupHookOnErrorAnnotationKey: string(v1.HookErrorModeContinue),
|
||||
{
|
||||
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},
|
||||
},
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"/usr/bin/foo"},
|
||||
OnError: v1.HookErrorModeContinue,
|
||||
{
|
||||
name: "invalid timeout is ignored",
|
||||
annotations: map[string]string{
|
||||
phasedKey(phase, podBackupHookCommandAnnotationKey): "/usr/bin/foo",
|
||||
phasedKey(phase, podBackupHookTimeoutAnnotationKey): "invalid",
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"/usr/bin/foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hook mode set to fail",
|
||||
annotations: map[string]string{
|
||||
podBackupHookCommandAnnotationKey: "/usr/bin/foo",
|
||||
podBackupHookOnErrorAnnotationKey: string(v1.HookErrorModeFail),
|
||||
{
|
||||
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"},
|
||||
},
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"/usr/bin/foo"},
|
||||
OnError: v1.HookErrorModeFail,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "use the specified timeout",
|
||||
annotations: map[string]string{
|
||||
podBackupHookCommandAnnotationKey: "/usr/bin/foo",
|
||||
podBackupHookTimeoutAnnotationKey: "5m3s",
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"/usr/bin/foo"},
|
||||
Timeout: metav1.Duration{Duration: 5*time.Minute + 3*time.Second},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid timeout is ignored",
|
||||
annotations: map[string]string{
|
||||
podBackupHookCommandAnnotationKey: "/usr/bin/foo",
|
||||
podBackupHookTimeoutAnnotationKey: "invalid",
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Command: []string{"/usr/bin/foo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "use the specified container",
|
||||
annotations: map[string]string{
|
||||
podBackupHookContainerAnnotationKey: "some-container",
|
||||
podBackupHookCommandAnnotationKey: "/usr/bin/foo",
|
||||
},
|
||||
expectedHook: &v1.ExecHook{
|
||||
Container: "some-container",
|
||||
Command: []string{"/usr/bin/foo"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
hook := getPodExecHookFromAnnotations(test.annotations)
|
||||
assert.Equal(t, test.expectedHook, hook)
|
||||
})
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%s (phase=%q)", test.name, phase), func(t *testing.T) {
|
||||
hook := getPodExecHookFromAnnotations(test.annotations, phase)
|
||||
assert.Equal(t, test.expectedHook, hook)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
Copyright 2017 Heptio Inc.
|
||||
|
||||
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 (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/heptio/ark/pkg/apis/ark/v1"
|
||||
"github.com/heptio/ark/pkg/util/collections"
|
||||
)
|
||||
|
||||
// podAction implements ItemAction.
|
||||
type podAction struct {
|
||||
log logrus.FieldLogger
|
||||
}
|
||||
|
||||
// NewPodAction creates a new ItemAction for pods.
|
||||
func NewPodAction(log logrus.FieldLogger) ItemAction {
|
||||
return &podAction{log: log}
|
||||
}
|
||||
|
||||
var pvcGroupResource = schema.GroupResource{Group: "", Resource: "persistentvolumeclaims"}
|
||||
|
||||
// AppliesTo returns a ResourceSelector that applies only to pods.
|
||||
func (a *podAction) AppliesTo() (ResourceSelector, error) {
|
||||
return ResourceSelector{
|
||||
IncludedResources: []string{"pods"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Execute scans the pod's spec.volumes for persistentVolumeClaim volumes and returns a
|
||||
// ResourceIdentifier list containing references to all of the persistentVolumeClaim volumes used by
|
||||
// the pod. This ensures that when a pod is backed up, all referenced PVCs are backed up too.
|
||||
func (a *podAction) Execute(item runtime.Unstructured, backup *v1.Backup) (runtime.Unstructured, []ResourceIdentifier, error) {
|
||||
a.log.Info("Executing podAction")
|
||||
defer a.log.Info("Done executing podAction")
|
||||
|
||||
pod := item.UnstructuredContent()
|
||||
if !collections.Exists(pod, "spec.volumes") {
|
||||
a.log.Info("pod has no volumes")
|
||||
return item, nil, nil
|
||||
}
|
||||
|
||||
metadata, err := meta.Accessor(item)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "unable to access pod metadata")
|
||||
}
|
||||
|
||||
volumes, err := collections.GetSlice(pod, "spec.volumes")
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "error getting spec.volumes")
|
||||
}
|
||||
|
||||
var errs []error
|
||||
var additionalItems []ResourceIdentifier
|
||||
|
||||
for i := range volumes {
|
||||
volume, ok := volumes[i].(map[string]interface{})
|
||||
if !ok {
|
||||
errs = append(errs, errors.Errorf("unexpected type %T", volumes[i]))
|
||||
continue
|
||||
}
|
||||
if !collections.Exists(volume, "persistentVolumeClaim.claimName") {
|
||||
continue
|
||||
}
|
||||
|
||||
claimName, err := collections.GetString(volume, "persistentVolumeClaim.claimName")
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
a.log.Infof("Adding pvc %s to additionalItems", claimName)
|
||||
|
||||
additionalItems = append(additionalItems, ResourceIdentifier{
|
||||
GroupResource: pvcGroupResource,
|
||||
Namespace: metadata.GetNamespace(),
|
||||
Name: claimName,
|
||||
})
|
||||
}
|
||||
|
||||
return item, additionalItems, nil
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
Copyright 2018 the Heptio Ark 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 (
|
||||
"testing"
|
||||
|
||||
arktest "github.com/heptio/ark/pkg/util/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func TestPodActionAppliesTo(t *testing.T) {
|
||||
a := NewPodAction(arktest.NewLogger())
|
||||
|
||||
actual, err := a.AppliesTo()
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := ResourceSelector{
|
||||
IncludedResources: []string{"pods"},
|
||||
}
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestPodActionExecute(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pod runtime.Unstructured
|
||||
expected []ResourceIdentifier
|
||||
}{
|
||||
{
|
||||
name: "no spec.volumes",
|
||||
pod: unstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "foo",
|
||||
"name": "bar"
|
||||
}
|
||||
}
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "persistentVolumeClaim without claimName",
|
||||
pod: unstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "foo",
|
||||
"name": "bar"
|
||||
},
|
||||
"spec": {
|
||||
"volumes": [
|
||||
{
|
||||
"persistentVolumeClaim": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "full test, mix of volume types",
|
||||
pod: unstructuredOrDie(`
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": "foo",
|
||||
"name": "bar"
|
||||
},
|
||||
"spec": {
|
||||
"volumes": [
|
||||
{
|
||||
"persistentVolumeClaim": {}
|
||||
},
|
||||
{
|
||||
"emptyDir": {}
|
||||
},
|
||||
{
|
||||
"persistentVolumeClaim": {"claimName": "claim1"}
|
||||
},
|
||||
{
|
||||
"emptyDir": {}
|
||||
},
|
||||
{
|
||||
"persistentVolumeClaim": {"claimName": "claim2"}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`),
|
||||
expected: []ResourceIdentifier{
|
||||
{GroupResource: pvcGroupResource, Namespace: "foo", Name: "claim1"},
|
||||
{GroupResource: pvcGroupResource, Namespace: "foo", Name: "claim2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
a := NewPodAction(arktest.NewLogger())
|
||||
|
||||
updated, additionalItems, err := a.Execute(test.pod, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.pod, updated)
|
||||
assert.Equal(t, test.expected, additionalItems)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -556,7 +556,8 @@ func TestBackupResourceOnlyIncludesSpecifiedNamespaces(t *testing.T) {
|
|||
ns1 := unstructuredOrDie(`{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"ns-1"}}`)
|
||||
client.On("Get", "ns-1", metav1.GetOptions{}).Return(ns1, nil)
|
||||
|
||||
itemHookHandler.On("handleHooks", mock.Anything, schema.GroupResource{Group: "", Resource: "namespaces"}, ns1, resourceHooks).Return(nil)
|
||||
itemHookHandler.On("handleHooks", mock.Anything, schema.GroupResource{Group: "", Resource: "namespaces"}, ns1, resourceHooks, hookPhasePre).Return(nil)
|
||||
itemHookHandler.On("handleHooks", mock.Anything, schema.GroupResource{Group: "", Resource: "namespaces"}, ns1, resourceHooks, hookPhasePost).Return(nil)
|
||||
|
||||
err := rb.backupResource(v1Group, namespacesResource)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -46,7 +46,8 @@ func NewCommand() *cobra.Command {
|
|||
}
|
||||
|
||||
backupItemActions := map[string]backup.ItemAction{
|
||||
"pv": backup.NewBackupPVAction(logger),
|
||||
"pv": backup.NewBackupPVAction(logger),
|
||||
"pod": backup.NewPodAction(logger),
|
||||
}
|
||||
|
||||
restoreItemActions := map[string]restore.ItemAction{
|
||||
|
|
|
@ -181,9 +181,10 @@ func (m *manager) registerPlugins() error {
|
|||
m.pluginRegistry.register(provider, "/ark", []string{"run-plugin", "cloudprovider", provider}, PluginKindObjectStore, PluginKindBlockStore)
|
||||
}
|
||||
m.pluginRegistry.register("pv", "/ark", []string{"run-plugin", string(PluginKindBackupItemAction), "pv"}, PluginKindBackupItemAction)
|
||||
m.pluginRegistry.register("backup-pod", "/ark", []string{"run-plugin", string(PluginKindBackupItemAction), "pod"}, PluginKindBackupItemAction)
|
||||
|
||||
m.pluginRegistry.register("job", "/ark", []string{"run-plugin", string(PluginKindRestoreItemAction), "job"}, PluginKindRestoreItemAction)
|
||||
m.pluginRegistry.register("pod", "/ark", []string{"run-plugin", string(PluginKindRestoreItemAction), "pod"}, PluginKindRestoreItemAction)
|
||||
m.pluginRegistry.register("restore-pod", "/ark", []string{"run-plugin", string(PluginKindRestoreItemAction), "pod"}, PluginKindRestoreItemAction)
|
||||
m.pluginRegistry.register("svc", "/ark", []string{"run-plugin", string(PluginKindRestoreItemAction), "svc"}, PluginKindRestoreItemAction)
|
||||
|
||||
// second, register external plugins (these will override internal plugins, if applicable)
|
||||
|
|
Loading…
Reference in New Issue