pkg/restore: remove usage of pkg/util/collections
Signed-off-by: Steve Kriss <krisss@vmware.com>pull/1146/head
parent
e91c841c59
commit
38ad7d71f5
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
// }
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue