Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
K
k3s
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
k3s
Commits
f9364da3
Commit
f9364da3
authored
Oct 08, 2015
by
Robert Bailey
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #13820 from jackgr/apply_patch
Add method to apply strategic merge patch
parents
cac3390d
ddda379b
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
1069 additions
and
244 deletions
+1069
-244
patch.go
pkg/util/strategicpatch/patch.go
+234
-74
patch_test.go
pkg/util/strategicpatch/patch_test.go
+835
-170
No files found.
pkg/util/strategicpatch/patch.go
View file @
f9364da3
...
...
@@ -35,27 +35,100 @@ import (
// Some of the content of this package was borrowed with minor adaptations from
// evanphx/json-patch and openshift/origin.
const
specialKey
=
"$patch"
const
specialValue
=
"delete"
const
(
directiveMarker
=
"$patch"
deleteDirective
=
"delete"
replaceDirective
=
"replace"
mergeDirective
=
"merge"
)
// IsPreconditionFailed returns true if the provided error indicates
// a precondition failed.
func
IsPreconditionFailed
(
err
error
)
bool
{
_
,
ok
:=
err
.
(
errPreconditionFailed
)
return
ok
}
type
errPreconditionFailed
struct
{
message
string
}
func
newErrPreconditionFailed
(
target
map
[
string
]
interface
{})
errPreconditionFailed
{
s
:=
fmt
.
Sprintf
(
"precondition failed for: %v"
,
target
)
return
errPreconditionFailed
{
s
}
}
func
(
err
errPreconditionFailed
)
Error
()
string
{
return
err
.
message
}
type
errConflict
struct
{
message
string
}
func
newErrConflict
(
patch
,
current
[]
byte
)
errConflict
{
s
:=
fmt
.
Sprintf
(
"patch:
\n
%s
\n
conflicts with current:
\n
%s
\n
"
,
patch
,
current
)
return
errConflict
{
s
}
}
func
(
err
errConflict
)
Error
()
string
{
return
err
.
message
}
// IsConflict returns true if the provided error indicates
// a conflict between the patch and the current configuration.
func
IsConflict
(
err
error
)
bool
{
_
,
ok
:=
err
.
(
errConflict
)
return
ok
}
var
errBadJSONDoc
=
fmt
.
Errorf
(
"Invalid JSON document"
)
var
errNoListOfLists
=
fmt
.
Errorf
(
"Lists of lists are not supported"
)
// CreateStrategicMergePatch creates a patch that can be passed to StrategicMergePatch.
// The original and modified documents must be passed to the method as json encoded content.
// It will return a mergeable json document with differences from original to modified, or an error
// if either of the two documents is invalid.
// The following code is adapted from github.com/openshift/origin/pkg/util/jsonmerge.
// Instead of defining a Delta that holds an original, a patch and a set of preconditions,
// the reconcile method accepts a set of preconditions as an argument.
// PreconditionFunc asserts that an incompatible change is not present within a patch.
type
PreconditionFunc
func
(
interface
{})
bool
// RequireKeyUnchanged returns a precondition function that fails if the provided key
// is present in the patch (indicating that its value has changed).
func
RequireKeyUnchanged
(
key
string
)
PreconditionFunc
{
return
func
(
patch
interface
{})
bool
{
patchMap
,
ok
:=
patch
.
(
map
[
string
]
interface
{})
if
!
ok
{
return
true
}
// The presence of key means that its value has been changed, so the test fails.
_
,
ok
=
patchMap
[
key
]
return
!
ok
}
}
// Deprecated: Use the synonym CreateTwoWayMergePatch, instead.
func
CreateStrategicMergePatch
(
original
,
modified
[]
byte
,
dataStruct
interface
{})
([]
byte
,
error
)
{
return
CreateTwoWayMergePatch
(
original
,
modified
,
dataStruct
)
}
// CreateTwoWayMergePatch creates a patch that can be passed to StrategicMergePatch from an original
// document and a modified documernt, which are passed to the method as json encoded content. It will
// return a patch that yields the modified document when applied to the original document, or an error
// if either of the two documents is invalid.
func
CreateTwoWayMergePatch
(
original
,
modified
[]
byte
,
dataStruct
interface
{},
fns
...
PreconditionFunc
)
([]
byte
,
error
)
{
originalMap
:=
map
[
string
]
interface
{}{}
err
:=
json
.
Unmarshal
(
original
,
&
originalMap
)
if
err
!=
nil
{
return
nil
,
errBadJSONDoc
if
len
(
original
)
>
0
{
if
err
:=
json
.
Unmarshal
(
original
,
&
originalMap
);
err
!=
nil
{
return
nil
,
errBadJSONDoc
}
}
modifiedMap
:=
map
[
string
]
interface
{}{}
err
=
json
.
Unmarshal
(
modified
,
&
modifiedMap
)
if
err
!=
nil
{
return
nil
,
errBadJSONDoc
if
len
(
modified
)
>
0
{
if
err
:=
json
.
Unmarshal
(
modified
,
&
modifiedMap
);
err
!=
nil
{
return
nil
,
errBadJSONDoc
}
}
t
,
err
:=
getTagStructType
(
dataStruct
)
...
...
@@ -68,11 +141,18 @@ func CreateStrategicMergePatch(original, modified []byte, dataStruct interface{}
return
nil
,
err
}
// Apply the preconditions to the patch, and return an error if any of them fail.
for
_
,
fn
:=
range
fns
{
if
!
fn
(
patchMap
)
{
return
nil
,
newErrPreconditionFailed
(
patchMap
)
}
}
return
json
.
Marshal
(
patchMap
)
}
// Returns a (recursive) strategic merge patch that yields modified when applied to original.
func
diffMaps
(
original
,
modified
map
[
string
]
interface
{},
t
reflect
.
Type
,
ignore
Additions
,
ignoreChangesAnd
Deletions
bool
)
(
map
[
string
]
interface
{},
error
)
{
func
diffMaps
(
original
,
modified
map
[
string
]
interface
{},
t
reflect
.
Type
,
ignore
ChangesAndAdditions
,
ignore
Deletions
bool
)
(
map
[
string
]
interface
{},
error
)
{
patch
:=
map
[
string
]
interface
{}{}
if
t
.
Kind
()
==
reflect
.
Ptr
{
t
=
t
.
Elem
()
...
...
@@ -80,42 +160,43 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreA
for
key
,
modifiedValue
:=
range
modified
{
originalValue
,
ok
:=
original
[
key
]
// value was added
if
!
ok
{
if
!
ignoreAdditions
{
// Key was added, so add to patch
if
!
ignoreChangesAndAdditions
{
patch
[
key
]
=
modifiedValue
}
continue
}
if
key
==
specialKey
{
if
key
==
directiveMarker
{
originalString
,
ok
:=
originalValue
.
(
string
)
if
!
ok
{
return
nil
,
fmt
.
Errorf
(
"invalid value for special key: %s"
,
specialKey
)
return
nil
,
fmt
.
Errorf
(
"invalid value for special key: %s"
,
directiveMarker
)
}
modifiedString
,
ok
:=
modifiedValue
.
(
string
)
if
!
ok
{
return
nil
,
fmt
.
Errorf
(
"invalid value for special key: %s"
,
specialKey
)
return
nil
,
fmt
.
Errorf
(
"invalid value for special key: %s"
,
directiveMarker
)
}
if
modifiedString
!=
originalString
{
patch
[
key
]
=
modifiedValue
patch
[
directiveMarker
]
=
modifiedValue
}
continue
}
if
!
ignoreChangesAndDeletions
{
//
If types have changed, replace completely
if
reflect
.
TypeOf
(
originalValue
)
!=
reflect
.
TypeOf
(
modifiedValue
)
{
if
reflect
.
TypeOf
(
originalValue
)
!=
reflect
.
TypeOf
(
modifiedValue
)
{
//
Types have changed, so add to patch
if
!
ignoreChangesAndAdditions
{
patch
[
key
]
=
modifiedValue
continue
}
continue
}
// Types are the same, compare values
// Types are the same,
so
compare values
switch
originalValueTyped
:=
originalValue
.
(
type
)
{
case
map
[
string
]
interface
{}
:
modifiedValueTyped
:=
modifiedValue
.
(
map
[
string
]
interface
{})
...
...
@@ -124,7 +205,7 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreA
return
nil
,
err
}
patchValue
,
err
:=
diffMaps
(
originalValueTyped
,
modifiedValueTyped
,
fieldType
,
ignore
Additions
,
ignoreChangesAnd
Deletions
)
patchValue
,
err
:=
diffMaps
(
originalValueTyped
,
modifiedValueTyped
,
fieldType
,
ignore
ChangesAndAdditions
,
ignore
Deletions
)
if
err
!=
nil
{
return
nil
,
err
}
...
...
@@ -141,8 +222,8 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreA
return
nil
,
err
}
if
fieldPatchStrategy
==
"merge"
{
patchValue
,
err
:=
diffLists
(
originalValueTyped
,
modifiedValueTyped
,
fieldType
.
Elem
(),
fieldPatchMergeKey
,
ignore
Additions
,
ignoreChangesAnd
Deletions
)
if
fieldPatchStrategy
==
mergeDirective
{
patchValue
,
err
:=
diffLists
(
originalValueTyped
,
modifiedValueTyped
,
fieldType
.
Elem
(),
fieldPatchMergeKey
,
ignore
ChangesAndAdditions
,
ignore
Deletions
)
if
err
!=
nil
{
return
nil
,
err
}
...
...
@@ -155,15 +236,16 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreA
}
}
if
!
ignoreChangesAnd
Dele
tions
{
if
!
ignoreChangesAnd
Addi
tions
{
if
!
reflect
.
DeepEqual
(
originalValue
,
modifiedValue
)
{
// Values are different, so add to patch
patch
[
key
]
=
modifiedValue
}
}
}
if
!
ignore
ChangesAnd
Deletions
{
//
Now add all deleted values as nil
if
!
ignoreDeletions
{
//
Add nils for deleted values
for
key
:=
range
original
{
_
,
found
:=
modified
[
key
]
if
!
found
{
...
...
@@ -177,9 +259,9 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreA
// Returns a (recursive) strategic merge patch that yields modified when applied to original,
// for a pair of lists with merge semantics.
func
diffLists
(
original
,
modified
[]
interface
{},
t
reflect
.
Type
,
mergeKey
string
,
ignore
Additions
,
ignoreChangesAnd
Deletions
bool
)
([]
interface
{},
error
)
{
func
diffLists
(
original
,
modified
[]
interface
{},
t
reflect
.
Type
,
mergeKey
string
,
ignore
ChangesAndAdditions
,
ignore
Deletions
bool
)
([]
interface
{},
error
)
{
if
len
(
original
)
==
0
{
if
len
(
modified
)
==
0
||
ignoreAdditions
{
if
len
(
modified
)
==
0
||
ignore
ChangesAnd
Additions
{
return
nil
,
nil
}
...
...
@@ -193,11 +275,10 @@ func diffLists(original, modified []interface{}, t reflect.Type, mergeKey string
var
patch
[]
interface
{}
// If the elements are not maps...
if
elementType
.
Kind
()
==
reflect
.
Map
{
patch
,
err
=
diffListsOfMaps
(
original
,
modified
,
t
,
mergeKey
,
ignore
Additions
,
ignoreChangesAnd
Deletions
)
}
else
{
patch
,
err
=
diffListsOfScalars
(
original
,
modified
,
ignoreAdditions
)
patch
,
err
=
diffListsOfMaps
(
original
,
modified
,
t
,
mergeKey
,
ignore
ChangesAndAdditions
,
ignore
Deletions
)
}
else
if
!
ignoreChangesAndAdditions
{
patch
,
err
=
diffListsOfScalars
(
original
,
modified
)
}
if
err
!=
nil
{
...
...
@@ -209,7 +290,7 @@ func diffLists(original, modified []interface{}, t reflect.Type, mergeKey string
// Returns a (recursive) strategic merge patch that yields modified when applied to original,
// for a pair of lists of scalars with merge semantics.
func
diffListsOfScalars
(
original
,
modified
[]
interface
{}
,
ignoreAdditions
bool
)
([]
interface
{},
error
)
{
func
diffListsOfScalars
(
original
,
modified
[]
interface
{})
([]
interface
{},
error
)
{
if
len
(
modified
)
==
0
{
// There is no need to check the length of original because there is no way to create
// a patch that deletes a scalar from a list of scalars with merge semantics.
...
...
@@ -229,9 +310,7 @@ loopB:
modifiedString
:=
fmt
.
Sprintf
(
"%v"
,
modified
[
modifiedIndex
])
if
originalString
>=
modifiedString
{
if
originalString
!=
modifiedString
{
if
!
ignoreAdditions
{
patch
=
append
(
patch
,
modified
[
modifiedIndex
])
}
patch
=
append
(
patch
,
modified
[
modifiedIndex
])
}
continue
loopB
...
...
@@ -243,11 +322,9 @@ loopB:
break
}
if
!
ignoreAdditions
{
// Add any remaining items found only in modified
for
;
modifiedIndex
<
len
(
modifiedScalars
);
modifiedIndex
++
{
patch
=
append
(
patch
,
modified
[
modifiedIndex
])
}
// Add any remaining items found only in modified
for
;
modifiedIndex
<
len
(
modifiedScalars
);
modifiedIndex
++
{
patch
=
append
(
patch
,
modified
[
modifiedIndex
])
}
return
patch
,
nil
...
...
@@ -258,7 +335,7 @@ var errBadArgTypeFmt = "expected a %s, but received a %t"
// Returns a (recursive) strategic merge patch that yields modified when applied to original,
// for a pair of lists of maps with merge semantics.
func
diffListsOfMaps
(
original
,
modified
[]
interface
{},
t
reflect
.
Type
,
mergeKey
string
,
ignore
Additions
,
ignoreChangesAnd
Deletions
bool
)
([]
interface
{},
error
)
{
func
diffListsOfMaps
(
original
,
modified
[]
interface
{},
t
reflect
.
Type
,
mergeKey
string
,
ignore
ChangesAndAdditions
,
ignore
Deletions
bool
)
([]
interface
{},
error
)
{
patch
:=
make
([]
interface
{},
0
)
originalSorted
,
err
:=
sortMergeListsByNameArray
(
original
,
t
,
mergeKey
,
false
)
...
...
@@ -301,7 +378,8 @@ loopB:
modifiedString
:=
fmt
.
Sprintf
(
"%v"
,
modifiedValue
)
if
originalString
>=
modifiedString
{
if
originalString
==
modifiedString
{
patchValue
,
err
:=
diffMaps
(
originalMap
,
modifiedMap
,
t
,
ignoreAdditions
,
ignoreChangesAndDeletions
)
// Merge key values are equal, so recurse
patchValue
,
err
:=
diffMaps
(
originalMap
,
modifiedMap
,
t
,
ignoreChangesAndAdditions
,
ignoreDeletions
)
if
err
!=
nil
{
return
nil
,
err
}
...
...
@@ -311,22 +389,24 @@ loopB:
patchValue
[
mergeKey
]
=
modifiedValue
patch
=
append
(
patch
,
patchValue
)
}
}
else
if
!
ignoreAdditions
{
}
else
if
!
ignoreChangesAndAdditions
{
// Item was added, so add to patch
patch
=
append
(
patch
,
modifiedMap
)
}
continue
loopB
}
if
!
ignoreChangesAndDeletions
{
patch
=
append
(
patch
,
map
[
string
]
interface
{}{
mergeKey
:
originalValue
,
specialKey
:
specialValue
})
if
!
ignoreDeletions
{
// Item was deleted, so add delete directive
patch
=
append
(
patch
,
map
[
string
]
interface
{}{
mergeKey
:
originalValue
,
directiveMarker
:
deleteDirective
})
}
}
break
}
if
!
ignore
ChangesAnd
Deletions
{
if
!
ignoreDeletions
{
// Delete any remaining items found only in original
for
;
originalIndex
<
len
(
originalSorted
);
originalIndex
++
{
originalMap
,
ok
:=
originalSorted
[
originalIndex
]
.
(
map
[
string
]
interface
{})
...
...
@@ -339,11 +419,11 @@ loopB:
return
nil
,
fmt
.
Errorf
(
errNoMergeKeyFmt
,
originalMap
,
mergeKey
)
}
patch
=
append
(
patch
,
map
[
string
]
interface
{}{
mergeKey
:
originalValue
,
specialKey
:
specialValu
e
})
patch
=
append
(
patch
,
map
[
string
]
interface
{}{
mergeKey
:
originalValue
,
directiveMarker
:
deleteDirectiv
e
})
}
}
if
!
ignoreAdditions
{
if
!
ignore
ChangesAnd
Additions
{
// Add any remaining items found only in modified
for
;
modifiedIndex
<
len
(
modifiedSorted
);
modifiedIndex
++
{
patch
=
append
(
patch
,
modified
[
modifiedIndex
])
...
...
@@ -353,7 +433,6 @@ loopB:
return
patch
,
nil
}
// StrategicMergePatchData applies a patch using strategic merge patch semantics.
// Deprecated: StrategicMergePatchData is deprecated. Use the synonym StrategicMergePatch,
// instead, which follows the naming convention of evanphx/json-patch.
func
StrategicMergePatchData
(
original
,
patch
[]
byte
,
dataStruct
interface
{})
([]
byte
,
error
)
{
...
...
@@ -364,6 +443,14 @@ func StrategicMergePatchData(original, patch []byte, dataStruct interface{}) ([]
// must be json encoded content. A patch can be created from an original and a modified document
// by calling CreateStrategicMergePatch.
func
StrategicMergePatch
(
original
,
patch
[]
byte
,
dataStruct
interface
{})
([]
byte
,
error
)
{
if
original
==
nil
{
original
=
[]
byte
{}
}
if
patch
==
nil
{
patch
=
[]
byte
{}
}
originalMap
:=
map
[
string
]
interface
{}{}
err
:=
json
.
Unmarshal
(
original
,
&
originalMap
)
if
err
!=
nil
{
...
...
@@ -390,6 +477,10 @@ func StrategicMergePatch(original, patch []byte, dataStruct interface{}) ([]byte
}
func
getTagStructType
(
dataStruct
interface
{})
(
reflect
.
Type
,
error
)
{
if
dataStruct
==
nil
{
return
nil
,
fmt
.
Errorf
(
errBadArgTypeFmt
,
"struct"
,
"nil"
)
}
t
:=
reflect
.
TypeOf
(
dataStruct
)
if
t
.
Kind
()
==
reflect
.
Ptr
{
t
=
t
.
Elem
()
...
...
@@ -408,21 +499,26 @@ var errBadPatchTypeFmt = "unknown patch type: %s in map: %v"
// both the original map and the patch because getting a deep copy of a map in
// golang is highly non-trivial.
func
mergeMap
(
original
,
patch
map
[
string
]
interface
{},
t
reflect
.
Type
)
(
map
[
string
]
interface
{},
error
)
{
// If the map contains "$patch: replace", don't merge it, just use the
// patch map directly. Later on, can add a non-recursive replace that only
// affects the map that the $patch is in.
if
v
,
ok
:=
patch
[
specialKey
];
ok
{
if
v
==
"replace"
{
delete
(
patch
,
specialKey
)
if
v
,
ok
:=
patch
[
directiveMarker
];
ok
{
if
v
==
replaceDirective
{
// If the patch contains "$patch: replace", don't merge it, just use the
// patch directly. Later on, we can add a single level replace that only
// affects the map that the $patch is in.
delete
(
patch
,
directiveMarker
)
return
patch
,
nil
}
if
v
==
deleteDirective
{
// If the patch contains "$patch: delete", don't merge it, just return
// an empty map.
return
map
[
string
]
interface
{}{},
nil
}
return
nil
,
fmt
.
Errorf
(
errBadPatchTypeFmt
,
v
,
patch
)
}
// nil is an accepted value for original to simplify logic in other places.
// If original is nil, create a map so if patch requires us to modify the
// map, it'll work.
// If original is nil, replace it with an empty map and then apply the patch.
if
original
==
nil
{
original
=
map
[
string
]
interface
{}{}
}
...
...
@@ -461,7 +557,7 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[strin
return
nil
,
err
}
if
originalType
.
Kind
()
==
reflect
.
Map
&&
fieldPatchStrategy
!=
"replace"
{
if
originalType
.
Kind
()
==
reflect
.
Map
&&
fieldPatchStrategy
!=
replaceDirective
{
typedOriginal
:=
original
[
k
]
.
(
map
[
string
]
interface
{})
typedPatch
:=
patchV
.
(
map
[
string
]
interface
{})
var
err
error
...
...
@@ -473,7 +569,7 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[strin
continue
}
if
originalType
.
Kind
()
==
reflect
.
Slice
&&
fieldPatchStrategy
==
"merge"
{
if
originalType
.
Kind
()
==
reflect
.
Slice
&&
fieldPatchStrategy
==
mergeDirective
{
elemType
:=
fieldType
.
Elem
()
typedOriginal
:=
original
[
k
]
.
([]
interface
{})
typedPatch
:=
patchV
.
([]
interface
{})
...
...
@@ -527,9 +623,9 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
replace
:=
false
for
_
,
v
:=
range
patch
{
typedV
:=
v
.
(
map
[
string
]
interface
{})
patchType
,
ok
:=
typedV
[
specialKey
]
patchType
,
ok
:=
typedV
[
directiveMarker
]
if
ok
{
if
patchType
==
specialValu
e
{
if
patchType
==
deleteDirectiv
e
{
mergeValue
,
ok
:=
typedV
[
mergeKey
]
if
ok
{
_
,
originalKey
,
found
,
err
:=
findMapInSliceBasedOnKeyValue
(
original
,
mergeKey
,
mergeValue
)
...
...
@@ -544,10 +640,10 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
}
else
{
return
nil
,
fmt
.
Errorf
(
"delete patch type with no merge key defined"
)
}
}
else
if
patchType
==
"replace"
{
}
else
if
patchType
==
replaceDirective
{
replace
=
true
// Continue iterating through the array to prune any other $patch elements.
}
else
if
patchType
==
"merge"
{
}
else
if
patchType
==
mergeDirective
{
return
nil
,
fmt
.
Errorf
(
"merging lists cannot yet be specified in the patch"
)
}
else
{
return
nil
,
fmt
.
Errorf
(
errBadPatchTypeFmt
,
patchType
,
typedV
)
...
...
@@ -636,7 +732,7 @@ func sortMergeListsByName(mapJSON []byte, dataStruct interface{}) ([]byte, error
func
sortMergeListsByNameMap
(
s
map
[
string
]
interface
{},
t
reflect
.
Type
)
(
map
[
string
]
interface
{},
error
)
{
newS
:=
map
[
string
]
interface
{}{}
for
k
,
v
:=
range
s
{
if
k
!=
specialKey
{
if
k
!=
directiveMarker
{
fieldType
,
fieldPatchStrategy
,
fieldPatchMergeKey
,
err
:=
forkedjson
.
LookupPatchMetadata
(
t
,
k
)
if
err
!=
nil
{
return
nil
,
err
...
...
@@ -650,7 +746,7 @@ func sortMergeListsByNameMap(s map[string]interface{}, t reflect.Type) (map[stri
return
nil
,
err
}
}
else
if
typedV
,
ok
:=
v
.
([]
interface
{});
ok
{
if
fieldPatchStrategy
==
"merge"
{
if
fieldPatchStrategy
==
mergeDirective
{
var
err
error
v
,
err
=
sortMergeListsByNameArray
(
typedV
,
fieldType
.
Elem
(),
fieldPatchMergeKey
,
true
)
if
err
!=
nil
{
...
...
@@ -829,10 +925,8 @@ func sliceElementType(slices ...[]interface{}) (reflect.Type, error) {
}
// HasConflicts returns true if the left and right JSON interface objects overlap with
// different values in any key. The code will panic if an unrecognized type is passed
// (anything not returned by a JSON decode). All keys are required to be strings.
// Since patches of the same Type have congruent keys, this is valid for multiple patch
// types.
// different values in any key. All keys are required to be strings. Since patches of the
// same Type have congruent keys, this is valid for multiple patch types.
func
HasConflicts
(
left
,
right
interface
{})
(
bool
,
error
)
{
switch
typedLeft
:=
left
.
(
type
)
{
case
map
[
string
]
interface
{}
:
...
...
@@ -868,3 +962,69 @@ func HasConflicts(left, right interface{}) (bool, error) {
return
true
,
fmt
.
Errorf
(
"unknown type: %v"
,
reflect
.
TypeOf
(
left
))
}
}
// CreateThreeWayMergePatch reconciles a modified configuration with an original configuration,
// while preserving any changes or deletions made to the original configuration in the interim,
// and not overridden by the current configuration. All three documents must be passed to the
// method as json encoded content. It will return a strategic merge patch, or an error if any
// of the documents is invalid, or if there are any preconditions that fail against the modified
// configuration, or, if force is false and there are conflicts between the modified and current
// configurations.
func
CreateThreeWayMergePatch
(
original
,
modified
,
current
[]
byte
,
dataStruct
interface
{},
force
bool
,
fns
...
PreconditionFunc
)
([]
byte
,
error
)
{
originalMap
:=
map
[
string
]
interface
{}{}
if
len
(
original
)
>
0
{
if
err
:=
json
.
Unmarshal
(
original
,
&
originalMap
);
err
!=
nil
{
return
nil
,
errBadJSONDoc
}
}
modifiedMap
:=
map
[
string
]
interface
{}{}
if
len
(
modified
)
>
0
{
if
err
:=
json
.
Unmarshal
(
modified
,
&
modifiedMap
);
err
!=
nil
{
return
nil
,
errBadJSONDoc
}
}
currentMap
:=
map
[
string
]
interface
{}{}
if
len
(
current
)
>
0
{
if
err
:=
json
.
Unmarshal
(
current
,
&
currentMap
);
err
!=
nil
{
return
nil
,
errBadJSONDoc
}
}
t
,
err
:=
getTagStructType
(
dataStruct
)
if
err
!=
nil
{
return
nil
,
err
}
// The patch is the difference from current to modified without deletions, plus deletions
// from original to modified. To find it, we compute deletions, which are the deletions from
// original to modified, and delta, which is the difference from current to modified without
// deletions, and then apply delta to deletions as a patch, which should be strictly additive.
deltaMap
,
err
:=
diffMaps
(
currentMap
,
modifiedMap
,
t
,
false
,
true
)
if
err
!=
nil
{
return
nil
,
err
}
deletionsMap
,
err
:=
diffMaps
(
originalMap
,
modifiedMap
,
t
,
true
,
false
)
if
err
!=
nil
{
return
nil
,
err
}
patchMap
,
err
:=
mergeMap
(
deletionsMap
,
deltaMap
,
t
)
if
err
!=
nil
{
return
nil
,
err
}
// Apply the preconditions to the patch, and return an error if any of them fail.
for
_
,
fn
:=
range
fns
{
if
!
fn
(
patchMap
)
{
return
nil
,
newErrPreconditionFailed
(
patchMap
)
}
}
// TODO(jackgr): If force is false, and the patch contains any keys that are also in current,
// then return a conflict error.
return
json
.
Marshal
(
patchMap
)
}
pkg/util/strategicpatch/patch_test.go
View file @
f9364da3
...
...
@@ -26,18 +26,18 @@ import (
"github.com/ghodss/yaml"
)
type
StrategicMergePatchTestCases
struct
{
TestCases
[]
StrategicMergePatchTestCase
}
type
SortMergeListTestCases
struct
{
TestCases
[]
SortMergeListTestCase
}
type
StrategicMergePatchTestCaseData
struct
{
Original
map
[
string
]
interface
{}
Patch
map
[
string
]
interface
{}
Modified
map
[
string
]
interface
{}
type
SortMergeListTestCase
struct
{
Description
string
Original
map
[
string
]
interface
{}
Sorted
map
[
string
]
interface
{}
}
type
StrategicMergePatchTestCases
struct
{
TestCases
[]
StrategicMergePatchTestCase
}
type
StrategicMergePatchTestCase
struct
{
...
...
@@ -45,10 +45,13 @@ type StrategicMergePatchTestCase struct {
StrategicMergePatchTestCaseData
}
type
SortMergeListTestCase
struct
{
Description
string
Original
map
[
string
]
interface
{}
Sorted
map
[
string
]
interface
{}
type
StrategicMergePatchTestCaseData
struct
{
Original
map
[
string
]
interface
{}
TwoWay
map
[
string
]
interface
{}
Modified
map
[
string
]
interface
{}
Current
map
[
string
]
interface
{}
ThreeWay
map
[
string
]
interface
{}
Result
map
[
string
]
interface
{}
}
type
MergeItem
struct
{
...
...
@@ -63,6 +66,8 @@ type MergeItem struct {
SimpleMap
map
[
string
]
string
}
var
mergeItem
MergeItem
// These are test cases for SortMergeList, used to assert that it (recursively)
// sorts both merging and non merging lists correctly.
var
sortMergeListTestCaseData
=
[]
byte
(
`
...
...
@@ -225,26 +230,22 @@ func TestSortMergeLists(t *testing.T) {
tc
:=
SortMergeListTestCases
{}
err
:=
yaml
.
Unmarshal
(
sortMergeListTestCaseData
,
&
tc
)
if
err
!=
nil
{
t
.
Errorf
(
"can't unmarshal test cases:
%v
"
,
err
)
t
.
Errorf
(
"can't unmarshal test cases:
%s
\n
"
,
err
)
return
}
var
e
MergeItem
for
_
,
c
:=
range
tc
.
TestCases
{
sorted
,
err
:=
sortMergeListsByName
(
toJSONOrFail
(
c
.
Original
,
t
),
e
)
if
err
!=
nil
{
t
.
Errorf
(
"sort arrays returned error: %v"
,
err
)
}
if
!
reflect
.
DeepEqual
(
sorted
,
toJSONOrFail
(
c
.
Sorted
,
t
))
{
t
.
Errorf
(
"sorting failed: %v
\n
tried to sort:
\n
%vexpected:
\n
%vgot:
\n
%v"
,
c
.
Description
,
toYAMLOrError
(
c
.
Original
),
toYAMLOrError
(
c
.
Sorted
),
jsonToYAMLOrError
(
sorted
))
original
:=
testObjectToJSONOrFail
(
t
,
c
.
Original
,
c
.
Description
)
sorted
:=
testObjectToJSONOrFail
(
t
,
c
.
Sorted
,
c
.
Description
)
if
!
reflect
.
DeepEqual
(
original
,
sorted
)
{
t
.
Errorf
(
"error in test case: %s
\n
cannot sort object:
\n
%s
\n
%sexpected:
\n
%s
\n
got:
\n
%s
\n
"
,
c
.
Description
,
toYAMLOrError
(
c
.
Original
),
toYAMLOrError
(
c
.
Sorted
),
jsonToYAMLOrError
(
original
))
}
}
}
// These are test cases for StrategicMergePatch that cannot be generated using
// Create
Strategic
MergePatch because it doesn't use the replace directive, generate
// Create
TwoWay
MergePatch because it doesn't use the replace directive, generate
// duplicate integers for a merging list patch, or generate empty merging lists.
var
customStrategicMergePatchTestCaseData
=
[]
byte
(
`
testCases:
...
...
@@ -253,7 +254,7 @@ testCases:
mergingIntList:
- 1
- 2
patch
:
twoWay
:
mergingIntList:
- 2
- 3
...
...
@@ -267,7 +268,7 @@ testCases:
mergingList:
- name: 1
- name: 2
patch
:
twoWay
:
mergingList:
- $patch: replace
modified:
...
...
@@ -275,7 +276,7 @@ testCases:
- description: merge empty merging lists
original:
mergingList: []
patch
:
twoWay
:
mergingList: []
modified:
mergingList: []
...
...
@@ -283,14 +284,14 @@ testCases:
original:
name: 1
value: 1
patch
:
twoWay
:
$patch: replace
modified: {}
- description: add key and delete all keys from map
original:
name: 1
value: 1
patch
:
twoWay
:
other: a
$patch: replace
modified:
...
...
@@ -301,110 +302,165 @@ func TestCustomStrategicMergePatch(t *testing.T) {
tc
:=
StrategicMergePatchTestCases
{}
err
:=
yaml
.
Unmarshal
(
customStrategicMergePatchTestCaseData
,
&
tc
)
if
err
!=
nil
{
t
.
Errorf
(
"can't unmarshal test cases:
%v
"
,
err
)
t
.
Errorf
(
"can't unmarshal test cases:
%v
\n
"
,
err
)
return
}
for
_
,
c
:=
range
tc
.
TestCases
{
cOriginal
,
cPatch
,
cModified
:=
testCaseToJSONOrFail
(
t
,
c
)
testPatchApplication
(
t
,
cOriginal
,
cPatch
,
cModified
,
c
.
Description
)
}
}
func
testCaseToJSONOrFail
(
t
*
testing
.
T
,
c
StrategicMergePatchTestCase
)
([]
byte
,
[]
byte
,
[]
byte
)
{
var
e
MergeItem
cOriginal
:=
toJSONOrFail
(
c
.
Original
,
t
)
cPatch
,
err
:=
sortMergeListsByName
(
toJSONOrFail
(
c
.
Patch
,
t
),
e
)
if
err
!=
nil
{
t
.
Errorf
(
"error:%v sorting patch object:
\n
%v"
,
err
,
c
.
Patch
)
}
cModified
,
err
:=
sortMergeListsByName
(
toJSONOrFail
(
c
.
Modified
,
t
),
e
)
if
err
!=
nil
{
t
.
Errorf
(
"error: %v sorting modified object:
\n
%v"
,
err
,
c
.
Modified
)
}
return
cOriginal
,
cPatch
,
cModified
}
func
testPatchApplication
(
t
*
testing
.
T
,
cOriginal
,
cPatch
,
cModified
[]
byte
,
description
string
)
{
var
e
MergeItem
modified
,
err
:=
StrategicMergePatch
(
cOriginal
,
cPatch
,
e
)
if
err
!=
nil
{
t
.
Errorf
(
"error applying patch: %v:
\n
original:
\n
%vpatch:
\n
%v"
,
err
,
jsonToYAMLOrError
(
cOriginal
),
jsonToYAMLOrError
(
cPatch
))
}
// Sort the lists that have merged maps, since order is not significant.
modified
,
err
=
sortMergeListsByName
(
modified
,
e
)
if
err
!=
nil
{
t
.
Errorf
(
"error: %v sorting modified object:
\n
%v"
,
err
,
modified
)
}
if
!
reflect
.
DeepEqual
(
modified
,
cModified
)
{
t
.
Errorf
(
"patch application failed: %v
\n
original:
\n
%vpatch:
\n
%vexpected modified:
\n
%vgot modified:
\n
%v"
,
description
,
jsonToYAMLOrError
(
cOriginal
),
jsonToYAMLOrError
(
cPatch
),
jsonToYAMLOrError
(
cModified
),
jsonToYAMLOrError
(
modified
))
original
,
twoWay
,
modified
:=
twoWayTestCaseToJSONOrFail
(
t
,
c
)
testPatchApplication
(
t
,
original
,
twoWay
,
modified
,
c
.
Description
)
}
}
// These are test cases for CreateStrategicMergePatch, used to assert that it
// generates the correct patch for a given outcome. They are also test cases for
// StrategicMergePatch, used to assert that applying a patch yields the correct
// outcome.
// These are test cases for StrategicMergePatch, to assert that applying a patch
// yields the correct outcome. They are also test cases for CreateTwoWayMergePatch
// and CreateThreeWayMergePatch, to assert that they both generate the correct patch
// for the given set of input documents.
//
var
createStrategicMergePatchTestCaseData
=
[]
byte
(
`
testCases:
- description: add field to map
original:
name: 1
patch:
twoWay:
value: 1
modified:
name: 1
value: 1
current:
name: 1
other: a
threeWay:
value: 1
result:
name: 1
value: 1
other: a
- description: add field to map with conflict
original:
name: 1
twoWay:
value: 1
modified:
name: 1
value: 1
current:
name: a
other: a
threeWay:
name: 1
value: 1
result:
name: 1
value: 1
other: a
- description: add field and delete field from map
original:
name: 1
patch
:
twoWay
:
name: null
value: 1
modified:
value: 1
current:
name: 1
other: a
threeWay:
name: null
value: 1
result:
value: 1
other: a
- description: delete field from nested map
original:
simpleMap:
key1: 1
key2: 1
patch:
twoWay:
simpleMap:
key2: null
modified:
simpleMap:
key1: 1
current:
simpleMap:
key1: 1
key2: 1
other: a
threeWay:
simpleMap:
key2: null
result:
simpleMap:
key1: 1
other: a
- description: delete field from nested map with conflict
original:
simpleMap:
key1: 1
key2: 1
twoWay:
simpleMap:
key2: null
modified:
simpleMap:
key1: 1
current:
simpleMap:
key1: a
key2: 1
other: a
threeWay:
simpleMap:
key1: 1
key2: null
result:
simpleMap:
key1: 1
other: a
- description: delete all fields from map
original:
name: 1
value: 1
patch
:
twoWay
:
name: null
value: null
modified: {}
current:
name: 1
value: 1
other: a
threeWay:
name: null
value: null
result:
other: a
- description: add field and delete all fields from map
original:
name: 1
value: 1
patch:
other: a
twoWay:
name: null
value: null
other: a
modified:
other: a
current:
name: 1
value: a
other: b
threeWay:
name: null
value: null
other: a
result:
other: a
- description: replace list of scalars
original:
nonMergingIntList:
- 1
- 2
patch
:
twoWay
:
nonMergingIntList:
- 2
- 3
...
...
@@ -412,12 +468,23 @@ testCases:
nonMergingIntList:
- 2
- 3
current:
nonMergingIntList:
- 1
threeWay:
nonMergingIntList:
- 2
- 3
result:
nonMergingIntList:
- 2
- 3
- description: merge lists of scalars
original:
mergingIntList:
- 1
- 2
patch
:
twoWay
:
mergingIntList:
- 3
modified:
...
...
@@ -425,13 +492,27 @@ testCases:
- 1
- 2
- 3
current:
mergingIntList:
- 1
- 2
- 4
threeWay:
mergingIntList:
- 3
result:
mergingIntList:
- 1
- 2
- 3
- 4
- description: merge lists of maps
original:
mergingList:
- name: 1
- name: 2
value: 2
patch
:
twoWay
:
mergingList:
- name: 3
value: 3
...
...
@@ -442,13 +523,33 @@ testCases:
value: 2
- name: 3
value: 3
current:
mergingList:
- name: 1
other: a
- name: 2
value: 2
other: b
threeWay:
mergingList:
- name: 3
value: 3
result:
mergingList:
- name: 1
other: a
- name: 2
value: 2
other: b
- name: 3
value: 3
- description: add field to map in merging list
original:
mergingList:
- name: 1
- name: 2
value: 2
patch
:
twoWay
:
mergingList:
- name: 1
value: 1
...
...
@@ -458,13 +559,32 @@ testCases:
value: 1
- name: 2
value: 2
current:
mergingList:
- name: 1
other: a
- name: 2
value: 2
other: b
threeWay:
mergingList:
- name: 1
value: 1
result:
mergingList:
- name: 1
value: 1
other: a
- name: 2
value: 2
other: b
- description: add duplicate field to map in merging list
original:
mergingList:
- name: 1
- name: 2
value: 2
patch
:
twoWay
:
mergingList:
- name: 1
value: 1
...
...
@@ -474,6 +594,24 @@ testCases:
value: 1
- name: 2
value: 2
current:
mergingList:
- name: 1
value: 1
other: a
- name: 2
value: 2
other: b
threeWay:
{}
result:
mergingList:
- name: 1
value: 1
other: a
- name: 2
value: 2
other: b
- description: replace map field value in merging list
original:
mergingList:
...
...
@@ -481,7 +619,7 @@ testCases:
value: 1
- name: 2
value: 2
patch
:
twoWay
:
mergingList:
- name: 1
value: a
...
...
@@ -491,114 +629,364 @@ testCases:
value: a
- name: 2
value: 2
- description: delete map from merging list
original:
current:
mergingList:
- name: 1
value: 1
other: a
- name: 2
patch:
value: 2
other: b
threeWay:
mergingList:
- name: 1
$patch: delete
modified
:
value: a
result
:
mergingList:
- name: 1
value: a
other: a
- name: 2
- description: delete missing map from merging list
value: 2
other: b
- description: replace map field value in merging list with conflict
original:
mergingList:
- name: 1
value: 1
- name: 2
patch:
value: 2
twoWay:
mergingList:
- name: 1
$patch: delete
value: a
modified:
mergingList:
- name: 2
- description: add map and delete map from merging list
original:
merginglist:
- name: 1
value: a
- name: 2
patch:
merginglist:
value: 2
current:
mergingList:
- name: 1
$patch: delete
- name: 3
modified:
merginglist:
value: 1
other: a
- name: 2
- name: 3
- description: delete all maps from merging list
original
:
value: b
other: b
threeWay
:
mergingList:
- name: 1
value: a
- name: 2
patch:
value: 2
result:
mergingList:
- name: 1
$patch: delete
value: a
other: a
- name: 2
$patch: delete
modified:
mergingList: []
- description: delete all maps from partially empty merging list
value: 2
other: b
- description: delete map from merging list
original:
mergingList:
- name: 1
- name: 2
patch
:
twoWay
:
mergingList:
- name: 1
$patch: delete
- name: 2
$patch: delete
modified:
mergingList:
[]
- description: delete all maps from empty merging list
original
:
mergingList:
- name: 2
current
:
mergingList:
- name: 1
other: a
- name: 2
patch:
other: b
threeWay:
mergingList:
- name: 1
$patch: delete
result:
mergingList:
- name: 2
$patch: delete
modified:
mergingList: []
- description: delete field from map in merging list
other: b
- description: delete missing map from merging list
original:
mergingList:
- name: 1
value: 1
- name: 2
value: 2
patch:
twoWay:
mergingList:
- name: 1
value: null
$patch: delete
modified:
mergingList:
- name: 1
- name: 2
value: 2
- description: replace non merging list nested in merging list
original:
current:
mergingList:
- name: 1
nonMergingList:
- name: 1
- name: 2
value: 2
- name: 2
patch:
other: b
threeWay:
mergingList:
- name: 1
nonMergingList:
- name: 1
value: 1
$patch: delete
result:
mergingList:
- name: 2
other: b
- description: delete map from merging list with conflict
original:
mergingList:
- name: 1
- name: 2
twoWay:
mergingList:
- name: 1
$patch: delete
modified:
mergingList:
- name: 2
current:
mergingList:
- name: 1
other: a
threeWay:
mergingList:
- name: 1
$patch: delete
- name: 2
result:
mergingList:
- name: 2
- description: add map and delete map from merging list
original:
merginglist:
- name: 1
- name: 2
twoWay:
merginglist:
- name: 1
$patch: delete
- name: 3
modified:
merginglist:
- name: 2
- name: 3
current:
merginglist:
- name: 1
other: a
- name: 2
other: b
- name: 4
other: c
threeWay:
merginglist:
- name: 1
$patch: delete
- name: 3
result:
merginglist:
- name: 2
other: b
- name: 3
- name: 4
other: c
- description: add map and delete map from merging list with conflict
original:
merginglist:
- name: 1
- name: 2
twoWay:
merginglist:
- name: 1
$patch: delete
- name: 3
modified:
merginglist:
- name: 2
- name: 3
current:
merginglist:
- name: 1
other: a
- name: 4
other: c
threeWay:
merginglist:
- name: 1
$patch: delete
- name: 2
- name: 3
result:
merginglist:
- name: 2
- name: 3
- name: 4
other: c
- description: delete all maps from merging list
original:
mergingList:
- name: 1
- name: 2
twoWay:
mergingList:
- name: 1
$patch: delete
- name: 2
$patch: delete
modified:
mergingList: []
current:
mergingList:
- name: 1
other: a
- name: 2
other: b
threeWay:
mergingList:
- name: 1
$patch: delete
- name: 2
$patch: delete
result:
mergingList: []
- description: delete all maps from partially empty merging list
original:
mergingList:
- name: 1
- name: 2
twoWay:
mergingList:
- name: 1
$patch: delete
- name: 2
$patch: delete
modified:
mergingList: []
current:
mergingList:
- name: 1
other: a
threeWay:
mergingList:
- name: 1
$patch: delete
- name: 2
$patch: delete
result:
mergingList: []
- description: delete all maps from empty merging list
original:
mergingList:
- name: 1
- name: 2
twoWay:
mergingList:
- name: 1
$patch: delete
- name: 2
$patch: delete
modified:
mergingList: []
current:
mergingList: []
threeWay:
mergingList:
- name: 1
$patch: delete
- name: 2
$patch: delete
result:
mergingList: []
- description: delete field from map in merging list
original:
mergingList:
- name: 1
value: 1
- name: 2
value: 2
twoWay:
mergingList:
- name: 1
value: null
modified:
mergingList:
- name: 1
- name: 2
value: 2
current:
mergingList:
- name: 1
value: 1
other: a
- name: 2
other: b
threeWay:
mergingList:
- name: 1
value: null
- name: 2
value: 2
result:
mergingList:
- name: 1
other: a
- name: 2
value: 2
other: b
- description: delete field from map in merging list with conflict
original:
mergingList:
- name: 1
value: 1
- name: 2
value: 2
twoWay:
mergingList:
- name: 1
value: null
modified:
mergingList:
- name: 1
- name: 2
value: 2
current:
mergingList:
- name: 1
value: a
other: a
threeWay:
mergingList:
- name: 1
value: null
- name: 2
value: 2
result:
mergingList:
- name: 1
other: a
- name: 2
value: 2
- description: replace non merging list nested in merging list
original:
mergingList:
- name: 1
nonMergingList:
- name: 1
- name: 2
value: 2
- name: 2
twoWay:
mergingList:
- name: 1
nonMergingList:
- name: 1
value: 1
modified:
mergingList:
- name: 1
...
...
@@ -606,6 +994,31 @@ testCases:
- name: 1
value: 1
- name: 2
current:
mergingList:
- name: 1
other: a
nonMergingList:
- name: 1
- name: 2
value: 2
- name: 2
other: b
threeWay:
mergingList:
- name: 1
nonMergingList:
- name: 1
value: 1
result:
mergingList:
- name: 1
other: a
nonMergingList:
- name: 1
value: 1
- name: 2
other: b
- description: add field to map in merging list nested in merging list
original:
mergingList:
...
...
@@ -615,7 +1028,112 @@ testCases:
- name: 2
value: 2
- name: 2
patch:
twoWay:
mergingList:
- name: 1
mergingList:
- name: 1
value: 1
modified:
mergingList:
- name: 1
mergingList:
- name: 1
value: 1
- name: 2
value: 2
- name: 2
current:
mergingList:
- name: 1
other: a
mergingList:
- name: 1
- name: 2
value: 2
- name: 2
other: b
threeWay:
mergingList:
- name: 1
mergingList:
- name: 1
value: 1
result:
mergingList:
- name: 1
other: a
mergingList:
- name: 1
value: 1
- name: 2
value: 2
- name: 2
other: b
- description: add field to map in merging list nested in merging list with value conflict
original:
mergingList:
- name: 1
mergingList:
- name: 1
- name: 2
value: 2
- name: 2
twoWay:
mergingList:
- name: 1
mergingList:
- name: 1
value: 1
modified:
mergingList:
- name: 1
mergingList:
- name: 1
value: 1
- name: 2
value: 2
- name: 2
current:
mergingList:
- name: 1
other: a
mergingList:
- name: 1
value: a
- name: 2
value: b
- name: 2
other: b
threeWay:
mergingList:
- name: 1
mergingList:
- name: 1
value: 1
- name: 2
value: 2
result:
mergingList:
- name: 1
other: a
mergingList:
- name: 1
value: 1
- name: 2
value: 2
- name: 2
other: b
- description: add field to map in merging list nested in merging list with deletion conflict
original:
mergingList:
- name: 1
mergingList:
- name: 1
- name: 2
value: 2
- name: 2
twoWay:
mergingList:
- name: 1
mergingList:
...
...
@@ -630,28 +1148,70 @@ testCases:
- name: 2
value: 2
- name: 2
current:
mergingList:
- name: 1
other: a
mergingList:
- name: 2
value: 2
- name: 2
other: b
threeWay:
mergingList:
- name: 1
mergingList:
- name: 1
value: 1
result:
mergingList:
- name: 1
other: a
mergingList:
- name: 1
value: 1
- name: 2
value: 2
- name: 2
other: b
- description: merge empty merging lists
original:
mergingList: []
patch
:
twoWay
:
{}
modified:
mergingList: []
current:
mergingList: []
threeWay:
{}
result:
mergingList: []
- description: add map to merging list by pointer
original:
mergeItemPtr:
- name: 1
patch
:
twoWay
:
mergeItemPtr:
- name: 2
modified:
mergeItemPtr:
- name: 1
- name: 2
current:
mergeItemPtr:
- name: 1
other: a
- name: 3
threeWay:
mergeItemPtr:
- name: 2
result:
mergeItemPtr:
- name: 1
other: a
- name: 2
- name: 3
- description: add field to map in merging list by pointer
original:
mergeItemPtr:
...
...
@@ -661,7 +1221,7 @@ testCases:
- name: 2
value: 2
- name: 2
patch
:
twoWay
:
mergeItemPtr:
- name: 1
mergeItemPtr:
...
...
@@ -676,57 +1236,162 @@ testCases:
- name: 2
value: 2
- name: 2
current:
mergeItemPtr:
- name: 1
other: a
mergeItemPtr:
- name: 1
other: a
- name: 2
value: 2
other: b
- name: 2
other: b
threeWay:
mergeItemPtr:
- name: 1
mergeItemPtr:
- name: 1
value: 1
result:
mergeItemPtr:
- name: 1
other: a
mergeItemPtr:
- name: 1
value: 1
other: a
- name: 2
value: 2
other: b
- name: 2
other: b
`
)
func
TestStrategicMergePatch
(
t
*
testing
.
T
)
{
tc
:=
StrategicMergePatchTestCases
{}
err
:=
yaml
.
Unmarshal
(
createStrategicMergePatchTestCaseData
,
&
tc
)
if
err
!=
nil
{
t
.
Errorf
(
"can't unmarshal test cases:
%v
"
,
err
)
t
.
Errorf
(
"can't unmarshal test cases:
%s
\n
"
,
err
)
return
}
var
e
MergeItem
for
_
,
c
:=
range
tc
.
TestCases
{
cOriginal
,
cPatch
,
cModified
:=
testCaseToJSONOrFail
(
t
,
c
)
testTwoWayPatch
(
t
,
c
)
testThreeWayPatch
(
t
,
c
)
}
}
// Test patch generation
patch
,
err
:=
CreateStrategicMergePatch
(
cOriginal
,
cModified
,
e
)
if
err
!=
nil
{
t
.
Errorf
(
"error generating patch: %s:
\n
%v"
,
err
,
toYAMLOrError
(
c
.
StrategicMergePatchTestCaseData
))
}
func
testTwoWayPatch
(
t
*
testing
.
T
,
c
StrategicMergePatchTestCase
)
{
original
,
expected
,
modified
:=
twoWayTestCaseToJSONOrFail
(
t
,
c
)
// Sort the lists that have merged maps, since order is not significant.
patch
,
err
=
sortMergeListsByName
(
patch
,
e
)
if
err
!=
nil
{
t
.
Errorf
(
"error: %s sorting patch object:
\n
%v"
,
err
,
patch
)
}
actual
,
err
:=
CreateTwoWayMergePatch
(
original
,
modified
,
mergeItem
)
if
err
!=
nil
{
t
.
Errorf
(
"error: %s in test case: %s
\n
cannot create two way patch:%s:
\n
%s
\n
"
,
err
,
c
.
Description
,
toYAMLOrError
(
c
.
StrategicMergePatchTestCaseData
))
}
testPatchCreation
(
t
,
expected
,
actual
,
c
.
Description
)
testPatchApplication
(
t
,
original
,
actual
,
modified
,
c
.
Description
)
}
if
!
reflect
.
DeepEqual
(
patch
,
cPatch
)
{
t
.
Errorf
(
"patch generation failed:
\n
%vgot patch:
\n
%v"
,
toYAMLOrError
(
c
.
StrategicMergePatchTestCaseData
),
jsonToYAMLOrError
(
patch
))
func
twoWayTestCaseToJSONOrFail
(
t
*
testing
.
T
,
c
StrategicMergePatchTestCase
)
([]
byte
,
[]
byte
,
[]
byte
)
{
return
testObjectToJSONOrFail
(
t
,
c
.
Original
,
c
.
Description
),
testObjectToJSONOrFail
(
t
,
c
.
TwoWay
,
c
.
Description
),
testObjectToJSONOrFail
(
t
,
c
.
Modified
,
c
.
Description
)
}
func
testThreeWayPatch
(
t
*
testing
.
T
,
c
StrategicMergePatchTestCase
)
{
original
,
modified
,
current
,
expected
,
result
:=
threeWayTestCaseToJSONOrFail
(
t
,
c
)
actual
,
err
:=
CreateThreeWayMergePatch
(
original
,
modified
,
current
,
mergeItem
,
false
)
if
err
!=
nil
{
if
IsConflict
(
err
)
{
if
len
(
c
.
Result
)
>
0
{
t
.
Errorf
(
"error in test case: %s
\n
unexpected conflict occurred:
\n
%s
\n
"
,
c
.
Description
,
toYAMLOrError
(
c
.
StrategicMergePatchTestCaseData
))
}
return
}
// Test patch application
testPatchApplication
(
t
,
cOriginal
,
cPatch
,
cModified
,
c
.
Description
)
t
.
Errorf
(
"error: %s in test case: %s
\n
cannot create three way patch:
\n
%s
\n
"
,
err
,
c
.
Description
,
toYAMLOrError
(
c
.
StrategicMergePatchTestCaseData
))
}
if
len
(
c
.
Result
)
<
1
{
t
.
Errorf
(
"error in test case: %s
\n
expected conflict did not occur:
\n
%s
\n
"
,
c
.
Description
,
toYAMLOrError
(
c
.
StrategicMergePatchTestCaseData
))
}
testPatchCreation
(
t
,
expected
,
actual
,
c
.
Description
)
testPatchApplication
(
t
,
current
,
actual
,
result
,
c
.
Description
)
}
func
toYAMLOrError
(
v
interface
{})
string
{
y
,
err
:=
toYAML
(
v
)
func
threeWayTestCaseToJSONOrFail
(
t
*
testing
.
T
,
c
StrategicMergePatchTestCase
)
([]
byte
,
[]
byte
,
[]
byte
,
[]
byte
,
[]
byte
)
{
return
testObjectToJSONOrFail
(
t
,
c
.
Original
,
c
.
Description
),
testObjectToJSONOrFail
(
t
,
c
.
Modified
,
c
.
Description
),
testObjectToJSONOrFail
(
t
,
c
.
Current
,
c
.
Description
),
testObjectToJSONOrFail
(
t
,
c
.
ThreeWay
,
c
.
Description
),
testObjectToJSONOrFail
(
t
,
c
.
Result
,
c
.
Description
)
}
func
testPatchCreation
(
t
*
testing
.
T
,
expected
,
actual
[]
byte
,
description
string
)
{
sorted
,
err
:=
sortMergeListsByName
(
actual
,
mergeItem
)
if
err
!=
nil
{
return
err
.
Error
()
t
.
Errorf
(
"error: %s in test case: %s
\n
cannot sort patch:
\n
%s
\n
"
,
err
,
description
,
jsonToYAMLOrError
(
actual
))
}
return
y
if
!
reflect
.
DeepEqual
(
sorted
,
expected
)
{
t
.
Errorf
(
"error in test case: %s
\n
expected patch:
\n
%s
\n
got:
\n
%s
\n
"
,
description
,
jsonToYAMLOrError
(
expected
),
jsonToYAMLOrError
(
sorted
))
}
}
func
testPatchApplication
(
t
*
testing
.
T
,
original
,
patch
,
expected
[]
byte
,
description
string
)
{
result
,
err
:=
StrategicMergePatch
(
original
,
patch
,
mergeItem
)
if
err
!=
nil
{
t
.
Errorf
(
"error: %s in test case: %s
\n
cannot apply patch:
\n
%s
\n
to original:
\n
%s
\n
"
,
err
,
description
,
jsonToYAMLOrError
(
patch
),
jsonToYAMLOrError
(
original
))
}
sorted
,
err
:=
sortMergeListsByName
(
result
,
mergeItem
)
if
err
!=
nil
{
t
.
Errorf
(
"error: %s in test case: %s
\n
cannot sort result object:
\n
%s
\n
"
,
err
,
description
,
jsonToYAMLOrError
(
result
))
}
if
!
reflect
.
DeepEqual
(
sorted
,
expected
)
{
format
:=
"error in test case: %s
\n
patch application failed:
\n
original:
\n
%s
\n
patch:
\n
%s
\n
expected:
\n
%s
\n
got:
\n
%s
\n
"
t
.
Errorf
(
format
,
description
,
jsonToYAMLOrError
(
original
),
jsonToYAMLOrError
(
patch
),
jsonToYAMLOrError
(
expected
),
jsonToYAMLOrError
(
sorted
))
}
}
func
t
oJSONOrFail
(
v
interface
{},
t
*
testing
.
T
)
[]
byte
{
theJSON
,
err
:=
toJSON
(
v
)
func
t
estObjectToJSONOrFail
(
t
*
testing
.
T
,
o
map
[
string
]
interface
{},
description
string
)
[]
byte
{
j
,
err
:=
toJSON
(
o
)
if
err
!=
nil
{
t
.
Error
(
err
)
}
return
theJSON
r
,
err
:=
sortMergeListsByName
(
j
,
mergeItem
)
if
err
!=
nil
{
t
.
Errorf
(
"error: %s in test case: %s
\n
cannot sort object:
\n
%s
\n
"
,
err
,
description
,
j
)
}
return
r
}
func
toYAMLOrError
(
v
interface
{})
string
{
y
,
err
:=
toYAML
(
v
)
if
err
!=
nil
{
return
err
.
Error
()
}
return
y
}
func
jsonToYAMLOrError
(
j
[]
byte
)
string
{
...
...
@@ -741,7 +1406,7 @@ func jsonToYAMLOrError(j []byte) string {
func
toYAML
(
v
interface
{})
(
string
,
error
)
{
y
,
err
:=
yaml
.
Marshal
(
v
)
if
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"yaml marshal failed:
%v
\n
%v
"
,
err
,
spew
.
Sdump
(
v
))
return
""
,
fmt
.
Errorf
(
"yaml marshal failed:
%v
\n
%v
\n
"
,
err
,
spew
.
Sdump
(
v
))
}
return
string
(
y
),
nil
...
...
@@ -750,7 +1415,7 @@ func toYAML(v interface{}) (string, error) {
func
toJSON
(
v
interface
{})
([]
byte
,
error
)
{
j
,
err
:=
json
.
Marshal
(
v
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"json marshal failed:
%v
\n
%v
"
,
err
,
spew
.
Sdump
(
v
))
return
nil
,
fmt
.
Errorf
(
"json marshal failed:
%v
\n
%v
\n
"
,
err
,
spew
.
Sdump
(
v
))
}
return
j
,
nil
...
...
@@ -759,7 +1424,7 @@ func toJSON(v interface{}) ([]byte, error) {
func
jsonToYAML
(
j
[]
byte
)
([]
byte
,
error
)
{
y
,
err
:=
yaml
.
JSONToYAML
(
j
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"json to yaml failed:
%v
\n
%v
"
,
err
,
j
)
return
nil
,
fmt
.
Errorf
(
"json to yaml failed:
%v
\n
%v
\n
"
,
err
,
j
)
}
return
y
,
nil
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment