Merge pull request #13820 from jackgr/apply_patch

Add method to apply strategic merge patch
This commit is contained in:
Robert Bailey 2015-10-08 12:44:13 -07:00
commit f9364da306
2 changed files with 1032 additions and 207 deletions

View File

@ -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\nconflicts 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, ignoreAdditions, ignoreChangesAndDeletions bool) (map[string]interface{}, error) {
func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreChangesAndAdditions, ignoreDeletions 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[directiveMarker] = modifiedValue
}
continue
}
if reflect.TypeOf(originalValue) != reflect.TypeOf(modifiedValue) {
// Types have changed, so add to patch
if !ignoreChangesAndAdditions {
patch[key] = modifiedValue
}
continue
}
if !ignoreChangesAndDeletions {
// If types have changed, replace completely
if reflect.TypeOf(originalValue) != reflect.TypeOf(modifiedValue) {
patch[key] = modifiedValue
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, ignoreAdditions, ignoreChangesAndDeletions)
patchValue, err := diffMaps(originalValueTyped, modifiedValueTyped, fieldType, ignoreChangesAndAdditions, ignoreDeletions)
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, ignoreAdditions, ignoreChangesAndDeletions)
if fieldPatchStrategy == mergeDirective {
patchValue, err := diffLists(originalValueTyped, modifiedValueTyped, fieldType.Elem(), fieldPatchMergeKey, ignoreChangesAndAdditions, ignoreDeletions)
if err != nil {
return nil, err
}
@ -155,15 +236,16 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreA
}
}
if !ignoreChangesAndDeletions {
if !ignoreChangesAndAdditions {
if !reflect.DeepEqual(originalValue, modifiedValue) {
// Values are different, so add to patch
patch[key] = modifiedValue
}
}
}
if !ignoreChangesAndDeletions {
// 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, ignoreAdditions, ignoreChangesAndDeletions bool) ([]interface{}, error) {
func diffLists(original, modified []interface{}, t reflect.Type, mergeKey string, ignoreChangesAndAdditions, ignoreDeletions bool) ([]interface{}, error) {
if len(original) == 0 {
if len(modified) == 0 || ignoreAdditions {
if len(modified) == 0 || ignoreChangesAndAdditions {
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, ignoreAdditions, ignoreChangesAndDeletions)
} else {
patch, err = diffListsOfScalars(original, modified, ignoreAdditions)
patch, err = diffListsOfMaps(original, modified, t, mergeKey, ignoreChangesAndAdditions, ignoreDeletions)
} 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, ignoreAdditions, ignoreChangesAndDeletions bool) ([]interface{}, error) {
func diffListsOfMaps(original, modified []interface{}, t reflect.Type, mergeKey string, ignoreChangesAndAdditions, ignoreDeletions 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 !ignoreChangesAndDeletions {
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: specialValue})
patch = append(patch, map[string]interface{}{mergeKey: originalValue, directiveMarker: deleteDirective})
}
}
if !ignoreAdditions {
if !ignoreChangesAndAdditions {
// 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 == specialValue {
if patchType == deleteDirective {
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)
}

File diff suppressed because it is too large Load Diff