Commit 723fa087 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #39324 from wojtek-t/change_patch_api

Automatic merge from submit-queue Prepare for using optimized conversion to/from map[string]interface{} in Patch operation Ref #39017
parents f90bb177 7ac60f23
...@@ -244,7 +244,7 @@ func (e *eventLogger) eventObserve(newEvent *v1.Event) (*v1.Event, []byte, error ...@@ -244,7 +244,7 @@ func (e *eventLogger) eventObserve(newEvent *v1.Event) (*v1.Event, []byte, error
newData, _ := json.Marshal(event) newData, _ := json.Marshal(event)
oldData, _ := json.Marshal(eventCopy2) oldData, _ := json.Marshal(eventCopy2)
patch, err = strategicpatch.CreateStrategicMergePatch(oldData, newData, event) patch, err = strategicpatch.CreateTwoWayMergePatch(oldData, newData, event)
} }
// record our new observation // record our new observation
......
...@@ -111,10 +111,10 @@ func (nsu *nodeStatusUpdater) UpdateNodeStatuses() error { ...@@ -111,10 +111,10 @@ func (nsu *nodeStatusUpdater) UpdateNodeStatuses() error {
} }
patchBytes, err := patchBytes, err :=
strategicpatch.CreateStrategicMergePatch(oldData, newData, node) strategicpatch.CreateTwoWayMergePatch(oldData, newData, node)
if err != nil { if err != nil {
return fmt.Errorf( return fmt.Errorf(
"failed to CreateStrategicMergePatch for node %q. %v", "failed to CreateTwoWayMergePatch for node %q. %v",
nodeName, nodeName,
err) err)
} }
......
...@@ -36,6 +36,7 @@ go_library( ...@@ -36,6 +36,7 @@ go_library(
srcs = [ srcs = [
"discovery.go", "discovery.go",
"doc.go", "doc.go",
"patch.go",
"proxy.go", "proxy.go",
"rest.go", "rest.go",
"watch.go", "watch.go",
......
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package handlers
import (
"encoding/json"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubernetes/pkg/util/strategicpatch"
"github.com/evanphx/json-patch"
)
// patchObjectJSON patches the <originalObject> with <patchJS> and stores
// the result in <objToUpdate>.
// Currently it also returns the original and patched objects serialized to
// JSONs (this may not be needed once we can apply patches at the
// map[string]interface{} level).
func patchObjectJSON(
patchType types.PatchType,
codec runtime.Codec,
originalObject runtime.Object,
patchJS []byte,
objToUpdate runtime.Object,
versionedObj runtime.Object,
) (originalObjJS []byte, patchedObjJS []byte, retErr error) {
js, err := runtime.Encode(codec, originalObject)
if err != nil {
return nil, nil, err
}
originalObjJS = js
switch patchType {
case types.JSONPatchType:
patchObj, err := jsonpatch.DecodePatch(patchJS)
if err != nil {
return nil, nil, err
}
if patchedObjJS, err = patchObj.Apply(originalObjJS); err != nil {
return nil, nil, err
}
case types.MergePatchType:
if patchedObjJS, err = jsonpatch.MergePatch(originalObjJS, patchJS); err != nil {
return nil, nil, err
}
case types.StrategicMergePatchType:
if patchedObjJS, err = strategicpatch.StrategicMergePatch(originalObjJS, patchJS, versionedObj); err != nil {
return nil, nil, err
}
default:
// only here as a safety net - go-restful filters content-type
return nil, nil, fmt.Errorf("unknown Content-Type header for patch: %v", patchType)
}
if err := runtime.DecodeInto(codec, patchedObjJS, objToUpdate); err != nil {
return nil, nil, err
}
return
}
// strategicPatchObject applies a strategic merge patch of <patchJS> to
// <originalObject> and stores the result in <objToUpdate>.
// It additionally returns the map[string]interface{} representation of the
// <originalObject> and <patchJS>.
func strategicPatchObject(
codec runtime.Codec,
originalObject runtime.Object,
patchJS []byte,
objToUpdate runtime.Object,
versionedObj runtime.Object,
) (originalObjMap map[string]interface{}, patchMap map[string]interface{}, retErr error) {
// TODO: This should be one-step conversion that doesn't require
// json marshaling and unmarshaling once #39017 is fixed.
data, err := runtime.Encode(codec, originalObject)
if err != nil {
return nil, nil, err
}
originalObjMap = make(map[string]interface{})
if err := json.Unmarshal(data, &originalObjMap); err != nil {
return nil, nil, err
}
patchMap = make(map[string]interface{})
if err := json.Unmarshal(patchJS, &patchMap); err != nil {
return nil, nil, err
}
if err := applyPatchToObject(codec, originalObjMap, patchMap, objToUpdate, versionedObj); err != nil {
return nil, nil, err
}
return
}
// applyPatchToObject applies a strategic merge patch of <patchMap> to
// <originalMap> and stores the result in <objToUpdate>, though it operates
// on versioned map[string]interface{} representations.
func applyPatchToObject(
codec runtime.Codec,
originalMap map[string]interface{},
patchMap map[string]interface{},
objToUpdate runtime.Object,
versionedObj runtime.Object,
) error {
patchedObjMap, err := strategicpatch.StrategicMergeMapPatch(originalMap, patchMap, versionedObj)
if err != nil {
return err
}
// TODO: This should be one-step conversion that doesn't require
// json marshaling and unmarshaling once #39017 is fixed.
data, err := json.Marshal(patchedObjMap)
if err != nil {
return err
}
return runtime.DecodeInto(codec, data, objToUpdate)
}
...@@ -45,7 +45,6 @@ import ( ...@@ -45,7 +45,6 @@ import (
"k8s.io/kubernetes/pkg/util/strategicpatch" "k8s.io/kubernetes/pkg/util/strategicpatch"
"github.com/emicklei/go-restful" "github.com/emicklei/go-restful"
"github.com/evanphx/json-patch"
"github.com/golang/glog" "github.com/golang/glog"
) )
...@@ -556,6 +555,8 @@ func patchResource( ...@@ -556,6 +555,8 @@ func patchResource(
var ( var (
originalObjJS []byte originalObjJS []byte
originalPatchedObjJS []byte originalPatchedObjJS []byte
originalObjMap map[string]interface{}
originalPatchMap map[string]interface{}
lastConflictErr error lastConflictErr error
) )
...@@ -570,25 +571,30 @@ func patchResource( ...@@ -570,25 +571,30 @@ func patchResource(
} }
switch { switch {
case len(originalObjJS) == 0 || len(originalPatchedObjJS) == 0: case originalObjJS == nil && originalObjMap == nil:
// first time through, // first time through,
// 1. apply the patch // 1. apply the patch
// 2. save the originalJS and patchedJS to detect whether there were conflicting changes on retries // 2. save the original and patched to detect whether there were conflicting changes on retries
if js, err := runtime.Encode(codec, currentObject); err != nil {
return nil, err
} else {
originalObjJS = js
}
if js, err := getPatchedJS(patchType, originalObjJS, patchJS, versionedObj); err != nil {
return nil, err
} else {
originalPatchedObjJS = js
}
objToUpdate := patcher.New() objToUpdate := patcher.New()
if err := runtime.DecodeInto(codec, originalPatchedObjJS, objToUpdate); err != nil {
return nil, err // For performance reasons, in case of strategicpatch, we avoid json
// marshaling and unmarshaling and operate just on map[string]interface{}.
// In case of other patch types, we still have to operate on JSON
// representations.
switch patchType {
case types.JSONPatchType, types.MergePatchType:
originalJS, patchedJS, err := patchObjectJSON(patchType, codec, currentObject, patchJS, objToUpdate, versionedObj)
if err != nil {
return nil, err
}
originalObjJS, originalPatchedObjJS = originalJS, patchedJS
case types.StrategicMergePatchType:
originalMap, patchMap, err := strategicPatchObject(codec, currentObject, patchJS, objToUpdate, versionedObj)
if err != nil {
return nil, err
}
originalObjMap, originalPatchMap = originalMap, patchMap
} }
if err := checkName(objToUpdate, name, namespace, namer); err != nil { if err := checkName(objToUpdate, name, namespace, namer); err != nil {
return nil, err return nil, err
...@@ -603,33 +609,62 @@ func patchResource( ...@@ -603,33 +609,62 @@ func patchResource(
// 2. build a strategic merge patch from originalJS and the currentJS // 2. build a strategic merge patch from originalJS and the currentJS
// 3. ensure no conflicts between the two patches // 3. ensure no conflicts between the two patches
// 4. apply the #1 patch to the currentJS object // 4. apply the #1 patch to the currentJS object
currentObjectJS, err := runtime.Encode(codec, currentObject)
if err != nil { // TODO: This should be one-step conversion that doesn't require
return nil, err // json marshaling and unmarshaling once #39017 is fixed.
} data, err := runtime.Encode(codec, currentObject)
currentPatch, err := strategicpatch.CreateStrategicMergePatch(originalObjJS, currentObjectJS, versionedObj)
if err != nil { if err != nil {
return nil, err return nil, err
} }
originalPatch, err := strategicpatch.CreateStrategicMergePatch(originalObjJS, originalPatchedObjJS, versionedObj) currentObjMap := make(map[string]interface{})
if err != nil { if err := json.Unmarshal(data, &currentObjMap); err != nil {
return nil, err return nil, err
} }
diff1 := make(map[string]interface{}) var currentPatchMap map[string]interface{}
if err := json.Unmarshal(originalPatch, &diff1); err != nil { if originalObjMap != nil {
return nil, err var err error
} currentPatchMap, err = strategicpatch.CreateTwoWayMergeMapPatch(originalObjMap, currentObjMap, versionedObj)
diff2 := make(map[string]interface{}) if err != nil {
if err := json.Unmarshal(currentPatch, &diff2); err != nil { return nil, err
return nil, err }
} else {
if originalPatchMap == nil {
// Compute original patch, if we already didn't do this in previous retries.
originalPatch, err := strategicpatch.CreateTwoWayMergePatch(originalObjJS, originalPatchedObjJS, versionedObj)
if err != nil {
return nil, err
}
originalPatchMap = make(map[string]interface{})
if err := json.Unmarshal(originalPatch, &originalPatchMap); err != nil {
return nil, err
}
}
// Compute current patch.
currentObjJS, err := runtime.Encode(codec, currentObject)
if err != nil {
return nil, err
}
currentPatch, err := strategicpatch.CreateTwoWayMergePatch(originalObjJS, currentObjJS, versionedObj)
if err != nil {
return nil, err
}
currentPatchMap = make(map[string]interface{})
if err := json.Unmarshal(currentPatch, &currentPatchMap); err != nil {
return nil, err
}
} }
hasConflicts, err := strategicpatch.HasConflicts(diff1, diff2)
hasConflicts, err := strategicpatch.HasConflicts(originalPatchMap, currentPatchMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if hasConflicts { if hasConflicts {
glog.V(4).Infof("patchResource failed for resource %s, because there is a meaningful conflict.\n diff1=%v\n, diff2=%v\n", name, diff1, diff2) if glog.V(4) {
diff1, _ := json.Marshal(currentPatchMap)
diff2, _ := json.Marshal(originalPatchMap)
glog.Infof("patchResource failed for resource %s, because there is a meaningful conflict.\n diff1=%v\n, diff2=%v\n", name, diff1, diff2)
}
// Return the last conflict error we got if we have one // Return the last conflict error we got if we have one
if lastConflictErr != nil { if lastConflictErr != nil {
return nil, lastConflictErr return nil, lastConflictErr
...@@ -638,14 +673,11 @@ func patchResource( ...@@ -638,14 +673,11 @@ func patchResource(
return nil, errors.NewConflict(resource.GroupResource(), name, nil) return nil, errors.NewConflict(resource.GroupResource(), name, nil)
} }
newlyPatchedObjJS, err := getPatchedJS(types.StrategicMergePatchType, currentObjectJS, originalPatch, versionedObj)
if err != nil {
return nil, err
}
objToUpdate := patcher.New() objToUpdate := patcher.New()
if err := runtime.DecodeInto(codec, newlyPatchedObjJS, objToUpdate); err != nil { if err := applyPatchToObject(codec, currentObjMap, originalPatchMap, objToUpdate, versionedObj); err != nil {
return nil, err return nil, err
} }
return objToUpdate, nil return objToUpdate, nil
} }
} }
...@@ -1079,24 +1111,6 @@ func setListSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer) ...@@ -1079,24 +1111,6 @@ func setListSelfLink(obj runtime.Object, req *restful.Request, namer ScopeNamer)
return count, err return count, err
} }
func getPatchedJS(patchType types.PatchType, originalJS, patchJS []byte, obj runtime.Object) ([]byte, error) {
switch patchType {
case types.JSONPatchType:
patchObj, err := jsonpatch.DecodePatch(patchJS)
if err != nil {
return nil, err
}
return patchObj.Apply(originalJS)
case types.MergePatchType:
return jsonpatch.MergePatch(originalJS, patchJS)
case types.StrategicMergePatchType:
return strategicpatch.StrategicMergePatchData(originalJS, patchJS, obj)
default:
// only here as a safety net - go-restful filters content-type
return nil, fmt.Errorf("unknown Content-Type header for patch: %v", patchType)
}
}
func summarizeData(data []byte, maxLength int) string { func summarizeData(data []byte, maxLength int) string {
switch { switch {
case len(data) == 0: case len(data) == 0:
......
...@@ -55,16 +55,27 @@ type TestPatchSubType struct { ...@@ -55,16 +55,27 @@ type TestPatchSubType struct {
func (obj *testPatchType) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta } func (obj *testPatchType) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta }
func TestPatchAnonymousField(t *testing.T) { func TestPatchAnonymousField(t *testing.T) {
originalJS := `{"kind":"testPatchType","theField":"my-value"}` testGV := schema.GroupVersion{Group: "", Version: "v"}
api.Scheme.AddKnownTypes(testGV, &testPatchType{})
codec := api.Codecs.LegacyCodec(testGV)
original := &testPatchType{
TypeMeta: metav1.TypeMeta{Kind: "testPatchType", APIVersion: "v"},
TestPatchSubType: TestPatchSubType{StringField: "my-value"},
}
patch := `{"theField": "changed!"}` patch := `{"theField": "changed!"}`
expectedJS := `{"kind":"testPatchType","theField":"changed!"}` expected := &testPatchType{
TypeMeta: metav1.TypeMeta{Kind: "testPatchType", APIVersion: "v"},
TestPatchSubType: TestPatchSubType{StringField: "changed!"},
}
actualBytes, err := getPatchedJS(types.StrategicMergePatchType, []byte(originalJS), []byte(patch), &testPatchType{}) actual := &testPatchType{}
_, _, err := strategicPatchObject(codec, original, []byte(patch), actual, &testPatchType{})
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
if string(actualBytes) != expectedJS { if !api.Semantic.DeepEqual(actual, expected) {
t.Errorf("expected %v, got %v", expectedJS, string(actualBytes)) t.Errorf("expected %#v, got %#v", expected, actual)
} }
} }
...@@ -215,7 +226,7 @@ func (tc *patchTestCase) Run(t *testing.T) { ...@@ -215,7 +226,7 @@ func (tc *patchTestCase) Run(t *testing.T) {
continue continue
case types.StrategicMergePatchType: case types.StrategicMergePatchType:
patch, err = strategicpatch.CreateStrategicMergePatch(originalObjJS, changedJS, versionedObj) patch, err = strategicpatch.CreateTwoWayMergePatch(originalObjJS, changedJS, versionedObj)
if err != nil { if err != nil {
t.Errorf("%s: unexpected error: %v", tc.name, err) t.Errorf("%s: unexpected error: %v", tc.name, err)
return return
......
...@@ -261,7 +261,7 @@ func getPatchedJSON(patchType types.PatchType, originalJS, patchJS []byte, obj r ...@@ -261,7 +261,7 @@ func getPatchedJSON(patchType types.PatchType, originalJS, patchJS []byte, obj r
return jsonpatch.MergePatch(originalJS, patchJS) return jsonpatch.MergePatch(originalJS, patchJS)
case types.StrategicMergePatchType: case types.StrategicMergePatchType:
return strategicpatch.StrategicMergePatchData(originalJS, patchJS, obj) return strategicpatch.StrategicMergePatch(originalJS, patchJS, obj)
default: default:
// only here as a safety net - go-restful filters content-type // only here as a safety net - go-restful filters content-type
......
...@@ -48,6 +48,13 @@ const ( ...@@ -48,6 +48,13 @@ const (
deleteFromPrimitiveListDirectivePrefix = "$deleteFromPrimitiveList" deleteFromPrimitiveListDirectivePrefix = "$deleteFromPrimitiveList"
) )
// JSONMap is a representations of JSON object encoded as map[string]interface{}
// where the children can be either map[string]interface{}, []interface{} or
// primitive type).
// Operating on JSONMap representation is much faster as it doesn't require any
// json marshaling and/or unmarshaling operations.
type JSONMap map[string]interface{}
// IsPreconditionFailed returns true if the provided error indicates // IsPreconditionFailed returns true if the provided error indicates
// a precondition failed. // a precondition failed.
func IsPreconditionFailed(err error) bool { func IsPreconditionFailed(err error) bool {
...@@ -136,11 +143,6 @@ func RequireMetadataKeyUnchanged(key string) PreconditionFunc { ...@@ -136,11 +143,6 @@ func RequireMetadataKeyUnchanged(key string) PreconditionFunc {
} }
} }
// 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 // CreateTwoWayMergePatch creates a patch that can be passed to StrategicMergePatch from an original
// document and a modified document, which are passed to the method as json encoded content. It will // document and a modified document, 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 // return a patch that yields the modified document when applied to the original document, or an error
...@@ -160,12 +162,24 @@ func CreateTwoWayMergePatch(original, modified []byte, dataStruct interface{}, f ...@@ -160,12 +162,24 @@ func CreateTwoWayMergePatch(original, modified []byte, dataStruct interface{}, f
} }
} }
patchMap, err := CreateTwoWayMergeMapPatch(originalMap, modifiedMap, dataStruct, fns...)
if err != nil {
return nil, err
}
return json.Marshal(patchMap)
}
// CreateTwoWayMergeMapPatch creates a patch from an original and modified JSON objects,
// encoded JSONMap.
// The serialized version of the map can then be passed to StrategicMergeMapPatch.
func CreateTwoWayMergeMapPatch(original, modified JSONMap, dataStruct interface{}, fns ...PreconditionFunc) (JSONMap, error) {
t, err := getTagStructType(dataStruct) t, err := getTagStructType(dataStruct)
if err != nil { if err != nil {
return nil, err return nil, err
} }
patchMap, err := diffMaps(originalMap, modifiedMap, t, false, false) patchMap, err := diffMaps(original, modified, t, false, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -177,7 +191,7 @@ func CreateTwoWayMergePatch(original, modified []byte, dataStruct interface{}, f ...@@ -177,7 +191,7 @@ func CreateTwoWayMergePatch(original, modified []byte, dataStruct interface{}, f
} }
} }
return json.Marshal(patchMap) return patchMap, nil
} }
// Returns a (recursive) strategic merge patch that yields modified when applied to original. // Returns a (recursive) strategic merge patch that yields modified when applied to original.
...@@ -494,12 +508,6 @@ loopB: ...@@ -494,12 +508,6 @@ loopB:
return patch, nil return patch, nil
} }
// 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) {
return StrategicMergePatch(original, patch, dataStruct)
}
// StrategicMergePatch applies a strategic merge patch. The patch and the original document // StrategicMergePatch applies a strategic merge patch. The patch and the original document
// must be json encoded content. A patch can be created from an original and a modified document // must be json encoded content. A patch can be created from an original and a modified document
// by calling CreateStrategicMergePatch. // by calling CreateStrategicMergePatch.
...@@ -524,17 +532,23 @@ func StrategicMergePatch(original, patch []byte, dataStruct interface{}) ([]byte ...@@ -524,17 +532,23 @@ func StrategicMergePatch(original, patch []byte, dataStruct interface{}) ([]byte
return nil, errBadJSONDoc return nil, errBadJSONDoc
} }
t, err := getTagStructType(dataStruct) result, err := StrategicMergeMapPatch(originalMap, patchMap, dataStruct)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result, err := mergeMap(originalMap, patchMap, t, true) return json.Marshal(result)
}
// StrategicMergePatch applies a strategic merge patch. The original and patch documents
// must be JSONMap. A patch can be created from an original and modified document by
// calling CreateTwoWayMergeMapPatch.
func StrategicMergeMapPatch(original, patch JSONMap, dataStruct interface{}) (JSONMap, error) {
t, err := getTagStructType(dataStruct)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return mergeMap(original, patch, t, true)
return json.Marshal(result)
} }
func getTagStructType(dataStruct interface{}) (reflect.Type, error) { func getTagStructType(dataStruct interface{}) (reflect.Type, error) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment