Add Paved.MargeValue method
- Add "fieldpath/object" package that deals with runtime.Objects - Move MergeOptions struct to package "apis/common/v1". Signed-off-by: Alper Rifat Ulucinar <ulucinar@users.noreply.github.com>
This commit is contained in:
parent
fcbfd04067
commit
e7b4a22e42
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
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 v1
|
||||
|
||||
import (
|
||||
"github.com/imdario/mergo"
|
||||
)
|
||||
|
||||
// MergeOptions Specifies merge options on a field path
|
||||
type MergeOptions struct { // TODO(aru): add more options that control merging behavior
|
||||
// Specifies that already existing values in a merged map should be preserved
|
||||
// +optional
|
||||
KeepMapValues *bool `json:"keepMapValues,omitempty"`
|
||||
// Specifies that already existing elements in a merged slice should be preserved
|
||||
// +optional
|
||||
AppendSlice *bool `json:"appendSlice,omitempty"`
|
||||
}
|
||||
|
||||
// MergoConfiguration the default behavior is to replace maps and slices
|
||||
func (mo *MergeOptions) MergoConfiguration() []func(*mergo.Config) {
|
||||
config := []func(*mergo.Config){mergo.WithOverride}
|
||||
if mo == nil {
|
||||
return config
|
||||
}
|
||||
|
||||
if mo.KeepMapValues != nil && *mo.KeepMapValues {
|
||||
config = config[:0]
|
||||
}
|
||||
if mo.AppendSlice != nil && *mo.AppendSlice {
|
||||
config = append(config, mergo.WithAppendSlice)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
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 v1
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/imdario/mergo"
|
||||
)
|
||||
|
||||
type mergoOptArr []func(*mergo.Config)
|
||||
|
||||
func (arr mergoOptArr) names() []string {
|
||||
names := make([]string, len(arr))
|
||||
for i, opt := range arr {
|
||||
names[i] = runtime.FuncForPC(reflect.ValueOf(opt).Pointer()).Name()
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func TestMergoConfiguration(t *testing.T) {
|
||||
valTrue := true
|
||||
tests := map[string]struct {
|
||||
mo *MergeOptions
|
||||
want mergoOptArr
|
||||
}{
|
||||
"DefaultOptionsNil": {
|
||||
want: mergoOptArr{
|
||||
mergo.WithOverride,
|
||||
},
|
||||
},
|
||||
"DefaultOptionsEmptyStruct": {
|
||||
mo: &MergeOptions{},
|
||||
want: mergoOptArr{
|
||||
mergo.WithOverride,
|
||||
},
|
||||
},
|
||||
"MapKeepOnly": {
|
||||
mo: &MergeOptions{
|
||||
KeepMapValues: &valTrue,
|
||||
},
|
||||
want: mergoOptArr{},
|
||||
},
|
||||
"AppendSliceOnly": {
|
||||
mo: &MergeOptions{
|
||||
AppendSlice: &valTrue,
|
||||
},
|
||||
want: mergoOptArr{
|
||||
mergo.WithAppendSlice,
|
||||
mergo.WithOverride,
|
||||
},
|
||||
},
|
||||
"MapKeepAppendSlice": {
|
||||
mo: &MergeOptions{
|
||||
AppendSlice: &valTrue,
|
||||
KeepMapValues: &valTrue,
|
||||
},
|
||||
want: mergoOptArr{
|
||||
mergo.WithAppendSlice,
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if diff := cmp.Diff(tc.want.names(), mergoOptArr(tc.mo.MergoConfiguration()).names()); diff != "" {
|
||||
t.Errorf("\nmo.MergoConfiguration(): -want, +got:\n %s", diff)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -137,6 +137,31 @@ func (in *LocalSecretReference) DeepCopy() *LocalSecretReference {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *MergeOptions) DeepCopyInto(out *MergeOptions) {
|
||||
*out = *in
|
||||
if in.KeepMapValues != nil {
|
||||
in, out := &in.KeepMapValues, &out.KeepMapValues
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.AppendSlice != nil {
|
||||
in, out := &in.AppendSlice, &out.AppendSlice
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeOptions.
|
||||
func (in *MergeOptions) DeepCopy() *MergeOptions {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(MergeOptions)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ProviderConfigStatus) DeepCopyInto(out *ProviderConfigStatus) {
|
||||
*out = *in
|
||||
|
|
|
|||
|
|
@ -1,156 +0,0 @@
|
|||
/*
|
||||
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 fieldpath
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
const (
|
||||
errInvalidMerge = "failed to merge values"
|
||||
)
|
||||
|
||||
// MergeOptions Specifies merge options on a field path
|
||||
type MergeOptions struct { // TODO(aru): add more options that control merging behavior
|
||||
// Specifies that already existing values in a merged map should be preserved
|
||||
KeepMapValues bool `json:"keepMapValues,omitempty"`
|
||||
// Specifies that already existing elements in a merged slice should be preserved
|
||||
AppendSlice bool `json:"appendSlice,omitempty"`
|
||||
}
|
||||
|
||||
// MergoConfiguration the default behavior is to replace maps and slices
|
||||
func (mo *MergeOptions) MergoConfiguration() []func(*mergo.Config) {
|
||||
config := []func(*mergo.Config){mergo.WithOverride}
|
||||
if mo == nil {
|
||||
return config
|
||||
}
|
||||
|
||||
if mo.KeepMapValues {
|
||||
config = config[:0]
|
||||
}
|
||||
if mo.AppendSlice {
|
||||
config = append(config, mergo.WithAppendSlice)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// ToPaved tries to convert a runtime.Object into a *Paved via
|
||||
// runtime.DefaultUnstructuredConverter if needed. Returns the paved if
|
||||
// the conversion is successful along with whether the
|
||||
// runtime.DefaultUnstructuredConverter has been employed during the
|
||||
// conversion.
|
||||
func ToPaved(o runtime.Object) (*Paved, bool, error) {
|
||||
if u, ok := o.(interface{ UnstructuredContent() map[string]interface{} }); ok {
|
||||
return Pave(u.UnstructuredContent()), false, nil
|
||||
}
|
||||
|
||||
oMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return Pave(oMap), true, nil
|
||||
}
|
||||
|
||||
// PatchFieldValueToObject applies the value to the "to" object at the given
|
||||
// path with the given merge options, returning any errors as they occur.
|
||||
// If no merge options is supplied, then destination field is replaced
|
||||
// with the given value.
|
||||
func PatchFieldValueToObject(fieldPath string, value interface{}, to runtime.Object,
|
||||
mergeOptions *MergeOptions) error {
|
||||
paved, copied, err := ToPaved(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dst, err := paved.GetValue(fieldPath)
|
||||
if IsNotFound(err) || mergeOptions == nil {
|
||||
dst = nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dst, err = merge(dst, value, mergeOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := paved.SetValue(fieldPath, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if copied {
|
||||
return runtime.DefaultUnstructuredConverter.FromUnstructured(paved.object, to)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MergePath merges the value at the given field path of the src object into
|
||||
// the dst object.
|
||||
func MergePath(fieldPath string, dst, src runtime.Object, mergeOptions *MergeOptions) error {
|
||||
srcPaved, _, err := ToPaved(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val, err := srcPaved.GetValue(fieldPath)
|
||||
// if src has no value at the specified path, then nothing to merge
|
||||
if IsNotFound(err) || val == nil {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return PatchFieldValueToObject(fieldPath, val, dst, mergeOptions)
|
||||
}
|
||||
|
||||
// MergeReplace merges the value at fieldPath from desired into
|
||||
// a copy of current and then replaces the value at fieldPath of
|
||||
// desired with the merged value. current object is not modified.
|
||||
func MergeReplace(fieldPath string, current, desired runtime.Object, mergeOptions *MergeOptions) error {
|
||||
copyCurrent := current.DeepCopyObject()
|
||||
if err := MergePath(fieldPath, copyCurrent, desired, mergeOptions); err != nil {
|
||||
return err
|
||||
}
|
||||
// replace desired object's value at fieldPath with
|
||||
// the computed (merged) current value at the same path
|
||||
return MergePath(fieldPath, desired, copyCurrent, nil)
|
||||
}
|
||||
|
||||
// merges the given src onto the given dst.
|
||||
// If a nil merge options is supplied, the default behavior is MergeOptions'
|
||||
// default behavior. If dst or src is nil, src is returned
|
||||
// (i.e., dst replaced by src).
|
||||
func merge(dst, src interface{}, mergeOptions *MergeOptions) (interface{}, error) {
|
||||
if dst == nil || src == nil {
|
||||
return src, nil // no merge, replace
|
||||
}
|
||||
|
||||
m, ok := dst.(map[string]interface{})
|
||||
if reflect.TypeOf(src).Kind() != reflect.Map || !ok {
|
||||
return src, nil // not a map nor a struct, mergo cannot merge
|
||||
}
|
||||
|
||||
// use merge semantics with the configured merge options to obtain the target dst value
|
||||
if err := mergo.Merge(&m, src, mergeOptions.MergoConfiguration()...); err != nil {
|
||||
return nil, errors.Wrap(err, errInvalidMerge)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
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 (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
|
||||
)
|
||||
|
||||
// ToPaved tries to convert a runtime.Object into a *Paved via
|
||||
// runtime.DefaultUnstructuredConverter if needed. Returns the paved if
|
||||
// the conversion is successful along with whether the
|
||||
// runtime.DefaultUnstructuredConverter has been employed during the
|
||||
// conversion.
|
||||
func ToPaved(o runtime.Object) (*fieldpath.Paved, bool, error) {
|
||||
if u, ok := o.(interface{ UnstructuredContent() map[string]interface{} }); ok {
|
||||
return fieldpath.Pave(u.UnstructuredContent()), false, nil
|
||||
}
|
||||
|
||||
oMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return fieldpath.Pave(oMap), true, nil
|
||||
}
|
||||
|
||||
// PatchFieldValueToObject applies the value to the "to" object at the given
|
||||
// path with the given merge options, returning any errors as they occur.
|
||||
// If no merge options is supplied, then destination field is replaced
|
||||
// with the given value.
|
||||
func PatchFieldValueToObject(fieldPath string, value interface{}, to runtime.Object, mo *v1.MergeOptions) error {
|
||||
paved, copied, err := ToPaved(to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := paved.MergeValue(fieldPath, value, mo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if copied {
|
||||
return runtime.DefaultUnstructuredConverter.FromUnstructured(paved.UnstructuredContent(), to)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MergePath merges the value at the given field path of the src object into
|
||||
// the dst object.
|
||||
func MergePath(path string, dst, src runtime.Object, mergeOptions *v1.MergeOptions) error {
|
||||
srcPaved, _, err := ToPaved(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val, err := srcPaved.GetValue(path)
|
||||
// if src has no value at the specified path, then nothing to merge
|
||||
if fieldpath.IsNotFound(err) || val == nil {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return PatchFieldValueToObject(path, val, dst, mergeOptions)
|
||||
}
|
||||
|
||||
// MergeReplace merges the value at path from dst into
|
||||
// a copy of src and then replaces the value at path of
|
||||
// dst with the merged value. src object is not modified.
|
||||
func MergeReplace(path string, src, dst runtime.Object, mo *v1.MergeOptions) error {
|
||||
copySrc := src.DeepCopyObject()
|
||||
if err := MergePath(path, copySrc, dst, mo); err != nil {
|
||||
return err
|
||||
}
|
||||
// replace desired object's value at fieldPath with
|
||||
// the computed (merged) current value at the same path
|
||||
return MergePath(path, dst, copySrc, nil)
|
||||
}
|
||||
|
|
@ -14,86 +14,23 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package fieldpath
|
||||
package object
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/imdario/mergo"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
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 mergoOptArr []func(*mergo.Config)
|
||||
|
||||
func (arr mergoOptArr) names() []string {
|
||||
names := make([]string, len(arr))
|
||||
for i, opt := range arr {
|
||||
names[i] = runtime.FuncForPC(reflect.ValueOf(opt).Pointer()).Name()
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func TestMergoConfiguration(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
mo *MergeOptions
|
||||
want mergoOptArr
|
||||
}{
|
||||
"DefaultOptionsNil": {
|
||||
want: mergoOptArr{
|
||||
mergo.WithOverride,
|
||||
},
|
||||
},
|
||||
"DefaultOptionsEmptyStruct": {
|
||||
mo: &MergeOptions{},
|
||||
want: mergoOptArr{
|
||||
mergo.WithOverride,
|
||||
},
|
||||
},
|
||||
"MapKeepOnly": {
|
||||
mo: &MergeOptions{
|
||||
KeepMapValues: true,
|
||||
},
|
||||
want: mergoOptArr{},
|
||||
},
|
||||
"AppendSliceOnly": {
|
||||
mo: &MergeOptions{
|
||||
AppendSlice: true,
|
||||
},
|
||||
want: mergoOptArr{
|
||||
mergo.WithAppendSlice,
|
||||
mergo.WithOverride,
|
||||
},
|
||||
},
|
||||
"MapKeepAppendSlice": {
|
||||
mo: &MergeOptions{
|
||||
AppendSlice: true,
|
||||
KeepMapValues: true,
|
||||
},
|
||||
want: mergoOptArr{
|
||||
mergo.WithAppendSlice,
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if diff := cmp.Diff(tc.want.names(), mergoOptArr(tc.mo.MergoConfiguration()).names()); diff != "" {
|
||||
t.Errorf("\nmo.MergoConfiguration(): -want, +got:\n %s", diff)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type objWithUnstructuredContent struct {
|
||||
m map[string]interface{}
|
||||
}
|
||||
|
|
@ -110,7 +47,7 @@ func (o objWithUnstructuredContent) UnstructuredContent() map[string]interface{}
|
|||
return o.m
|
||||
}
|
||||
|
||||
func pavedComparer(p1, p2 Paved) bool {
|
||||
func pavedComparer(p1, p2 fieldpath.Paved) bool {
|
||||
return reflect.DeepEqual(p1, p2)
|
||||
}
|
||||
|
||||
|
|
@ -119,7 +56,7 @@ func TestToPaved(t *testing.T) {
|
|||
o k8s.Object
|
||||
}
|
||||
type want struct {
|
||||
paved *Paved
|
||||
paved *fieldpath.Paved
|
||||
copied bool
|
||||
err error
|
||||
}
|
||||
|
|
@ -138,25 +75,25 @@ func TestToPaved(t *testing.T) {
|
|||
},
|
||||
},
|
||||
want: want{
|
||||
paved: &Paved{
|
||||
object: map[string]interface{}{
|
||||
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: &v1.ConfigMap{},
|
||||
o: &corev1.ConfigMap{},
|
||||
},
|
||||
want: want{
|
||||
copied: true,
|
||||
paved: &Paved{
|
||||
object: map[string]interface{}{
|
||||
paved: fieldpath.Pave(
|
||||
map[string]interface{}{
|
||||
"metadata": map[string]interface{}{"creationTimestamp": nil},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -180,6 +117,16 @@ func TestToPaved(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type strObject string
|
||||
|
||||
func (strObject) GetObjectKind() schema.ObjectKind {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (strObject) DeepCopyObject() k8s.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
type object struct {
|
||||
P1 *p1
|
||||
}
|
||||
|
|
@ -201,16 +148,6 @@ func (object) DeepCopyObject() k8s.Object {
|
|||
return nil
|
||||
}
|
||||
|
||||
type strObject string
|
||||
|
||||
func (strObject) GetObjectKind() schema.ObjectKind {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (strObject) DeepCopyObject() k8s.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
valStringDst = "value-from-dst"
|
||||
valStringSrc = "value-from-src"
|
||||
|
|
@ -219,6 +156,7 @@ var (
|
|||
valArrDst = []string{valStringDst}
|
||||
valArrSrc = []string{valStringSrc}
|
||||
valArrAppended = []string{valStringDst, valStringSrc}
|
||||
valTrue = true
|
||||
)
|
||||
|
||||
func dstObject() *object {
|
||||
|
|
@ -252,7 +190,7 @@ func TestMergePath(t *testing.T) {
|
|||
fieldPath string
|
||||
dst k8s.Object
|
||||
src k8s.Object
|
||||
mergeOptions *MergeOptions
|
||||
mergeOptions *v1.MergeOptions
|
||||
}
|
||||
type want struct {
|
||||
err error
|
||||
|
|
@ -290,8 +228,8 @@ func TestMergePath(t *testing.T) {
|
|||
fieldPath: "p1.p2",
|
||||
dst: dstObject(),
|
||||
src: srcObject(),
|
||||
mergeOptions: &MergeOptions{
|
||||
KeepMapValues: true,
|
||||
mergeOptions: &v1.MergeOptions{
|
||||
KeepMapValues: &valTrue,
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
|
|
@ -313,9 +251,9 @@ func TestMergePath(t *testing.T) {
|
|||
fieldPath: "p1.p2",
|
||||
dst: dstObject(),
|
||||
src: srcObject(),
|
||||
mergeOptions: &MergeOptions{
|
||||
KeepMapValues: true,
|
||||
AppendSlice: true,
|
||||
mergeOptions: &v1.MergeOptions{
|
||||
KeepMapValues: &valTrue,
|
||||
AppendSlice: &valTrue,
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
|
|
@ -367,9 +305,9 @@ func TestMergePath(t *testing.T) {
|
|||
S: &valStringDst,
|
||||
},
|
||||
},
|
||||
mergeOptions: &MergeOptions{
|
||||
KeepMapValues: true,
|
||||
AppendSlice: true,
|
||||
mergeOptions: &v1.MergeOptions{
|
||||
KeepMapValues: &valTrue,
|
||||
AppendSlice: &valTrue,
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
|
|
@ -392,7 +330,7 @@ func TestMergePath(t *testing.T) {
|
|||
src: strObject("src"),
|
||||
},
|
||||
want: want{
|
||||
err: fmt.Errorf("ToUnstructured requires a non-nil pointer to an object, got fieldpath.strObject"),
|
||||
err: fmt.Errorf("ToUnstructured requires a non-nil pointer to an object, got object.strObject"),
|
||||
},
|
||||
},
|
||||
"ErrDstNotPaved": {
|
||||
|
|
@ -403,7 +341,7 @@ func TestMergePath(t *testing.T) {
|
|||
src: srcObject(),
|
||||
},
|
||||
want: want{
|
||||
err: fmt.Errorf("ToUnstructured requires a non-nil pointer to an object, got fieldpath.strObject"),
|
||||
err: fmt.Errorf("ToUnstructured requires a non-nil pointer to an object, got object.strObject"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -429,7 +367,7 @@ func TestMergeReplace(t *testing.T) {
|
|||
fieldPath string
|
||||
current k8s.Object
|
||||
desired k8s.Object
|
||||
mergeOptions *MergeOptions
|
||||
mergeOptions *v1.MergeOptions
|
||||
}
|
||||
type want struct {
|
||||
current k8s.Object
|
||||
|
|
@ -443,28 +381,28 @@ func TestMergeReplace(t *testing.T) {
|
|||
"HappyPath": {
|
||||
args: args{
|
||||
fieldPath: "data",
|
||||
current: &v1.ConfigMap{
|
||||
current: &corev1.ConfigMap{
|
||||
Data: map[string]string{
|
||||
"key1": "value-from-current",
|
||||
},
|
||||
},
|
||||
desired: &v1.ConfigMap{
|
||||
desired: &corev1.ConfigMap{
|
||||
Data: map[string]string{
|
||||
"key1": "value-from-desired",
|
||||
"key2": "value-from-desired",
|
||||
},
|
||||
},
|
||||
mergeOptions: &MergeOptions{
|
||||
KeepMapValues: true,
|
||||
mergeOptions: &v1.MergeOptions{
|
||||
KeepMapValues: &valTrue,
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
current: &v1.ConfigMap{
|
||||
current: &corev1.ConfigMap{
|
||||
Data: map[string]string{
|
||||
"key1": "value-from-current",
|
||||
},
|
||||
},
|
||||
desired: &v1.ConfigMap{
|
||||
desired: &corev1.ConfigMap{
|
||||
Data: map[string]string{
|
||||
"key1": "value-from-current",
|
||||
"key2": "value-from-desired",
|
||||
|
|
@ -479,7 +417,7 @@ func TestMergeReplace(t *testing.T) {
|
|||
desired: strObject("desired"),
|
||||
},
|
||||
want: want{
|
||||
err: fmt.Errorf("ToUnstructured requires a non-nil pointer to an object, got fieldpath.strObject"),
|
||||
err: fmt.Errorf("ToUnstructured requires a non-nil pointer to an object, got object.strObject"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -17,9 +17,18 @@ limitations under the License.
|
|||
package fieldpath
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
|
||||
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
errInvalidMerge = "failed to merge values"
|
||||
)
|
||||
|
||||
type errNotFound struct {
|
||||
|
|
@ -392,3 +401,43 @@ func (p *Paved) SetBool(path string, value bool) error {
|
|||
func (p *Paved) SetNumber(path string, value float64) error {
|
||||
return p.SetValue(path, value)
|
||||
}
|
||||
|
||||
// merges the given src onto the given dst.
|
||||
// dst and src must have the same map type.
|
||||
// If a nil merge options is supplied, the default behavior is MergeOptions'
|
||||
// default behavior. If dst or src is nil, src is returned
|
||||
// (i.e., dst replaced by src).
|
||||
func merge(dst, src interface{}, mergeOptions *xpv1.MergeOptions) (interface{}, error) {
|
||||
if dst == nil || src == nil {
|
||||
return src, nil // no merge, replace
|
||||
}
|
||||
|
||||
m, ok := dst.(map[string]interface{})
|
||||
if reflect.TypeOf(src).Kind() != reflect.Map || !ok {
|
||||
return src, nil // not a map nor a struct, mergo cannot merge
|
||||
}
|
||||
|
||||
// use merge semantics with the configured merge options to obtain the target dst value
|
||||
if err := mergo.Merge(&m, src, mergeOptions.MergoConfiguration()...); err != nil {
|
||||
return nil, errors.Wrap(err, errInvalidMerge)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// MergeValue of the receiver p at the specified field path with the supplied
|
||||
// value according to supplied merge options
|
||||
func (p *Paved) MergeValue(path string, value interface{}, mo *xpv1.MergeOptions) error {
|
||||
dst, err := p.GetValue(path)
|
||||
if IsNotFound(err) || mo == nil {
|
||||
dst = nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dst, err = merge(dst, value, mo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return p.SetValue(path, dst)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ import (
|
|||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
|
||||
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||
fobj "github.com/crossplane/crossplane-runtime/pkg/fieldpath/object"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
||||
)
|
||||
|
||||
|
|
@ -156,9 +157,9 @@ func (a *APIPatchingApplicator) Apply(ctx context.Context, o client.Object, ao .
|
|||
// WithMergeOptions returns an ApplyOption for merging the value at the given
|
||||
// fieldPath of desired object onto the current object with
|
||||
// the given merge options.
|
||||
func WithMergeOptions(fieldPath string, mergeOptions *fieldpath.MergeOptions) ApplyOption {
|
||||
func WithMergeOptions(fieldPath string, mergeOptions *xpv1.MergeOptions) ApplyOption {
|
||||
return func(_ context.Context, current, desired runtime.Object) error {
|
||||
return fieldpath.MergeReplace(fieldPath, current, desired, mergeOptions)
|
||||
return fobj.MergeReplace(fieldPath, current, desired, mergeOptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue