crossplane-runtime/pkg/fieldpath/object/object_test.go

443 lines
10 KiB
Go

/*
Copyright 2021 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package object
import (
"fmt"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
corev1 "k8s.io/api/core/v1"
k8s "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
type objWithUnstructuredContent struct {
m map[string]interface{}
}
func (objWithUnstructuredContent) GetObjectKind() schema.ObjectKind {
return nil
}
func (objWithUnstructuredContent) DeepCopyObject() k8s.Object {
return nil
}
func (o objWithUnstructuredContent) UnstructuredContent() map[string]interface{} {
return o.m
}
func pavedComparer(p1, p2 fieldpath.Paved) bool {
return reflect.DeepEqual(p1, p2)
}
func TestToPaved(t *testing.T) {
type args struct {
o k8s.Object
}
type want struct {
paved *fieldpath.Paved
copied bool
err error
}
tests := map[string]struct {
reason string
args args
want want
}{
"ProvidesUnstructuredContent": {
reason: "If object already provides UnstructuredContent, it should be used to pave it, with no copying of contents",
args: args{
o: objWithUnstructuredContent{
m: map[string]interface{}{
"key": "val",
},
},
},
want: want{
paved: fieldpath.Pave(
map[string]interface{}{
"key": "val",
},
),
},
},
"NoUnstructuredContent": {
reason: "If object does not provide UnstructuredContent, unstructured converter will be used to pave it, with its contents being copied",
args: args{
o: &corev1.ConfigMap{},
},
want: want{
copied: true,
paved: fieldpath.Pave(
map[string]interface{}{
"metadata": map[string]interface{}{"creationTimestamp": nil},
},
),
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
gotPaved, gotCopied, err := ToPaved(tc.args.o)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Fatalf("\nToPaved(...) unexpected error: %s: -want error, +got error:\n%s", tc.reason, diff)
}
if err != nil {
return
}
if diff := cmp.Diff(tc.want.paved, gotPaved, cmp.Comparer(pavedComparer)); diff != "" {
t.Errorf("\nToPaved(...) unexpected paved: %s: -want paved, +got paved:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.copied, gotCopied); diff != "" {
t.Errorf("\nToPaved(...) unexpected copied: %s: -want copied, +got copied:\n%s", tc.reason, diff)
}
})
}
}
type strObject string
func (strObject) GetObjectKind() schema.ObjectKind {
return nil
}
func (strObject) DeepCopyObject() k8s.Object {
return nil
}
type object struct {
P1 *p1
}
type p1 struct {
P2 *p2
S *string
}
type p2 struct {
S *string
B *bool
A []string
}
func (object) GetObjectKind() schema.ObjectKind {
return nil
}
func (object) DeepCopyObject() k8s.Object {
return nil
}
var (
valStringDst = "value-from-dst"
valStringSrc = "value-from-src"
valBoolTrue = true
valBoolFalse = false
valArrDst = []string{valStringDst}
valArrSrc = []string{valStringSrc}
valArrAppended = []string{valStringDst, valStringSrc}
valTrue = true
)
func dstObject() *object {
return &object{
P1: &p1{
S: &valStringDst,
P2: &p2{
S: &valStringDst,
B: &valBoolTrue,
A: valArrDst,
},
},
}
}
func srcObject() *object {
return &object{
P1: &p1{
S: &valStringSrc,
P2: &p2{
S: &valStringSrc,
B: &valBoolFalse,
A: valArrSrc,
},
},
}
}
func TestMergePath(t *testing.T) {
type args struct {
fieldPath string
dst k8s.Object
src k8s.Object
mergeOptions *v1.MergeOptions
}
type want struct {
err error
dst k8s.Object
}
tests := map[string]struct {
reason string
args args
want want
}{
"ReplacePath": {
reason: "Default behavior if no merge options are supplied is to replace dst with src",
args: args{
fieldPath: "p1.p2",
dst: dstObject(),
src: srcObject(),
mergeOptions: nil,
},
want: want{
dst: &object{
P1: &p1{
S: &valStringDst,
P2: &p2{
S: &valStringSrc,
B: &valBoolFalse,
A: valArrSrc,
},
},
},
},
},
"MergePathNoSliceAppend": {
reason: "When KeepMapValues is set but AppendSlice is not, dst should preserve its values at the merge path",
args: args{
fieldPath: "p1.p2",
dst: dstObject(),
src: srcObject(),
mergeOptions: &v1.MergeOptions{
KeepMapValues: &valTrue,
},
},
want: want{
dst: &object{
P1: &p1{
S: &valStringDst,
P2: &p2{
S: &valStringDst,
B: &valBoolTrue,
A: valArrDst,
},
},
},
},
},
"MergePathWithSliceAppend": {
reason: "When both KeepMapValues and AppendSlice are ser, dst should preserve map values but arrays being appended",
args: args{
fieldPath: "p1.p2",
dst: dstObject(),
src: srcObject(),
mergeOptions: &v1.MergeOptions{
KeepMapValues: &valTrue,
AppendSlice: &valTrue,
},
},
want: want{
dst: &object{
P1: &p1{
S: &valStringDst,
P2: &p2{
S: &valStringDst,
B: &valBoolTrue,
A: valArrAppended,
},
},
},
},
},
"PathNotFound": {
reason: "If specified merge path does not exist, dst should be unmodified even if replace is requested (empty src value is merged onto dst value)",
args: args{
fieldPath: "p1.non.existent",
dst: dstObject(),
src: srcObject(),
},
want: want{
dst: dstObject(),
},
},
"SrcValueEmpty": {
reason: "If value at the specified merge path is zero in src, dst should be unmodified, even if replace is requested",
args: args{
fieldPath: "p1.p2",
dst: dstObject(),
src: &object{
P1: &p1{
S: &valStringSrc,
},
},
},
want: want{
dst: dstObject(),
},
},
"DstValueEmpty": {
reason: "If value at the specified merge path is zero in dst but not in src, should be identical to a replace, even if merge is configured",
args: args{
fieldPath: "p1.p2",
src: srcObject(),
dst: &object{
P1: &p1{
S: &valStringDst,
},
},
mergeOptions: &v1.MergeOptions{
KeepMapValues: &valTrue,
AppendSlice: &valTrue,
},
},
want: want{
dst: &object{
P1: &p1{
S: &valStringDst,
P2: &p2{
S: &valStringSrc,
B: &valBoolFalse,
A: valArrSrc,
},
},
},
},
},
"ErrSrcNotPaved": {
reason: "If src cannot be paved, MergePath should be failing",
args: args{
dst: dstObject(),
src: strObject("src"),
},
want: want{
err: fmt.Errorf("ToUnstructured requires a non-nil pointer to an object, got object.strObject"),
},
},
"ErrDstNotPaved": {
reason: "If dst cannot be paved, MergePath should be failing",
args: args{
fieldPath: "p1.p2",
dst: strObject("dst"),
src: srcObject(),
},
want: want{
err: fmt.Errorf("ToUnstructured requires a non-nil pointer to an object, got object.strObject"),
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
err := MergePath(tc.args.fieldPath, tc.args.dst, tc.args.src, tc.args.mergeOptions)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Fatalf("\nMergePath(...) unexpected error: %s: -want error, +got error:\n%s", tc.reason, diff)
}
if err != nil {
return
}
if diff := cmp.Diff(tc.want.dst, tc.args.dst); diff != "" {
t.Errorf("\nMergePath(...) unexpected dst: %s: -want dst, +got dst:\n%s", tc.reason, diff)
}
})
}
}
func TestMergeReplace(t *testing.T) {
type args struct {
fieldPath string
current k8s.Object
desired k8s.Object
mergeOptions *v1.MergeOptions
}
type want struct {
current k8s.Object
desired k8s.Object
err error
}
tests := map[string]struct {
args args
want want
}{
"HappyPath": {
args: args{
fieldPath: "data",
current: &corev1.ConfigMap{
Data: map[string]string{
"key1": "value-from-current",
},
},
desired: &corev1.ConfigMap{
Data: map[string]string{
"key1": "value-from-desired",
"key2": "value-from-desired",
},
},
mergeOptions: &v1.MergeOptions{
KeepMapValues: &valTrue,
},
},
want: want{
current: &corev1.ConfigMap{
Data: map[string]string{
"key1": "value-from-current",
},
},
desired: &corev1.ConfigMap{
Data: map[string]string{
"key1": "value-from-current",
"key2": "value-from-desired",
},
},
},
},
"ErrFromMergePath": {
args: args{
fieldPath: "data",
current: strObject("current"),
desired: strObject("desired"),
},
want: want{
err: fmt.Errorf("ToUnstructured requires a non-nil pointer to an object, got object.strObject"),
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
err := MergeReplace(tc.args.fieldPath, tc.args.current, tc.args.desired, tc.args.mergeOptions)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Fatalf("\nMergeReplace(...) unexpected error: -want error, +got error:\n%s", diff)
}
if err != nil {
return
}
if diff := cmp.Diff(tc.want.current, tc.args.current); diff != "" {
t.Errorf("\nMergeReplace(...) unexpected current: -want, +got:\n%s", diff)
}
if diff := cmp.Diff(tc.want.desired, tc.args.desired); diff != "" {
t.Errorf("\nMergeReplace(...) unexpected desired: -want, +got:\n%s", diff)
}
})
}
}