pkg/restore: remove usage of pkg/util/collections

Signed-off-by: Steve Kriss <krisss@vmware.com>
pull/1146/head
Steve Kriss 2019-01-07 15:07:53 -07:00
parent e91c841c59
commit 38ad7d71f5
8 changed files with 755 additions and 447 deletions

View File

@ -17,11 +17,13 @@ limitations under the License.
package restore package restore
import ( import (
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
batchv1api "k8s.io/api/batch/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/velero/pkg/apis/velero/v1" velerov1api "github.com/heptio/velero/pkg/apis/velero/v1"
"github.com/heptio/velero/pkg/util/collections"
) )
type jobAction struct { type jobAction struct {
@ -38,21 +40,21 @@ func (a *jobAction) AppliesTo() (ResourceSelector, error) {
}, nil }, nil
} }
func (a *jobAction) Execute(obj runtime.Unstructured, restore *api.Restore) (runtime.Unstructured, error, error) { func (a *jobAction) Execute(obj runtime.Unstructured, restore *velerov1api.Restore) (runtime.Unstructured, error, error) {
fieldDeletions := map[string]string{ job := new(batchv1api.Job)
"spec.selector.matchLabels": "controller-uid", if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), job); err != nil {
"spec.template.metadata.labels": "controller-uid", return nil, nil, errors.WithStack(err)
} }
for k, v := range fieldDeletions { if job.Spec.Selector != nil {
a.logger.Debugf("Getting %s", k) delete(job.Spec.Selector.MatchLabels, "controller-uid")
labels, err := collections.GetMap(obj.UnstructuredContent(), k) }
if err != nil { delete(job.Spec.Template.ObjectMeta.Labels, "controller-uid")
a.logger.WithError(err).Debugf("Unable to get %s", k)
} else { res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(job)
delete(labels, v) if err != nil {
} return nil, nil, errors.WithStack(err)
} }
return obj, nil, nil return &unstructured.Unstructured{Object: res}, nil, nil
} }

View File

@ -20,6 +20,11 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "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" "k8s.io/apimachinery/pkg/runtime"
velerotest "github.com/heptio/velero/pkg/util/test" velerotest "github.com/heptio/velero/pkg/util/test"
@ -28,95 +33,100 @@ import (
func TestJobActionExecute(t *testing.T) { func TestJobActionExecute(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
obj runtime.Unstructured obj batchv1api.Job
expectedErr bool expectedErr bool
expectedRes runtime.Unstructured expectedRes batchv1api.Job
}{ }{
{ {
name: "missing spec.selector and/or spec.template should not error", name: "missing spec.selector and/or spec.template should not error",
obj: NewTestUnstructured().WithName("job-1"). obj: batchv1api.Job{
WithSpec(). ObjectMeta: metav1.ObjectMeta{Name: "job-1"},
Unstructured, },
expectedErr: false, expectedRes: batchv1api.Job{
expectedRes: NewTestUnstructured().WithName("job-1"). ObjectMeta: metav1.ObjectMeta{Name: "job-1"},
WithSpec(). },
Unstructured,
}, },
{ {
name: "missing spec.selector.matchLabels should not error", name: "missing spec.selector.matchLabels should not error",
obj: NewTestUnstructured().WithName("job-1"). obj: batchv1api.Job{
WithSpecField("selector", map[string]interface{}{}). ObjectMeta: metav1.ObjectMeta{Name: "job-1"},
Unstructured, Spec: batchv1api.JobSpec{
expectedErr: false, Selector: new(metav1.LabelSelector),
expectedRes: NewTestUnstructured().WithName("job-1"). },
WithSpecField("selector", map[string]interface{}{}). },
Unstructured, expectedRes: batchv1api.Job{
ObjectMeta: metav1.ObjectMeta{Name: "job-1"},
Spec: batchv1api.JobSpec{
Selector: new(metav1.LabelSelector),
},
},
}, },
{ {
name: "spec.selector.matchLabels[controller-uid] is removed", name: "spec.selector.matchLabels[controller-uid] is removed",
obj: NewTestUnstructured().WithName("job-1"). obj: batchv1api.Job{
WithSpecField("selector", map[string]interface{}{ ObjectMeta: metav1.ObjectMeta{Name: "job-1"},
"matchLabels": map[string]interface{}{ Spec: batchv1api.JobSpec{
"controller-uid": "foo", Selector: &metav1.LabelSelector{
"hello": "world", MatchLabels: map[string]string{
},
}).
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{}{
"controller-uid": "foo", "controller-uid": "foo",
"hello": "world", "hello": "world",
}, },
}, },
}). },
Unstructured, },
expectedErr: false, expectedRes: batchv1api.Job{
expectedRes: NewTestUnstructured().WithName("job-1"). ObjectMeta: metav1.ObjectMeta{Name: "job-1"},
WithSpecField("template", map[string]interface{}{ Spec: batchv1api.JobSpec{
"metadata": map[string]interface{}{ Selector: &metav1.LabelSelector{
"labels": map[string]interface{}{ MatchLabels: map[string]string{
"hello": "world", "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) { t.Run(test.name, func(t *testing.T) {
action := NewJobAction(velerotest.NewLogger()) 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) { 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)
} }
}) })
} }

View File

@ -19,11 +19,13 @@ package restore
import ( import (
"strings" "strings"
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
api "github.com/heptio/velero/pkg/apis/velero/v1" api "github.com/heptio/velero/pkg/apis/velero/v1"
"github.com/heptio/velero/pkg/util/collections"
) )
type podAction struct { 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) { func (a *podAction) Execute(obj runtime.Unstructured, restore *api.Restore) (runtime.Unstructured, error, error) {
a.logger.Debug("getting spec") pod := new(v1.Pod)
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec") if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), pod); err != nil {
if err != nil { return nil, nil, errors.WithStack(err)
return nil, nil, err
} }
a.logger.Debug("deleting spec.NodeName") pod.Spec.NodeName = ""
delete(spec, "nodeName") pod.Spec.Priority = nil
a.logger.Debug("deleting spec.priority")
delete(spec, "priority")
// if there are no volumes, then there can't be any volume mounts, so we're done. // if there are no volumes, then there can't be any volume mounts, so we're done.
if !collections.Exists(spec, "volumes") { if len(pod.Spec.Volumes) == 0 {
return obj, nil, nil res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pod)
}
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 err != nil { if err != nil {
return err return nil, nil, errors.WithStack(err)
} }
return &unstructured.Unstructured{Object: res}, nil, nil
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
} }
unstructuredObj[key] = preservedItems serviceAccountTokenPrefix := pod.Spec.ServiceAccountName + "-token-"
return nil
}
// removeVolumeMounts iterates through a slice of containers stored at 'containersKey' in var preservedVolumes []v1.Volume
// 'podSpec' and removes any volume mounts with a name starting with 'prefix'. for _, vol := range pod.Spec.Volumes {
func removeVolumeMounts(podSpec map[string]interface{}, containersKey, prefix string, log logrus.FieldLogger) error { if !strings.HasPrefix(vol.Name, serviceAccountTokenPrefix) {
return collections.ForEach(podSpec, containersKey, func(container map[string]interface{}) error { preservedVolumes = append(preservedVolumes, vol)
if !collections.Exists(container, "volumeMounts") {
return nil
} }
}
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
// }

View File

@ -20,148 +20,173 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "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" "k8s.io/apimachinery/pkg/runtime"
velerotest "github.com/heptio/velero/pkg/util/test" velerotest "github.com/heptio/velero/pkg/util/test"
) )
func TestPodActionExecute(t *testing.T) { func TestPodActionExecute(t *testing.T) {
var priority int32 = 1
tests := []struct { tests := []struct {
name string name string
obj runtime.Unstructured obj corev1api.Pod
expectedErr bool 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", name: "nodeName (only) should be deleted from spec",
obj: NewTestUnstructured().WithName("pod-1").WithSpec("nodeName", "foo"). obj: corev1api.Pod{
WithSpecField("containers", []interface{}{}). ObjectMeta: metav1.ObjectMeta{Name: "pod-1"},
Unstructured, Spec: corev1api.PodSpec{
expectedErr: false, NodeName: "foo",
expectedRes: NewTestUnstructured().WithName("pod-1").WithSpec("foo"). ServiceAccountName: "bar",
WithSpecField("containers", []interface{}{}). },
Unstructured, },
expectedRes: corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod-1"},
Spec: corev1api.PodSpec{
ServiceAccountName: "bar",
},
},
}, },
{ {
name: "priority (only) should be deleted from spec", name: "priority (only) should be deleted from spec",
obj: NewTestUnstructured().WithName("pod-1").WithSpec("priority", "foo"). obj: corev1api.Pod{
WithSpecField("containers", []interface{}{}). ObjectMeta: metav1.ObjectMeta{Name: "pod-1"},
Unstructured, Spec: corev1api.PodSpec{
expectedErr: false, Priority: &priority,
expectedRes: NewTestUnstructured().WithName("pod-1").WithSpec("foo"). ServiceAccountName: "bar",
WithSpecField("containers", []interface{}{}). },
Unstructured, },
expectedRes: corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod-1"},
Spec: corev1api.PodSpec{
ServiceAccountName: "bar",
},
},
}, },
{ {
name: "volumes matching prefix <service account name>-token- should be deleted", name: "volumes matching prefix <service account name>-token- should be deleted",
obj: NewTestUnstructured().WithName("pod-1"). obj: corev1api.Pod{
WithSpec("serviceAccountName", "foo"). ObjectMeta: metav1.ObjectMeta{Name: "pod-1"},
WithSpecField("volumes", []interface{}{ Spec: corev1api.PodSpec{
map[string]interface{}{"name": "foo"}, ServiceAccountName: "foo",
map[string]interface{}{"name": "foo-token-foo"}, Volumes: []corev1api.Volume{
}). {Name: "foo"},
WithSpecField("containers", []interface{}{}). {Name: "foo-token-foo"},
Unstructured, },
expectedErr: false, },
expectedRes: NewTestUnstructured().WithName("pod-1"). },
WithSpec("serviceAccountName", "foo"). expectedRes: corev1api.Pod{
WithSpecField("volumes", []interface{}{ ObjectMeta: metav1.ObjectMeta{Name: "pod-1"},
map[string]interface{}{"name": "foo"}, Spec: corev1api.PodSpec{
}). ServiceAccountName: "foo",
WithSpecField("containers", []interface{}{}). Volumes: []corev1api.Volume{
Unstructured, {Name: "foo"},
},
},
},
}, },
{ {
name: "container volumeMounts matching prefix <service account name>-token- should be deleted", name: "container volumeMounts matching prefix <service account name>-token- should be deleted",
obj: NewTestUnstructured().WithName("svc-1"). obj: corev1api.Pod{
WithSpec("serviceAccountName", "foo"). ObjectMeta: metav1.ObjectMeta{Name: "pod-1"},
WithSpecField("volumes", []interface{}{ Spec: corev1api.PodSpec{
map[string]interface{}{"name": "foo"}, ServiceAccountName: "foo",
map[string]interface{}{"name": "foo-token-foo"}, Volumes: []corev1api.Volume{
}). {Name: "foo"},
WithSpecField("containers", []interface{}{ {Name: "foo-token-foo"},
map[string]interface{}{ },
"volumeMounts": []interface{}{ Containers: []corev1api.Container{
map[string]interface{}{"name": "foo"}, {
map[string]interface{}{"name": "foo-token-foo"}, VolumeMounts: []corev1api.VolumeMount{
{Name: "foo"},
{Name: "foo-token-foo"},
},
}, },
}, },
}). },
Unstructured, },
expectedErr: false, expectedRes: corev1api.Pod{
expectedRes: NewTestUnstructured().WithName("svc-1"). ObjectMeta: metav1.ObjectMeta{Name: "pod-1"},
WithSpec("serviceAccountName", "foo"). Spec: corev1api.PodSpec{
WithSpecField("volumes", []interface{}{ ServiceAccountName: "foo",
map[string]interface{}{"name": "foo"}, Volumes: []corev1api.Volume{
}). {Name: "foo"},
WithSpecField("containers", []interface{}{ },
map[string]interface{}{ Containers: []corev1api.Container{
"volumeMounts": []interface{}{ {
map[string]interface{}{"name": "foo"}, VolumeMounts: []corev1api.VolumeMount{
{Name: "foo"},
},
}, },
}, },
}). },
Unstructured, },
}, },
{ {
name: "initContainer volumeMounts matching prefix <service account name>-token- should be deleted", name: "initContainer volumeMounts matching prefix <service account name>-token- should be deleted",
obj: NewTestUnstructured().WithName("svc-1"). obj: corev1api.Pod{
WithSpec("serviceAccountName", "foo"). ObjectMeta: metav1.ObjectMeta{Name: "pod-1"},
WithSpecField("containers", []interface{}{}). Spec: corev1api.PodSpec{
WithSpecField("volumes", []interface{}{ ServiceAccountName: "foo",
map[string]interface{}{"name": "foo"}, Volumes: []corev1api.Volume{
map[string]interface{}{"name": "foo-token-foo"}, {Name: "foo"},
}). {Name: "foo-token-foo"},
WithSpecField("initContainers", []interface{}{ },
map[string]interface{}{ InitContainers: []corev1api.Container{
"volumeMounts": []interface{}{ {
map[string]interface{}{"name": "foo"}, VolumeMounts: []corev1api.VolumeMount{
map[string]interface{}{"name": "foo-token-foo"}, {Name: "foo"},
{Name: "foo-token-foo"},
},
}, },
}, },
}). },
Unstructured, },
expectedErr: false, expectedRes: corev1api.Pod{
expectedRes: NewTestUnstructured().WithName("svc-1"). ObjectMeta: metav1.ObjectMeta{Name: "pod-1"},
WithSpec("serviceAccountName", "foo"). Spec: corev1api.PodSpec{
WithSpecField("containers", []interface{}{}). ServiceAccountName: "foo",
WithSpecField("volumes", []interface{}{ Volumes: []corev1api.Volume{
map[string]interface{}{"name": "foo"}, {Name: "foo"},
}). },
WithSpecField("initContainers", []interface{}{ InitContainers: []corev1api.Container{
map[string]interface{}{ {
"volumeMounts": []interface{}{ VolumeMounts: []corev1api.VolumeMount{
map[string]interface{}{"name": "foo"}, {Name: "foo"},
},
}, },
}, },
}). },
Unstructured, },
}, },
{ {
name: "containers and initContainers with no volume mounts should not error", name: "containers and initContainers with no volume mounts should not error",
obj: NewTestUnstructured().WithName("pod-1"). obj: corev1api.Pod{
WithSpec("serviceAccountName", "foo"). ObjectMeta: metav1.ObjectMeta{Name: "pod-1"},
WithSpecField("volumes", []interface{}{ Spec: corev1api.PodSpec{
map[string]interface{}{"name": "foo"}, ServiceAccountName: "foo",
map[string]interface{}{"name": "foo-token-foo"}, Volumes: []corev1api.Volume{
}). {Name: "foo"},
WithSpecField("containers", []interface{}{}). {Name: "foo-token-foo"},
WithSpecField("initContainers", []interface{}{}). },
Unstructured, },
expectedErr: false, },
expectedRes: NewTestUnstructured().WithName("pod-1"). expectedRes: corev1api.Pod{
WithSpec("serviceAccountName", "foo"). ObjectMeta: metav1.ObjectMeta{Name: "pod-1"},
WithSpecField("volumes", []interface{}{ Spec: corev1api.PodSpec{
map[string]interface{}{"name": "foo"}, ServiceAccountName: "foo",
}). Volumes: []corev1api.Volume{
WithSpecField("containers", []interface{}{}). {Name: "foo"},
WithSpecField("initContainers", []interface{}{}). },
Unstructured, },
},
}, },
} }
@ -169,8 +194,10 @@ func TestPodActionExecute(t *testing.T) {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
action := NewPodAction(velerotest.NewLogger()) 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) assert.Nil(t, warning)
if test.expectedErr { if test.expectedErr {
@ -179,7 +206,10 @@ func TestPodActionExecute(t *testing.T) {
assert.Nil(t, err, "expected no error, got %v", err) 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)
}) })
} }
} }

View File

@ -839,21 +839,25 @@ func (ctx *context) restoreResource(resource, namespace, resourcePath string) (a
} }
if groupResource == kuberesource.PersistentVolumeClaims { if groupResource == kuberesource.PersistentVolumeClaims {
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec") pvc := new(v1.PersistentVolumeClaim)
if err != nil { if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), pvc); err != nil {
addToResult(&errs, namespace, err) addToResult(&errs, namespace, err)
continue continue
} }
if volumeName, exists := spec["volumeName"]; exists && ctx.pvsToProvision.Has(volumeName.(string)) { 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, 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() res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pvc)
delete(annotations, "pv.kubernetes.io/bind-completed") if err != nil {
delete(annotations, "pv.kubernetes.io/bound-by-controller") addToResult(&errs, namespace, err)
obj.SetAnnotations(annotations) continue
}
obj.Object = res
} }
} }
@ -992,12 +996,8 @@ func (ctx *context) restoreResource(resource, namespace, resourcePath string) (a
} }
func hasDeleteReclaimPolicy(obj map[string]interface{}) bool { func hasDeleteReclaimPolicy(obj map[string]interface{}) bool {
reclaimPolicy, err := collections.GetString(obj, "spec.persistentVolumeReclaimPolicy") policy, _, _ := unstructured.NestedString(obj, "spec", "persistentVolumeReclaimPolicy")
if err != nil { return policy == string(v1.PersistentVolumeReclaimDelete)
return false
}
return reclaimPolicy == "Delete"
} }
func waitForReady( func waitForReady(
@ -1120,9 +1120,13 @@ func (r *pvRestorer) executePVAction(obj *unstructured.Unstructured) (*unstructu
return nil, errors.New("PersistentVolume is missing its name") return nil, errors.New("PersistentVolume is missing its name")
} }
spec, err := collections.GetMap(obj.UnstructuredContent(), "spec") res, ok := obj.Object["spec"]
if err != nil { if !ok {
return nil, errors.WithStack(err) 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") delete(spec, "claimRef")
@ -1177,18 +1181,18 @@ func (r *pvRestorer) executePVAction(obj *unstructured.Unstructured) (*unstructu
} }
func isPVReady(obj runtime.Unstructured) bool { func isPVReady(obj runtime.Unstructured) bool {
phase, err := collections.GetString(obj.UnstructuredContent(), "status.phase") phase, _, _ := unstructured.NestedString(obj.UnstructuredContent(), "status", "phase")
if err != nil {
return false
}
return phase == string(v1.VolumeAvailable) return phase == string(v1.VolumeAvailable)
} }
func resetMetadataAndStatus(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { func resetMetadataAndStatus(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
metadata, err := collections.GetMap(obj.UnstructuredContent(), "metadata") res, ok := obj.Object["metadata"]
if err != nil { if !ok {
return nil, err 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 { for k := range metadata {

View File

@ -1054,9 +1054,7 @@ status:
pvClient.On("Watch", metav1.ListOptions{}).Return(pvWatch, nil) pvClient.On("Watch", metav1.ListOptions{}).Return(pvWatch, nil)
pvWatchChan := make(chan watch.Event, 1) pvWatchChan := make(chan watch.Event, 1)
readyPV := restoredPV.DeepCopy() readyPV := restoredPV.DeepCopy()
readyStatus, err := collections.GetMap(readyPV.Object, "status") require.NoError(t, unstructured.SetNestedField(readyPV.UnstructuredContent(), string(v1.VolumeAvailable), "status", "phase"))
require.NoError(t, err)
readyStatus["phase"] = string(v1.VolumeAvailable)
pvWatchChan <- watch.Event{ pvWatchChan <- watch.Event{
Type: watch.Modified, Type: watch.Modified,
Object: readyPV, 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) { 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 { if err != nil {
return nil, nil, err return nil, nil, err
} }
if !found {
if _, found := metadata["labels"]; !found { labels = make(map[string]interface{})
metadata["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) unstructuredObj, ok := obj.(*unstructured.Unstructured)
if !ok { if !ok {

View File

@ -23,10 +23,11 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1" corev1api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
api "github.com/heptio/velero/pkg/apis/velero/v1" api "github.com/heptio/velero/pkg/apis/velero/v1"
"github.com/heptio/velero/pkg/util/collections"
) )
const annotationLastAppliedConfig = "kubectl.kubernetes.io/last-applied-configuration" 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) { 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 { 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. return &unstructured.Unstructured{Object: res}, nil, nil
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
} }
func getPreservedPorts(obj runtime.Unstructured) (map[string]bool, error) { 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 return preservedPorts, nil
} }
func deleteNodePorts(obj runtime.Unstructured, spec *map[string]interface{}) error { func deleteNodePorts(service *corev1api.Service) error {
if serviceType, _ := collections.GetString(*spec, "type"); serviceType == "ExternalName" { if service.Spec.Type == corev1api.ServiceTypeExternalName {
return nil return nil
} }
preservedPorts, err := getPreservedPorts(obj) // find any NodePorts whose values were explicitly specified according
if err != nil { // to the last-applied-config annotation. We'll retain these values, and
return err // 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") for i, port := range service.Spec.Ports {
if err != nil { if !explicitNodePorts.Has(port.Name) {
return err 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 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
} }

View File

@ -21,7 +21,10 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/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" "k8s.io/apimachinery/pkg/runtime"
velerotest "github.com/heptio/velero/pkg/util/test" velerotest "github.com/heptio/velero/pkg/util/test"
@ -46,136 +49,221 @@ func TestServiceActionExecute(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
obj runtime.Unstructured obj corev1api.Service
expectedErr bool expectedErr bool
expectedRes runtime.Unstructured expectedRes corev1api.Service
}{ }{
{ {
name: "no spec should error", name: "clusterIP (only) should be deleted from spec",
obj: NewTestUnstructured().WithName("svc-1").Unstructured, obj: corev1api.Service{
expectedErr: true, ObjectMeta: metav1.ObjectMeta{
}, Name: "svc-1",
{ },
name: "no spec ports should error", Spec: corev1api.ServiceSpec{
obj: NewTestUnstructured().WithName("svc-1").WithSpec().Unstructured, ClusterIP: "should-be-removed",
expectedErr: true, LoadBalancerIP: "should-be-kept",
}, },
{ },
name: "clusterIP (only) should be deleted from spec",
obj: NewTestUnstructured().WithName("svc-1").WithSpec("clusterIP", "foo").WithSpecField("ports", []interface{}{}).Unstructured,
expectedErr: false, 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", name: "headless clusterIP should not be deleted from spec",
obj: NewTestUnstructured().WithName("svc-1").WithSpecField("clusterIP", "None").WithSpecField("ports", []interface{}{}).Unstructured, obj: corev1api.Service{
expectedErr: false, ObjectMeta: metav1.ObjectMeta{
expectedRes: NewTestUnstructured().WithName("svc-1").WithSpecField("clusterIP", "None").WithSpecField("ports", []interface{}{}).Unstructured, 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", name: "nodePort (only) should be deleted from all spec.ports",
obj: NewTestUnstructured().WithName("svc-1"). obj: corev1api.Service{
WithSpecField("ports", []interface{}{ ObjectMeta: metav1.ObjectMeta{
map[string]interface{}{"nodePort": ""}, Name: "svc-1",
map[string]interface{}{"nodePort": "", "foo": "bar"}, },
}).Unstructured, Spec: corev1api.ServiceSpec{
expectedErr: false, Ports: []corev1api.ServicePort{
expectedRes: NewTestUnstructured().WithName("svc-1"). {
WithSpecField("ports", []interface{}{ Port: 32000,
map[string]interface{}{}, NodePort: 32000,
map[string]interface{}{"foo": "bar"}, },
}).Unstructured, {
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", name: "unnamed nodePort should be deleted when missing in annotation",
obj: NewTestUnstructured().WithName("svc-1"). obj: corev1api.Service{
WithAnnotationValues(map[string]string{ ObjectMeta: metav1.ObjectMeta{
annotationLastAppliedConfig: svcJSON(), Name: "svc-1",
}). Annotations: map[string]string{
WithSpecField("ports", []interface{}{ annotationLastAppliedConfig: svcJSON(),
map[string]interface{}{"nodePort": 8080}, },
}).Unstructured, },
expectedErr: false, Spec: corev1api.ServiceSpec{
expectedRes: NewTestUnstructured().WithName("svc-1"). Ports: []corev1api.ServicePort{
WithAnnotationValues(map[string]string{ {
annotationLastAppliedConfig: svcJSON(), NodePort: 8080,
}). },
WithSpecField("ports", []interface{}{ },
map[string]interface{}{}, },
}).Unstructured, },
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", name: "unnamed nodePort should be preserved when specified in annotation",
obj: NewTestUnstructured().WithName("svc-1"). obj: corev1api.Service{
WithAnnotationValues(map[string]string{ ObjectMeta: metav1.ObjectMeta{
annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{NodePort: 8080}), Name: "svc-1",
}). Annotations: map[string]string{
WithSpecField("ports", []interface{}{ annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{NodePort: 8080}),
map[string]interface{}{
"nodePort": 8080,
}, },
}).Unstructured, },
expectedErr: false, Spec: corev1api.ServiceSpec{
expectedRes: NewTestUnstructured().WithName("svc-1"). Ports: []corev1api.ServicePort{
WithAnnotationValues(map[string]string{ {
annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{NodePort: 8080}), NodePort: 8080,
}). },
WithSpecField("ports", []interface{}{
map[string]interface{}{
"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", name: "unnamed nodePort should be deleted when named nodePort specified in annotation",
obj: NewTestUnstructured().WithName("svc-1"). obj: corev1api.Service{
WithAnnotationValues(map[string]string{ ObjectMeta: metav1.ObjectMeta{
annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}), Name: "svc-1",
}). Annotations: map[string]string{
WithSpecField("ports", []interface{}{ annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}),
map[string]interface{}{
"nodePort": 8080,
}, },
}).Unstructured, },
expectedErr: false, Spec: corev1api.ServiceSpec{
expectedRes: NewTestUnstructured().WithName("svc-1"). Ports: []corev1api.ServicePort{
WithAnnotationValues(map[string]string{ {
annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}), NodePort: 8080,
}). },
WithSpecField("ports", []interface{}{ },
map[string]interface{}{}, },
}).Unstructured, },
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", name: "named nodePort should be preserved when specified in annotation",
obj: NewTestUnstructured().WithName("svc-1"). obj: corev1api.Service{
WithAnnotationValues(map[string]string{ ObjectMeta: metav1.ObjectMeta{
annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}), Name: "svc-1",
}). Annotations: map[string]string{
WithSpecField("ports", []interface{}{ annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}),
map[string]interface{}{
"name": "http",
"nodePort": 8080,
}, },
map[string]interface{}{ },
"name": "admin", Spec: corev1api.ServiceSpec{
"nodePort": 9090, Ports: []corev1api.ServicePort{
{
Name: "http",
NodePort: 8080,
},
{
Name: "admin",
NodePort: 9090,
},
}, },
}).Unstructured, },
expectedErr: false, },
expectedRes: NewTestUnstructured().WithName("svc-1"). expectedRes: corev1api.Service{
WithAnnotationValues(map[string]string{ ObjectMeta: metav1.ObjectMeta{
annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}), Name: "svc-1",
}). Annotations: map[string]string{
WithSpecField("ports", []interface{}{ annotationLastAppliedConfig: svcJSON(corev1api.ServicePort{Name: "http", NodePort: 8080}),
map[string]interface{}{
"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) { t.Run(test.name, func(t *testing.T) {
action := NewServiceAction(velerotest.NewLogger()) 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) { 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)
} }
}) })
} }