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
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
}

View File

@ -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)
}
})
}

View File

@ -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
// }

View File

@ -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 <service account name>-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 <service account name>-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 <service account name>-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)
})
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}
})
}