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