mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-01 15:58:37 +00:00
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:
commit
14362160ba
@ -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
|
||||
}
|
||||
|
@ -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{}
|
||||
|
Loading…
Reference in New Issue
Block a user