From 34891ad9f628f831fad9eacc25860841c68464a5 Mon Sep 17 00:00:00 2001 From: ymqytw Date: Tue, 1 Nov 2016 09:30:21 -0700 Subject: [PATCH] support patch list of primitives --- pkg/apiserver/resthandler.go | 4 +- pkg/apiserver/resthandler_test.go | 2 +- pkg/client/record/events_cache.go | 4 +- .../statusupdater/node_status_updater.go | 6 +- pkg/kubectl/cmd/BUILD | 1 + pkg/kubectl/cmd/annotate.go | 8 +- pkg/kubectl/cmd/annotate_test.go | 30 +- pkg/kubectl/cmd/apply.go | 20 +- pkg/kubectl/cmd/apply_test.go | 28 + pkg/kubectl/cmd/cmd_test.go | 5 + pkg/kubectl/cmd/edit.go | 21 +- pkg/kubectl/cmd/label.go | 10 +- pkg/kubectl/cmd/label_test.go | 16 +- pkg/kubectl/cmd/patch.go | 10 +- pkg/kubectl/cmd/patch_test.go | 14 + pkg/kubectl/cmd/rollout/rollout_pause.go | 4 +- pkg/kubectl/cmd/rollout/rollout_resume.go | 4 +- pkg/kubectl/cmd/scale.go | 7 +- pkg/kubectl/cmd/set/helper.go | 20 +- pkg/kubectl/cmd/set/set_image.go | 15 +- pkg/kubectl/cmd/set/set_resources.go | 4 +- pkg/kubectl/cmd/taint.go | 7 +- pkg/kubectl/cmd/taint_test.go | 7 +- pkg/kubectl/cmd/util/helpers.go | 14 +- pkg/util/strategicpatch/BUILD | 1 + pkg/util/strategicpatch/patch.go | 282 +- pkg/util/strategicpatch/patch_test.go | 3798 ++++++++++------- 27 files changed, 2670 insertions(+), 1672 deletions(-) diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index 4ad837260d030..24663107f3873 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -591,11 +591,11 @@ func patchResource( if err != nil { return nil, err } - currentPatch, err := strategicpatch.CreateStrategicMergePatch(originalObjJS, currentObjectJS, versionedObj) + currentPatch, err := strategicpatch.CreateStrategicMergePatch(originalObjJS, currentObjectJS, versionedObj, strategicpatch.SMPatchVersionLatest) if err != nil { return nil, err } - originalPatch, err := strategicpatch.CreateStrategicMergePatch(originalObjJS, originalPatchedObjJS, versionedObj) + originalPatch, err := strategicpatch.CreateStrategicMergePatch(originalObjJS, originalPatchedObjJS, versionedObj, strategicpatch.SMPatchVersionLatest) if err != nil { return nil, err } diff --git a/pkg/apiserver/resthandler_test.go b/pkg/apiserver/resthandler_test.go index fe85840541ce4..577092e16ad68 100644 --- a/pkg/apiserver/resthandler_test.go +++ b/pkg/apiserver/resthandler_test.go @@ -213,7 +213,7 @@ func (tc *patchTestCase) Run(t *testing.T) { continue case api.StrategicMergePatchType: - patch, err = strategicpatch.CreateStrategicMergePatch(originalObjJS, changedJS, versionedObj) + patch, err = strategicpatch.CreateStrategicMergePatch(originalObjJS, changedJS, versionedObj, strategicpatch.SMPatchVersionLatest) if err != nil { t.Errorf("%s: unexpected error: %v", tc.name, err) return diff --git a/pkg/client/record/events_cache.go b/pkg/client/record/events_cache.go index 8ff65776cb11e..6b2d6429e764a 100644 --- a/pkg/client/record/events_cache.go +++ b/pkg/client/record/events_cache.go @@ -244,7 +244,9 @@ func (e *eventLogger) eventObserve(newEvent *api.Event) (*api.Event, []byte, err newData, _ := json.Marshal(event) oldData, _ := json.Marshal(eventCopy2) - patch, err = strategicpatch.CreateStrategicMergePatch(oldData, newData, event) + // TODO: need to figure out if we need to let eventObserve() use the new behavior of StrategicMergePatch. + // Currently default to old behavior now. Ref: issue #35936 + patch, err = strategicpatch.CreateStrategicMergePatch(oldData, newData, event, strategicpatch.SMPatchVersion_1_0) } // record our new observation diff --git a/pkg/controller/volume/attachdetach/statusupdater/node_status_updater.go b/pkg/controller/volume/attachdetach/statusupdater/node_status_updater.go index 0cca9f1464c99..ec58e8e5b1043 100644 --- a/pkg/controller/volume/attachdetach/statusupdater/node_status_updater.go +++ b/pkg/controller/volume/attachdetach/statusupdater/node_status_updater.go @@ -59,6 +59,10 @@ type nodeStatusUpdater struct { } func (nsu *nodeStatusUpdater) UpdateNodeStatuses() error { + smPatchVersion, err := strategicpatch.GetServerSupportedSMPatchVersion(nsu.kubeClient.Discovery()) + if err != nil { + return err + } nodesToUpdate := nsu.actualStateOfWorld.GetVolumesToReportAttached() for nodeName, attachedVolumes := range nodesToUpdate { nodeObj, exists, err := nsu.nodeInformer.GetStore().GetByKey(string(nodeName)) @@ -108,7 +112,7 @@ func (nsu *nodeStatusUpdater) UpdateNodeStatuses() error { } patchBytes, err := - strategicpatch.CreateStrategicMergePatch(oldData, newData, node) + strategicpatch.CreateStrategicMergePatch(oldData, newData, node, smPatchVersion) if err != nil { return fmt.Errorf( "failed to CreateStrategicMergePatch for node %q. %v", diff --git a/pkg/kubectl/cmd/BUILD b/pkg/kubectl/cmd/BUILD index e0eb83dc3bc11..0953be7917d69 100644 --- a/pkg/kubectl/cmd/BUILD +++ b/pkg/kubectl/cmd/BUILD @@ -191,6 +191,7 @@ go_test( "//pkg/util/strings:go_default_library", "//pkg/util/term:go_default_library", "//pkg/util/wait:go_default_library", + "//pkg/version:go_default_library", "//pkg/watch:go_default_library", "//pkg/watch/versioned:go_default_library", "//vendor:github.com/spf13/cobra", diff --git a/pkg/kubectl/cmd/annotate.go b/pkg/kubectl/cmd/annotate.go index 6562ab5a6eea4..60ea3786a6229 100644 --- a/pkg/kubectl/cmd/annotate.go +++ b/pkg/kubectl/cmd/annotate.go @@ -223,6 +223,12 @@ func (o AnnotateOptions) RunAnnotate(f cmdutil.Factory, cmd *cobra.Command) erro } outputObj = obj } else { + // retrieves server version to determine which SMPatchVersion to use. + smPatchVersion, err := cmdutil.GetServerSupportedSMPatchVersionFromFactory(f) + if err != nil { + return err + } + name, namespace := info.Name, info.Namespace oldData, err := json.Marshal(obj) if err != nil { @@ -239,7 +245,7 @@ func (o AnnotateOptions) RunAnnotate(f cmdutil.Factory, cmd *cobra.Command) erro if err != nil { return err } - patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, obj) + patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, obj, smPatchVersion) createdPatch := err == nil if err != nil { glog.V(2).Infof("couldn't compute patch: %v", err) diff --git a/pkg/kubectl/cmd/annotate_test.go b/pkg/kubectl/cmd/annotate_test.go index c1c8661f34114..e16e1c30b1d66 100644 --- a/pkg/kubectl/cmd/annotate_test.go +++ b/pkg/kubectl/cmd/annotate_test.go @@ -24,8 +24,6 @@ import ( "testing" "k8s.io/kubernetes/pkg/api" - "k8s.io/kubernetes/pkg/apimachinery/registered" - "k8s.io/kubernetes/pkg/client/restclient" "k8s.io/kubernetes/pkg/client/restclient/fake" cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" "k8s.io/kubernetes/pkg/runtime" @@ -394,7 +392,7 @@ func TestAnnotateErrors(t *testing.T) { f, tf, _, _ := cmdtesting.NewAPIFactory() tf.Printer = &testPrinter{} tf.Namespace = "test" - tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: ®istered.GroupOrDie(api.GroupName).GroupVersion}} + tf.ClientConfig = defaultClientConfig() buf := bytes.NewBuffer([]byte{}) cmd := NewCmdAnnotate(f, buf) @@ -432,6 +430,12 @@ func TestAnnotateObject(t *testing.T) { switch req.Method { case "GET": switch req.URL.Path { + case "/version": + resp, err := genResponseWithJsonEncodedBody(serverVersion_1_5_0) + if err != nil { + t.Fatalf("error: failed to generate server version response: %#v\n", serverVersion_1_5_0) + } + return resp, nil case "/namespaces/test/pods/foo": return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &pods.Items[0])}, nil default: @@ -453,7 +457,7 @@ func TestAnnotateObject(t *testing.T) { }), } tf.Namespace = "test" - tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: ®istered.GroupOrDie(api.GroupName).GroupVersion}} + tf.ClientConfig = defaultClientConfig() buf := bytes.NewBuffer([]byte{}) cmd := NewCmdAnnotate(f, buf) @@ -482,6 +486,12 @@ func TestAnnotateObjectFromFile(t *testing.T) { switch req.Method { case "GET": switch req.URL.Path { + case "/version": + resp, err := genResponseWithJsonEncodedBody(serverVersion_1_5_0) + if err != nil { + t.Fatalf("error: failed to generate server version response: %#v\n", serverVersion_1_5_0) + } + return resp, nil case "/namespaces/test/replicationcontrollers/cassandra": return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &pods.Items[0])}, nil default: @@ -503,7 +513,7 @@ func TestAnnotateObjectFromFile(t *testing.T) { }), } tf.Namespace = "test" - tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: ®istered.GroupOrDie(api.GroupName).GroupVersion}} + tf.ClientConfig = defaultClientConfig() buf := bytes.NewBuffer([]byte{}) cmd := NewCmdAnnotate(f, buf) @@ -532,7 +542,7 @@ func TestAnnotateLocal(t *testing.T) { }), } tf.Namespace = "test" - tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: ®istered.GroupOrDie(api.GroupName).GroupVersion}} + tf.ClientConfig = defaultClientConfig() buf := bytes.NewBuffer([]byte{}) cmd := NewCmdAnnotate(f, buf) @@ -562,6 +572,12 @@ func TestAnnotateMultipleObjects(t *testing.T) { switch req.Method { case "GET": switch req.URL.Path { + case "/version": + resp, err := genResponseWithJsonEncodedBody(serverVersion_1_5_0) + if err != nil { + t.Fatalf("error: failed to generate server version response: %#v\n", serverVersion_1_5_0) + } + return resp, nil case "/namespaces/test/pods": return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, pods)}, nil default: @@ -585,7 +601,7 @@ func TestAnnotateMultipleObjects(t *testing.T) { }), } tf.Namespace = "test" - tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: ®istered.GroupOrDie(api.GroupName).GroupVersion}} + tf.ClientConfig = defaultClientConfig() buf := bytes.NewBuffer([]byte{}) cmd := NewCmdAnnotate(f, buf) diff --git a/pkg/kubectl/cmd/apply.go b/pkg/kubectl/cmd/apply.go index d85a64feb8d86..a7347731ecbf5 100644 --- a/pkg/kubectl/cmd/apply.go +++ b/pkg/kubectl/cmd/apply.go @@ -190,6 +190,11 @@ func RunApply(f cmdutil.Factory, cmd *cobra.Command, out io.Writer, options *App visitedUids := sets.NewString() visitedNamespaces := sets.NewString() + smPatchVersion, err := cmdutil.GetServerSupportedSMPatchVersionFromFactory(f) + if err != nil { + return err + } + count := 0 err = r.Visit(func(info *resource.Info, err error) error { // In this method, info.Object contains the object retrieved from the server @@ -248,13 +253,13 @@ func RunApply(f cmdutil.Factory, cmd *cobra.Command, out io.Writer, options *App helper := resource.NewHelper(info.Client, info.Mapping) patcher := NewPatcher(encoder, decoder, info.Mapping, helper, overwrite) - patchBytes, err := patcher.patch(info.Object, modified, info.Source, info.Namespace, info.Name) + patchBytes, err := patcher.patch(info.Object, modified, info.Source, info.Namespace, info.Name, smPatchVersion) if err != nil { return cmdutil.AddSourceToErr(fmt.Sprintf("applying patch:\n%s\nto:\n%v\nfor:", patchBytes, info), info.Source, err) } if cmdutil.ShouldRecord(cmd, info) { - patch, err := cmdutil.ChangeResourcePatch(info, f.Command()) + patch, err := cmdutil.ChangeResourcePatch(info, f.Command(), smPatchVersion) if err != nil { return err } @@ -480,7 +485,7 @@ func NewPatcher(encoder runtime.Encoder, decoder runtime.Decoder, mapping *meta. } } -func (p *patcher) patchSimple(obj runtime.Object, modified []byte, source, namespace, name string) ([]byte, error) { +func (p *patcher) patchSimple(obj runtime.Object, modified []byte, source, namespace, name string, smPatchVersion strategicpatch.StrategicMergePatchVersion) ([]byte, error) { // Serialize the current configuration of the object from the server. current, err := runtime.Encode(p.encoder, obj) if err != nil { @@ -504,7 +509,8 @@ func (p *patcher) patchSimple(obj runtime.Object, modified []byte, source, names } // Compute a three way strategic merge patch to send to server. - patch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, versionedObject, p.overwrite) + patch, err := strategicpatch.CreateThreeWayMergePatch(original, modified, current, versionedObject, p.overwrite, smPatchVersion) + if err != nil { format := "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor:" return nil, cmdutil.AddSourceToErr(fmt.Sprintf(format, original, modified, current), source, err) @@ -514,9 +520,9 @@ func (p *patcher) patchSimple(obj runtime.Object, modified []byte, source, names return patch, err } -func (p *patcher) patch(current runtime.Object, modified []byte, source, namespace, name string) ([]byte, error) { +func (p *patcher) patch(current runtime.Object, modified []byte, source, namespace, name string, smPatchVersion strategicpatch.StrategicMergePatchVersion) ([]byte, error) { var getErr error - patchBytes, err := p.patchSimple(current, modified, source, namespace, name) + patchBytes, err := p.patchSimple(current, modified, source, namespace, name, smPatchVersion) for i := 1; i <= maxPatchRetry && errors.IsConflict(err); i++ { if i > triesBeforeBackOff { p.backOff.Sleep(backOffPeriod) @@ -525,7 +531,7 @@ func (p *patcher) patch(current runtime.Object, modified []byte, source, namespa if getErr != nil { return nil, getErr } - patchBytes, err = p.patchSimple(current, modified, source, namespace, name) + patchBytes, err = p.patchSimple(current, modified, source, namespace, name, smPatchVersion) } return patchBytes, err diff --git a/pkg/kubectl/cmd/apply_test.go b/pkg/kubectl/cmd/apply_test.go index d3803638efd25..ab86ef0e101da 100644 --- a/pkg/kubectl/cmd/apply_test.go +++ b/pkg/kubectl/cmd/apply_test.go @@ -188,6 +188,12 @@ func TestApplyObject(t *testing.T) { NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { + case p == "/version" && m == "GET": + resp, err := genResponseWithJsonEncodedBody(serverVersion_1_5_0) + if err != nil { + t.Fatalf("error: failed to generate server version response: %#v\n", serverVersion_1_5_0) + } + return resp, nil case p == pathRC && m == "GET": bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil @@ -202,6 +208,7 @@ func TestApplyObject(t *testing.T) { }), } tf.Namespace = "test" + tf.ClientConfig = defaultClientConfig() buf := bytes.NewBuffer([]byte{}) cmd := NewCmdApply(f, buf) @@ -230,6 +237,12 @@ func TestApplyRetry(t *testing.T) { NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { + case p == "/version" && m == "GET": + resp, err := genResponseWithJsonEncodedBody(serverVersion_1_5_0) + if err != nil { + t.Fatalf("error: failed to generate server version response: %#v\n", serverVersion_1_5_0) + } + return resp, nil case p == pathRC && m == "GET": getCount++ bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC)) @@ -253,6 +266,7 @@ func TestApplyRetry(t *testing.T) { }), } tf.Namespace = "test" + tf.ClientConfig = defaultClientConfig() buf := bytes.NewBuffer([]byte{}) cmd := NewCmdApply(f, buf) @@ -282,6 +296,12 @@ func TestApplyNonExistObject(t *testing.T) { NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { + case p == "/version" && m == "GET": + resp, err := genResponseWithJsonEncodedBody(serverVersion_1_5_0) + if err != nil { + t.Fatalf("error: failed to generate server version response: %#v\n", serverVersion_1_5_0) + } + return resp, nil case p == "/api/v1/namespaces/test" && m == "GET": return &http.Response{StatusCode: 404, Header: defaultHeader(), Body: ioutil.NopCloser(bytes.NewReader(nil))}, nil case p == pathNameRC && m == "GET": @@ -296,6 +316,7 @@ func TestApplyNonExistObject(t *testing.T) { }), } tf.Namespace = "test" + tf.ClientConfig = defaultClientConfig() buf := bytes.NewBuffer([]byte{}) cmd := NewCmdApply(f, buf) @@ -331,6 +352,12 @@ func testApplyMultipleObjects(t *testing.T, asList bool) { NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { + case p == "/version" && m == "GET": + resp, err := genResponseWithJsonEncodedBody(serverVersion_1_5_0) + if err != nil { + t.Fatalf("error: failed to generate server version response: %#v\n", serverVersion_1_5_0) + } + return resp, nil case p == pathRC && m == "GET": bodyRC := ioutil.NopCloser(bytes.NewReader(currentRC)) return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: bodyRC}, nil @@ -352,6 +379,7 @@ func testApplyMultipleObjects(t *testing.T, asList bool) { }), } tf.Namespace = "test" + tf.ClientConfig = defaultClientConfig() buf := bytes.NewBuffer([]byte{}) cmd := NewCmdApply(f, buf) diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index d5a3bdae566ad..869934bbddae6 100644 --- a/pkg/kubectl/cmd/cmd_test.go +++ b/pkg/kubectl/cmd/cmd_test.go @@ -39,8 +39,13 @@ import ( cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/strings" + "k8s.io/kubernetes/pkg/version" ) +var serverVersion_1_5_0 = version.Info{ + GitVersion: "v1.5.0", +} + func initTestErrorHandler(t *testing.T) { cmdutil.BehaviorOnFatal(func(str string, code int) { t.Errorf("Error running command (exit code %d): %s", code, str) diff --git a/pkg/kubectl/cmd/edit.go b/pkg/kubectl/cmd/edit.go index 22479dc58cd4f..236bc7d480d70 100644 --- a/pkg/kubectl/cmd/edit.go +++ b/pkg/kubectl/cmd/edit.go @@ -281,7 +281,7 @@ func runEdit(f cmdutil.Factory, out, errOut io.Writer, cmd *cobra.Command, args switch editMode { case NormalEditMode: - err = visitToPatch(originalObj, updates, mapper, resourceMapper, encoder, out, errOut, defaultVersion, &results, file) + err = visitToPatch(originalObj, updates, f, mapper, resourceMapper, encoder, out, errOut, defaultVersion, &results, file) case EditBeforeCreateMode: err = visitToCreate(updates, mapper, resourceMapper, out, errOut, defaultVersion, &results, file) default: @@ -393,9 +393,22 @@ func getMapperAndResult(f cmdutil.Factory, args []string, options *resource.File return mapper, resourceMapper, r, cmdNamespace, err } -func visitToPatch(originalObj runtime.Object, updates *resource.Info, mapper meta.RESTMapper, resourceMapper *resource.Mapper, encoder runtime.Encoder, out, errOut io.Writer, defaultVersion unversioned.GroupVersion, results *editResults, file string) error { +func visitToPatch(originalObj runtime.Object, updates *resource.Info, + f cmdutil.Factory, + mapper meta.RESTMapper, resourceMapper *resource.Mapper, + encoder runtime.Encoder, + out, errOut io.Writer, + defaultVersion unversioned.GroupVersion, + results *editResults, + file string) error { + + smPatchVersion, err := cmdutil.GetServerSupportedSMPatchVersionFromFactory(f) + if err != nil { + return err + } + patchVisitor := resource.NewFlattenListVisitor(updates, resourceMapper) - err := patchVisitor.Visit(func(info *resource.Info, incomingErr error) error { + err = patchVisitor.Visit(func(info *resource.Info, incomingErr error) error { currOriginalObj := originalObj // if we're editing a list, then navigate the list to find the item that we're currently trying to edit @@ -456,7 +469,7 @@ func visitToPatch(originalObj runtime.Object, updates *resource.Info, mapper met preconditions := []strategicpatch.PreconditionFunc{strategicpatch.RequireKeyUnchanged("apiVersion"), strategicpatch.RequireKeyUnchanged("kind"), strategicpatch.RequireMetadataKeyUnchanged("name")} - patch, err := strategicpatch.CreateTwoWayMergePatch(originalJS, editedJS, currOriginalObj, preconditions...) + patch, err := strategicpatch.CreateTwoWayMergePatch(originalJS, editedJS, currOriginalObj, smPatchVersion, preconditions...) if err != nil { glog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err) if strategicpatch.IsPreconditionFailed(err) { diff --git a/pkg/kubectl/cmd/label.go b/pkg/kubectl/cmd/label.go index 99e853056d9db..c5836b4d5ad63 100644 --- a/pkg/kubectl/cmd/label.go +++ b/pkg/kubectl/cmd/label.go @@ -192,6 +192,14 @@ func (o *LabelOptions) RunLabel(f cmdutil.Factory, cmd *cobra.Command) error { return err } + smPatchVersion := strategicpatch.SMPatchVersionLatest + if !o.local { + smPatchVersion, err = cmdutil.GetServerSupportedSMPatchVersionFromFactory(f) + if err != nil { + return err + } + } + // only apply resource version locking on a single resource if !one && len(o.resourceVersion) > 0 { return fmt.Errorf("--resource-version may only be used with a single resource") @@ -246,7 +254,7 @@ func (o *LabelOptions) RunLabel(f cmdutil.Factory, cmd *cobra.Command) error { if !reflect.DeepEqual(oldData, newData) { dataChangeMsg = "labeled" } - patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, obj) + patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, obj, smPatchVersion) createdPatch := err == nil if err != nil { glog.V(2).Infof("couldn't compute patch: %v", err) diff --git a/pkg/kubectl/cmd/label_test.go b/pkg/kubectl/cmd/label_test.go index 83a50cb1779ed..39000a0accec1 100644 --- a/pkg/kubectl/cmd/label_test.go +++ b/pkg/kubectl/cmd/label_test.go @@ -354,6 +354,12 @@ func TestLabelForResourceFromFile(t *testing.T) { switch req.Method { case "GET": switch req.URL.Path { + case "/version": + resp, err := genResponseWithJsonEncodedBody(serverVersion_1_5_0) + if err != nil { + t.Fatalf("error: failed to generate server version response: %#v\n", serverVersion_1_5_0) + } + return resp, nil case "/namespaces/test/replicationcontrollers/cassandra": return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &pods.Items[0])}, nil default: @@ -375,7 +381,7 @@ func TestLabelForResourceFromFile(t *testing.T) { }), } tf.Namespace = "test" - tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: ®istered.GroupOrDie(api.GroupName).GroupVersion}} + tf.ClientConfig = defaultClientConfig() buf := bytes.NewBuffer([]byte{}) cmd := NewCmdLabel(f, buf) @@ -437,6 +443,12 @@ func TestLabelMultipleObjects(t *testing.T) { switch req.Method { case "GET": switch req.URL.Path { + case "/version": + resp, err := genResponseWithJsonEncodedBody(serverVersion_1_5_0) + if err != nil { + t.Fatalf("error: failed to generate server version response: %#v\n", serverVersion_1_5_0) + } + return resp, nil case "/namespaces/test/pods": return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, pods)}, nil default: @@ -460,7 +472,7 @@ func TestLabelMultipleObjects(t *testing.T) { }), } tf.Namespace = "test" - tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: ®istered.GroupOrDie(api.GroupName).GroupVersion}} + tf.ClientConfig = defaultClientConfig() buf := bytes.NewBuffer([]byte{}) cmd := NewCmdLabel(f, buf) diff --git a/pkg/kubectl/cmd/patch.go b/pkg/kubectl/cmd/patch.go index 6debf1f8624cc..7552bf68a4d24 100644 --- a/pkg/kubectl/cmd/patch.go +++ b/pkg/kubectl/cmd/patch.go @@ -154,6 +154,14 @@ func RunPatch(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []strin return err } + smPatchVersion := strategicpatch.SMPatchVersionLatest + if !options.Local { + smPatchVersion, err = cmdutil.GetServerSupportedSMPatchVersionFromFactory(f) + if err != nil { + return err + } + } + count := 0 err = r.Visit(func(info *resource.Info, err error) error { if err != nil { @@ -177,7 +185,7 @@ func RunPatch(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []strin // don't bother checking for failures of this replace, because a failure to indicate the hint doesn't fail the command // also, don't force the replacement. If the replacement fails on a resourceVersion conflict, then it means this // record hint is likely to be invalid anyway, so avoid the bad hint - patch, err := cmdutil.ChangeResourcePatch(info, f.Command()) + patch, err := cmdutil.ChangeResourcePatch(info, f.Command(), smPatchVersion) if err == nil { helper.Patch(info.Namespace, info.Name, api.StrategicMergePatchType, patch) } diff --git a/pkg/kubectl/cmd/patch_test.go b/pkg/kubectl/cmd/patch_test.go index 9bb8f98dbf351..fc4109ec9e775 100644 --- a/pkg/kubectl/cmd/patch_test.go +++ b/pkg/kubectl/cmd/patch_test.go @@ -34,6 +34,12 @@ func TestPatchObject(t *testing.T) { NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { + case p == "/version" && m == "GET": + resp, err := genResponseWithJsonEncodedBody(serverVersion_1_5_0) + if err != nil { + t.Fatalf("error: failed to generate server version response: %#v\n", serverVersion_1_5_0) + } + return resp, nil case p == "/namespaces/test/services/frontend" && (m == "PATCH" || m == "GET"): return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &svc.Items[0])}, nil default: @@ -43,6 +49,7 @@ func TestPatchObject(t *testing.T) { }), } tf.Namespace = "test" + tf.ClientConfig = defaultClientConfig() buf := bytes.NewBuffer([]byte{}) cmd := NewCmdPatch(f, buf) @@ -66,6 +73,12 @@ func TestPatchObjectFromFile(t *testing.T) { NegotiatedSerializer: ns, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { switch p, m := req.URL.Path, req.Method; { + case p == "/version" && m == "GET": + resp, err := genResponseWithJsonEncodedBody(serverVersion_1_5_0) + if err != nil { + t.Fatalf("error: failed to generate server version response: %#v\n", serverVersion_1_5_0) + } + return resp, nil case p == "/namespaces/test/services/frontend" && (m == "PATCH" || m == "GET"): return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, &svc.Items[0])}, nil default: @@ -75,6 +88,7 @@ func TestPatchObjectFromFile(t *testing.T) { }), } tf.Namespace = "test" + tf.ClientConfig = defaultClientConfig() buf := bytes.NewBuffer([]byte{}) cmd := NewCmdPatch(f, buf) diff --git a/pkg/kubectl/cmd/rollout/rollout_pause.go b/pkg/kubectl/cmd/rollout/rollout_pause.go index 4e02d384eaf74..3c4a32778c60b 100644 --- a/pkg/kubectl/cmd/rollout/rollout_pause.go +++ b/pkg/kubectl/cmd/rollout/rollout_pause.go @@ -38,6 +38,7 @@ import ( type PauseConfig struct { resource.FilenameOptions + f cmdutil.Factory Pauser func(info *resource.Info) (bool, error) Mapper meta.RESTMapper Typer runtime.ObjectTyper @@ -99,6 +100,7 @@ func (o *PauseConfig) CompletePause(f cmdutil.Factory, cmd *cobra.Command, out i return cmdutil.UsageError(cmd, cmd.Use) } + o.f = f o.Mapper, o.Typer = f.Object() o.Encoder = f.JSONEncoder() @@ -132,7 +134,7 @@ func (o *PauseConfig) CompletePause(f cmdutil.Factory, cmd *cobra.Command, out i func (o PauseConfig) RunPause() error { allErrs := []error{} - for _, patch := range set.CalculatePatches(o.Infos, o.Encoder, o.Pauser) { + for _, patch := range set.CalculatePatches(o.f, o.Infos, o.Encoder, false, o.Pauser) { info := patch.Info if patch.Err != nil { allErrs = append(allErrs, fmt.Errorf("error: %s %q %v", info.Mapping.Resource, info.Name, patch.Err)) diff --git a/pkg/kubectl/cmd/rollout/rollout_resume.go b/pkg/kubectl/cmd/rollout/rollout_resume.go index 56623934083a2..f247f19ff2621 100644 --- a/pkg/kubectl/cmd/rollout/rollout_resume.go +++ b/pkg/kubectl/cmd/rollout/rollout_resume.go @@ -38,6 +38,7 @@ import ( type ResumeConfig struct { resource.FilenameOptions + f cmdutil.Factory Resumer func(object *resource.Info) (bool, error) Mapper meta.RESTMapper Typer runtime.ObjectTyper @@ -97,6 +98,7 @@ func (o *ResumeConfig) CompleteResume(f cmdutil.Factory, cmd *cobra.Command, out return cmdutil.UsageError(cmd, cmd.Use) } + o.f = f o.Mapper, o.Typer = f.Object() o.Encoder = f.JSONEncoder() @@ -136,7 +138,7 @@ func (o *ResumeConfig) CompleteResume(f cmdutil.Factory, cmd *cobra.Command, out func (o ResumeConfig) RunResume() error { allErrs := []error{} - for _, patch := range set.CalculatePatches(o.Infos, o.Encoder, o.Resumer) { + for _, patch := range set.CalculatePatches(o.f, o.Infos, o.Encoder, false, o.Resumer) { info := patch.Info if patch.Err != nil { diff --git a/pkg/kubectl/cmd/scale.go b/pkg/kubectl/cmd/scale.go index c7f266b70d243..5b6a07ed017dc 100644 --- a/pkg/kubectl/cmd/scale.go +++ b/pkg/kubectl/cmd/scale.go @@ -139,6 +139,11 @@ func RunScale(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []strin return fmt.Errorf("cannot use --resource-version with multiple resources") } + smPatchVersion, err := cmdutil.GetServerSupportedSMPatchVersionFromFactory(f) + if err != nil { + return err + } + counter := 0 err = r.Visit(func(info *resource.Info, err error) error { if err != nil { @@ -164,7 +169,7 @@ func RunScale(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []strin return err } if cmdutil.ShouldRecord(cmd, info) { - patchBytes, err := cmdutil.ChangeResourcePatch(info, f.Command()) + patchBytes, err := cmdutil.ChangeResourcePatch(info, f.Command(), smPatchVersion) if err != nil { return err } diff --git a/pkg/kubectl/cmd/set/helper.go b/pkg/kubectl/cmd/set/helper.go index 7b4a380a5c53c..97423908c35b0 100644 --- a/pkg/kubectl/cmd/set/helper.go +++ b/pkg/kubectl/cmd/set/helper.go @@ -23,7 +23,7 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/errors" - kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/strategicpatch" @@ -61,7 +61,7 @@ func handlePodUpdateError(out io.Writer, err error, resource string) { return } } else { - if ok := kcmdutil.PrintErrorWithCauses(err, out); ok { + if ok := cmdutil.PrintErrorWithCauses(err, out); ok { return } } @@ -120,8 +120,20 @@ type Patch struct { // CalculatePatches calls the mutation function on each provided info object, and generates a strategic merge patch for // the changes in the object. Encoder must be able to encode the info into the appropriate destination type. If mutateFn // returns false, the object is not included in the final list of patches. -func CalculatePatches(infos []*resource.Info, encoder runtime.Encoder, mutateFn func(*resource.Info) (bool, error)) []*Patch { +// If local is true, it will be default to use SMPatchVersionLatest to calculate a patch without contacting the server to +// get the server supported SMPatchVersion. If you are using a patch's Patch field generated in local mode, be careful. +// If local is false, it will talk to the server to check which StategicMergePatchVersion to use. +func CalculatePatches(f cmdutil.Factory, infos []*resource.Info, encoder runtime.Encoder, local bool, mutateFn func(*resource.Info) (bool, error)) []*Patch { var patches []*Patch + smPatchVersion := strategicpatch.SMPatchVersionLatest + var err error + if !local { + smPatchVersion, err = cmdutil.GetServerSupportedSMPatchVersionFromFactory(f) + if err != nil { + return patches + } + } + for _, info := range infos { patch := &Patch{Info: info} patch.Before, patch.Err = runtime.Encode(encoder, info.Object) @@ -156,7 +168,7 @@ func CalculatePatches(infos []*resource.Info, encoder runtime.Encoder, mutateFn continue } - patch.Patch, patch.Err = strategicpatch.CreateTwoWayMergePatch(patch.Before, patch.After, versioned) + patch.Patch, patch.Err = strategicpatch.CreateTwoWayMergePatch(patch.Before, patch.After, versioned, smPatchVersion) } return patches } diff --git a/pkg/kubectl/cmd/set/set_image.go b/pkg/kubectl/cmd/set/set_image.go index 5baa00b3d43ee..f3ad8dbae1985 100644 --- a/pkg/kubectl/cmd/set/set_image.go +++ b/pkg/kubectl/cmd/set/set_image.go @@ -28,6 +28,7 @@ import ( "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/runtime" utilerrors "k8s.io/kubernetes/pkg/util/errors" + "k8s.io/kubernetes/pkg/util/strategicpatch" ) // ImageOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of @@ -35,6 +36,7 @@ import ( type ImageOptions struct { resource.FilenameOptions + f cmdutil.Factory Mapper meta.RESTMapper Typer runtime.ObjectTyper Infos []*resource.Info @@ -108,6 +110,7 @@ func NewCmdImage(f cmdutil.Factory, out, err io.Writer) *cobra.Command { } func (o *ImageOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + o.f = f o.Mapper, o.Typer = f.Object() o.UpdatePodSpecForObject = f.UpdatePodSpecForObject o.Encoder = f.JSONEncoder() @@ -162,7 +165,7 @@ func (o *ImageOptions) Validate() error { func (o *ImageOptions) Run() error { allErrs := []error{} - patches := CalculatePatches(o.Infos, o.Encoder, func(info *resource.Info) (bool, error) { + patches := CalculatePatches(o.f, o.Infos, o.Encoder, o.Local, func(info *resource.Info) (bool, error) { transformed := false _, err := o.UpdatePodSpecForObject(info.Object, func(spec *api.PodSpec) error { for name, image := range o.ContainerImages { @@ -186,6 +189,14 @@ func (o *ImageOptions) Run() error { return transformed, err }) + smPatchVersion := strategicpatch.SMPatchVersionLatest + var err error + if !o.Local { + smPatchVersion, err = cmdutil.GetServerSupportedSMPatchVersionFromFactory(o.f) + if err != nil { + return err + } + } for _, patch := range patches { info := patch.Info if patch.Err != nil { @@ -212,7 +223,7 @@ func (o *ImageOptions) Run() error { // record this change (for rollout history) if o.Record || cmdutil.ContainsChangeCause(info) { - if patch, err := cmdutil.ChangeResourcePatch(info, o.ChangeCause); err == nil { + if patch, err := cmdutil.ChangeResourcePatch(info, o.ChangeCause, smPatchVersion); err == nil { if obj, err = resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, api.StrategicMergePatchType, patch); err != nil { fmt.Fprintf(o.Err, "WARNING: changes to %s/%s can't be recorded: %v\n", info.Mapping.Resource, info.Name, err) } diff --git a/pkg/kubectl/cmd/set/set_resources.go b/pkg/kubectl/cmd/set/set_resources.go index 3883e38732e5c..5686d058957d9 100644 --- a/pkg/kubectl/cmd/set/set_resources.go +++ b/pkg/kubectl/cmd/set/set_resources.go @@ -60,6 +60,7 @@ var ( type ResourcesOptions struct { resource.FilenameOptions + f cmdutil.Factory Mapper meta.RESTMapper Typer runtime.ObjectTyper Infos []*resource.Info @@ -122,6 +123,7 @@ func NewCmdResources(f cmdutil.Factory, out io.Writer, errOut io.Writer) *cobra. } func (o *ResourcesOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + o.f = f o.Mapper, o.Typer = f.Object() o.UpdatePodSpecForObject = f.UpdatePodSpecForObject o.Encoder = f.JSONEncoder() @@ -172,7 +174,7 @@ func (o *ResourcesOptions) Validate() error { func (o *ResourcesOptions) Run() error { allErrs := []error{} - patches := CalculatePatches(o.Infos, o.Encoder, func(info *resource.Info) (bool, error) { + patches := CalculatePatches(o.f, o.Infos, o.Encoder, cmdutil.GetDryRunFlag(o.Cmd), func(info *resource.Info) (bool, error) { transformed := false _, err := o.UpdatePodSpecForObject(info.Object, func(spec *api.PodSpec) error { containers, _ := selectContainers(spec.Containers, o.ContainerSelector) diff --git a/pkg/kubectl/cmd/taint.go b/pkg/kubectl/cmd/taint.go index 142137f587d3f..d68408e40c0ce 100644 --- a/pkg/kubectl/cmd/taint.go +++ b/pkg/kubectl/cmd/taint.go @@ -321,6 +321,11 @@ func (o TaintOptions) RunTaint() error { return err } + smPatchVersion, err := cmdutil.GetServerSupportedSMPatchVersionFromFactory(o.f) + if err != nil { + return err + } + return r.Visit(func(info *resource.Info, err error) error { if err != nil { return err @@ -343,7 +348,7 @@ func (o TaintOptions) RunTaint() error { if err != nil { return err } - patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, obj) + patchBytes, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, obj, smPatchVersion) createdPatch := err == nil if err != nil { glog.V(2).Infof("couldn't compute patch: %v", err) diff --git a/pkg/kubectl/cmd/taint_test.go b/pkg/kubectl/cmd/taint_test.go index 32d693851fbcc..f7a6fcf634fa0 100644 --- a/pkg/kubectl/cmd/taint_test.go +++ b/pkg/kubectl/cmd/taint_test.go @@ -252,7 +252,6 @@ func TestTaint(t *testing.T) { for _, test := range tests { oldNode, expectNewNode := generateNodeAndTaintedNode(test.oldTaints, test.newTaints) - new_node := &api.Node{} tainted := false f, tf, codec, ns := cmdtesting.NewAPIFactory() @@ -262,6 +261,12 @@ func TestTaint(t *testing.T) { Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { m := &MyReq{req} switch { + case m.isFor("GET", "/version"): + resp, err := genResponseWithJsonEncodedBody(serverVersion_1_5_0) + if err != nil { + t.Fatalf("error: failed to generate server version response: %#v\n", serverVersion_1_5_0) + } + return resp, nil case m.isFor("GET", "/nodes/node-name"): return &http.Response{StatusCode: 200, Header: defaultHeader(), Body: objBody(codec, oldNode)}, nil case m.isFor("PATCH", "/nodes/node-name"), m.isFor("PUT", "/nodes/node-name"): diff --git a/pkg/kubectl/cmd/util/helpers.go b/pkg/kubectl/cmd/util/helpers.go index 01e34a64edf14..54176b5d5d655 100644 --- a/pkg/kubectl/cmd/util/helpers.go +++ b/pkg/kubectl/cmd/util/helpers.go @@ -521,7 +521,7 @@ func RecordChangeCause(obj runtime.Object, changeCause string) error { // ChangeResourcePatch creates a strategic merge patch between the origin input resource info // and the annotated with change-cause input resource info. -func ChangeResourcePatch(info *resource.Info, changeCause string) ([]byte, error) { +func ChangeResourcePatch(info *resource.Info, changeCause string, smPatchVersion strategicpatch.StrategicMergePatchVersion) ([]byte, error) { oldData, err := json.Marshal(info.Object) if err != nil { return nil, err @@ -533,7 +533,7 @@ func ChangeResourcePatch(info *resource.Info, changeCause string) ([]byte, error if err != nil { return nil, err } - return strategicpatch.CreateTwoWayMergePatch(oldData, newData, info.Object) + return strategicpatch.CreateTwoWayMergePatch(oldData, newData, info.Object, smPatchVersion) } // containsChangeCause checks if input resource info contains change-cause annotation. @@ -725,3 +725,13 @@ func RequireNoArguments(c *cobra.Command, args []string) { CheckErr(UsageError(c, fmt.Sprintf(`unknown command %q`, strings.Join(args, " ")))) } } + +// GetServerSupportedSMPatchVersionFromFactory is a wrapper of GetServerSupportedSMPatchVersion(), +// It takes a Factory, returns the max version the server supports. +func GetServerSupportedSMPatchVersionFromFactory(f Factory) (strategicpatch.StrategicMergePatchVersion, error) { + clientSet, err := f.ClientSet() + if err != nil { + return strategicpatch.Unknown, err + } + return strategicpatch.GetServerSupportedSMPatchVersion(clientSet.Discovery()) +} diff --git a/pkg/util/strategicpatch/BUILD b/pkg/util/strategicpatch/BUILD index 433b7a295d857..8340ba0c5ad86 100644 --- a/pkg/util/strategicpatch/BUILD +++ b/pkg/util/strategicpatch/BUILD @@ -15,6 +15,7 @@ go_library( srcs = ["patch.go"], tags = ["automanaged"], deps = [ + "//pkg/client/typed/discovery:go_default_library", "//pkg/util/json:go_default_library", "//third_party/forked/golang/json:go_default_library", "//vendor:github.com/davecgh/go-spew/spew", diff --git a/pkg/util/strategicpatch/patch.go b/pkg/util/strategicpatch/patch.go index 66a33f7ad08a5..601594ae4e9d3 100644 --- a/pkg/util/strategicpatch/patch.go +++ b/pkg/util/strategicpatch/patch.go @@ -21,6 +21,7 @@ import ( "reflect" "sort" + "k8s.io/kubernetes/pkg/client/typed/discovery" "k8s.io/kubernetes/pkg/util/json" forkedjson "k8s.io/kubernetes/third_party/forked/golang/json" @@ -38,11 +39,20 @@ import ( // Some of the content of this package was borrowed with minor adaptations from // evanphx/json-patch and openshift/origin. +type StrategicMergePatchVersion string + const ( - directiveMarker = "$patch" - deleteDirective = "delete" - replaceDirective = "replace" - mergeDirective = "merge" + directiveMarker = "$patch" + deleteDirective = "delete" + replaceDirective = "replace" + mergeDirective = "merge" + mergePrimitivesListDirective = "mergeprimitiveslist" + + // different versions of StrategicMergePatch + SMPatchVersion_1_0 StrategicMergePatchVersion = "v1.0.0" + SMPatchVersion_1_5 StrategicMergePatchVersion = "v1.5.0" + Unknown StrategicMergePatchVersion = "Unknown" + SMPatchVersionLatest = SMPatchVersion_1_5 ) // IsPreconditionFailed returns true if the provided error indicates @@ -87,6 +97,7 @@ func IsConflict(err error) bool { var errBadJSONDoc = fmt.Errorf("Invalid JSON document") var errNoListOfLists = fmt.Errorf("Lists of lists are not supported") +var errNoElementsInSlice = fmt.Errorf("no elements in any of the given slices") // 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, @@ -133,15 +144,15 @@ 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) +func CreateStrategicMergePatch(original, modified []byte, dataStruct interface{}, smPatchVersion StrategicMergePatchVersion) ([]byte, error) { + return CreateTwoWayMergePatch(original, modified, dataStruct, smPatchVersion) } // 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 // 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) { +func CreateTwoWayMergePatch(original, modified []byte, dataStruct interface{}, smPatchVersion StrategicMergePatchVersion, fns ...PreconditionFunc) ([]byte, error) { originalMap := map[string]interface{}{} if len(original) > 0 { if err := json.Unmarshal(original, &originalMap); err != nil { @@ -161,7 +172,7 @@ func CreateTwoWayMergePatch(original, modified []byte, dataStruct interface{}, f return nil, err } - patchMap, err := diffMaps(originalMap, modifiedMap, t, false, false) + patchMap, err := diffMaps(originalMap, modifiedMap, t, false, false, smPatchVersion) if err != nil { return nil, err } @@ -177,7 +188,7 @@ func CreateTwoWayMergePatch(original, modified []byte, dataStruct interface{}, f } // Returns a (recursive) strategic merge patch that yields modified when applied to original. -func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreChangesAndAdditions, ignoreDeletions bool) (map[string]interface{}, error) { +func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreChangesAndAdditions, ignoreDeletions bool, smPatchVersion StrategicMergePatchVersion) (map[string]interface{}, error) { patch := map[string]interface{}{} if t.Kind() == reflect.Ptr { t = t.Elem() @@ -230,7 +241,7 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreC return nil, err } - patchValue, err := diffMaps(originalValueTyped, modifiedValueTyped, fieldType, ignoreChangesAndAdditions, ignoreDeletions) + patchValue, err := diffMaps(originalValueTyped, modifiedValueTyped, fieldType, ignoreChangesAndAdditions, ignoreDeletions, smPatchVersion) if err != nil { return nil, err } @@ -248,13 +259,25 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreC } if fieldPatchStrategy == mergeDirective { - patchValue, err := diffLists(originalValueTyped, modifiedValueTyped, fieldType.Elem(), fieldPatchMergeKey, ignoreChangesAndAdditions, ignoreDeletions) + patchValue, err := diffLists(originalValueTyped, modifiedValueTyped, fieldType.Elem(), fieldPatchMergeKey, ignoreChangesAndAdditions, ignoreDeletions, smPatchVersion) if err != nil { return nil, err } + if patchValue == nil { + continue + } - if len(patchValue) > 0 { - patch[key] = patchValue + switch typedPatchValue := patchValue.(type) { + case []interface{}: + if len(typedPatchValue) > 0 { + patch[key] = typedPatchValue + } + case map[string]interface{}: + if len(typedPatchValue) > 0 { + patch[key] = typedPatchValue + } + default: + return nil, fmt.Errorf("invalid type of patch: %v", reflect.TypeOf(patchValue)) } continue @@ -284,7 +307,7 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreC // 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) { +func diffLists(original, modified []interface{}, t reflect.Type, mergeKey string, ignoreChangesAndAdditions, ignoreDeletions bool, smPatchVersion StrategicMergePatchVersion) (interface{}, error) { if len(original) == 0 { if len(modified) == 0 || ignoreChangesAndAdditions { return nil, nil @@ -298,12 +321,14 @@ func diffLists(original, modified []interface{}, t reflect.Type, mergeKey string return nil, err } - var patch []interface{} + 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) + patch, err = diffListsOfMaps(original, modified, t, mergeKey, ignoreChangesAndAdditions, ignoreDeletions, smPatchVersion) + } else if elementType.Kind() == reflect.Slice { + err = errNoListOfLists + } else { + patch, err = diffListsOfScalars(original, modified, ignoreChangesAndAdditions, ignoreDeletions, smPatchVersion) } if err != nil { @@ -315,8 +340,23 @@ 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{}) ([]interface{}, error) { - if len(modified) == 0 { +func diffListsOfScalars(original, modified []interface{}, ignoreChangesAndAdditions, ignoreDeletions bool, smPatchVersion StrategicMergePatchVersion) (interface{}, error) { + originalScalars := uniqifyAndSortScalars(original) + modifiedScalars := uniqifyAndSortScalars(modified) + + switch smPatchVersion { + case SMPatchVersion_1_5: + return diffListsOfScalarsIntoMap(originalScalars, modifiedScalars, ignoreChangesAndAdditions, ignoreDeletions) + case SMPatchVersion_1_0: + return diffListsOfScalarsIntoSlice(originalScalars, modifiedScalars, ignoreChangesAndAdditions, ignoreDeletions) + default: + return nil, fmt.Errorf("Unknown StrategicMergePatchVersion: %v", smPatchVersion) + } +} + +func diffListsOfScalarsIntoSlice(originalScalars, modifiedScalars []interface{}, ignoreChangesAndAdditions, ignoreDeletions bool) ([]interface{}, error) { + originalIndex, modifiedIndex := 0, 0 + if len(modifiedScalars) == 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 @@ -324,18 +364,14 @@ func diffListsOfScalars(original, modified []interface{}) ([]interface{}, error) patch := []interface{}{} - originalScalars := uniqifyAndSortScalars(original) - modifiedScalars := uniqifyAndSortScalars(modified) - originalIndex, modifiedIndex := 0, 0 - loopB: for ; modifiedIndex < len(modifiedScalars); modifiedIndex++ { for ; originalIndex < len(originalScalars); originalIndex++ { - originalString := fmt.Sprintf("%v", original[originalIndex]) - modifiedString := fmt.Sprintf("%v", modified[modifiedIndex]) + originalString := fmt.Sprintf("%v", originalScalars[originalIndex]) + modifiedString := fmt.Sprintf("%v", modifiedScalars[modifiedIndex]) if originalString >= modifiedString { if originalString != modifiedString { - patch = append(patch, modified[modifiedIndex]) + patch = append(patch, modifiedScalars[modifiedIndex]) } continue loopB @@ -349,7 +385,57 @@ loopB: // Add any remaining items found only in modified for ; modifiedIndex < len(modifiedScalars); modifiedIndex++ { - patch = append(patch, modified[modifiedIndex]) + patch = append(patch, modifiedScalars[modifiedIndex]) + } + + return patch, nil +} + +func diffListsOfScalarsIntoMap(originalScalars, modifiedScalars []interface{}, ignoreChangesAndAdditions, ignoreDeletions bool) (map[string]interface{}, error) { + originalIndex, modifiedIndex := 0, 0 + patch := map[string]interface{}{} + patch[directiveMarker] = mergePrimitivesListDirective + + for originalIndex < len(originalScalars) && modifiedIndex < len(modifiedScalars) { + originalString := fmt.Sprintf("%v", originalScalars[originalIndex]) + modifiedString := fmt.Sprintf("%v", modifiedScalars[modifiedIndex]) + + // objects are identical + if originalString == modifiedString { + originalIndex++ + modifiedIndex++ + continue + } + + if originalString > modifiedString { + if !ignoreChangesAndAdditions { + modifiedValue := modifiedScalars[modifiedIndex] + patch[modifiedString] = modifiedValue + } + modifiedIndex++ + } else { + if !ignoreDeletions { + patch[originalString] = nil + } + originalIndex++ + } + } + + // Delete any remaining items found only in original + if !ignoreDeletions { + for ; originalIndex < len(originalScalars); originalIndex++ { + originalString := fmt.Sprintf("%v", originalScalars[originalIndex]) + patch[originalString] = nil + } + } + + // Add any remaining items found only in modified + if !ignoreChangesAndAdditions { + for ; modifiedIndex < len(modifiedScalars); modifiedIndex++ { + modifiedString := fmt.Sprintf("%v", modifiedScalars[modifiedIndex]) + modifiedValue := modifiedScalars[modifiedIndex] + patch[modifiedString] = modifiedValue + } } return patch, nil @@ -360,7 +446,7 @@ var errBadArgTypeFmt = "expected a %s, but received a %s" // 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, ignoreChangesAndAdditions, ignoreDeletions bool) ([]interface{}, error) { +func diffListsOfMaps(original, modified []interface{}, t reflect.Type, mergeKey string, ignoreChangesAndAdditions, ignoreDeletions bool, smPatchVersion StrategicMergePatchVersion) ([]interface{}, error) { patch := make([]interface{}, 0) originalSorted, err := sortMergeListsByNameArray(original, t, mergeKey, false) @@ -406,7 +492,7 @@ loopB: if originalString >= modifiedString { if originalString == modifiedString { // Merge key values are equal, so recurse - patchValue, err := diffMaps(originalMap, modifiedMap, t, ignoreChangesAndAdditions, ignoreDeletions) + patchValue, err := diffMaps(originalMap, modifiedMap, t, ignoreChangesAndAdditions, ignoreDeletions, smPatchVersion) if err != nil { return nil, err } @@ -542,7 +628,15 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[strin return map[string]interface{}{}, nil } - return nil, fmt.Errorf(errBadPatchTypeFmt, v, patch) + if v == mergePrimitivesListDirective { + // delete the directiveMarker's key-value pair to avoid delta map and delete map + // overlaping with each other when calculating a ThreeWayDiff for list of Primitives. + // Otherwise, the overlaping will cause it calling LookupPatchMetadata() which will + // return an error since the metadata shows it's a slice but it is actually a map. + delete(original, directiveMarker) + } else { + return nil, fmt.Errorf(errBadPatchTypeFmt, v, patch) + } } // nil is an accepted value for original to simplify logic in other places. @@ -578,7 +672,9 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[strin // If they're both maps or lists, recurse into the value. originalType := reflect.TypeOf(original[k]) patchType := reflect.TypeOf(patchV) - if originalType == patchType { + // check if we are trying to merge a slice with a map for list of primitives + isMergeSliceOfPrimitivesWithAPatchMap := originalType != nil && patchType != nil && originalType.Kind() == reflect.Slice && patchType.Kind() == reflect.Map + if originalType == patchType || isMergeSliceOfPrimitivesWithAPatchMap { // First find the fieldPatchStrategy and fieldPatchMergeKey. fieldType, fieldPatchStrategy, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, k) if err != nil { @@ -600,9 +696,8 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[strin if originalType.Kind() == reflect.Slice && fieldPatchStrategy == mergeDirective { elemType := fieldType.Elem() typedOriginal := original[k].([]interface{}) - typedPatch := patchV.([]interface{}) var err error - original[k], err = mergeSlice(typedOriginal, typedPatch, elemType, fieldPatchMergeKey) + original[k], err = mergeSlice(typedOriginal, patchV, elemType, fieldPatchMergeKey) if err != nil { return nil, err } @@ -623,13 +718,34 @@ 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) { - if len(original) == 0 && len(patch) == 0 { +// The patch could be a map[string]interface{} representing a slice of primitives. +// If the patch map doesn't has the specific directiveMarker (mergePrimitivesListDirective), +// it returns an error. Please check patch_test.go and find the test case named +// "merge lists of scalars for list of primitives" to see what the patch looks like. +// Patch is still []interface{} for all the other types. +func mergeSlice(original []interface{}, patch interface{}, elemType reflect.Type, mergeKey string) ([]interface{}, error) { + t, err := sliceElementType(original) + if err != nil && err != errNoElementsInSlice { + return nil, err + } + + if patchMap, ok := patch.(map[string]interface{}); ok { + // We try to merge the original slice with a patch map only when the map has + // a specific directiveMarker. Otherwise, this patch will be treated as invalid. + if directiveValue, ok := patchMap[directiveMarker]; ok && directiveValue == mergePrimitivesListDirective { + return mergeSliceOfScalarsWithPatchMap(original, patchMap) + } else { + return nil, fmt.Errorf("Unable to merge a slice with an invalid map") + } + } + + typedPatch := patch.([]interface{}) + if len(original) == 0 && len(typedPatch) == 0 { return original, nil } // All the values must be of the same type, but not a list. - t, err := sliceElementType(original, patch) + t, err = sliceElementType(original, typedPatch) if err != nil { return nil, err } @@ -638,7 +754,7 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s if t.Kind() != reflect.Map { // Maybe in the future add a "concat" mode that doesn't // uniqify. - both := append(original, patch...) + both := append(original, typedPatch...) return uniqifyScalars(both), nil } @@ -649,7 +765,7 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s // First look for any special $patch elements. patchWithoutSpecialElements := []interface{}{} replace := false - for _, v := range patch { + for _, v := range typedPatch { typedV := v.(map[string]interface{}) patchType, ok := typedV[directiveMarker] if ok { @@ -685,10 +801,10 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s return patchWithoutSpecialElements, nil } - patch = patchWithoutSpecialElements + typedPatch = patchWithoutSpecialElements // Merge patch into original. - for _, v := range patch { + for _, v := range typedPatch { // Because earlier we confirmed that all the elements are maps. typedV := v.(map[string]interface{}) mergeValue, ok := typedV[mergeKey] @@ -721,6 +837,36 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s return original, nil } +// mergeSliceOfScalarsWithPatchMap merges the original slice with a patch map and +// returns an uniqified and sorted slice of primitives. +// The patch map must have the specific directiveMarker (mergePrimitivesListDirective). +func mergeSliceOfScalarsWithPatchMap(original []interface{}, patch map[string]interface{}) ([]interface{}, error) { + // make sure the patch has the specific directiveMarker () + if directiveValue, ok := patch[directiveMarker]; ok && directiveValue != mergePrimitivesListDirective { + return nil, fmt.Errorf("Unable to merge a slice with an invalid map") + } + delete(patch, directiveMarker) + output := make([]interface{}, 0, len(original)+len(patch)) + for _, value := range original { + valueString := fmt.Sprintf("%v", value) + if v, ok := patch[valueString]; ok { + if v != nil { + output = append(output, v) + } + delete(patch, valueString) + } else { + output = append(output, value) + } + } + for _, value := range patch { + if value != nil { + output = append(output, value) + } + // No action required to delete items that missing from the original slice. + } + return uniqifyAndSortScalars(output), nil +} + // 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 { @@ -946,7 +1092,7 @@ func sliceElementType(slices ...[]interface{}) (reflect.Type, error) { } if prevType == nil { - return nil, fmt.Errorf("no elements in any of the given slices") + return nil, errNoElementsInSlice } return prevType, nil @@ -1035,6 +1181,10 @@ func mergingMapFieldsHaveConflicts( if leftMarker != rightMarker { return true, nil } + + if leftMarker == mergePrimitivesListDirective && rightMarker == mergePrimitivesListDirective { + return false, nil + } } // Check the individual keys. @@ -1057,12 +1207,29 @@ func mergingMapFieldsHaveConflicts( } func mapsHaveConflicts(typedLeft, typedRight map[string]interface{}, structType reflect.Type) (bool, error) { + isForListOfPrimitives := false + if leftDirective, ok := typedLeft[directiveMarker]; ok { + if rightDirective, ok := typedRight[directiveMarker]; ok { + if leftDirective == mergePrimitivesListDirective && rightDirective == rightDirective { + isForListOfPrimitives = true + } + } + } for key, leftValue := range typedLeft { if key != directiveMarker { if rightValue, ok := typedRight[key]; ok { - fieldType, fieldPatchStrategy, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(structType, key) - if err != nil { - return true, err + var fieldType reflect.Type + var fieldPatchStrategy, fieldPatchMergeKey string + var err error + if isForListOfPrimitives { + fieldType = reflect.TypeOf(leftValue) + fieldPatchStrategy = "" + fieldPatchMergeKey = "" + } else { + fieldType, fieldPatchStrategy, fieldPatchMergeKey, err = forkedjson.LookupPatchMetadata(structType, key) + if err != nil { + return true, err + } } if hasConflicts, err := mergingMapFieldsHaveConflicts(leftValue, rightValue, @@ -1172,7 +1339,7 @@ func mapsOfMapsHaveConflicts(typedLeft, typedRight map[string]interface{}, struc // than from original to current. In other words, a conflict occurs if modified changes any key // in a way that is different from how it is changed in current (e.g., deleting it, changing its // value). -func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct interface{}, overwrite bool, fns ...PreconditionFunc) ([]byte, error) { +func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct interface{}, overwrite bool, smPatchVersion StrategicMergePatchVersion, fns ...PreconditionFunc) ([]byte, error) { originalMap := map[string]interface{}{} if len(original) > 0 { if err := json.Unmarshal(original, &originalMap); err != nil { @@ -1203,12 +1370,12 @@ func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct int // 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) + deltaMap, err := diffMaps(currentMap, modifiedMap, t, false, true, smPatchVersion) if err != nil { return nil, err } - deletionsMap, err := diffMaps(originalMap, modifiedMap, t, true, false) + deletionsMap, err := diffMaps(originalMap, modifiedMap, t, true, false, smPatchVersion) if err != nil { return nil, err } @@ -1228,7 +1395,7 @@ func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct int // If overwrite is false, and the patch contains any keys that were changed differently, // then return a conflict error. if !overwrite { - changedMap, err := diffMaps(originalMap, currentMap, t, false, false) + changedMap, err := diffMaps(originalMap, currentMap, t, false, false, smPatchVersion) if err != nil { return nil, err } @@ -1263,3 +1430,20 @@ func toYAML(v interface{}) (string, error) { return string(y), nil } + +// GetServerSupportedSMPatchVersion takes a discoveryClient, +// returns the max StrategicMergePatch version supported +func GetServerSupportedSMPatchVersion(discoveryClient discovery.DiscoveryInterface) (StrategicMergePatchVersion, error) { + serverVersion, err := discoveryClient.ServerVersion() + if err != nil { + return Unknown, err + } + serverGitVersion := serverVersion.GitVersion + if serverGitVersion >= string(SMPatchVersion_1_5) { + return SMPatchVersion_1_5, nil + } + if serverGitVersion >= string(SMPatchVersion_1_5) { + return SMPatchVersion_1_0, nil + } + return Unknown, fmt.Errorf("The version is too old: %v\n", serverVersion) +} diff --git a/pkg/util/strategicpatch/patch_test.go b/pkg/util/strategicpatch/patch_test.go index 0aa7c37cb222a..279beda48d23d 100644 --- a/pkg/util/strategicpatch/patch_test.go +++ b/pkg/util/strategicpatch/patch_test.go @@ -17,6 +17,7 @@ limitations under the License. package strategicpatch import ( + "bytes" "encoding/json" "fmt" "reflect" @@ -47,12 +48,12 @@ type StrategicMergePatchTestCase struct { } type StrategicMergePatchTestCaseData struct { - Original map[string]interface{} - TwoWay map[string]interface{} - Modified map[string]interface{} - Current map[string]interface{} - ThreeWay map[string]interface{} - Result map[string]interface{} + Original []byte + TwoWay []byte + Modified []byte + Current []byte + ThreeWay []byte + Result []byte } type MergeItem struct { @@ -248,1535 +249,2144 @@ func TestSortMergeLists(t *testing.T) { // These are test cases for StrategicMergePatch that cannot be generated using // CreateTwoWayMergePatch because it doesn't use the replace directive, generate // duplicate integers for a merging list patch, or generate empty merging lists. -var customStrategicMergePatchTestCaseData = []byte(` -testCases: - - description: unique scalars when merging lists - original: - mergingIntList: - - 1 - - 2 - twoWay: - mergingIntList: - - 2 - - 3 - modified: - mergingIntList: - - 1 - - 2 - - 3 - - description: delete map from nested map - original: - simpleMap: - key1: 1 - key2: 1 - twoWay: - simpleMap: - $patch: delete - modified: - simpleMap: - {} - - description: delete all items from merging list - original: - mergingList: - - name: 1 - - name: 2 - twoWay: - mergingList: - - $patch: replace - modified: - mergingList: [] - - description: merge empty merging lists - original: - mergingList: [] - twoWay: - mergingList: [] - modified: - mergingList: [] - - description: delete all keys from map - original: - name: 1 - value: 1 - twoWay: - $patch: replace - modified: {} - - description: add key and delete all keys from map - original: - name: 1 - value: 1 - twoWay: - other: a - $patch: replace - modified: - other: a -`) - -func TestCustomStrategicMergePatch(t *testing.T) { - tc := StrategicMergePatchTestCases{} - err := yaml.Unmarshal(customStrategicMergePatchTestCaseData, &tc) - if err != nil { - t.Errorf("can't unmarshal test cases: %v\n", err) - return - } - - for _, c := range tc.TestCases { - original, twoWay, modified := twoWayTestCaseToJSONOrFail(t, c) - testPatchApplication(t, original, twoWay, modified, c.Description) - } -} - -// These are test cases for StrategicMergePatch, to assert that applying a patch -// yields the correct outcome. They are also test cases for CreateTwoWayMergePatch -// and CreateThreeWayMergePatch, to assert that they both generate the correct patch -// for the given set of input documents. -// -var createStrategicMergePatchTestCaseData = []byte(` -testCases: - - description: nil original - twoWay: - name: 1 - value: 1 - modified: - name: 1 - value: 1 - current: - name: 1 - other: a - threeWay: - value: 1 - result: - name: 1 - value: 1 - other: a - - description: nil patch - original: - name: 1 - twoWay: - {} - modified: - name: 1 - current: - name: 1 - threeWay: - {} - result: - name: 1 - - description: add field to map - original: - name: 1 - twoWay: - value: 1 - modified: - name: 1 - value: 1 - current: - name: 1 - other: a - threeWay: - value: 1 - result: - name: 1 - value: 1 - other: a - - description: add field to map with conflict - original: - name: 1 - twoWay: - value: 1 - modified: - name: 1 - value: 1 - current: - name: a - other: a - threeWay: - name: 1 - value: 1 - result: - name: 1 - value: 1 - other: a - - description: add field and delete field from map - original: - name: 1 - twoWay: - name: null - value: 1 - modified: - value: 1 - current: - name: 1 - other: a - threeWay: - name: null - value: 1 - result: - value: 1 - other: a - - description: add field and delete field from map with conflict - original: - name: 1 - twoWay: - name: null - value: 1 - modified: - value: 1 - current: - name: a - other: a - threeWay: - name: null - value: 1 - result: - value: 1 - other: a - - description: delete field from nested map - original: - simpleMap: - key1: 1 - key2: 1 - twoWay: - simpleMap: - key2: null - modified: - simpleMap: - key1: 1 - current: - simpleMap: - key1: 1 - key2: 1 - other: a - threeWay: - simpleMap: - key2: null - result: - simpleMap: - key1: 1 - other: a - - description: delete field from nested map with conflict - original: - simpleMap: - key1: 1 - key2: 1 - twoWay: - simpleMap: - key2: null - modified: - simpleMap: - key1: 1 - current: - simpleMap: - key1: a - key2: 1 - other: a - threeWay: - simpleMap: - key1: 1 - key2: null - result: - simpleMap: - key1: 1 - other: a - - description: delete all fields from map - original: - name: 1 - value: 1 - twoWay: - name: null - value: null - modified: {} - current: - name: 1 - value: 1 - other: a - threeWay: - name: null - value: null - result: - other: a - - description: delete all fields from map with conflict - original: - name: 1 - value: 1 - twoWay: - name: null - value: null - modified: {} - current: - name: 1 - value: a - other: a - threeWay: - name: null - value: null - result: - other: a - - description: add field and delete all fields from map - original: - name: 1 - value: 1 - twoWay: - name: null - value: null - other: a - modified: - other: a - current: - name: 1 - value: 1 - other: a - threeWay: - name: null - value: null - result: - other: a - - description: add field and delete all fields from map with conflict - original: - name: 1 - value: 1 - twoWay: - name: null - value: null - other: a - modified: - other: a - current: - name: 1 - value: 1 - other: b - threeWay: - name: null - value: null - other: a - result: - other: a - - description: replace list of scalars - original: - nonMergingIntList: - - 1 - - 2 - twoWay: - nonMergingIntList: - - 2 - - 3 - modified: - nonMergingIntList: - - 2 - - 3 - current: - nonMergingIntList: - - 1 - - 2 - threeWay: - nonMergingIntList: - - 2 - - 3 - result: - nonMergingIntList: - - 2 - - 3 - - description: replace list of scalars with conflict - original: - nonMergingIntList: - - 1 - - 2 - twoWay: - nonMergingIntList: - - 2 - - 3 - modified: - nonMergingIntList: - - 2 - - 3 - current: - nonMergingIntList: - - 1 - - 4 - threeWay: - nonMergingIntList: - - 2 - - 3 - result: - nonMergingIntList: - - 2 - - 3 - - description: merge lists of scalars - original: - mergingIntList: - - 1 - - 2 - twoWay: - mergingIntList: - - 3 - modified: - mergingIntList: - - 1 - - 2 - - 3 - current: - mergingIntList: - - 1 - - 2 - - 4 - threeWay: - mergingIntList: - - 3 - result: - mergingIntList: - - 1 - - 2 - - 3 - - 4 - - description: merge lists of maps - original: - mergingList: - - name: 1 - - name: 2 - value: 2 - twoWay: - mergingList: - - name: 3 - value: 3 - - name: 4 - value: 4 - modified: - mergingList: - - name: 4 - value: 4 - - name: 1 - - name: 2 - value: 2 - - name: 3 - value: 3 - current: - mergingList: - - name: 1 - other: a - - name: 2 - value: 2 - other: b - threeWay: - mergingList: - - name: 3 - value: 3 - - name: 4 - value: 4 - result: - mergingList: - - name: 1 - other: a - - name: 2 - value: 2 - other: b - - name: 3 - value: 3 - - name: 4 - value: 4 - - description: merge lists of maps with conflict - original: - mergingList: - - name: 1 - - name: 2 - value: 2 - twoWay: - mergingList: - - name: 3 - value: 3 - modified: - mergingList: - - name: 1 - - name: 2 - value: 2 - - name: 3 - value: 3 - current: - mergingList: - - name: 1 - other: a - - name: 2 - value: 3 - other: b - threeWay: - mergingList: - - name: 2 - value: 2 - - name: 3 - value: 3 - result: - mergingList: - - name: 1 - other: a - - name: 2 - value: 2 - other: b - - name: 3 - value: 3 - - description: add field to map in merging list - original: - mergingList: - - name: 1 - - name: 2 - value: 2 - twoWay: - mergingList: - - name: 1 - value: 1 - modified: - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - current: - mergingList: - - name: 1 - other: a - - name: 2 - value: 2 - other: b - threeWay: - mergingList: - - name: 1 - value: 1 - result: - mergingList: - - name: 1 - value: 1 - other: a - - name: 2 - value: 2 - other: b - - description: add field to map in merging list with conflict - original: - mergingList: - - name: 1 - - name: 2 - value: 2 - twoWay: - mergingList: - - name: 1 - value: 1 - modified: - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - current: - mergingList: - - name: 1 - other: a - - name: 3 - value: 2 - other: b - threeWay: - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - result: - mergingList: - - name: 1 - value: 1 - other: a - - name: 2 - value: 2 - - name: 3 - value: 2 - other: b - - description: add duplicate field to map in merging list - original: - mergingList: - - name: 1 - - name: 2 - value: 2 - twoWay: - mergingList: - - name: 1 - value: 1 - modified: - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - current: - mergingList: - - name: 1 - value: 1 - other: a - - name: 2 - value: 2 - other: b - threeWay: - {} - result: - mergingList: - - name: 1 - value: 1 - other: a - - name: 2 - value: 2 - other: b - - description: add duplicate field to map in merging list with conflict - original: - mergingList: - - name: 1 - - name: 2 - value: 2 - twoWay: - mergingList: - - name: 1 - value: 1 - modified: - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - current: - mergingList: - - name: 1 - value: 1 - other: a - - name: 2 - value: 3 - other: b - threeWay: - mergingList: - - name: 2 - value: 2 - result: - mergingList: - - name: 1 - value: 1 - other: a - - name: 2 - value: 2 - other: b - - description: replace map field value in merging list - original: - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - twoWay: - mergingList: - - name: 1 - value: a - modified: - mergingList: - - name: 1 - value: a - - name: 2 - value: 2 - current: - mergingList: - - name: 1 - value: 1 - other: a - - name: 2 - value: 2 - other: b - threeWay: - mergingList: - - name: 1 - value: a - result: - mergingList: - - name: 1 - value: a - other: a - - name: 2 - value: 2 - other: b - - description: replace map field value in merging list with conflict - original: - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - twoWay: - mergingList: - - name: 1 - value: a - modified: - mergingList: - - name: 1 - value: a - - name: 2 - value: 2 - current: - mergingList: - - name: 1 - value: 3 - other: a - - name: 2 - value: 2 - other: b - threeWay: - mergingList: - - name: 1 - value: a - result: - mergingList: - - name: 1 - value: a - other: a - - name: 2 - value: 2 - other: b - - description: delete map from merging list - original: - mergingList: - - name: 1 - - name: 2 - twoWay: - mergingList: - - name: 1 - $patch: delete - modified: - mergingList: - - name: 2 - current: - mergingList: - - name: 1 - - name: 2 - other: b - threeWay: - mergingList: - - name: 1 - $patch: delete - result: - mergingList: - - name: 2 - other: b - - description: delete map from merging list with conflict - original: - mergingList: - - name: 1 - - name: 2 - twoWay: - mergingList: - - name: 1 - $patch: delete - modified: - mergingList: - - name: 2 - current: - mergingList: - - name: 1 - other: a - - name: 2 - other: b - threeWay: - mergingList: - - name: 1 - $patch: delete - result: - mergingList: - - name: 2 - other: b - - description: delete missing map from merging list - original: - mergingList: - - name: 1 - - name: 2 - twoWay: - mergingList: - - name: 1 - $patch: delete - modified: - mergingList: - - name: 2 - current: - mergingList: - - name: 2 - other: b - threeWay: - mergingList: - - name: 1 - $patch: delete - result: - mergingList: - - name: 2 - other: b - - description: delete missing map from merging list with conflict - original: - mergingList: - - name: 1 - - name: 2 - twoWay: - mergingList: - - name: 1 - $patch: delete - modified: - mergingList: - - name: 2 - current: - mergingList: - - name: 3 - other: a - threeWay: - mergingList: - - name: 1 - $patch: delete - - name: 2 - result: - mergingList: - - name: 2 - - name: 3 - other: a - - description: add map and delete map from merging list - original: - merginglist: - - name: 1 - - name: 2 - twoWay: - merginglist: - - name: 1 - $patch: delete - - name: 3 - modified: - merginglist: - - name: 2 - - name: 3 - current: - merginglist: - - name: 1 - - name: 2 - other: b - - name: 4 - other: c - threeWay: - merginglist: - - name: 1 - $patch: delete - - name: 3 - result: - merginglist: - - name: 2 - other: b - - name: 3 - - name: 4 - other: c - - description: add map and delete map from merging list with conflict - original: - merginglist: - - name: 1 - - name: 2 - twoWay: - merginglist: - - name: 1 - $patch: delete - - name: 3 - modified: - merginglist: - - name: 2 - - name: 3 - current: - merginglist: - - name: 1 - other: a - - name: 4 - other: c - threeWay: - merginglist: - - name: 1 - $patch: delete - - name: 2 - - name: 3 - result: - merginglist: - - name: 2 - - name: 3 - - name: 4 - other: c - - description: delete all maps from merging list - original: - mergingList: - - name: 1 - - name: 2 - twoWay: - mergingList: - - name: 1 - $patch: delete - - name: 2 - $patch: delete - modified: - mergingList: [] - current: - mergingList: - - name: 1 - - name: 2 - threeWay: - mergingList: - - name: 1 - $patch: delete - - name: 2 - $patch: delete - result: - mergingList: [] - - description: delete all maps from merging list with conflict - original: - mergingList: - - name: 1 - - name: 2 - twoWay: - mergingList: - - name: 1 - $patch: delete - - name: 2 - $patch: delete - modified: - mergingList: [] - current: - mergingList: - - name: 1 - other: a - - name: 2 - other: b - threeWay: - mergingList: - - name: 1 - $patch: delete - - name: 2 - $patch: delete - result: - mergingList: [] - - description: delete all maps from empty merging list - original: - mergingList: - - name: 1 - - name: 2 - twoWay: - mergingList: - - name: 1 - $patch: delete - - name: 2 - $patch: delete - modified: - mergingList: [] - current: - mergingList: [] - threeWay: - mergingList: - - name: 1 - $patch: delete - - name: 2 - $patch: delete - result: - mergingList: [] - - description: delete field from map in merging list - original: - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - twoWay: - mergingList: - - name: 1 - value: null - modified: - mergingList: - - name: 1 - - name: 2 - value: 2 - current: - mergingList: - - name: 1 - value: 1 - other: a - - name: 2 - value: 2 - other: b - threeWay: - mergingList: - - name: 1 - value: null - result: - mergingList: - - name: 1 - other: a - - name: 2 - value: 2 - other: b - - description: delete field from map in merging list with conflict - original: - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - twoWay: - mergingList: - - name: 1 - value: null - modified: - mergingList: - - name: 1 - - name: 2 - value: 2 - current: - mergingList: - - name: 1 - value: a - other: a - - name: 2 - value: 2 - threeWay: - mergingList: - - name: 1 - value: null - result: - mergingList: - - name: 1 - other: a - - name: 2 - value: 2 - - description: delete missing field from map in merging list - original: - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - twoWay: - mergingList: - - name: 1 - value: null - modified: - mergingList: - - name: 1 - - name: 2 - value: 2 - current: - mergingList: - - name: 1 - other: a - - name: 2 - value: 2 - other: b - threeWay: - mergingList: - - name: 1 - value: null - result: - mergingList: - - name: 1 - other: a - - name: 2 - value: 2 - other: b - - description: delete missing field from map in merging list with conflict - original: - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - twoWay: - mergingList: - - name: 1 - value: null - modified: - mergingList: - - name: 1 - - name: 2 - value: 2 - current: - mergingList: - - name: 1 - other: a - - name: 2 - other: b - threeWay: - mergingList: - - name: 1 - value: null - - name: 2 - value: 2 - result: - mergingList: - - name: 1 - other: a - - name: 2 - value: 2 - other: b - - description: replace non merging list nested in merging list - original: - mergingList: - - name: 1 - nonMergingList: - - name: 1 - - name: 2 - value: 2 - - name: 2 - twoWay: - mergingList: - - name: 1 - nonMergingList: - - name: 1 - value: 1 - modified: - mergingList: - - name: 1 - nonMergingList: - - name: 1 - value: 1 - - name: 2 - current: - mergingList: - - name: 1 - other: a - nonMergingList: - - name: 1 - - name: 2 - value: 2 - - name: 2 - other: b - threeWay: - mergingList: - - name: 1 - nonMergingList: - - name: 1 - value: 1 - result: - mergingList: - - name: 1 - other: a - nonMergingList: - - name: 1 - value: 1 - - name: 2 - other: b - - description: replace non merging list nested in merging list with value conflict - original: - mergingList: - - name: 1 - nonMergingList: - - name: 1 - - name: 2 - value: 2 - - name: 2 - twoWay: - mergingList: - - name: 1 - nonMergingList: - - name: 1 - value: 1 - modified: - mergingList: - - name: 1 - nonMergingList: - - name: 1 - value: 1 - - name: 2 - current: - mergingList: - - name: 1 - other: a - nonMergingList: - - name: 1 - value: c - - name: 2 - other: b - threeWay: - mergingList: - - name: 1 - nonMergingList: - - name: 1 - value: 1 - result: - mergingList: - - name: 1 - other: a - nonMergingList: - - name: 1 - value: 1 - - name: 2 - other: b - - description: replace non merging list nested in merging list with deletion conflict - original: - mergingList: - - name: 1 - nonMergingList: - - name: 1 - - name: 2 - value: 2 - - name: 2 - twoWay: - mergingList: - - name: 1 - nonMergingList: - - name: 1 - value: 1 - modified: - mergingList: - - name: 1 - nonMergingList: - - name: 1 - value: 1 - - name: 2 - current: - mergingList: - - name: 1 - other: a - nonMergingList: - - name: 2 - value: 2 - - name: 2 - other: b - threeWay: - mergingList: - - name: 1 - nonMergingList: - - name: 1 - value: 1 - result: - mergingList: - - name: 1 - other: a - nonMergingList: - - name: 1 - value: 1 - - name: 2 - other: b - - description: add field to map in merging list nested in merging list - original: - mergingList: - - name: 1 - mergingList: - - name: 1 - - name: 2 - value: 2 - - name: 2 - twoWay: - mergingList: - - name: 1 - mergingList: - - name: 1 - value: 1 - modified: - mergingList: - - name: 1 - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - - name: 2 - current: - mergingList: - - name: 1 - other: a - mergingList: - - name: 1 - - name: 2 - value: 2 - - name: 2 - other: b - threeWay: - mergingList: - - name: 1 - mergingList: - - name: 1 - value: 1 - result: - mergingList: - - name: 1 - other: a - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - - name: 2 - other: b - - description: add field to map in merging list nested in merging list with value conflict - original: - mergingList: - - name: 1 - mergingList: - - name: 1 - - name: 2 - value: 2 - - name: 2 - twoWay: - mergingList: - - name: 1 - mergingList: - - name: 1 - value: 1 - modified: - mergingList: - - name: 1 - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - - name: 2 - current: - mergingList: - - name: 1 - other: a - mergingList: - - name: 1 - value: a - other: c - - name: 2 - value: b - other: d - - name: 2 - other: b - threeWay: - mergingList: - - name: 1 - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - result: - mergingList: - - name: 1 - other: a - mergingList: - - name: 1 - value: 1 - other: c - - name: 2 - value: 2 - other: d - - name: 2 - other: b - - description: add field to map in merging list nested in merging list with deletion conflict - original: - mergingList: - - name: 1 - mergingList: - - name: 1 - - name: 2 - value: 2 - - name: 2 - twoWay: - mergingList: - - name: 1 - mergingList: - - name: 1 - value: 1 - modified: - mergingList: - - name: 1 - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - - name: 2 - current: - mergingList: - - name: 1 - other: a - mergingList: - - name: 2 - value: 2 - other: d - - name: 2 - other: b - threeWay: - mergingList: - - name: 1 - mergingList: - - name: 1 - value: 1 - result: - mergingList: - - name: 1 - other: a - mergingList: - - name: 1 - value: 1 - - name: 2 - value: 2 - other: d - - name: 2 - other: b - - description: merge empty merging lists - original: - mergingList: [] - twoWay: - {} - modified: - mergingList: [] - current: - mergingList: [] - threeWay: - {} - result: - mergingList: [] - - description: add map to merging list by pointer - original: - mergeItemPtr: - - name: 1 - twoWay: - mergeItemPtr: - - name: 2 - modified: - mergeItemPtr: - - name: 1 - - name: 2 - current: - mergeItemPtr: - - name: 1 - other: a - - name: 3 - threeWay: - mergeItemPtr: - - name: 2 - result: - mergeItemPtr: - - name: 1 - other: a - - name: 2 - - name: 3 - - description: add map to merging list by pointer with conflict - original: - mergeItemPtr: - - name: 1 - twoWay: - mergeItemPtr: - - name: 2 - modified: - mergeItemPtr: - - name: 1 - - name: 2 - current: - mergeItemPtr: - - name: 3 - threeWay: - mergeItemPtr: - - name: 1 - - name: 2 - result: - mergeItemPtr: - - name: 1 - - name: 2 - - name: 3 - - description: add field to map in merging list by pointer - original: - mergeItemPtr: - - name: 1 - mergeItemPtr: - - name: 1 - - name: 2 - value: 2 - - name: 2 - twoWay: - mergeItemPtr: - - name: 1 - mergeItemPtr: - - name: 1 - value: 1 - modified: - mergeItemPtr: - - name: 1 - mergeItemPtr: - - name: 1 - value: 1 - - name: 2 - value: 2 - - name: 2 - current: - mergeItemPtr: - - name: 1 - other: a - mergeItemPtr: - - name: 1 - other: a - - name: 2 - value: 2 - other: b - - name: 2 - other: b - threeWay: - mergeItemPtr: - - name: 1 - mergeItemPtr: - - name: 1 - value: 1 - result: - mergeItemPtr: - - name: 1 - other: a - mergeItemPtr: - - name: 1 - value: 1 - other: a - - name: 2 - value: 2 - other: b - - name: 2 - other: b - - description: add field to map in merging list by pointer with conflict - original: - mergeItemPtr: - - name: 1 - mergeItemPtr: - - name: 1 - - name: 2 - value: 2 - - name: 2 - twoWay: - mergeItemPtr: - - name: 1 - mergeItemPtr: - - name: 1 - value: 1 - modified: - mergeItemPtr: - - name: 1 - mergeItemPtr: - - name: 1 - value: 1 - - name: 2 - value: 2 - - name: 2 - current: - mergeItemPtr: - - name: 1 - other: a - mergeItemPtr: - - name: 1 - value: a - - name: 2 - value: 2 - other: b - - name: 2 - other: b - threeWay: - mergeItemPtr: - - name: 1 - mergeItemPtr: - - name: 1 - value: 1 - result: - mergeItemPtr: - - name: 1 - other: a - mergeItemPtr: - - name: 1 - value: 1 - - name: 2 - value: 2 - other: b - - name: 2 - other: b -`) +var customStrategicMergePatchTestCaseData = StrategicMergePatchTestCases{ + TestCases: []StrategicMergePatchTestCase{ + { + Description: "unique scalars when merging lists using SMPatchVersion_1_0", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingIntList: + - 1 + - 2 +`), + TwoWay: []byte(` +mergingIntList: + - 2 + - 3 +`), + Modified: []byte(` +mergingIntList: + - 1 + - 2 + - 3 +`), + }, + }, + { + Description: "unique scalars when merging lists for list of primitives", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingIntList: + - 1 + - 2 +`), + TwoWay: []byte(` +mergingIntList: + $patch: mergeprimitiveslist + "2": 2 + "3": 3 +`), + Modified: []byte(` +mergingIntList: + - 1 + - 2 + - 3 +`), + }, + }, + { + Description: "delete map from nested map", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +simpleMap: + key1: 1 + key2: 1 +`), + TwoWay: []byte(` +simpleMap: + $patch: delete +`), + Modified: []byte(` +simpleMap: + {} +`), + }, + }, + { + Description: "delete all items from merging list", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - $patch: replace +`), + Modified: []byte(` +mergingList: [] +`), + }, + }, + { + Description: "merge empty merging lists", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: [] +`), + TwoWay: []byte(` +mergingList: [] +`), + Modified: []byte(` +mergingList: [] +`), + }, + }, + { + Description: "delete all keys from map", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +name: 1 +value: 1 +`), + TwoWay: []byte(` +$patch: replace +`), + Modified: []byte(` +{} +`), + }, + }, + { + Description: "add key and delete all keys from map", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +name: 1 +value: 1 +`), + TwoWay: []byte(` +other: a +$patch: replace +`), + Modified: []byte(` +other: a +`), + }, + }, + }, +} + +func TestCustomStrategicMergePatch(t *testing.T) { + for _, c := range customStrategicMergePatchTestCaseData.TestCases { + originalJSON := yamlToJSONOrError(c.Original) + twoWayJSON := yamlToJSONOrError(c.TwoWay) + modifiedJSON := yamlToJSONOrError(c.Modified) + testPatchApplication(t, originalJSON, twoWayJSON, modifiedJSON, c.Description) + } +} + +// These are test cases for StrategicMergePatch, to assert that applying a patch +// yields the correct outcome. They are also test cases for CreateTwoWayMergePatch +// and CreateThreeWayMergePatch, to assert that they both generate the correct patch +// for the given set of input documents. +var createStrategicMergePatchTestCaseData = StrategicMergePatchTestCases{ + TestCases: []StrategicMergePatchTestCase{ + { + Description: "nil original", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + TwoWay: []byte(` +name: 1 +value: 1 +`), + Modified: []byte(` +name: 1 +value: 1 +`), + Current: []byte(` +name: 1 +other: a +`), + ThreeWay: []byte(` +value: 1 +`), + Result: []byte(` +name: 1 +value: 1 +other: a +`), + }, + }, + { + Description: "nil patch", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +name: 1 +`), + TwoWay: []byte(` +{} +`), + Modified: []byte(` +name: 1 +`), + Current: []byte(` +name: 1 +`), + ThreeWay: []byte(` +{} +`), + Result: []byte(` +name: 1 +`), + }, + }, + { + Description: "add field to map", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +name: 1 +`), + TwoWay: []byte(` +value: 1 +`), + Modified: []byte(` +name: 1 +value: 1 +`), + Current: []byte(` +name: 1 +other: a +`), + ThreeWay: []byte(` +value: 1 +`), + Result: []byte(` +name: 1 +value: 1 +other: a +`), + }, + }, + { + Description: "add field to map with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +name: 1 +`), + TwoWay: []byte(` +value: 1 +`), + Modified: []byte(` +name: 1 +value: 1 +`), + Current: []byte(` +name: a +other: a +`), + ThreeWay: []byte(` +name: 1 +value: 1 +`), + Result: []byte(` +name: 1 +value: 1 +other: a +`), + }, + }, + { + Description: "add field and delete field from map", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +name: 1 +`), + TwoWay: []byte(` +name: null +value: 1 +`), + Modified: []byte(` +value: 1 +`), + Current: []byte(` +name: 1 +other: a +`), + ThreeWay: []byte(` +name: null +value: 1 +`), + Result: []byte(` +value: 1 +other: a +`), + }, + }, + { + Description: "add field and delete field from map with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +name: 1 +`), + TwoWay: []byte(` +name: null +value: 1 +`), + Modified: []byte(` +value: 1 +`), + Current: []byte(` +name: a +other: a +`), + ThreeWay: []byte(` +name: null +value: 1 +`), + Result: []byte(` +value: 1 +other: a +`), + }, + }, + { + Description: "delete field from nested map", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +simpleMap: + key1: 1 + key2: 1 +`), + TwoWay: []byte(` +simpleMap: + key2: null +`), + Modified: []byte(` +simpleMap: + key1: 1 +`), + Current: []byte(` +simpleMap: + key1: 1 + key2: 1 + other: a +`), + ThreeWay: []byte(` +simpleMap: + key2: null +`), + Result: []byte(` +simpleMap: + key1: 1 + other: a +`), + }, + }, + { + Description: "delete field from nested map with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +simpleMap: + key1: 1 + key2: 1 +`), + TwoWay: []byte(` +simpleMap: + key2: null +`), + Modified: []byte(` +simpleMap: + key1: 1 +`), + Current: []byte(` +simpleMap: + key1: a + key2: 1 + other: a +`), + ThreeWay: []byte(` +simpleMap: + key1: 1 + key2: null +`), + Result: []byte(` +simpleMap: + key1: 1 + other: a +`), + }, + }, + { + Description: "delete all fields from map", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +name: 1 +value: 1 +`), + TwoWay: []byte(` +name: null +value: null +`), + Modified: []byte(` +{} +`), + Current: []byte(` +name: 1 +value: 1 +other: a +`), + ThreeWay: []byte(` +name: null +value: null +`), + Result: []byte(` +other: a +`), + }, + }, + { + Description: "delete all fields from map with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +name: 1 +value: 1 +`), + TwoWay: []byte(` +name: null +value: null +`), + Modified: []byte(` +{} +`), + Current: []byte(` +name: 1 +value: a +other: a +`), + ThreeWay: []byte(` +name: null +value: null +`), + Result: []byte(` +other: a +`), + }, + }, + { + Description: "add field and delete all fields from map", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +name: 1 +value: 1 +`), + TwoWay: []byte(` +name: null +value: null +other: a +`), + Modified: []byte(` +other: a +`), + Current: []byte(` +name: 1 +value: 1 +other: a +`), + ThreeWay: []byte(` +name: null +value: null +`), + Result: []byte(` +other: a +`), + }, + }, + { + Description: "add field and delete all fields from map with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +name: 1 +value: 1 +`), + TwoWay: []byte(` +name: null +value: null +other: a +`), + Modified: []byte(` +other: a +`), + Current: []byte(` +name: 1 +value: 1 +other: b +`), + ThreeWay: []byte(` +name: null +value: null +other: a +`), + Result: []byte(` +other: a +`), + }, + }, + { + Description: "replace list of scalars", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +nonMergingIntList: + - 1 + - 2 +`), + TwoWay: []byte(` +nonMergingIntList: + - 2 + - 3 +`), + Modified: []byte(` +nonMergingIntList: + - 2 + - 3 +`), + Current: []byte(` +nonMergingIntList: + - 1 + - 2 +`), + ThreeWay: []byte(` +nonMergingIntList: + - 2 + - 3 +`), + Result: []byte(` +nonMergingIntList: + - 2 + - 3 +`), + }, + }, + { + Description: "replace list of scalars with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +nonMergingIntList: + - 1 + - 2 +`), + TwoWay: []byte(` +nonMergingIntList: + - 2 + - 3 +`), + Modified: []byte(` +nonMergingIntList: + - 2 + - 3 +`), + Current: []byte(` +nonMergingIntList: + - 1 + - 4 +`), + ThreeWay: []byte(` +nonMergingIntList: + - 2 + - 3 +`), + Result: []byte(` +nonMergingIntList: + - 2 + - 3 +`), + }, + }, + { + Description: "merge lists of scalars using SMPatchVersion_1_0", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingIntList: + - 1 + - 2 +`), + TwoWay: []byte(` +mergingIntList: + - 3 +`), + Modified: []byte(` +mergingIntList: + - 1 + - 2 + - 3 +`), + Current: []byte(` +mergingIntList: + - 1 + - 2 + - 4 +`), + ThreeWay: []byte(` +mergingIntList: + - 3 +`), + Result: []byte(` +mergingIntList: + - 1 + - 2 + - 3 + - 4 +`), + }, + }, + { + Description: "merge lists of scalars for list of primitives", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingIntList: + - 1 + - 2 +`), + TwoWay: []byte(` +mergingIntList: + $patch: mergeprimitiveslist + "1": null + "3": 3 +`), + Modified: []byte(` +mergingIntList: + - 2 + - 3 +`), + Current: []byte(` +mergingIntList: + - 1 + - 2 + - 4 +`), + ThreeWay: []byte(` +mergingIntList: + $patch: mergeprimitiveslist + "1": null + "3": 3 +`), + Result: []byte(` +mergingIntList: + - 2 + - 3 + - 4 +`), + }, + }, + { + Description: "another merge lists of scalars for list of primitives", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingIntList: + - 1 + - 2 +`), + TwoWay: []byte(` +mergingIntList: + $patch: mergeprimitiveslist + "1": null + "3": 3 +`), + Modified: []byte(` +mergingIntList: + - 2 + - 3 +`), + Current: []byte(` +mergingIntList: + - 2 + - 4 +`), + ThreeWay: []byte(` +mergingIntList: + $patch: mergeprimitiveslist + "1": null + "3": 3 +`), + Result: []byte(` +mergingIntList: + - 2 + - 3 + - 4 +`), + }, + }, + { + Description: "merge lists of maps", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 + value: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 3 + value: 3 + - name: 4 + value: 4 +`), + Modified: []byte(` +mergingList: + - name: 1 + - name: 2 + value: 2 + - name: 3 + value: 3 + - name: 4 + value: 4 +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + - name: 2 + value: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 3 + value: 3 + - name: 4 + value: 4 +`), + Result: []byte(` +mergingList: + - name: 1 + other: a + - name: 2 + value: 2 + other: b + - name: 3 + value: 3 + - name: 4 + value: 4 +`), + }, + }, + { + Description: "merge lists of maps with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 + value: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 3 + value: 3 +`), + Modified: []byte(` +mergingList: + - name: 1 + - name: 2 + value: 2 + - name: 3 + value: 3 +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + - name: 2 + value: 3 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 2 + value: 2 + - name: 3 + value: 3 +`), + Result: []byte(` +mergingList: + - name: 1 + other: a + - name: 2 + value: 2 + other: b + - name: 3 + value: 3 +`), + }, + }, + { + Description: "add field to map in merging list", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 + value: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + value: 1 +`), + Modified: []byte(` +mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + - name: 2 + value: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + value: 1 +`), + Result: []byte(` +mergingList: + - name: 1 + value: 1 + other: a + - name: 2 + value: 2 + other: b +`), + }, + }, + { + Description: "add field to map in merging list with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 + value: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + value: 1 +`), + Modified: []byte(` +mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + - name: 3 + value: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 +`), + Result: []byte(` +mergingList: + - name: 1 + value: 1 + other: a + - name: 2 + value: 2 + - name: 3 + value: 2 + other: b +`), + }, + }, + { + Description: "add duplicate field to map in merging list", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 + value: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + value: 1 +`), + Modified: []byte(` +mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + value: 1 + other: a + - name: 2 + value: 2 + other: b +`), + ThreeWay: []byte(` +{} +`), + Result: []byte(` +mergingList: + - name: 1 + value: 1 + other: a + - name: 2 + value: 2 + other: b +`), + }, + }, + { + Description: "add duplicate field to map in merging list with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 + value: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + value: 1 +`), + Modified: []byte(` +mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + value: 1 + other: a + - name: 2 + value: 3 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 2 + value: 2 +`), + Result: []byte(` +mergingList: + - name: 1 + value: 1 + other: a + - name: 2 + value: 2 + other: b +`), + }, + }, + { + Description: "replace map field value in merging list", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + value: a +`), + Modified: []byte(` +mergingList: + - name: 1 + value: a + - name: 2 + value: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + value: 1 + other: a + - name: 2 + value: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + value: a +`), + Result: []byte(` +mergingList: + - name: 1 + value: a + other: a + - name: 2 + value: 2 + other: b +`), + }, + }, + { + Description: "replace map field value in merging list with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + value: a +`), + Modified: []byte(` +mergingList: + - name: 1 + value: a + - name: 2 + value: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + value: 3 + other: a + - name: 2 + value: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + value: a +`), + Result: []byte(` +mergingList: + - name: 1 + value: a + other: a + - name: 2 + value: 2 + other: b +`), + }, + }, + { + Description: "delete map from merging list", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + $patch: delete +`), + Modified: []byte(` +mergingList: + - name: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + - name: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + $patch: delete +`), + Result: []byte(` +mergingList: + - name: 2 + other: b +`), + }, + }, + { + Description: "delete map from merging list with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + $patch: delete +`), + Modified: []byte(` +mergingList: + - name: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + - name: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + $patch: delete +`), + Result: []byte(` +mergingList: + - name: 2 + other: b +`), + }, + }, + { + Description: "delete missing map from merging list", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + $patch: delete +`), + Modified: []byte(` +mergingList: + - name: 2 +`), + Current: []byte(` +mergingList: + - name: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + $patch: delete +`), + Result: []byte(` +mergingList: + - name: 2 + other: b +`), + }, + }, + { + Description: "delete missing map from merging list with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + $patch: delete +`), + Modified: []byte(` +mergingList: + - name: 2 +`), + Current: []byte(` +mergingList: + - name: 3 + other: a +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + $patch: delete + - name: 2 +`), + Result: []byte(` +mergingList: + - name: 2 + - name: 3 + other: a +`), + }, + }, + { + Description: "add map and delete map from merging list", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + $patch: delete + - name: 3 +`), + Modified: []byte(` +mergingList: + - name: 2 + - name: 3 +`), + Current: []byte(` +mergingList: + - name: 1 + - name: 2 + other: b + - name: 4 + other: c +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + $patch: delete + - name: 3 +`), + Result: []byte(` +mergingList: + - name: 2 + other: b + - name: 3 + - name: 4 + other: c +`), + }, + }, + { + Description: "add map and delete map from merging list with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + $patch: delete + - name: 3 +`), + Modified: []byte(` +mergingList: + - name: 2 + - name: 3 +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + - name: 4 + other: c +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + $patch: delete + - name: 2 + - name: 3 +`), + Result: []byte(` +mergingList: + - name: 2 + - name: 3 + - name: 4 + other: c +`), + }, + }, + { + Description: "delete all maps from merging list", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + $patch: delete + - name: 2 + $patch: delete +`), + Modified: []byte(` +mergingList: [] +`), + Current: []byte(` +mergingList: + - name: 1 + - name: 2 +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + $patch: delete + - name: 2 + $patch: delete +`), + Result: []byte(` +mergingList: [] +`), + }, + }, + { + Description: "delete all maps from merging list with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + $patch: delete + - name: 2 + $patch: delete +`), + Modified: []byte(` +mergingList: [] +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + - name: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + $patch: delete + - name: 2 + $patch: delete +`), + Result: []byte(` +mergingList: [] +`), + }, + }, + { + Description: "delete all maps from empty merging list", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + $patch: delete + - name: 2 + $patch: delete +`), + Modified: []byte(` +mergingList: [] +`), + Current: []byte(` +mergingList: [] +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + $patch: delete + - name: 2 + $patch: delete +`), + Result: []byte(` +mergingList: [] +`), + }, + }, + { + Description: "delete field from map in merging list", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + value: null +`), + Modified: []byte(` +mergingList: + - name: 1 + - name: 2 + value: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + value: 1 + other: a + - name: 2 + value: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + value: null +`), + Result: []byte(` +mergingList: + - name: 1 + other: a + - name: 2 + value: 2 + other: b +`), + }, + }, + { + Description: "delete field from map in merging list with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + value: null +`), + Modified: []byte(` +mergingList: + - name: 1 + - name: 2 + value: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + value: a + other: a + - name: 2 + value: 2 +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + value: null +`), + Result: []byte(` +mergingList: + - name: 1 + other: a + - name: 2 + value: 2 +`), + }, + }, + { + Description: "delete missing field from map in merging list", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + value: null +`), + Modified: []byte(` +mergingList: + - name: 1 + - name: 2 + value: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + - name: 2 + value: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + value: null +`), + Result: []byte(` +mergingList: + - name: 1 + other: a + - name: 2 + value: 2 + other: b +`), + }, + }, + { + Description: "delete missing field from map in merging list with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + value: null +`), + Modified: []byte(` +mergingList: + - name: 1 + - name: 2 + value: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + - name: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + value: null + - name: 2 + value: 2 +`), + Result: []byte(` +mergingList: + - name: 1 + other: a + - name: 2 + value: 2 + other: b +`), + }, + }, + { + Description: "replace non merging list nested in merging list", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + nonMergingList: + - name: 1 + - name: 2 + value: 2 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + nonMergingList: + - name: 1 + value: 1 +`), + Modified: []byte(` +mergingList: + - name: 1 + nonMergingList: + - name: 1 + value: 1 + - name: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + nonMergingList: + - name: 1 + - name: 2 + value: 2 + - name: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + nonMergingList: + - name: 1 + value: 1 +`), + Result: []byte(` +mergingList: + - name: 1 + other: a + nonMergingList: + - name: 1 + value: 1 + - name: 2 + other: b +`), + }, + }, + { + Description: "replace non merging list nested in merging list with value conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + nonMergingList: + - name: 1 + - name: 2 + value: 2 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + nonMergingList: + - name: 1 + value: 1 +`), + Modified: []byte(` +mergingList: + - name: 1 + nonMergingList: + - name: 1 + value: 1 + - name: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + nonMergingList: + - name: 1 + value: c + - name: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + nonMergingList: + - name: 1 + value: 1 +`), + Result: []byte(` +mergingList: + - name: 1 + other: a + nonMergingList: + - name: 1 + value: 1 + - name: 2 + other: b +`), + }, + }, + { + Description: "replace non merging list nested in merging list with deletion conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + nonMergingList: + - name: 1 + - name: 2 + value: 2 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + nonMergingList: + - name: 1 + value: 1 +`), + Modified: []byte(` +mergingList: + - name: 1 + nonMergingList: + - name: 1 + value: 1 + - name: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + nonMergingList: + - name: 2 + value: 2 + - name: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + nonMergingList: + - name: 1 + value: 1 +`), + Result: []byte(` +mergingList: + - name: 1 + other: a + nonMergingList: + - name: 1 + value: 1 + - name: 2 + other: b +`), + }, + }, + { + Description: "add field to map in merging list nested in merging list", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + mergingList: + - name: 1 + - name: 2 + value: 2 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + mergingList: + - name: 1 + value: 1 +`), + Modified: []byte(` +mergingList: + - name: 1 + mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 + - name: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + mergingList: + - name: 1 + - name: 2 + value: 2 + - name: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + mergingList: + - name: 1 + value: 1 +`), + Result: []byte(` +mergingList: + - name: 1 + other: a + mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 + - name: 2 + other: b +`), + }, + }, + { + Description: "add field to map in merging list nested in merging list with value conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + mergingList: + - name: 1 + - name: 2 + value: 2 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + mergingList: + - name: 1 + value: 1 +`), + Modified: []byte(` +mergingList: + - name: 1 + mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 + - name: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + mergingList: + - name: 1 + value: a + other: c + - name: 2 + value: b + other: d + - name: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 +`), + Result: []byte(` +mergingList: + - name: 1 + other: a + mergingList: + - name: 1 + value: 1 + other: c + - name: 2 + value: 2 + other: d + - name: 2 + other: b +`), + }, + }, + { + Description: "add field to map in merging list nested in merging list with deletion conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: + - name: 1 + mergingList: + - name: 1 + - name: 2 + value: 2 + - name: 2 +`), + TwoWay: []byte(` +mergingList: + - name: 1 + mergingList: + - name: 1 + value: 1 +`), + Modified: []byte(` +mergingList: + - name: 1 + mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 + - name: 2 +`), + Current: []byte(` +mergingList: + - name: 1 + other: a + mergingList: + - name: 2 + value: 2 + other: d + - name: 2 + other: b +`), + ThreeWay: []byte(` +mergingList: + - name: 1 + mergingList: + - name: 1 + value: 1 +`), + Result: []byte(` +mergingList: + - name: 1 + other: a + mergingList: + - name: 1 + value: 1 + - name: 2 + value: 2 + other: d + - name: 2 + other: b +`), + }, + }, + { + Description: "merge empty merging lists", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergingList: [] +`), + TwoWay: []byte(` +{} +`), + Modified: []byte(` +mergingList: [] +`), + Current: []byte(` +mergingList: [] +`), + ThreeWay: []byte(` +{} +`), + Result: []byte(` +mergingList: [] +`), + }, + }, + { + Description: "add map to merging list by pointer", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergeItemPtr: + - name: 1 +`), + TwoWay: []byte(` +mergeItemPtr: + - name: 2 +`), + Modified: []byte(` +mergeItemPtr: + - name: 1 + - name: 2 +`), + Current: []byte(` +mergeItemPtr: + - name: 1 + other: a + - name: 3 +`), + ThreeWay: []byte(` +mergeItemPtr: + - name: 2 +`), + Result: []byte(` +mergeItemPtr: + - name: 1 + other: a + - name: 2 + - name: 3 +`), + }, + }, + { + Description: "add map to merging list by pointer with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergeItemPtr: + - name: 1 +`), + TwoWay: []byte(` +mergeItemPtr: + - name: 2 +`), + Modified: []byte(` +mergeItemPtr: + - name: 1 + - name: 2 +`), + Current: []byte(` +mergeItemPtr: + - name: 3 +`), + ThreeWay: []byte(` +mergeItemPtr: + - name: 1 + - name: 2 +`), + Result: []byte(` +mergeItemPtr: + - name: 1 + - name: 2 + - name: 3 +`), + }, + }, + { + Description: "add field to map in merging list by pointer", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergeItemPtr: + - name: 1 + mergeItemPtr: + - name: 1 + - name: 2 + value: 2 + - name: 2 +`), + TwoWay: []byte(` +mergeItemPtr: + - name: 1 + mergeItemPtr: + - name: 1 + value: 1 +`), + Modified: []byte(` +mergeItemPtr: + - name: 1 + mergeItemPtr: + - name: 1 + value: 1 + - name: 2 + value: 2 + - name: 2 +`), + Current: []byte(` +mergeItemPtr: + - name: 1 + other: a + mergeItemPtr: + - name: 1 + other: a + - name: 2 + value: 2 + other: b + - name: 2 + other: b +`), + ThreeWay: []byte(` +mergeItemPtr: + - name: 1 + mergeItemPtr: + - name: 1 + value: 1 +`), + Result: []byte(` +mergeItemPtr: + - name: 1 + other: a + mergeItemPtr: + - name: 1 + value: 1 + other: a + - name: 2 + value: 2 + other: b + - name: 2 + other: b +`), + }, + }, + { + Description: "add field to map in merging list by pointer with conflict", + StrategicMergePatchTestCaseData: StrategicMergePatchTestCaseData{ + Original: []byte(` +mergeItemPtr: + - name: 1 + mergeItemPtr: + - name: 1 + - name: 2 + value: 2 + - name: 2 +`), + TwoWay: []byte(` +mergeItemPtr: + - name: 1 + mergeItemPtr: + - name: 1 + value: 1 +`), + Modified: []byte(` +mergeItemPtr: + - name: 1 + mergeItemPtr: + - name: 1 + value: 1 + - name: 2 + value: 2 + - name: 2 +`), + Current: []byte(` +mergeItemPtr: + - name: 1 + other: a + mergeItemPtr: + - name: 1 + value: a + - name: 2 + value: 2 + other: b + - name: 2 + other: b +`), + ThreeWay: []byte(` +mergeItemPtr: + - name: 1 + mergeItemPtr: + - name: 1 + value: 1 +`), + Result: []byte(` +mergeItemPtr: + - name: 1 + other: a + mergeItemPtr: + - name: 1 + value: 1 + - name: 2 + value: 2 + other: b + - name: 2 + other: b +`), + }, + }, + }, +} func TestStrategicMergePatch(t *testing.T) { testStrategicMergePatchWithCustomArguments(t, "bad original", @@ -1788,14 +2398,7 @@ func TestStrategicMergePatch(t *testing.T) { testStrategicMergePatchWithCustomArguments(t, "nil struct", "{}", "{}", nil, fmt.Errorf(errBadArgTypeFmt, "struct", "nil")) - tc := StrategicMergePatchTestCases{} - err := yaml.Unmarshal(createStrategicMergePatchTestCaseData, &tc) - if err != nil { - t.Errorf("can't unmarshal test cases: %s\n", err) - return - } - - for _, c := range tc.TestCases { + for _, c := range createStrategicMergePatchTestCaseData.TestCases { testTwoWayPatch(t, c) testThreeWayPatch(t, c) } @@ -1817,72 +2420,88 @@ func testStrategicMergePatchWithCustomArguments(t *testing.T, description, origi } func testTwoWayPatch(t *testing.T, c StrategicMergePatchTestCase) { - original, expected, modified := twoWayTestCaseToJSONOrFail(t, c) - - actual, err := CreateTwoWayMergePatch(original, modified, mergeItem) + originalJSON := yamlToJSONOrError(c.Original) + modifiedJSON := yamlToJSONOrError(c.Modified) + expectedJSON := yamlToJSONOrError(c.TwoWay) + smPatchVersion := SMPatchVersion_1_5 + if strings.Contains(c.Description, "using SMPatchVersion_1_0") { + smPatchVersion = SMPatchVersion_1_0 + } + actualJSON, err := CreateTwoWayMergePatch(originalJSON, modifiedJSON, mergeItem, smPatchVersion) if err != nil { - t.Errorf("error: %s\nin test case: %s\ncannot create two way patch: %s:\n%s\n", - err, c.Description, original, toYAMLOrError(c.StrategicMergePatchTestCaseData)) + 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) + if !strings.Contains(c.Description, "for list of primitives") { + testPatchCreation(t, expectedJSON, actualJSON, c.Description) + } else { + testPatchCreationWithoutSorting(t, expectedJSON, actualJSON, c.Description) + } + testPatchApplication(t, originalJSON, actualJSON, modifiedJSON, c.Description) } func testThreeWayPatch(t *testing.T, c StrategicMergePatchTestCase) { - original, modified, current, expected, result := threeWayTestCaseToJSONOrFail(t, c) - actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, false) + originalJSON := yamlToJSONOrError(c.Original) + modifiedJSON := yamlToJSONOrError(c.Modified) + currentJSON := yamlToJSONOrError(c.Current) + expectedJSON := yamlToJSONOrError(c.ThreeWay) + resultJSON := yamlToJSONOrError(c.Result) + smPatchVersion := SMPatchVersion_1_5 + if strings.Contains(c.Description, "using SMPatchVersion_1_0") { + smPatchVersion = SMPatchVersion_1_0 + } + actualJSON, err := CreateThreeWayMergePatch(originalJSON, modifiedJSON, currentJSON, mergeItem, false, smPatchVersion) if err != nil { if !IsConflict(err) { - t.Errorf("error: %s\nin test case: %s\ncannot create three way patch:\n%s\n", - err, c.Description, toYAMLOrError(c.StrategicMergePatchTestCaseData)) + 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:\n%s\n", - err, c.Description, toYAMLOrError(c.StrategicMergePatchTestCaseData)) - return + 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) } if len(c.Result) > 0 { - actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, true) + actualJSON, err := CreateThreeWayMergePatch(originalJSON, modifiedJSON, currentJSON, mergeItem, true, smPatchVersion) if err != nil { - t.Errorf("error: %s\nin test case: %s\ncannot force three way patch application:\n%s\n", - err, c.Description, toYAMLOrError(c.StrategicMergePatchTestCaseData)) + 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) + if !strings.Contains(c.Description, "for list of primitives") { + testPatchCreation(t, expectedJSON, actualJSON, c.Description) + } else { + testPatchCreationWithoutSorting(t, expectedJSON, actualJSON, c.Description) + } + testPatchApplication(t, currentJSON, actualJSON, resultJSON, c.Description) } return } if strings.Contains(c.Description, "conflict") || len(c.Result) < 1 { - t.Errorf("error in test case: %s\nexpected conflict did not occur:\n%s\n", - c.Description, toYAMLOrError(c.StrategicMergePatchTestCaseData)) - return + 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) } - testPatchCreation(t, expected, actual, c.Description) - testPatchApplication(t, current, actual, result, c.Description) + if !strings.Contains(c.Description, "for list of primitives") { + testPatchCreation(t, expectedJSON, actualJSON, c.Description) + } else { + testPatchCreationWithoutSorting(t, expectedJSON, actualJSON, c.Description) + } + testPatchApplication(t, currentJSON, actualJSON, resultJSON, 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), - testObjectToJSONOrFail(t, c.Current, c.Description), - testObjectToJSONOrFail(t, c.ThreeWay, c.Description), - testObjectToJSONOrFail(t, c.Result, c.Description) +func testPatchCreationWithoutSorting(t *testing.T, expectedYAML, actualYAML []byte, description string) { + if bytes.Compare(actualYAML, expectedYAML) != 0 { + t.Errorf("error in test case: %s\nexpected patch:\n%s\ngot:\n%s\n", + description, jsonToYAMLOrError(expectedYAML), jsonToYAMLOrError(actualYAML)) + } } func testPatchCreation(t *testing.T, expected, actual []byte, description string) { @@ -1907,7 +2526,6 @@ func testPatchApplication(t *testing.T, original, patch, expected []byte, descri err, description, jsonToYAMLOrError(patch), jsonToYAMLOrError(original)) return } - sorted, err := sortMergeListsByName(result, mergeItem) if err != nil { t.Errorf("error: %s\nin test case: %s\ncannot sort result object:\n%s\n", @@ -1970,6 +2588,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(y []byte) []byte { + j, err := yamlToJSON(y) + if err != nil { + return []byte(err.Error()) + } + + return j +} + func TestHasConflicts(t *testing.T) { testCases := []struct { A interface{} @@ -2095,7 +2731,7 @@ func TestNumberConversion(t *testing.T) { } for k, tc := range testcases { - patch, err := CreateTwoWayMergePatch([]byte(tc.Old), []byte(tc.New), precisionItem) + patch, err := CreateTwoWayMergePatch([]byte(tc.Old), []byte(tc.New), precisionItem, SMPatchVersionLatest) if err != nil { t.Errorf("%s: unexpected error %v", k, err) continue