mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-06 10:43:56 +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"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/util/json"
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
forkedjson "k8s.io/kubernetes/third_party/forked/golang/json"
|
forkedjson "k8s.io/kubernetes/third_party/forked/golang/json"
|
||||||
@ -43,6 +44,8 @@ const (
|
|||||||
deleteDirective = "delete"
|
deleteDirective = "delete"
|
||||||
replaceDirective = "replace"
|
replaceDirective = "replace"
|
||||||
mergeDirective = "merge"
|
mergeDirective = "merge"
|
||||||
|
|
||||||
|
deleteFromPrimitiveListDirectivePrefix = "$deleteFromPrimitiveList"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsPreconditionFailed returns true if the provided error indicates
|
// 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 errBadJSONDoc = fmt.Errorf("Invalid JSON document")
|
||||||
var errNoListOfLists = fmt.Errorf("Lists of lists are not supported")
|
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.
|
// 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,
|
// 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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The patch has a patch directive
|
||||||
if key == directiveMarker {
|
if key == directiveMarker {
|
||||||
originalString, ok := originalValue.(string)
|
originalString, ok := originalValue.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -248,13 +253,19 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreC
|
|||||||
}
|
}
|
||||||
|
|
||||||
if fieldPatchStrategy == mergeDirective {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(patchValue) > 0 {
|
if len(addList) > 0 {
|
||||||
patch[key] = patchValue
|
patch[key] = addList
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate a parallel list for deletion
|
||||||
|
if len(deletionList) > 0 {
|
||||||
|
parallelDeletionListKey := fmt.Sprintf("%s/%s", deleteFromPrimitiveListDirectivePrefix, key)
|
||||||
|
patch[parallelDeletionListKey] = deletionList
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
@ -282,77 +293,99 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreC
|
|||||||
return patch, nil
|
return patch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a (recursive) strategic merge patch that yields modified when applied to original,
|
// Returns a (recursive) strategic merge patch and a parallel deletion list if necessary.
|
||||||
// for a pair of lists with merge semantics.
|
// Only list of primitives with merge strategy will generate a parallel deletion list.
|
||||||
func diffLists(original, modified []interface{}, t reflect.Type, mergeKey string, ignoreChangesAndAdditions, ignoreDeletions bool) ([]interface{}, error) {
|
// 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 {
|
if len(original) == 0 {
|
||||||
|
// Both slices are empty - do nothing
|
||||||
if len(modified) == 0 || ignoreChangesAndAdditions {
|
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)
|
elementType, err := sliceElementType(original, modified)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var patch []interface{}
|
switch elementType.Kind() {
|
||||||
|
case reflect.Map:
|
||||||
if elementType.Kind() == reflect.Map {
|
patchList, err := diffListsOfMaps(original, modified, t, mergeKey, ignoreChangesAndAdditions, ignoreDeletions)
|
||||||
patch, err = diffListsOfMaps(original, modified, t, mergeKey, ignoreChangesAndAdditions, ignoreDeletions)
|
return patchList, nil, err
|
||||||
} else if !ignoreChangesAndAdditions {
|
case reflect.Slice:
|
||||||
patch, err = diffListsOfScalars(original, modified)
|
// 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,
|
// diffListsOfScalars returns 2 lists, the first one is addList and the second one is deletionList.
|
||||||
// for a pair of lists of scalars with merge semantics.
|
// Argument ignoreChangesAndAdditions controls if calculate addList. true means not calculate.
|
||||||
func diffListsOfScalars(original, modified []interface{}) ([]interface{}, error) {
|
// Argument ignoreDeletions controls if calculate deletionList. true means not calculate.
|
||||||
if len(modified) == 0 {
|
func diffListsOfScalars(original, modified []interface{}, ignoreChangesAndAdditions, ignoreDeletions bool) ([]interface{}, []interface{}, error) {
|
||||||
// There is no need to check the length of original because there is no way to create
|
// Sort the scalars for easier calculating the diff
|
||||||
// a patch that deletes a scalar from a list of scalars with merge semantics.
|
originalScalars := sortScalars(original)
|
||||||
return nil, nil
|
modifiedScalars := sortScalars(modified)
|
||||||
}
|
|
||||||
|
|
||||||
patch := []interface{}{}
|
|
||||||
|
|
||||||
originalScalars := uniqifyAndSortScalars(original)
|
|
||||||
modifiedScalars := uniqifyAndSortScalars(modified)
|
|
||||||
originalIndex, modifiedIndex := 0, 0
|
originalIndex, modifiedIndex := 0, 0
|
||||||
|
addList := []interface{}{}
|
||||||
|
deletionList := []interface{}{}
|
||||||
|
|
||||||
loopB:
|
originalInBounds := originalIndex < len(originalScalars)
|
||||||
for ; modifiedIndex < len(modifiedScalars); modifiedIndex++ {
|
modifiedInBounds := modifiedIndex < len(modifiedScalars)
|
||||||
for ; originalIndex < len(originalScalars); originalIndex++ {
|
bothInBounds := originalInBounds && modifiedInBounds
|
||||||
originalString := fmt.Sprintf("%v", original[originalIndex])
|
for originalInBounds || modifiedInBounds {
|
||||||
modifiedString := fmt.Sprintf("%v", modified[modifiedIndex])
|
|
||||||
if originalString >= modifiedString {
|
|
||||||
if originalString != modifiedString {
|
|
||||||
patch = append(patch, modified[modifiedIndex])
|
|
||||||
}
|
|
||||||
|
|
||||||
continue loopB
|
// we need to compare the string representation of the scalar,
|
||||||
}
|
// because the scalar is an interface which doesn't support neither < nor <
|
||||||
// There is no else clause because there is no way to create a patch that deletes
|
// And that's how func sortScalars compare scalars.
|
||||||
// a scalar from a list of scalars with merge semantics.
|
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
|
return addList, deletionList, nil
|
||||||
for ; modifiedIndex < len(modifiedScalars); modifiedIndex++ {
|
|
||||||
patch = append(patch, modified[modifiedIndex])
|
|
||||||
}
|
|
||||||
|
|
||||||
return patch, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var errNoMergeKeyFmt = "map: %v does not contain declared merge key: %s"
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := mergeMap(originalMap, patchMap, t)
|
result, err := mergeMap(originalMap, patchMap, t, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// 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
|
// both the original map and the patch because getting a deep copy of a map in
|
||||||
// golang is highly non-trivial.
|
// 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, ok := patch[directiveMarker]; ok {
|
||||||
if v == replaceDirective {
|
if v == replaceDirective {
|
||||||
// If the patch contains "$patch: replace", don't merge it, just use the
|
// 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.
|
// Start merging the patch into the original.
|
||||||
for k, patchV := range patch {
|
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
|
// If the value of this key is null, delete the key if it exists in the
|
||||||
// original. Otherwise, skip it.
|
// original. Otherwise, skip it.
|
||||||
if patchV == nil {
|
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{})
|
typedOriginal := original[k].(map[string]interface{})
|
||||||
typedPatch := patchV.(map[string]interface{})
|
typedPatch := patchV.(map[string]interface{})
|
||||||
var err error
|
var err error
|
||||||
original[k], err = mergeMap(typedOriginal, typedPatch, fieldType)
|
original[k], err = mergeMap(typedOriginal, typedPatch, fieldType, mergeDeleteList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -602,7 +653,7 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[strin
|
|||||||
typedOriginal := original[k].([]interface{})
|
typedOriginal := original[k].([]interface{})
|
||||||
typedPatch := patchV.([]interface{})
|
typedPatch := patchV.([]interface{})
|
||||||
var err error
|
var err error
|
||||||
original[k], err = mergeSlice(typedOriginal, typedPatch, elemType, fieldPatchMergeKey)
|
original[k], err = mergeSlice(typedOriginal, typedPatch, elemType, fieldPatchMergeKey, mergeDeleteList, isDeleteList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// 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
|
// the patch because getting a deep copy of a slice in golang is highly
|
||||||
// non-trivial.
|
// 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 {
|
if len(original) == 0 && len(patch) == 0 {
|
||||||
return original, nil
|
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 the elements are not maps, merge the slices of scalars.
|
||||||
if t.Kind() != reflect.Map {
|
if t.Kind() != reflect.Map {
|
||||||
|
if mergeDeleteList && isDeleteList {
|
||||||
|
return deleteFromSlice(original, patch), nil
|
||||||
|
}
|
||||||
// Maybe in the future add a "concat" mode that doesn't
|
// Maybe in the future add a "concat" mode that doesn't
|
||||||
// uniqify.
|
// uniqify.
|
||||||
both := append(original, patch...)
|
both := append(original, patch...)
|
||||||
@ -711,7 +765,7 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
|
|||||||
var mergedMaps interface{}
|
var mergedMaps interface{}
|
||||||
var err error
|
var err error
|
||||||
// Merge into original.
|
// Merge into original.
|
||||||
mergedMaps, err = mergeMap(originalMap, typedV, elemType)
|
mergedMaps, err = mergeMap(originalMap, typedV, elemType, mergeDeleteList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -725,6 +779,34 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
|
|||||||
return original, nil
|
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.
|
// 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) {
|
func findMapInSliceBasedOnKeyValue(m []interface{}, key string, value interface{}) (map[string]interface{}, int, bool, error) {
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
@ -761,10 +843,17 @@ func sortMergeListsByName(mapJSON []byte, dataStruct interface{}) ([]byte, error
|
|||||||
return json.Marshal(newM)
|
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) {
|
func sortMergeListsByNameMap(s map[string]interface{}, t reflect.Type) (map[string]interface{}, error) {
|
||||||
newS := map[string]interface{}{}
|
newS := map[string]interface{}{}
|
||||||
for k, v := range s {
|
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)
|
fieldType, fieldPatchStrategy, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, k)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -794,6 +883,7 @@ func sortMergeListsByNameMap(s map[string]interface{}, t reflect.Type) (map[stri
|
|||||||
return newS, nil
|
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) {
|
func sortMergeListsByNameArray(s []interface{}, elemType reflect.Type, mergeKey string, recurse bool) ([]interface{}, error) {
|
||||||
if len(s) == 0 {
|
if len(s) == 0 {
|
||||||
return s, nil
|
return s, nil
|
||||||
@ -883,7 +973,10 @@ func (ss SortableSliceOfMaps) Swap(i, j int) {
|
|||||||
|
|
||||||
func uniqifyAndSortScalars(s []interface{}) []interface{} {
|
func uniqifyAndSortScalars(s []interface{}) []interface{} {
|
||||||
s = uniqifyScalars(s)
|
s = uniqifyScalars(s)
|
||||||
|
return sortScalars(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortScalars(s []interface{}) []interface{} {
|
||||||
ss := SortableSliceOfScalars{s}
|
ss := SortableSliceOfScalars{s}
|
||||||
sort.Sort(ss)
|
sort.Sort(ss)
|
||||||
return ss.s
|
return ss.s
|
||||||
@ -1217,7 +1310,7 @@ func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct int
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
patchMap, err := mergeMap(deletionsMap, deltaMap, t)
|
patchMap, err := mergeMap(deletionsMap, deltaMap, t, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -46,13 +46,35 @@ type StrategicMergePatchTestCase struct {
|
|||||||
StrategicMergePatchTestCaseData
|
StrategicMergePatchTestCaseData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StrategicMergePatchRawTestCase struct {
|
||||||
|
Description string
|
||||||
|
StrategicMergePatchRawTestCaseData
|
||||||
|
}
|
||||||
|
|
||||||
type StrategicMergePatchTestCaseData struct {
|
type StrategicMergePatchTestCaseData struct {
|
||||||
|
// Original is the original object (last-applied config in annotation)
|
||||||
Original map[string]interface{}
|
Original map[string]interface{}
|
||||||
TwoWay map[string]interface{}
|
// Modified is the modified object (new config we want)
|
||||||
Modified map[string]interface{}
|
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{}
|
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 {
|
type MergeItem struct {
|
||||||
@ -1797,6 +1819,125 @@ testCases:
|
|||||||
other: b
|
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) {
|
func TestStrategicMergePatch(t *testing.T) {
|
||||||
testStrategicMergePatchWithCustomArguments(t, "bad original",
|
testStrategicMergePatchWithCustomArguments(t, "bad original",
|
||||||
"<THIS IS NOT JSON>", "{}", mergeItem, errBadJSONDoc)
|
"<THIS IS NOT JSON>", "{}", mergeItem, errBadJSONDoc)
|
||||||
@ -1818,6 +1959,11 @@ func TestStrategicMergePatch(t *testing.T) {
|
|||||||
testTwoWayPatch(t, c)
|
testTwoWayPatch(t, c)
|
||||||
testThreeWayPatch(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) {
|
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)
|
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) {
|
func twoWayTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchTestCase) ([]byte, []byte, []byte) {
|
||||||
return testObjectToJSONOrFail(t, c.Original, c.Description),
|
return testObjectToJSONOrFail(t, c.Original, c.Description),
|
||||||
testObjectToJSONOrFail(t, c.TwoWay, c.Description),
|
testObjectToJSONOrFail(t, c.TwoWay, c.Description),
|
||||||
testObjectToJSONOrFail(t, c.Modified, 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) {
|
func testThreeWayPatch(t *testing.T, c StrategicMergePatchTestCase) {
|
||||||
original, modified, current, expected, result := threeWayTestCaseToJSONOrFail(t, c)
|
original, modified, current, expected, result := threeWayTestCaseToJSONOrFail(t, c)
|
||||||
actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, false)
|
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)
|
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) {
|
func threeWayTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchTestCase) ([]byte, []byte, []byte, []byte, []byte) {
|
||||||
return testObjectToJSONOrFail(t, c.Original, c.Description),
|
return testObjectToJSONOrFail(t, c.Original, c.Description),
|
||||||
testObjectToJSONOrFail(t, c.Modified, 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)
|
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) {
|
func testPatchCreation(t *testing.T, expected, actual []byte, description string) {
|
||||||
sorted, err := sortMergeListsByName(actual, mergeItem)
|
sorted, err := sortMergeListsByName(actual, mergeItem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1989,6 +2204,24 @@ func jsonToYAML(j []byte) ([]byte, error) {
|
|||||||
return y, nil
|
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) {
|
func TestHasConflicts(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
A interface{}
|
A interface{}
|
||||||
|
Loading…
Reference in New Issue
Block a user