diff --git a/pkg/restore/job_action.go b/pkg/restore/job_action.go index 5e8668fd1..ae70ccf82 100644 --- a/pkg/restore/job_action.go +++ b/pkg/restore/job_action.go @@ -17,11 +17,13 @@ limitations under the License. package restore import ( + "github.com/pkg/errors" "github.com/sirupsen/logrus" + batchv1api "k8s.io/api/batch/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - api "github.com/heptio/velero/pkg/apis/velero/v1" - "github.com/heptio/velero/pkg/util/collections" + velerov1api "github.com/heptio/velero/pkg/apis/velero/v1" ) type jobAction struct { @@ -38,21 +40,21 @@ func (a *jobAction) AppliesTo() (ResourceSelector, error) { }, nil } -func (a *jobAction) Execute(obj runtime.Unstructured, restore *api.Restore) (runtime.Unstructured, error, error) { - fieldDeletions := map[string]string{ - "spec.selector.matchLabels": "controller-uid", - "spec.template.metadata.labels": "controller-uid", +func (a *jobAction) Execute(obj runtime.Unstructured, restore *velerov1api.Restore) (runtime.Unstructured, error, error) { + job := new(batchv1api.Job) + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), job); err != nil { + return nil, nil, errors.WithStack(err) } - for k, v := range fieldDeletions { - a.logger.Debugf("Getting %s", k) - labels, err := collections.GetMap(obj.UnstructuredContent(), k) - if err != nil { - a.logger.WithError(err).Debugf("Unable to get %s", k) - } else { - delete(labels, v) - } + if job.Spec.Selector != nil { + delete(job.Spec.Selector.MatchLabels, "controller-uid") + } + delete(job.Spec.Template.ObjectMeta.Labels, "controller-uid") + + res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(job) + if err != nil { + return nil, nil, errors.WithStack(err) } - return obj, nil, nil + return &unstructured.Unstructured{Object: res}, nil, nil } diff --git a/pkg/restore/job_action_test.go b/pkg/restore/job_action_test.go index d0096b892..0570935ae 100644 --- a/pkg/restore/job_action_test.go +++ b/pkg/restore/job_action_test.go @@ -20,6 +20,11 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + batchv1api "k8s.io/api/batch/v1" + corev1api "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" velerotest "github.com/heptio/velero/pkg/util/test" @@ -28,95 +33,100 @@ import ( func TestJobActionExecute(t *testing.T) { tests := []struct { name string - obj runtime.Unstructured + obj batchv1api.Job expectedErr bool - expectedRes runtime.Unstructured + expectedRes batchv1api.Job }{ { name: "missing spec.selector and/or spec.template should not error", - obj: NewTestUnstructured().WithName("job-1"). - WithSpec(). - Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("job-1"). - WithSpec(). - Unstructured, + obj: batchv1api.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "job-1"}, + }, + expectedRes: batchv1api.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "job-1"}, + }, }, { name: "missing spec.selector.matchLabels should not error", - obj: NewTestUnstructured().WithName("job-1"). - WithSpecField("selector", map[string]interface{}{}). - Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("job-1"). - WithSpecField("selector", map[string]interface{}{}). - Unstructured, + obj: batchv1api.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "job-1"}, + Spec: batchv1api.JobSpec{ + Selector: new(metav1.LabelSelector), + }, + }, + expectedRes: batchv1api.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "job-1"}, + Spec: batchv1api.JobSpec{ + Selector: new(metav1.LabelSelector), + }, + }, }, { name: "spec.selector.matchLabels[controller-uid] is removed", - obj: NewTestUnstructured().WithName("job-1"). - WithSpecField("selector", map[string]interface{}{ - "matchLabels": map[string]interface{}{ - "controller-uid": "foo", - "hello": "world", - }, - }). - Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("job-1"). - WithSpecField("selector", map[string]interface{}{ - "matchLabels": map[string]interface{}{ - "hello": "world", - }, - }). - Unstructured, - }, - { - name: "missing spec.template.metadata should not error", - obj: NewTestUnstructured().WithName("job-1"). - WithSpecField("template", map[string]interface{}{}). - Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("job-1"). - WithSpecField("template", map[string]interface{}{}). - Unstructured, - }, - { - name: "missing spec.template.metadata.labels should not error", - obj: NewTestUnstructured().WithName("job-1"). - WithSpecField("template", map[string]interface{}{ - "metadata": map[string]interface{}{}, - }). - Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("job-1"). - WithSpecField("template", map[string]interface{}{ - "metadata": map[string]interface{}{}, - }). - Unstructured, - }, - { - name: "spec.template.metadata.labels[controller-uid] is removed", - obj: NewTestUnstructured().WithName("job-1"). - WithSpecField("template", map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ + obj: batchv1api.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "job-1"}, + Spec: batchv1api.JobSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ "controller-uid": "foo", "hello": "world", }, }, - }). - Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("job-1"). - WithSpecField("template", map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ + }, + }, + expectedRes: batchv1api.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "job-1"}, + Spec: batchv1api.JobSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ "hello": "world", }, }, - }). - Unstructured, + }, + }, + }, + { + name: "missing spec.template.metadata.labels should not error", + obj: batchv1api.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "job-1"}, + Spec: batchv1api.JobSpec{ + Template: corev1api.PodTemplateSpec{}, + }, + }, + expectedRes: batchv1api.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "job-1"}, + Spec: batchv1api.JobSpec{ + Template: corev1api.PodTemplateSpec{}, + }, + }, + }, + { + name: "spec.template.metadata.labels[controller-uid] is removed", + obj: batchv1api.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "job-1"}, + Spec: batchv1api.JobSpec{ + Template: corev1api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "controller-uid": "foo", + "hello": "world", + }, + }, + }, + }, + }, + expectedRes: batchv1api.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "job-1"}, + Spec: batchv1api.JobSpec{ + Template: corev1api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "hello": "world", + }, + }, + }, + }, + }, }, } @@ -124,10 +134,16 @@ func TestJobActionExecute(t *testing.T) { t.Run(test.name, func(t *testing.T) { action := NewJobAction(velerotest.NewLogger()) - res, _, err := action.Execute(test.obj, nil) + unstructuredJob, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&test.obj) + require.NoError(t, err) + + res, _, err := action.Execute(&unstructured.Unstructured{Object: unstructuredJob}, nil) if assert.Equal(t, test.expectedErr, err != nil) { - assert.Equal(t, test.expectedRes, res) + var job batchv1api.Job + require.NoError(t, runtime.DefaultUnstructuredConverter.FromUnstructured(res.UnstructuredContent(), &job)) + + assert.Equal(t, test.expectedRes, job) } }) } diff --git a/pkg/restore/pod_action.go b/pkg/restore/pod_action.go index c7748c412..21bc6044a 100644 --- a/pkg/restore/pod_action.go +++ b/pkg/restore/pod_action.go @@ -19,11 +19,13 @@ package restore import ( "strings" + "github.com/pkg/errors" "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" api "github.com/heptio/velero/pkg/apis/velero/v1" - "github.com/heptio/velero/pkg/util/collections" ) type podAction struct { @@ -41,94 +43,213 @@ func (a *podAction) AppliesTo() (ResourceSelector, error) { } func (a *podAction) Execute(obj runtime.Unstructured, restore *api.Restore) (runtime.Unstructured, error, error) { - a.logger.Debug("getting spec") - spec, err := collections.GetMap(obj.UnstructuredContent(), "spec") - if err != nil { - return nil, nil, err + pod := new(v1.Pod) + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), pod); err != nil { + return nil, nil, errors.WithStack(err) } - a.logger.Debug("deleting spec.NodeName") - delete(spec, "nodeName") - - a.logger.Debug("deleting spec.priority") - delete(spec, "priority") + pod.Spec.NodeName = "" + pod.Spec.Priority = nil // if there are no volumes, then there can't be any volume mounts, so we're done. - if !collections.Exists(spec, "volumes") { - return obj, nil, nil - } - - serviceAccountName, err := collections.GetString(spec, "serviceAccountName") - if err != nil { - return nil, nil, err - } - prefix := serviceAccountName + "-token-" - - // remove the service account token from volumes - a.logger.Debug("iterating over volumes") - if err := removeItemsWithNamePrefix(spec, "volumes", prefix, a.logger); err != nil { - return nil, nil, err - } - - // remove the service account token volume mount from all containers - a.logger.Debug("iterating over containers") - if err := removeVolumeMounts(spec, "containers", prefix, a.logger); err != nil { - return nil, nil, err - } - - if !collections.Exists(spec, "initContainers") { - return obj, nil, nil - } - - // remove the service account token volume mount from all init containers - a.logger.Debug("iterating over init containers") - if err := removeVolumeMounts(spec, "initContainers", prefix, a.logger); err != nil { - return nil, nil, err - } - - return obj, nil, nil -} - -// removeItemsWithNamePrefix iterates through the collection stored at 'key' in 'unstructuredObj' -// and removes any item that has a name that starts with 'prefix'. -func removeItemsWithNamePrefix(unstructuredObj map[string]interface{}, key, prefix string, log logrus.FieldLogger) error { - var preservedItems []interface{} - - if err := collections.ForEach(unstructuredObj, key, func(item map[string]interface{}) error { - name, err := collections.GetString(item, "name") + if len(pod.Spec.Volumes) == 0 { + res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pod) if err != nil { - return err + return nil, nil, errors.WithStack(err) } - - singularKey := strings.TrimSuffix(key, "s") - log := log.WithField(singularKey, name) - - log.Debug("Checking " + singularKey) - switch { - case strings.HasPrefix(name, prefix): - log.Debug("Excluding ", singularKey) - default: - log.Debug("Preserving ", singularKey) - preservedItems = append(preservedItems, item) - } - - return nil - }); err != nil { - return err + return &unstructured.Unstructured{Object: res}, nil, nil } - unstructuredObj[key] = preservedItems - return nil -} + serviceAccountTokenPrefix := pod.Spec.ServiceAccountName + "-token-" -// removeVolumeMounts iterates through a slice of containers stored at 'containersKey' in -// 'podSpec' and removes any volume mounts with a name starting with 'prefix'. -func removeVolumeMounts(podSpec map[string]interface{}, containersKey, prefix string, log logrus.FieldLogger) error { - return collections.ForEach(podSpec, containersKey, func(container map[string]interface{}) error { - if !collections.Exists(container, "volumeMounts") { - return nil + var preservedVolumes []v1.Volume + for _, vol := range pod.Spec.Volumes { + if !strings.HasPrefix(vol.Name, serviceAccountTokenPrefix) { + preservedVolumes = append(preservedVolumes, vol) } + } + pod.Spec.Volumes = preservedVolumes - return removeItemsWithNamePrefix(container, "volumeMounts", prefix, log) - }) + for i, container := range pod.Spec.Containers { + var preservedVolumeMounts []v1.VolumeMount + for _, mount := range container.VolumeMounts { + if !strings.HasPrefix(mount.Name, serviceAccountTokenPrefix) { + preservedVolumeMounts = append(preservedVolumeMounts, mount) + } + } + pod.Spec.Containers[i].VolumeMounts = preservedVolumeMounts + } + + for i, container := range pod.Spec.InitContainers { + var preservedVolumeMounts []v1.VolumeMount + for _, mount := range container.VolumeMounts { + if !strings.HasPrefix(mount.Name, serviceAccountTokenPrefix) { + preservedVolumeMounts = append(preservedVolumeMounts, mount) + } + } + pod.Spec.InitContainers[i].VolumeMounts = preservedVolumeMounts + } + + res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pod) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + return &unstructured.Unstructured{Object: res}, nil, nil } + +// func (a *podAction) Execute(obj runtime.Unstructured, restore *api.Restore) (runtime.Unstructured, error, error) { +// unstructured.RemoveNestedField(obj.UnstructuredContent(), "spec", "nodeName") +// unstructured.RemoveNestedField(obj.UnstructuredContent(), "spec", "priority") + +// // if there are no volumes, then there can't be any volume mounts, so we're done. +// res, found, err := unstructured.NestedFieldNoCopy(obj.UnstructuredContent(), "spec", "volumes") +// if err != nil { +// return nil, nil, errors.WithStack(err) +// } +// if !found { +// return obj, nil, nil +// } +// volumes, ok := res.([]interface{}) +// if !ok { +// return nil, nil, errors.Errorf("unexpected type for .spec.volumes %T", res) +// } + +// serviceAccountName, found, err := unstructured.NestedString(obj.UnstructuredContent(), "spec", "serviceAccountName") +// if err != nil { +// return nil, nil, errors.WithStack(err) +// } +// if !found { +// return nil, nil, errors.New(".spec.serviceAccountName not found") +// } + +// prefix := serviceAccountName + "-token-" + +// var preservedVolumes []interface{} +// for _, obj := range volumes { +// volume, ok := obj.(map[string]interface{}) +// if !ok { +// return nil, nil, errors.Errorf("unexpected type for volume %T", obj) +// } + +// name, found, err := unstructured.NestedString(volume, "name") +// if err != nil { +// return nil, nil, errors.WithStack(err) +// } +// if !found { +// return nil, nil, errors.New("no name found for volume") +// } + +// if !strings.HasPrefix(name, prefix) { +// preservedVolumes = append(preservedVolumes, volume) +// } +// } + +// if err := unstructured.SetNestedSlice(obj.UnstructuredContent(), preservedVolumes, "spec", "volumes"); err != nil { +// return nil, nil, errors.WithStack(err) +// } + +// containers, err := nestedSliceRef(obj.UnstructuredContent(), "spec", "containers") +// if err != nil { +// return nil, nil, err +// } + +// for _, obj := range containers { +// container, ok := obj.(map[string]interface{}) +// if !ok { +// return nil, nil, errors.Errorf("unexpected type for container %T", obj) +// } + +// volumeMounts, err := nestedSliceRef(container, "volumeMounts") +// if err != nil { +// return nil, nil, err +// } + +// var preservedVolumeMounts []interface{} +// for _, obj := range volumeMounts { +// mount, ok := obj.(map[string]interface{}) +// if !ok { +// return nil, nil, errors.Errorf("unexpected type for volume mount %T", obj) +// } + +// name, found, err := unstructured.NestedString(mount, "name") +// if err != nil { +// return nil, nil, errors.WithStack(err) +// } +// if !found { +// return nil, nil, errors.New("no name found for volume mount") +// } + +// if !strings.HasPrefix(name, prefix) { +// preservedVolumeMounts = append(preservedVolumeMounts, mount) +// } +// } + +// container["volumeMounts"] = preservedVolumeMounts +// } + +// res, found, err = unstructured.NestedFieldNoCopy(obj.UnstructuredContent(), "spec", "initContainers") +// if err != nil { +// return nil, nil, errors.WithStack(err) +// } +// if !found { +// return obj, nil, nil +// } +// initContainers, ok := res.([]interface{}) +// if !ok { +// return nil, nil, errors.Errorf("unexpected type for .spec.initContainers %T", res) +// } + +// for _, obj := range initContainers { +// initContainer, ok := obj.(map[string]interface{}) +// if !ok { +// return nil, nil, errors.Errorf("unexpected type for init container %T", obj) +// } + +// volumeMounts, err := nestedSliceRef(initContainer, "volumeMounts") +// if err != nil { +// return nil, nil, err +// } + +// var preservedVolumeMounts []interface{} +// for _, obj := range volumeMounts { +// mount, ok := obj.(map[string]interface{}) +// if !ok { +// return nil, nil, errors.Errorf("unexpected type for volume mount %T", obj) +// } + +// name, found, err := unstructured.NestedString(mount, "name") +// if err != nil { +// return nil, nil, errors.WithStack(err) +// } +// if !found { +// return nil, nil, errors.New("no name found for volume mount") +// } + +// if !strings.HasPrefix(name, prefix) { +// preservedVolumeMounts = append(preservedVolumeMounts, mount) +// } +// } + +// initContainer["volumeMounts"] = preservedVolumeMounts +// } + +// return obj, nil, nil +// } + +// func nestedSliceRef(obj map[string]interface{}, fields ...string) ([]interface{}, error) { +// val, found, err := unstructured.NestedFieldNoCopy(obj, fields...) +// if err != nil { +// return nil, errors.WithStack(err) +// } +// if !found { +// return nil, errors.Errorf(".%s not found", strings.Join(fields, ".")) +// } + +// slice, ok := val.([]interface{}) +// if !ok { +// return nil, errors.Errorf("unexpected type for .%s %T", strings.Join(fields, "."), val) +// } + +// return slice, nil +// } diff --git a/pkg/restore/pod_action_test.go b/pkg/restore/pod_action_test.go index 1a5409000..02cfdbe63 100644 --- a/pkg/restore/pod_action_test.go +++ b/pkg/restore/pod_action_test.go @@ -20,148 +20,173 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1api "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" velerotest "github.com/heptio/velero/pkg/util/test" ) func TestPodActionExecute(t *testing.T) { + var priority int32 = 1 + tests := []struct { name string - obj runtime.Unstructured + obj corev1api.Pod expectedErr bool - expectedRes runtime.Unstructured + expectedRes corev1api.Pod }{ - { - name: "no spec should error", - obj: NewTestUnstructured().WithName("pod-1").Unstructured, - expectedErr: true, - }, { name: "nodeName (only) should be deleted from spec", - obj: NewTestUnstructured().WithName("pod-1").WithSpec("nodeName", "foo"). - WithSpecField("containers", []interface{}{}). - Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("pod-1").WithSpec("foo"). - WithSpecField("containers", []interface{}{}). - Unstructured, + obj: corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}, + Spec: corev1api.PodSpec{ + NodeName: "foo", + ServiceAccountName: "bar", + }, + }, + expectedRes: corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}, + Spec: corev1api.PodSpec{ + ServiceAccountName: "bar", + }, + }, }, { name: "priority (only) should be deleted from spec", - obj: NewTestUnstructured().WithName("pod-1").WithSpec("priority", "foo"). - WithSpecField("containers", []interface{}{}). - Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("pod-1").WithSpec("foo"). - WithSpecField("containers", []interface{}{}). - Unstructured, + obj: corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}, + Spec: corev1api.PodSpec{ + Priority: &priority, + ServiceAccountName: "bar", + }, + }, + expectedRes: corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}, + Spec: corev1api.PodSpec{ + ServiceAccountName: "bar", + }, + }, }, { name: "volumes matching prefix -token- should be deleted", - obj: NewTestUnstructured().WithName("pod-1"). - WithSpec("serviceAccountName", "foo"). - WithSpecField("volumes", []interface{}{ - map[string]interface{}{"name": "foo"}, - map[string]interface{}{"name": "foo-token-foo"}, - }). - WithSpecField("containers", []interface{}{}). - Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("pod-1"). - WithSpec("serviceAccountName", "foo"). - WithSpecField("volumes", []interface{}{ - map[string]interface{}{"name": "foo"}, - }). - WithSpecField("containers", []interface{}{}). - Unstructured, + obj: corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}, + Spec: corev1api.PodSpec{ + ServiceAccountName: "foo", + Volumes: []corev1api.Volume{ + {Name: "foo"}, + {Name: "foo-token-foo"}, + }, + }, + }, + expectedRes: corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}, + Spec: corev1api.PodSpec{ + ServiceAccountName: "foo", + Volumes: []corev1api.Volume{ + {Name: "foo"}, + }, + }, + }, }, { name: "container volumeMounts matching prefix -token- should be deleted", - obj: NewTestUnstructured().WithName("svc-1"). - WithSpec("serviceAccountName", "foo"). - WithSpecField("volumes", []interface{}{ - map[string]interface{}{"name": "foo"}, - map[string]interface{}{"name": "foo-token-foo"}, - }). - WithSpecField("containers", []interface{}{ - map[string]interface{}{ - "volumeMounts": []interface{}{ - map[string]interface{}{"name": "foo"}, - map[string]interface{}{"name": "foo-token-foo"}, + obj: corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}, + Spec: corev1api.PodSpec{ + ServiceAccountName: "foo", + Volumes: []corev1api.Volume{ + {Name: "foo"}, + {Name: "foo-token-foo"}, + }, + Containers: []corev1api.Container{ + { + VolumeMounts: []corev1api.VolumeMount{ + {Name: "foo"}, + {Name: "foo-token-foo"}, + }, }, }, - }). - Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("svc-1"). - WithSpec("serviceAccountName", "foo"). - WithSpecField("volumes", []interface{}{ - map[string]interface{}{"name": "foo"}, - }). - WithSpecField("containers", []interface{}{ - map[string]interface{}{ - "volumeMounts": []interface{}{ - map[string]interface{}{"name": "foo"}, + }, + }, + expectedRes: corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}, + Spec: corev1api.PodSpec{ + ServiceAccountName: "foo", + Volumes: []corev1api.Volume{ + {Name: "foo"}, + }, + Containers: []corev1api.Container{ + { + VolumeMounts: []corev1api.VolumeMount{ + {Name: "foo"}, + }, }, }, - }). - Unstructured, + }, + }, }, { name: "initContainer volumeMounts matching prefix -token- should be deleted", - obj: NewTestUnstructured().WithName("svc-1"). - WithSpec("serviceAccountName", "foo"). - WithSpecField("containers", []interface{}{}). - WithSpecField("volumes", []interface{}{ - map[string]interface{}{"name": "foo"}, - map[string]interface{}{"name": "foo-token-foo"}, - }). - WithSpecField("initContainers", []interface{}{ - map[string]interface{}{ - "volumeMounts": []interface{}{ - map[string]interface{}{"name": "foo"}, - map[string]interface{}{"name": "foo-token-foo"}, + obj: corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}, + Spec: corev1api.PodSpec{ + ServiceAccountName: "foo", + Volumes: []corev1api.Volume{ + {Name: "foo"}, + {Name: "foo-token-foo"}, + }, + InitContainers: []corev1api.Container{ + { + VolumeMounts: []corev1api.VolumeMount{ + {Name: "foo"}, + {Name: "foo-token-foo"}, + }, }, }, - }). - Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("svc-1"). - WithSpec("serviceAccountName", "foo"). - WithSpecField("containers", []interface{}{}). - WithSpecField("volumes", []interface{}{ - map[string]interface{}{"name": "foo"}, - }). - WithSpecField("initContainers", []interface{}{ - map[string]interface{}{ - "volumeMounts": []interface{}{ - map[string]interface{}{"name": "foo"}, + }, + }, + expectedRes: corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}, + Spec: corev1api.PodSpec{ + ServiceAccountName: "foo", + Volumes: []corev1api.Volume{ + {Name: "foo"}, + }, + InitContainers: []corev1api.Container{ + { + VolumeMounts: []corev1api.VolumeMount{ + {Name: "foo"}, + }, }, }, - }). - Unstructured, + }, + }, }, { name: "containers and initContainers with no volume mounts should not error", - obj: NewTestUnstructured().WithName("pod-1"). - WithSpec("serviceAccountName", "foo"). - WithSpecField("volumes", []interface{}{ - map[string]interface{}{"name": "foo"}, - map[string]interface{}{"name": "foo-token-foo"}, - }). - WithSpecField("containers", []interface{}{}). - WithSpecField("initContainers", []interface{}{}). - Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("pod-1"). - WithSpec("serviceAccountName", "foo"). - WithSpecField("volumes", []interface{}{ - map[string]interface{}{"name": "foo"}, - }). - WithSpecField("containers", []interface{}{}). - WithSpecField("initContainers", []interface{}{}). - Unstructured, + obj: corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}, + Spec: corev1api.PodSpec{ + ServiceAccountName: "foo", + Volumes: []corev1api.Volume{ + {Name: "foo"}, + {Name: "foo-token-foo"}, + }, + }, + }, + expectedRes: corev1api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-1"}, + Spec: corev1api.PodSpec{ + ServiceAccountName: "foo", + Volumes: []corev1api.Volume{ + {Name: "foo"}, + }, + }, + }, }, } @@ -169,8 +194,10 @@ func TestPodActionExecute(t *testing.T) { t.Run(test.name, func(t *testing.T) { action := NewPodAction(velerotest.NewLogger()) - res, warning, err := action.Execute(test.obj, nil) + unstructuredPod, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&test.obj) + require.NoError(t, err) + res, warning, err := action.Execute(&unstructured.Unstructured{Object: unstructuredPod}, nil) assert.Nil(t, warning) if test.expectedErr { @@ -179,7 +206,10 @@ func TestPodActionExecute(t *testing.T) { assert.Nil(t, err, "expected no error, got %v", err) } - assert.Equal(t, test.expectedRes, res) + var pod corev1api.Pod + require.NoError(t, runtime.DefaultUnstructuredConverter.FromUnstructured(res.UnstructuredContent(), &pod)) + + assert.Equal(t, test.expectedRes, pod) }) } } diff --git a/pkg/restore/restore.go b/pkg/restore/restore.go index 7ed090db5..ff1d2b854 100644 --- a/pkg/restore/restore.go +++ b/pkg/restore/restore.go @@ -839,21 +839,25 @@ func (ctx *context) restoreResource(resource, namespace, resourcePath string) (a } if groupResource == kuberesource.PersistentVolumeClaims { - spec, err := collections.GetMap(obj.UnstructuredContent(), "spec") - if err != nil { + pvc := new(v1.PersistentVolumeClaim) + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), pvc); err != nil { addToResult(&errs, namespace, err) continue } - if volumeName, exists := spec["volumeName"]; exists && ctx.pvsToProvision.Has(volumeName.(string)) { - ctx.log.Infof("Resetting PersistentVolumeClaim %s/%s for dynamic provisioning because its PV %v has a reclaim policy of Delete", namespace, name, volumeName) + if pvc.Spec.VolumeName != "" && ctx.pvsToProvision.Has(pvc.Spec.VolumeName) { + ctx.log.Infof("Resetting PersistentVolumeClaim %s/%s for dynamic provisioning because its PV %v has a reclaim policy of Delete", namespace, name, pvc.Spec.VolumeName) - delete(spec, "volumeName") + pvc.Spec.VolumeName = "" + delete(pvc.Annotations, "pv.kubernetes.io/bind-completed") + delete(pvc.Annotations, "pv.kubernetes.io/bound-by-controller") - annotations := obj.GetAnnotations() - delete(annotations, "pv.kubernetes.io/bind-completed") - delete(annotations, "pv.kubernetes.io/bound-by-controller") - obj.SetAnnotations(annotations) + res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pvc) + if err != nil { + addToResult(&errs, namespace, err) + continue + } + obj.Object = res } } @@ -992,12 +996,8 @@ func (ctx *context) restoreResource(resource, namespace, resourcePath string) (a } func hasDeleteReclaimPolicy(obj map[string]interface{}) bool { - reclaimPolicy, err := collections.GetString(obj, "spec.persistentVolumeReclaimPolicy") - if err != nil { - return false - } - - return reclaimPolicy == "Delete" + policy, _, _ := unstructured.NestedString(obj, "spec", "persistentVolumeReclaimPolicy") + return policy == string(v1.PersistentVolumeReclaimDelete) } func waitForReady( @@ -1120,9 +1120,13 @@ func (r *pvRestorer) executePVAction(obj *unstructured.Unstructured) (*unstructu return nil, errors.New("PersistentVolume is missing its name") } - spec, err := collections.GetMap(obj.UnstructuredContent(), "spec") - if err != nil { - return nil, errors.WithStack(err) + res, ok := obj.Object["spec"] + if !ok { + return nil, errors.New("spec not found") + } + spec, ok := res.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("spec was of type %T, expected map[string]interface{}", res) } delete(spec, "claimRef") @@ -1177,18 +1181,18 @@ func (r *pvRestorer) executePVAction(obj *unstructured.Unstructured) (*unstructu } func isPVReady(obj runtime.Unstructured) bool { - phase, err := collections.GetString(obj.UnstructuredContent(), "status.phase") - if err != nil { - return false - } - + phase, _, _ := unstructured.NestedString(obj.UnstructuredContent(), "status", "phase") return phase == string(v1.VolumeAvailable) } func resetMetadataAndStatus(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { - metadata, err := collections.GetMap(obj.UnstructuredContent(), "metadata") - if err != nil { - return nil, err + res, ok := obj.Object["metadata"] + if !ok { + return nil, errors.New("metadata not found") + } + metadata, ok := res.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("metadata was of type %T, expected map[string]interface{}", res) } for k := range metadata { diff --git a/pkg/restore/restore_test.go b/pkg/restore/restore_test.go index 475365b9b..3b1d6a254 100644 --- a/pkg/restore/restore_test.go +++ b/pkg/restore/restore_test.go @@ -1054,9 +1054,7 @@ status: pvClient.On("Watch", metav1.ListOptions{}).Return(pvWatch, nil) pvWatchChan := make(chan watch.Event, 1) readyPV := restoredPV.DeepCopy() - readyStatus, err := collections.GetMap(readyPV.Object, "status") - require.NoError(t, err) - readyStatus["phase"] = string(v1.VolumeAvailable) + require.NoError(t, unstructured.SetNestedField(readyPV.UnstructuredContent(), string(v1.VolumeAvailable), "status", "phase")) pvWatchChan <- watch.Event{ Type: watch.Modified, Object: readyPV, @@ -2135,16 +2133,19 @@ func (r *fakeAction) AppliesTo() (ResourceSelector, error) { } func (r *fakeAction) Execute(obj runtime.Unstructured, restore *api.Restore) (runtime.Unstructured, error, error) { - metadata, err := collections.GetMap(obj.UnstructuredContent(), "metadata") + labels, found, err := unstructured.NestedMap(obj.UnstructuredContent(), "metadata", "labels") if err != nil { return nil, nil, err } - - if _, found := metadata["labels"]; !found { - metadata["labels"] = make(map[string]interface{}) + if !found { + labels = make(map[string]interface{}) } - metadata["labels"].(map[string]interface{})["fake-restorer"] = "foo" + labels["fake-restorer"] = "foo" + + if err := unstructured.SetNestedField(obj.UnstructuredContent(), labels, "metadata", "labels"); err != nil { + return nil, nil, err + } unstructuredObj, ok := obj.(*unstructured.Unstructured) if !ok { diff --git a/pkg/restore/service_action.go b/pkg/restore/service_action.go index e425ba1c3..4d59f9fdf 100644 --- a/pkg/restore/service_action.go +++ b/pkg/restore/service_action.go @@ -23,10 +23,11 @@ import ( "github.com/sirupsen/logrus" corev1api "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" api "github.com/heptio/velero/pkg/apis/velero/v1" - "github.com/heptio/velero/pkg/util/collections" ) const annotationLastAppliedConfig = "kubectl.kubernetes.io/last-applied-configuration" @@ -46,20 +47,25 @@ func (a *serviceAction) AppliesTo() (ResourceSelector, error) { } func (a *serviceAction) Execute(obj runtime.Unstructured, restore *api.Restore) (runtime.Unstructured, error, error) { - spec, err := collections.GetMap(obj.UnstructuredContent(), "spec") + service := new(corev1api.Service) + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), service); err != nil { + return nil, nil, errors.WithStack(err) + } + + if service.Spec.ClusterIP != "None" { + service.Spec.ClusterIP = "" + } + + if err := deleteNodePorts(service); err != nil { + return nil, nil, err + } + + res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(service) if err != nil { - return nil, nil, err + return nil, nil, errors.WithStack(err) } - // Since clusterIP is an optional key, we can ignore 'not found' errors. Also assuming it was a string already. - if val, _ := collections.GetString(spec, "clusterIP"); val != "None" { - delete(spec, "clusterIP") - } - - if err := deleteNodePorts(obj, &spec); err != nil { - return nil, nil, err - } - return obj, nil, nil + return &unstructured.Unstructured{Object: res}, nil, nil } func getPreservedPorts(obj runtime.Unstructured) (map[string]bool, error) { @@ -82,31 +88,65 @@ func getPreservedPorts(obj runtime.Unstructured) (map[string]bool, error) { return preservedPorts, nil } -func deleteNodePorts(obj runtime.Unstructured, spec *map[string]interface{}) error { - if serviceType, _ := collections.GetString(*spec, "type"); serviceType == "ExternalName" { +func deleteNodePorts(service *corev1api.Service) error { + if service.Spec.Type == corev1api.ServiceTypeExternalName { return nil } - preservedPorts, err := getPreservedPorts(obj) - if err != nil { - return err + // find any NodePorts whose values were explicitly specified according + // to the last-applied-config annotation. We'll retain these values, and + // clear out any other (presumably auto-assigned) NodePort values. + explicitNodePorts := sets.NewString() + lastAppliedConfig, ok := service.Annotations[annotationLastAppliedConfig] + if ok { + appliedService := new(corev1api.Service) + if err := json.Unmarshal([]byte(lastAppliedConfig), appliedService); err != nil { + return errors.WithStack(err) + } + + for _, port := range appliedService.Spec.Ports { + if port.NodePort > 0 { + explicitNodePorts.Insert(port.Name) + } + } } - ports, err := collections.GetSlice(obj.UnstructuredContent(), "spec.ports") - if err != nil { - return err + for i, port := range service.Spec.Ports { + if !explicitNodePorts.Has(port.Name) { + service.Spec.Ports[i].NodePort = 0 + } } - for _, port := range ports { - p := port.(map[string]interface{}) - var name string - if nameVal, ok := p["name"]; ok { - name = nameVal.(string) - } - if preservedPorts[name] { - continue - } - delete(p, "nodePort") - } return nil + + // preservedPorts, err := getPreservedPorts(obj) + // if err != nil { + // return err + // } + + // res, found, err := unstructured.NestedFieldNoCopy(obj.UnstructuredContent(), "spec", "ports") + // if err != nil { + // return errors.WithStack(err) + // } + // if !found { + // return errors.New(".spec.ports not found") + // } + + // ports, ok := res.([]interface{}) + // if !ok { + // return errors.Errorf("unexpected type for .spec.ports %T", res) + // } + + // for _, port := range ports { + // p := port.(map[string]interface{}) + // var name string + // if nameVal, ok := p["name"]; ok { + // name = nameVal.(string) + // } + // if preservedPorts[name] { + // continue + // } + // delete(p, "nodePort") + // } + // return nil } diff --git a/pkg/restore/service_action_test.go b/pkg/restore/service_action_test.go index 815591ccb..c1f629936 100644 --- a/pkg/restore/service_action_test.go +++ b/pkg/restore/service_action_test.go @@ -21,7 +21,10 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" corev1api "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" velerotest "github.com/heptio/velero/pkg/util/test" @@ -46,136 +49,221 @@ func TestServiceActionExecute(t *testing.T) { tests := []struct { name string - obj runtime.Unstructured + obj corev1api.Service expectedErr bool - expectedRes runtime.Unstructured + expectedRes corev1api.Service }{ { - name: "no spec should error", - obj: NewTestUnstructured().WithName("svc-1").Unstructured, - expectedErr: true, - }, - { - name: "no spec ports should error", - obj: NewTestUnstructured().WithName("svc-1").WithSpec().Unstructured, - expectedErr: true, - }, - { - name: "clusterIP (only) should be deleted from spec", - obj: NewTestUnstructured().WithName("svc-1").WithSpec("clusterIP", "foo").WithSpecField("ports", []interface{}{}).Unstructured, + name: "clusterIP (only) should be deleted from spec", + obj: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + }, + Spec: corev1api.ServiceSpec{ + ClusterIP: "should-be-removed", + LoadBalancerIP: "should-be-kept", + }, + }, expectedErr: false, - expectedRes: NewTestUnstructured().WithName("svc-1").WithSpec("foo").WithSpecField("ports", []interface{}{}).Unstructured, + expectedRes: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + }, + Spec: corev1api.ServiceSpec{ + LoadBalancerIP: "should-be-kept", + }, + }, }, { - name: "headless clusterIP should not be deleted from spec", - obj: NewTestUnstructured().WithName("svc-1").WithSpecField("clusterIP", "None").WithSpecField("ports", []interface{}{}).Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("svc-1").WithSpecField("clusterIP", "None").WithSpecField("ports", []interface{}{}).Unstructured, + name: "headless clusterIP should not be deleted from spec", + obj: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + }, + Spec: corev1api.ServiceSpec{ + ClusterIP: "None", + }, + }, + expectedRes: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + }, + Spec: corev1api.ServiceSpec{ + ClusterIP: "None", + }, + }, }, { name: "nodePort (only) should be deleted from all spec.ports", - obj: NewTestUnstructured().WithName("svc-1"). - WithSpecField("ports", []interface{}{ - map[string]interface{}{"nodePort": ""}, - map[string]interface{}{"nodePort": "", "foo": "bar"}, - }).Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("svc-1"). - WithSpecField("ports", []interface{}{ - map[string]interface{}{}, - map[string]interface{}{"foo": "bar"}, - }).Unstructured, + obj: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + }, + Spec: corev1api.ServiceSpec{ + Ports: []corev1api.ServicePort{ + { + Port: 32000, + NodePort: 32000, + }, + { + Port: 32001, + NodePort: 32001, + }, + }, + }, + }, + expectedRes: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + }, + Spec: corev1api.ServiceSpec{ + Ports: []corev1api.ServicePort{ + { + Port: 32000, + }, + { + Port: 32001, + }, + }, + }, + }, }, { name: "unnamed nodePort should be deleted when missing in annotation", - obj: NewTestUnstructured().WithName("svc-1"). - WithAnnotationValues(map[string]string{ - annotationLastAppliedConfig: svcJSON(), - }). - WithSpecField("ports", []interface{}{ - map[string]interface{}{"nodePort": 8080}, - }).Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("svc-1"). - WithAnnotationValues(map[string]string{ - annotationLastAppliedConfig: svcJSON(), - }). - WithSpecField("ports", []interface{}{ - map[string]interface{}{}, - }).Unstructured, + obj: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Annotations: map[string]string{ + annotationLastAppliedConfig: svcJSON(), + }, + }, + Spec: corev1api.ServiceSpec{ + Ports: []corev1api.ServicePort{ + { + NodePort: 8080, + }, + }, + }, + }, + expectedRes: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Annotations: map[string]string{ + annotationLastAppliedConfig: svcJSON(), + }, + }, + Spec: corev1api.ServiceSpec{ + Ports: []corev1api.ServicePort{ + {}, + }, + }, + }, }, { name: "unnamed nodePort should be preserved when specified in annotation", - obj: NewTestUnstructured().WithName("svc-1"). - WithAnnotationValues(map[string]string{ - annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{NodePort: 8080}), - }). - WithSpecField("ports", []interface{}{ - map[string]interface{}{ - "nodePort": 8080, + obj: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Annotations: map[string]string{ + annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{NodePort: 8080}), }, - }).Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("svc-1"). - WithAnnotationValues(map[string]string{ - annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{NodePort: 8080}), - }). - WithSpecField("ports", []interface{}{ - map[string]interface{}{ - "nodePort": 8080, + }, + Spec: corev1api.ServiceSpec{ + Ports: []corev1api.ServicePort{ + { + NodePort: 8080, + }, }, - }).Unstructured, + }, + }, + expectedRes: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Annotations: map[string]string{ + annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{NodePort: 8080}), + }, + }, + Spec: corev1api.ServiceSpec{ + Ports: []corev1api.ServicePort{ + { + NodePort: 8080, + }, + }, + }, + }, }, { name: "unnamed nodePort should be deleted when named nodePort specified in annotation", - obj: NewTestUnstructured().WithName("svc-1"). - WithAnnotationValues(map[string]string{ - annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}), - }). - WithSpecField("ports", []interface{}{ - map[string]interface{}{ - "nodePort": 8080, + obj: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Annotations: map[string]string{ + annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}), }, - }).Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("svc-1"). - WithAnnotationValues(map[string]string{ - annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}), - }). - WithSpecField("ports", []interface{}{ - map[string]interface{}{}, - }).Unstructured, + }, + Spec: corev1api.ServiceSpec{ + Ports: []corev1api.ServicePort{ + { + NodePort: 8080, + }, + }, + }, + }, + expectedRes: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Annotations: map[string]string{ + annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}), + }, + }, + Spec: corev1api.ServiceSpec{ + Ports: []corev1api.ServicePort{ + {}, + }, + }, + }, }, { name: "named nodePort should be preserved when specified in annotation", - obj: NewTestUnstructured().WithName("svc-1"). - WithAnnotationValues(map[string]string{ - annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}), - }). - WithSpecField("ports", []interface{}{ - map[string]interface{}{ - "name": "http", - "nodePort": 8080, + obj: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Annotations: map[string]string{ + annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}), }, - map[string]interface{}{ - "name": "admin", - "nodePort": 9090, + }, + Spec: corev1api.ServiceSpec{ + Ports: []corev1api.ServicePort{ + { + Name: "http", + NodePort: 8080, + }, + { + Name: "admin", + NodePort: 9090, + }, }, - }).Unstructured, - expectedErr: false, - expectedRes: NewTestUnstructured().WithName("svc-1"). - WithAnnotationValues(map[string]string{ - annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}), - }). - WithSpecField("ports", []interface{}{ - map[string]interface{}{ - "name": "http", - "nodePort": 8080, + }, + }, + expectedRes: corev1api.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Annotations: map[string]string{ + annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}), }, - map[string]interface{}{ - "name": "admin", + }, + Spec: corev1api.ServiceSpec{ + Ports: []corev1api.ServicePort{ + { + Name: "http", + NodePort: 8080, + }, + { + Name: "admin", + }, }, - }).Unstructured, + }, + }, }, } @@ -183,10 +271,16 @@ func TestServiceActionExecute(t *testing.T) { t.Run(test.name, func(t *testing.T) { action := NewServiceAction(velerotest.NewLogger()) - res, _, err := action.Execute(test.obj, nil) + unstructuredSvc, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&test.obj) + require.NoError(t, err) + + res, _, err := action.Execute(&unstructured.Unstructured{Object: unstructuredSvc}, nil) if assert.Equal(t, test.expectedErr, err != nil) { - assert.Equal(t, test.expectedRes, res) + var svc corev1api.Service + require.NoError(t, runtime.DefaultUnstructuredConverter.FromUnstructured(res.UnstructuredContent(), &svc)) + + assert.Equal(t, test.expectedRes, svc) } }) }