Merge pull request #38665 from ymqytw/fix_list_of_primitives

Automatic merge from submit-queue (batch tested with PRs 39834, 38665)

Use parallel list for deleting items from a primitive list with merge strategy

Implemented parallel list for deleting items from a primitive list with merge strategy. Ref: [design doc](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#list-of-primitives)

fixes #35163 and #32398

When using parallel list, we don't need to worry about version skew.
When an old APIServer gets a new patch like:
```yaml
metadata:
  $deleteFromPrimitiveList/finalizers:
  - b
  finalizers:
  - c
```
It won't fail and work as before, because the parallel list will be dropped during json decoding.

Remaining issue: There is no check when creating a set (primitive list with merge strategy). Duplicates may get in.
It happens in two cases:
1) Creation using POST
2) Creating a list that doesn't exist before using PATCH

Fixing the first case is the beyond the scope of this PR.
The second case can be fixed in this PR if we need that.

cc: @pwittrock @kubernetes/kubectl @kubernetes/sig-api-machinery 

```release-note
Fix issue around merging lists of primitives when using PATCH or kubectl apply.
```
This commit is contained in:
Kubernetes Submit Queue 2017-01-12 20:03:23 -08:00 committed by GitHub
commit 14362160ba
2 changed files with 390 additions and 64 deletions

View File

@ -20,6 +20,7 @@ import (
"fmt"
"reflect"
"sort"
"strings"
"k8s.io/apimachinery/pkg/util/json"
forkedjson "k8s.io/kubernetes/third_party/forked/golang/json"
@ -43,6 +44,8 @@ const (
deleteDirective = "delete"
replaceDirective = "replace"
mergeDirective = "merge"
deleteFromPrimitiveListDirectivePrefix = "$deleteFromPrimitiveList"
)
// IsPreconditionFailed returns true if the provided error indicates
@ -87,6 +90,7 @@ func IsConflict(err error) bool {
var errBadJSONDoc = fmt.Errorf("Invalid JSON document")
var errNoListOfLists = fmt.Errorf("Lists of lists are not supported")
var errBadPatchFormatForPrimitiveList = fmt.Errorf("Invalid patch format of primitive list")
// 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,
@ -194,6 +198,7 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreC
continue
}
// The patch has a patch directive
if key == directiveMarker {
originalString, ok := originalValue.(string)
if !ok {
@ -248,13 +253,19 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreC
}
if fieldPatchStrategy == mergeDirective {
patchValue, err := diffLists(originalValueTyped, modifiedValueTyped, fieldType.Elem(), fieldPatchMergeKey, ignoreChangesAndAdditions, ignoreDeletions)
addList, deletionList, err := diffLists(originalValueTyped, modifiedValueTyped, fieldType.Elem(), fieldPatchMergeKey, ignoreChangesAndAdditions, ignoreDeletions)
if err != nil {
return nil, err
}
if len(patchValue) > 0 {
patch[key] = patchValue
if len(addList) > 0 {
patch[key] = addList
}
// generate a parallel list for deletion
if len(deletionList) > 0 {
parallelDeletionListKey := fmt.Sprintf("%s/%s", deleteFromPrimitiveListDirectivePrefix, key)
patch[parallelDeletionListKey] = deletionList
}
continue
@ -282,77 +293,99 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreC
return patch, nil
}
// 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, ignoreChangesAndAdditions, ignoreDeletions bool) ([]interface{}, error) {
// Returns a (recursive) strategic merge patch and a parallel deletion list if necessary.
// Only list of primitives with merge strategy will generate a parallel deletion list.
// These two lists should yield modified when applied to original, for lists with merge semantics.
func diffLists(original, modified []interface{}, t reflect.Type, mergeKey string, ignoreChangesAndAdditions, ignoreDeletions bool) ([]interface{}, []interface{}, error) {
if len(original) == 0 {
// Both slices are empty - do nothing
if len(modified) == 0 || ignoreChangesAndAdditions {
return nil, nil
return nil, nil, nil
}
return modified, nil
// Old slice was empty - add all elements from the new slice
return modified, nil, nil
}
elementType, err := sliceElementType(original, modified)
if err != nil {
return nil, err
return nil, nil, err
}
var patch []interface{}
if elementType.Kind() == reflect.Map {
patch, err = diffListsOfMaps(original, modified, t, mergeKey, ignoreChangesAndAdditions, ignoreDeletions)
} else if !ignoreChangesAndAdditions {
patch, err = diffListsOfScalars(original, modified)
switch elementType.Kind() {
case reflect.Map:
patchList, err := diffListsOfMaps(original, modified, t, mergeKey, ignoreChangesAndAdditions, ignoreDeletions)
return patchList, nil, err
case reflect.Slice:
// Lists of Lists are not permitted by the api
return nil, nil, errNoListOfLists
default:
return diffListsOfScalars(original, modified, ignoreChangesAndAdditions, ignoreDeletions)
}
if err != nil {
return nil, err
}
return patch, nil
}
// 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{}) ([]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.
return nil, nil
}
// diffListsOfScalars returns 2 lists, the first one is addList and the second one is deletionList.
// Argument ignoreChangesAndAdditions controls if calculate addList. true means not calculate.
// Argument ignoreDeletions controls if calculate deletionList. true means not calculate.
func diffListsOfScalars(original, modified []interface{}, ignoreChangesAndAdditions, ignoreDeletions bool) ([]interface{}, []interface{}, error) {
// Sort the scalars for easier calculating the diff
originalScalars := sortScalars(original)
modifiedScalars := sortScalars(modified)
patch := []interface{}{}
originalScalars := uniqifyAndSortScalars(original)
modifiedScalars := uniqifyAndSortScalars(modified)
originalIndex, modifiedIndex := 0, 0
addList := []interface{}{}
deletionList := []interface{}{}
loopB:
for ; modifiedIndex < len(modifiedScalars); modifiedIndex++ {
for ; originalIndex < len(originalScalars); originalIndex++ {
originalString := fmt.Sprintf("%v", original[originalIndex])
modifiedString := fmt.Sprintf("%v", modified[modifiedIndex])
if originalString >= modifiedString {
if originalString != modifiedString {
patch = append(patch, modified[modifiedIndex])
}
originalInBounds := originalIndex < len(originalScalars)
modifiedInBounds := modifiedIndex < len(modifiedScalars)
bothInBounds := originalInBounds && modifiedInBounds
for originalInBounds || modifiedInBounds {
continue loopB
}
// There is no else clause because there is no way to create a patch that deletes
// a scalar from a list of scalars with merge semantics.
// we need to compare the string representation of the scalar,
// because the scalar is an interface which doesn't support neither < nor <
// And that's how func sortScalars compare scalars.
var originalString, modifiedString string
if originalInBounds {
originalString = fmt.Sprintf("%v", originalScalars[originalIndex])
}
break
if modifiedInBounds {
modifiedString = fmt.Sprintf("%v", modifiedScalars[modifiedIndex])
}
switch {
// scalars are identical
case bothInBounds && originalString == modifiedString:
originalIndex++
modifiedIndex++
// only modified is in bound
case !originalInBounds:
fallthrough
// modified has additional scalar
case bothInBounds && originalString > modifiedString:
if !ignoreChangesAndAdditions {
modifiedValue := modifiedScalars[modifiedIndex]
addList = append(addList, modifiedValue)
}
modifiedIndex++
// only original is in bound
case !modifiedInBounds:
fallthrough
// original has additional scalar
case bothInBounds && originalString < modifiedString:
if !ignoreDeletions {
originalValue := originalScalars[originalIndex]
deletionList = append(deletionList, originalValue)
}
originalIndex++
}
originalInBounds = originalIndex < len(originalScalars)
modifiedInBounds = modifiedIndex < len(modifiedScalars)
bothInBounds = originalInBounds && modifiedInBounds
}
// Add any remaining items found only in modified
for ; modifiedIndex < len(modifiedScalars); modifiedIndex++ {
patch = append(patch, modified[modifiedIndex])
}
return patch, nil
return addList, deletionList, nil
}
var errNoMergeKeyFmt = "map: %v does not contain declared merge key: %s"
@ -496,7 +529,7 @@ func StrategicMergePatch(original, patch []byte, dataStruct interface{}) ([]byte
return nil, err
}
result, err := mergeMap(originalMap, patchMap, t)
result, err := mergeMap(originalMap, patchMap, t, true)
if err != nil {
return nil, err
}
@ -526,7 +559,8 @@ var errBadPatchTypeFmt = "unknown patch type: %s in map: %v"
// Merge fields from a patch map into the original map. Note: This may modify
// 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) {
// flag mergeDeleteList controls if using the parallel list to delete or keeping the list.
func mergeMap(original, patch map[string]interface{}, t reflect.Type, mergeDeleteList bool) (map[string]interface{}, error) {
if v, ok := patch[directiveMarker]; ok {
if v == replaceDirective {
// If the patch contains "$patch: replace", don't merge it, just use the
@ -553,6 +587,23 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[strin
// Start merging the patch into the original.
for k, patchV := range patch {
// If found a parallel list for deletion and we are going to merge the list,
// overwrite the key to the original key and set flag isDeleteList
isDeleteList := false
foundParallelListPrefix := strings.HasPrefix(k, deleteFromPrimitiveListDirectivePrefix)
if foundParallelListPrefix {
if !mergeDeleteList {
original[k] = patchV
continue
}
substrings := strings.SplitN(k, "/", 2)
if len(substrings) <= 1 {
return nil, errBadPatchFormatForPrimitiveList
}
isDeleteList = true
k = substrings[1]
}
// If the value of this key is null, delete the key if it exists in the
// original. Otherwise, skip it.
if patchV == nil {
@ -589,7 +640,7 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[strin
typedOriginal := original[k].(map[string]interface{})
typedPatch := patchV.(map[string]interface{})
var err error
original[k], err = mergeMap(typedOriginal, typedPatch, fieldType)
original[k], err = mergeMap(typedOriginal, typedPatch, fieldType, mergeDeleteList)
if err != nil {
return nil, err
}
@ -602,7 +653,7 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[strin
typedOriginal := original[k].([]interface{})
typedPatch := patchV.([]interface{})
var err error
original[k], err = mergeSlice(typedOriginal, typedPatch, elemType, fieldPatchMergeKey)
original[k], err = mergeSlice(typedOriginal, typedPatch, elemType, fieldPatchMergeKey, mergeDeleteList, isDeleteList)
if err != nil {
return nil, err
}
@ -623,7 +674,7 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[strin
// Merge two slices together. Note: This may modify both the original slice and
// the patch because getting a deep copy of a slice in golang is highly
// non-trivial.
func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey string) ([]interface{}, error) {
func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey string, mergeDeleteList, isDeleteList bool) ([]interface{}, error) {
if len(original) == 0 && len(patch) == 0 {
return original, nil
}
@ -636,6 +687,9 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
// If the elements are not maps, merge the slices of scalars.
if t.Kind() != reflect.Map {
if mergeDeleteList && isDeleteList {
return deleteFromSlice(original, patch), nil
}
// Maybe in the future add a "concat" mode that doesn't
// uniqify.
both := append(original, patch...)
@ -711,7 +765,7 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
var mergedMaps interface{}
var err error
// Merge into original.
mergedMaps, err = mergeMap(originalMap, typedV, elemType)
mergedMaps, err = mergeMap(originalMap, typedV, elemType, mergeDeleteList)
if err != nil {
return nil, err
}
@ -725,6 +779,34 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
return original, nil
}
// deleteFromSlice uses the parallel list to delete the items in a list of scalars
func deleteFromSlice(current, toDelete []interface{}) []interface{} {
currentScalars := uniqifyAndSortScalars(current)
toDeleteScalars := uniqifyAndSortScalars(toDelete)
currentIndex, toDeleteIndex := 0, 0
mergedList := []interface{}{}
for currentIndex < len(currentScalars) && toDeleteIndex < len(toDeleteScalars) {
originalString := fmt.Sprintf("%v", currentScalars[currentIndex])
modifiedString := fmt.Sprintf("%v", toDeleteScalars[toDeleteIndex])
switch {
// found an item to delete
case originalString == modifiedString:
currentIndex++
// Request to delete an item that was not found in the current list
case originalString > modifiedString:
toDeleteIndex++
// Found an item that was not part of the deletion list, keep it
case originalString < modifiedString:
mergedList = append(mergedList, currentScalars[currentIndex])
currentIndex++
}
}
return append(mergedList, currentScalars[currentIndex:]...)
}
// This method no longer panics if any element of the slice is not a map.
func findMapInSliceBasedOnKeyValue(m []interface{}, key string, value interface{}) (map[string]interface{}, int, bool, error) {
for k, v := range m {
@ -761,10 +843,17 @@ func sortMergeListsByName(mapJSON []byte, dataStruct interface{}) ([]byte, error
return json.Marshal(newM)
}
// Function sortMergeListsByNameMap recursively sorts the merge lists by its mergeKey in a map.
func sortMergeListsByNameMap(s map[string]interface{}, t reflect.Type) (map[string]interface{}, error) {
newS := map[string]interface{}{}
for k, v := range s {
if k != directiveMarker {
if strings.HasPrefix(k, deleteFromPrimitiveListDirectivePrefix) {
typedV, ok := v.([]interface{})
if !ok {
return nil, errBadPatchFormatForPrimitiveList
}
v = uniqifyAndSortScalars(typedV)
} else if k != directiveMarker {
fieldType, fieldPatchStrategy, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, k)
if err != nil {
return nil, err
@ -794,6 +883,7 @@ func sortMergeListsByNameMap(s map[string]interface{}, t reflect.Type) (map[stri
return newS, nil
}
// Function sortMergeListsByNameMap recursively sorts the merge lists by its mergeKey in an array.
func sortMergeListsByNameArray(s []interface{}, elemType reflect.Type, mergeKey string, recurse bool) ([]interface{}, error) {
if len(s) == 0 {
return s, nil
@ -883,7 +973,10 @@ func (ss SortableSliceOfMaps) Swap(i, j int) {
func uniqifyAndSortScalars(s []interface{}) []interface{} {
s = uniqifyScalars(s)
return sortScalars(s)
}
func sortScalars(s []interface{}) []interface{} {
ss := SortableSliceOfScalars{s}
sort.Sort(ss)
return ss.s
@ -1217,7 +1310,7 @@ func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct int
return nil, err
}
patchMap, err := mergeMap(deletionsMap, deltaMap, t)
patchMap, err := mergeMap(deletionsMap, deltaMap, t, false)
if err != nil {
return nil, err
}

View File

@ -46,13 +46,35 @@ type StrategicMergePatchTestCase struct {
StrategicMergePatchTestCaseData
}
type StrategicMergePatchRawTestCase struct {
Description string
StrategicMergePatchRawTestCaseData
}
type StrategicMergePatchTestCaseData struct {
// Original is the original object (last-applied config in annotation)
Original map[string]interface{}
TwoWay map[string]interface{}
// Modified is the modified object (new config we want)
Modified map[string]interface{}
Current map[string]interface{}
// Current is the current object (live config in the server)
Current map[string]interface{}
// TwoWay is the expected two-way merge patch diff between original and modified
TwoWay map[string]interface{}
// ThreeWay is the expected three-way merge patch
ThreeWay map[string]interface{}
Result map[string]interface{}
// Result is the expected object after applying the three-way patch on current object.
Result map[string]interface{}
}
// The meaning of each field is the same as StrategicMergePatchTestCaseData's.
// The difference is that all the fields in StrategicMergePatchRawTestCaseData are json-encoded data.
type StrategicMergePatchRawTestCaseData struct {
Original []byte
Modified []byte
Current []byte
TwoWay []byte
ThreeWay []byte
Result []byte
}
type MergeItem struct {
@ -1797,6 +1819,125 @@ testCases:
other: b
`)
var strategicMergePatchRawTestCases = []StrategicMergePatchRawTestCase{
{
Description: "delete items in lists of scalars",
StrategicMergePatchRawTestCaseData: StrategicMergePatchRawTestCaseData{
Original: []byte(`
mergingIntList:
- 1
- 2
- 3
`),
TwoWay: []byte(`
$deleteFromPrimitiveList/mergingIntList:
- 3
`),
Modified: []byte(`
mergingIntList:
- 1
- 2
`),
Current: []byte(`
mergingIntList:
- 1
- 2
- 3
- 4
`),
ThreeWay: []byte(`
$deleteFromPrimitiveList/mergingIntList:
- 3
`),
Result: []byte(`
mergingIntList:
- 1
- 2
- 4
`),
},
},
{
Description: "delete all duplicate items in lists of scalars",
StrategicMergePatchRawTestCaseData: StrategicMergePatchRawTestCaseData{
Original: []byte(`
mergingIntList:
- 1
- 2
- 3
- 3
`),
TwoWay: []byte(`
$deleteFromPrimitiveList/mergingIntList:
- 3
`),
Modified: []byte(`
mergingIntList:
- 1
- 2
`),
Current: []byte(`
mergingIntList:
- 1
- 2
- 3
- 3
- 4
`),
ThreeWay: []byte(`
$deleteFromPrimitiveList/mergingIntList:
- 3
`),
Result: []byte(`
mergingIntList:
- 1
- 2
- 4
`),
},
},
{
Description: "add and delete items in lists of scalars",
StrategicMergePatchRawTestCaseData: StrategicMergePatchRawTestCaseData{
Original: []byte(`
mergingIntList:
- 1
- 2
- 3
`),
TwoWay: []byte(`
$deleteFromPrimitiveList/mergingIntList:
- 3
mergingIntList:
- 4
`),
Modified: []byte(`
mergingIntList:
- 1
- 2
- 4
`),
Current: []byte(`
mergingIntList:
- 1
- 2
- 3
- 4
`),
ThreeWay: []byte(`
$deleteFromPrimitiveList/mergingIntList:
- 3
`),
Result: []byte(`
mergingIntList:
- 1
- 2
- 4
`),
},
},
}
func TestStrategicMergePatch(t *testing.T) {
testStrategicMergePatchWithCustomArguments(t, "bad original",
"<THIS IS NOT JSON>", "{}", mergeItem, errBadJSONDoc)
@ -1818,6 +1959,11 @@ func TestStrategicMergePatch(t *testing.T) {
testTwoWayPatch(t, c)
testThreeWayPatch(t, c)
}
for _, c := range strategicMergePatchRawTestCases {
testTwoWayPatchForRawTestCase(t, c)
testThreeWayPatchForRawTestCase(t, c)
}
}
func testStrategicMergePatchWithCustomArguments(t *testing.T, description, original, patch string, dataStruct interface{}, err error) {
@ -1849,12 +1995,32 @@ func testTwoWayPatch(t *testing.T, c StrategicMergePatchTestCase) {
testPatchApplication(t, original, actual, modified, c.Description)
}
func testTwoWayPatchForRawTestCase(t *testing.T, c StrategicMergePatchRawTestCase) {
original, expected, modified := twoWayRawTestCaseToJSONOrFail(t, c)
actual, err := CreateTwoWayMergePatch(original, modified, mergeItem)
if err != nil {
t.Errorf("error: %s\nin test case: %s\ncannot create two way patch:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
return
}
testPatchCreation(t, expected, actual, c.Description)
testPatchApplication(t, original, actual, modified, c.Description)
}
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 twoWayRawTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchRawTestCase) ([]byte, []byte, []byte) {
return yamlToJSONOrError(t, c.Original),
yamlToJSONOrError(t, c.TwoWay),
yamlToJSONOrError(t, c.Modified)
}
func testThreeWayPatch(t *testing.T, c StrategicMergePatchTestCase) {
original, modified, current, expected, result := threeWayTestCaseToJSONOrFail(t, c)
actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, false)
@ -1896,6 +2062,47 @@ func testThreeWayPatch(t *testing.T, c StrategicMergePatchTestCase) {
testPatchApplication(t, current, actual, result, c.Description)
}
func testThreeWayPatchForRawTestCase(t *testing.T, c StrategicMergePatchRawTestCase) {
original, modified, current, expected, result := threeWayRawTestCaseToJSONOrFail(t, c)
actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, false)
if err != nil {
if !IsConflict(err) {
t.Errorf("error: %s\nin test case: %s\ncannot create three way patch:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
return
}
if !strings.Contains(c.Description, "conflict") {
t.Errorf("unexpected conflict: %s\nin test case: %s\ncannot create three way patch:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
return
}
if len(c.Result) > 0 {
actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, true)
if err != nil {
t.Errorf("error: %s\nin test case: %s\ncannot force three way patch application:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
return
}
testPatchCreation(t, expected, actual, c.Description)
testPatchApplication(t, current, actual, result, c.Description)
}
return
}
if strings.Contains(c.Description, "conflict") || len(c.Result) < 1 {
t.Errorf("error: %s\nin test case: %s\nexpected conflict did not occur:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
return
}
testPatchCreation(t, expected, actual, c.Description)
testPatchApplication(t, current, actual, result, c.Description)
}
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),
@ -1904,6 +2111,14 @@ func threeWayTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchTestCase) (
testObjectToJSONOrFail(t, c.Result, c.Description)
}
func threeWayRawTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchRawTestCase) ([]byte, []byte, []byte, []byte, []byte) {
return yamlToJSONOrError(t, c.Original),
yamlToJSONOrError(t, c.Modified),
yamlToJSONOrError(t, c.Current),
yamlToJSONOrError(t, c.ThreeWay),
yamlToJSONOrError(t, c.Result)
}
func testPatchCreation(t *testing.T, expected, actual []byte, description string) {
sorted, err := sortMergeListsByName(actual, mergeItem)
if err != nil {
@ -1989,6 +2204,24 @@ func jsonToYAML(j []byte) ([]byte, error) {
return y, nil
}
func yamlToJSON(y []byte) ([]byte, error) {
j, err := yaml.YAMLToJSON(y)
if err != nil {
return nil, fmt.Errorf("yaml to json failed: %v\n%v\n", err, y)
}
return j, nil
}
func yamlToJSONOrError(t *testing.T, y []byte) []byte {
j, err := yamlToJSON(y)
if err != nil {
t.Errorf("%v", err)
}
return j
}
func TestHasConflicts(t *testing.T) {
testCases := []struct {
A interface{}