mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-27 13:37:30 +00:00
support generic 3-way json merge patch
This commit is contained in:
parent
9805b0bdfb
commit
8aa16e09d4
144
staging/src/k8s.io/apimachinery/pkg/util/jsonmergepatch/patch.go
Normal file
144
staging/src/k8s.io/apimachinery/pkg/util/jsonmergepatch/patch.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package jsonmergepatch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
|
"github.com/evanphx/json-patch"
|
||||||
|
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a 3-way merge patch based-on JSON merge patch.
|
||||||
|
// Calculate addition-and-change patch between current and modified.
|
||||||
|
// Calculate deletion patch between original and modified.
|
||||||
|
func CreateThreeWayJSONMergePatch(original, modified, current []byte, fns ...strategicpatch.PreconditionFunc) ([]byte, error) {
|
||||||
|
if len(original) == 0 {
|
||||||
|
original = []byte(`{}`)
|
||||||
|
}
|
||||||
|
if len(modified) == 0 {
|
||||||
|
modified = []byte(`{}`)
|
||||||
|
}
|
||||||
|
if len(current) == 0 {
|
||||||
|
current = []byte(`{}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
addAndChangePatch, err := jsonpatch.CreateMergePatch(current, modified)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Only keep addition and changes
|
||||||
|
addAndChangePatch, addAndChangePatchObj, err := keepOrDeleteNullInJsonPatch(addAndChangePatch, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
deletePatch, err := jsonpatch.CreateMergePatch(original, modified)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Only keep deletion
|
||||||
|
deletePatch, deletePatchObj, err := keepOrDeleteNullInJsonPatch(deletePatch, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasConflicts, err := strategicpatch.HasConflicts(addAndChangePatchObj, deletePatchObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if hasConflicts {
|
||||||
|
return nil, strategicpatch.NewErrConflict(strategicpatch.ToYAMLOrError(addAndChangePatchObj), strategicpatch.ToYAMLOrError(deletePatchObj))
|
||||||
|
}
|
||||||
|
patch, err := jsonpatch.MergePatch(deletePatch, addAndChangePatch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var patchMap map[string]interface{}
|
||||||
|
err = json.Unmarshal(patch, &patchMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to unmarshal patch for precondition check: %s", patch)
|
||||||
|
}
|
||||||
|
meetPreconditions, err := meetPreconditions(patchMap, fns...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !meetPreconditions {
|
||||||
|
return nil, strategicpatch.NewErrPreconditionFailed(patchMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
return patch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// keepOrDeleteNullInJsonPatch takes a json-encoded byte array and a boolean.
|
||||||
|
// It returns a filtered object and its corresponding json-encoded byte array.
|
||||||
|
// It is a wrapper of func keepOrDeleteNullInObj
|
||||||
|
func keepOrDeleteNullInJsonPatch(patch []byte, keepNull bool) ([]byte, map[string]interface{}, error) {
|
||||||
|
var patchMap map[string]interface{}
|
||||||
|
err := json.Unmarshal(patch, &patchMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
filteredMap, err := keepOrDeleteNullInObj(patchMap, keepNull)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
o, err := json.Marshal(filteredMap)
|
||||||
|
return o, filteredMap, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// keepOrDeleteNullInObj will keep only the null value and delete all the others,
|
||||||
|
// if keepNull is true. Otherwise, it will delete all the null value and keep the others.
|
||||||
|
func keepOrDeleteNullInObj(m map[string]interface{}, keepNull bool) (map[string]interface{}, error) {
|
||||||
|
filteredMap := make(map[string]interface{})
|
||||||
|
var err error
|
||||||
|
for key, val := range m {
|
||||||
|
switch {
|
||||||
|
case keepNull && val == nil:
|
||||||
|
filteredMap[key] = nil
|
||||||
|
case val != nil:
|
||||||
|
switch typedVal := val.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
filteredMap[key], err = keepOrDeleteNullInObj(typedVal, keepNull)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case []interface{}, string, float64, bool, int, int64, nil:
|
||||||
|
// Lists are always replaced in Json, no need to check each entry in the list.
|
||||||
|
if !keepNull {
|
||||||
|
filteredMap[key] = val
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown type: %v", reflect.TypeOf(typedVal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func meetPreconditions(patchObj map[string]interface{}, fns ...strategicpatch.PreconditionFunc) (bool, error) {
|
||||||
|
// Apply the preconditions to the patch, and return an error if any of them fail.
|
||||||
|
for _, fn := range fns {
|
||||||
|
if !fn(patchObj) {
|
||||||
|
return false, fmt.Errorf("precondition failed for: %v", patchObj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
@ -0,0 +1,664 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package jsonmergepatch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/evanphx/json-patch"
|
||||||
|
"github.com/ghodss/yaml"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FilterNullTestCases struct {
|
||||||
|
TestCases []FilterNullTestCase
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterNullTestCase struct {
|
||||||
|
Description string
|
||||||
|
OriginalObj map[string]interface{}
|
||||||
|
ExpectedWithNull map[string]interface{}
|
||||||
|
ExpectedWithoutNull map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var filterNullTestCaseData = []byte(`
|
||||||
|
testCases:
|
||||||
|
- description: nil original
|
||||||
|
originalObj: {}
|
||||||
|
expectedWithNull: {}
|
||||||
|
expectedWithoutNull: {}
|
||||||
|
- description: simple map
|
||||||
|
originalObj:
|
||||||
|
nilKey: null
|
||||||
|
nonNilKey: foo
|
||||||
|
expectedWithNull:
|
||||||
|
nilKey: null
|
||||||
|
expectedWithoutNull:
|
||||||
|
nonNilKey: foo
|
||||||
|
- description: simple map with all nil values
|
||||||
|
originalObj:
|
||||||
|
nilKey1: null
|
||||||
|
nilKey2: null
|
||||||
|
expectedWithNull:
|
||||||
|
nilKey1: null
|
||||||
|
nilKey2: null
|
||||||
|
expectedWithoutNull: {}
|
||||||
|
- description: simple map with all non-nil values
|
||||||
|
originalObj:
|
||||||
|
nonNilKey: foo
|
||||||
|
nonNilKey: bar
|
||||||
|
expectedWithNull: {}
|
||||||
|
expectedWithoutNull:
|
||||||
|
nonNilKey: foo
|
||||||
|
nonNilKey: bar
|
||||||
|
- description: nested map
|
||||||
|
originalObj:
|
||||||
|
mapKey:
|
||||||
|
nilKey: null
|
||||||
|
nonNilKey: foo
|
||||||
|
expectedWithNull:
|
||||||
|
mapKey:
|
||||||
|
nilKey: null
|
||||||
|
expectedWithoutNull:
|
||||||
|
mapKey:
|
||||||
|
nonNilKey: foo
|
||||||
|
- description: nested map that all subkeys are nil
|
||||||
|
originalObj:
|
||||||
|
mapKey:
|
||||||
|
nilKey1: null
|
||||||
|
nilKey2: null
|
||||||
|
expectedWithNull:
|
||||||
|
mapKey:
|
||||||
|
nilKey1: null
|
||||||
|
nilKey2: null
|
||||||
|
expectedWithoutNull:
|
||||||
|
mapKey: {}
|
||||||
|
- description: nested map that all subkeys are non-nil
|
||||||
|
originalObj:
|
||||||
|
mapKey:
|
||||||
|
nonNilKey: foo
|
||||||
|
nonNilKey: bar
|
||||||
|
expectedWithNull:
|
||||||
|
mapKey: {}
|
||||||
|
expectedWithoutNull:
|
||||||
|
mapKey:
|
||||||
|
nonNilKey: foo
|
||||||
|
nonNilKey: bar
|
||||||
|
- description: empty list
|
||||||
|
originalObj:
|
||||||
|
listKey: []
|
||||||
|
expectedWithNull: {}
|
||||||
|
expectedWithoutNull:
|
||||||
|
listKey: []
|
||||||
|
- description: list of primitives
|
||||||
|
originalObj:
|
||||||
|
listKey:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
expectedWithNull: {}
|
||||||
|
expectedWithoutNull:
|
||||||
|
listKey:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- description: list of maps
|
||||||
|
originalObj:
|
||||||
|
listKey:
|
||||||
|
- k1: v1
|
||||||
|
- k2: null
|
||||||
|
- k3: v3
|
||||||
|
k4: null
|
||||||
|
expectedWithNull: {}
|
||||||
|
expectedWithoutNull:
|
||||||
|
listKey:
|
||||||
|
- k1: v1
|
||||||
|
- k2: null
|
||||||
|
- k3: v3
|
||||||
|
k4: null
|
||||||
|
- description: list of different types
|
||||||
|
originalObj:
|
||||||
|
listKey:
|
||||||
|
- k1: v1
|
||||||
|
- k2: null
|
||||||
|
- v3
|
||||||
|
expectedWithNull: {}
|
||||||
|
expectedWithoutNull:
|
||||||
|
listKey:
|
||||||
|
- k1: v1
|
||||||
|
- k2: null
|
||||||
|
- v3
|
||||||
|
`)
|
||||||
|
|
||||||
|
func TestKeepOrDeleteNullInObj(t *testing.T) {
|
||||||
|
tc := FilterNullTestCases{}
|
||||||
|
err := yaml.Unmarshal(filterNullTestCaseData, &tc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't unmarshal test cases: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tc.TestCases {
|
||||||
|
resultWithNull, err := keepOrDeleteNullInObj(test.OriginalObj, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed in test case %q when trying to keep null values: %s", test.Description, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(test.ExpectedWithNull, resultWithNull) {
|
||||||
|
t.Errorf("Failed in test case %q when trying to keep null values:\nexpected expectedWithNull:\n%+v\nbut got:\n%+v\n", test.Description, test.ExpectedWithNull, resultWithNull)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultWithoutNull, err := keepOrDeleteNullInObj(test.OriginalObj, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed in test case %q when trying to keep non-null values: %s", test.Description, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(test.ExpectedWithoutNull, resultWithoutNull) {
|
||||||
|
t.Errorf("Failed in test case %q when trying to keep non-null values:\n expected expectedWithoutNull:\n%+v\nbut got:\n%+v\n", test.Description, test.ExpectedWithoutNull, resultWithoutNull)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type JSONMergePatchTestCases struct {
|
||||||
|
TestCases []JSONMergePatchTestCase
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONMergePatchTestCase struct {
|
||||||
|
Description string
|
||||||
|
JSONMergePatchTestCaseData
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONMergePatchTestCaseData struct {
|
||||||
|
// Original is the original object (last-applied config in annotation)
|
||||||
|
Original map[string]interface{}
|
||||||
|
// Modified is the modified object (new config we want)
|
||||||
|
Modified map[string]interface{}
|
||||||
|
// Current is the current object (live config in the server)
|
||||||
|
Current map[string]interface{}
|
||||||
|
// ThreeWay is the expected three-way merge patch
|
||||||
|
ThreeWay map[string]interface{}
|
||||||
|
// Result is the expected object after applying the three-way patch on current object.
|
||||||
|
Result map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var createJSONMergePatchTestCaseData = []byte(`
|
||||||
|
testCases:
|
||||||
|
- description: nil original
|
||||||
|
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
|
||||||
|
modified:
|
||||||
|
name: 1
|
||||||
|
current:
|
||||||
|
name: 1
|
||||||
|
threeWay:
|
||||||
|
{}
|
||||||
|
result:
|
||||||
|
name: 1
|
||||||
|
- description: add field to map
|
||||||
|
original:
|
||||||
|
name: 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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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:
|
||||||
|
intList:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
modified:
|
||||||
|
intList:
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
current:
|
||||||
|
intList:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
threeWay:
|
||||||
|
intList:
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
result:
|
||||||
|
intList:
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
- description: replace list of scalars with conflict
|
||||||
|
original:
|
||||||
|
intList:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
modified:
|
||||||
|
intList:
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
current:
|
||||||
|
intList:
|
||||||
|
- 1
|
||||||
|
- 4
|
||||||
|
threeWay:
|
||||||
|
intList:
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
result:
|
||||||
|
intList:
|
||||||
|
- 2
|
||||||
|
- 3
|
||||||
|
- description: patch with different scalar type
|
||||||
|
original:
|
||||||
|
foo: 1
|
||||||
|
modified:
|
||||||
|
foo: true
|
||||||
|
current:
|
||||||
|
foo: 1
|
||||||
|
bar: 2
|
||||||
|
threeWay:
|
||||||
|
foo: true
|
||||||
|
result:
|
||||||
|
foo: true
|
||||||
|
bar: 2
|
||||||
|
- description: patch from scalar to list
|
||||||
|
original:
|
||||||
|
foo: 0
|
||||||
|
modified:
|
||||||
|
foo:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
current:
|
||||||
|
foo: 0
|
||||||
|
bar: 2
|
||||||
|
threeWay:
|
||||||
|
foo:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
result:
|
||||||
|
foo:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
bar: 2
|
||||||
|
- description: patch from list to scalar
|
||||||
|
original:
|
||||||
|
foo:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
modified:
|
||||||
|
foo: 0
|
||||||
|
current:
|
||||||
|
foo:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
bar: 2
|
||||||
|
threeWay:
|
||||||
|
foo: 0
|
||||||
|
result:
|
||||||
|
foo: 0
|
||||||
|
bar: 2
|
||||||
|
- description: patch from scalar to map
|
||||||
|
original:
|
||||||
|
foo: 0
|
||||||
|
modified:
|
||||||
|
foo:
|
||||||
|
baz: 1
|
||||||
|
current:
|
||||||
|
foo: 0
|
||||||
|
bar: 2
|
||||||
|
threeWay:
|
||||||
|
foo:
|
||||||
|
baz: 1
|
||||||
|
result:
|
||||||
|
foo:
|
||||||
|
baz: 1
|
||||||
|
bar: 2
|
||||||
|
- description: patch from map to scalar
|
||||||
|
original:
|
||||||
|
foo:
|
||||||
|
baz: 1
|
||||||
|
modified:
|
||||||
|
foo: 0
|
||||||
|
current:
|
||||||
|
foo:
|
||||||
|
baz: 1
|
||||||
|
bar: 2
|
||||||
|
threeWay:
|
||||||
|
foo: 0
|
||||||
|
result:
|
||||||
|
foo: 0
|
||||||
|
bar: 2
|
||||||
|
- description: patch from map to list
|
||||||
|
original:
|
||||||
|
foo:
|
||||||
|
baz: 1
|
||||||
|
modified:
|
||||||
|
foo:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
current:
|
||||||
|
foo:
|
||||||
|
baz: 1
|
||||||
|
bar: 2
|
||||||
|
threeWay:
|
||||||
|
foo:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
result:
|
||||||
|
foo:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
bar: 2
|
||||||
|
- description: patch from list to map
|
||||||
|
original:
|
||||||
|
foo:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
modified:
|
||||||
|
foo:
|
||||||
|
baz: 0
|
||||||
|
current:
|
||||||
|
foo:
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
bar: 2
|
||||||
|
threeWay:
|
||||||
|
foo:
|
||||||
|
baz: 0
|
||||||
|
result:
|
||||||
|
foo:
|
||||||
|
baz: 0
|
||||||
|
bar: 2
|
||||||
|
- description: patch with different nested types
|
||||||
|
original:
|
||||||
|
foo:
|
||||||
|
- a: true
|
||||||
|
- 2
|
||||||
|
- false
|
||||||
|
modified:
|
||||||
|
foo:
|
||||||
|
- 1
|
||||||
|
- false
|
||||||
|
- b: 1
|
||||||
|
current:
|
||||||
|
foo:
|
||||||
|
- a: true
|
||||||
|
- 2
|
||||||
|
- false
|
||||||
|
bar: 0
|
||||||
|
threeWay:
|
||||||
|
foo:
|
||||||
|
- 1
|
||||||
|
- false
|
||||||
|
- b: 1
|
||||||
|
result:
|
||||||
|
foo:
|
||||||
|
- 1
|
||||||
|
- false
|
||||||
|
- b: 1
|
||||||
|
bar: 0
|
||||||
|
`)
|
||||||
|
|
||||||
|
func TestCreateThreeWayJSONMergePatch(t *testing.T) {
|
||||||
|
tc := JSONMergePatchTestCases{}
|
||||||
|
err := yaml.Unmarshal(createJSONMergePatchTestCaseData, &tc)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("can't unmarshal test cases: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range tc.TestCases {
|
||||||
|
testThreeWayPatch(t, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testThreeWayPatch(t *testing.T, c JSONMergePatchTestCase) {
|
||||||
|
original, modified, current, expected, result := threeWayTestCaseToJSONOrFail(t, c)
|
||||||
|
actual, err := CreateThreeWayJSONMergePatch(original, modified, current)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: %s", err)
|
||||||
|
}
|
||||||
|
testPatchCreation(t, expected, actual, c.Description)
|
||||||
|
testPatchApplication(t, current, actual, result, c.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPatchCreation(t *testing.T, expected, actual []byte, description string) {
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("error in test case: %s\nexpected patch:\n%s\ngot:\n%s\n",
|
||||||
|
description, jsonToYAMLOrError(expected), jsonToYAMLOrError(actual))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPatchApplication(t *testing.T, original, patch, expected []byte, description string) {
|
||||||
|
result, err := jsonpatch.MergePatch(original, patch)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error: %s\nin test case: %s\ncannot apply patch:\n%s\nto original:\n%s\n",
|
||||||
|
err, description, jsonToYAMLOrError(patch), jsonToYAMLOrError(original))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(result, expected) {
|
||||||
|
format := "error in test case: %s\npatch application failed:\noriginal:\n%s\npatch:\n%s\nexpected:\n%s\ngot:\n%s\n"
|
||||||
|
t.Errorf(format, description,
|
||||||
|
jsonToYAMLOrError(original), jsonToYAMLOrError(patch),
|
||||||
|
jsonToYAMLOrError(expected), jsonToYAMLOrError(result))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func threeWayTestCaseToJSONOrFail(t *testing.T, c JSONMergePatchTestCase) ([]byte, []byte, []byte, []byte, []byte) {
|
||||||
|
return testObjectToJSONOrFail(t, c.Original),
|
||||||
|
testObjectToJSONOrFail(t, c.Modified),
|
||||||
|
testObjectToJSONOrFail(t, c.Current),
|
||||||
|
testObjectToJSONOrFail(t, c.ThreeWay),
|
||||||
|
testObjectToJSONOrFail(t, c.Result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testObjectToJSONOrFail(t *testing.T, o map[string]interface{}) []byte {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
j, err := toJSON(o)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonToYAMLOrError(j []byte) string {
|
||||||
|
y, err := jsonToYAML(j)
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
return string(y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toJSON(v interface{}) ([]byte, error) {
|
||||||
|
j, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("json marshal failed: %v\n%v\n", err, spew.Sdump(v))
|
||||||
|
}
|
||||||
|
return j, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonToYAML(j []byte) ([]byte, error) {
|
||||||
|
y, err := yaml.JSONToYAML(j)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("json to yaml failed: %v\n%v\n", err, j)
|
||||||
|
}
|
||||||
|
return y, nil
|
||||||
|
}
|
@ -58,40 +58,40 @@ type JSONMap map[string]interface{}
|
|||||||
// IsPreconditionFailed returns true if the provided error indicates
|
// IsPreconditionFailed returns true if the provided error indicates
|
||||||
// a precondition failed.
|
// a precondition failed.
|
||||||
func IsPreconditionFailed(err error) bool {
|
func IsPreconditionFailed(err error) bool {
|
||||||
_, ok := err.(errPreconditionFailed)
|
_, ok := err.(ErrPreconditionFailed)
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
type errPreconditionFailed struct {
|
type ErrPreconditionFailed struct {
|
||||||
message string
|
message string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newErrPreconditionFailed(target map[string]interface{}) errPreconditionFailed {
|
func NewErrPreconditionFailed(target map[string]interface{}) ErrPreconditionFailed {
|
||||||
s := fmt.Sprintf("precondition failed for: %v", target)
|
s := fmt.Sprintf("precondition failed for: %v", target)
|
||||||
return errPreconditionFailed{s}
|
return ErrPreconditionFailed{s}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err errPreconditionFailed) Error() string {
|
func (err ErrPreconditionFailed) Error() string {
|
||||||
return err.message
|
return err.message
|
||||||
}
|
}
|
||||||
|
|
||||||
type errConflict struct {
|
type ErrConflict struct {
|
||||||
message string
|
message string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newErrConflict(patch, current string) errConflict {
|
func NewErrConflict(patch, current string) ErrConflict {
|
||||||
s := fmt.Sprintf("patch:\n%s\nconflicts with changes made from original to current:\n%s\n", patch, current)
|
s := fmt.Sprintf("patch:\n%s\nconflicts with changes made from original to current:\n%s\n", patch, current)
|
||||||
return errConflict{s}
|
return ErrConflict{s}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err errConflict) Error() string {
|
func (err ErrConflict) Error() string {
|
||||||
return err.message
|
return err.message
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsConflict returns true if the provided error indicates
|
// IsConflict returns true if the provided error indicates
|
||||||
// a conflict between the patch and the current configuration.
|
// a conflict between the patch and the current configuration.
|
||||||
func IsConflict(err error) bool {
|
func IsConflict(err error) bool {
|
||||||
_, ok := err.(errConflict)
|
_, ok := err.(ErrConflict)
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +187,7 @@ func CreateTwoWayMergeMapPatch(original, modified JSONMap, dataStruct interface{
|
|||||||
// Apply the preconditions to the patch, and return an error if any of them fail.
|
// Apply the preconditions to the patch, and return an error if any of them fail.
|
||||||
for _, fn := range fns {
|
for _, fn := range fns {
|
||||||
if !fn(patchMap) {
|
if !fn(patchMap) {
|
||||||
return nil, newErrPreconditionFailed(patchMap)
|
return nil, NewErrPreconditionFailed(patchMap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1340,7 +1340,7 @@ func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct int
|
|||||||
// Apply the preconditions to the patch, and return an error if any of them fail.
|
// Apply the preconditions to the patch, and return an error if any of them fail.
|
||||||
for _, fn := range fns {
|
for _, fn := range fns {
|
||||||
if !fn(patchMap) {
|
if !fn(patchMap) {
|
||||||
return nil, newErrPreconditionFailed(patchMap)
|
return nil, NewErrPreconditionFailed(patchMap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1358,14 +1358,14 @@ func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct int
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasConflicts {
|
if hasConflicts {
|
||||||
return nil, newErrConflict(toYAMLOrError(patchMap), toYAMLOrError(changedMap))
|
return nil, NewErrConflict(ToYAMLOrError(patchMap), ToYAMLOrError(changedMap))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.Marshal(patchMap)
|
return json.Marshal(patchMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toYAMLOrError(v interface{}) string {
|
func ToYAMLOrError(v interface{}) string {
|
||||||
y, err := toYAML(v)
|
y, err := toYAML(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err.Error()
|
return err.Error()
|
||||||
|
@ -266,7 +266,7 @@ func TestSortMergeLists(t *testing.T) {
|
|||||||
sorted := testObjectToJSONOrFail(t, c.Sorted, c.Description)
|
sorted := testObjectToJSONOrFail(t, c.Sorted, c.Description)
|
||||||
if !reflect.DeepEqual(original, sorted) {
|
if !reflect.DeepEqual(original, sorted) {
|
||||||
t.Errorf("error in test case: %s\ncannot sort object:\n%s\nexpected:\n%s\ngot:\n%s\n",
|
t.Errorf("error in test case: %s\ncannot sort object:\n%s\nexpected:\n%s\ngot:\n%s\n",
|
||||||
c.Description, toYAMLOrError(c.Original), toYAMLOrError(c.Sorted), jsonToYAMLOrError(original))
|
c.Description, ToYAMLOrError(c.Original), ToYAMLOrError(c.Sorted), jsonToYAMLOrError(original))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2037,7 +2037,7 @@ func testTwoWayPatch(t *testing.T, c StrategicMergePatchTestCase) {
|
|||||||
actualPatch, err := CreateTwoWayMergePatch(original, modified, mergeItem)
|
actualPatch, err := CreateTwoWayMergePatch(original, modified, mergeItem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error: %s\nin test case: %s\ncannot create two way patch: %s:\n%s\n",
|
t.Errorf("error: %s\nin test case: %s\ncannot create two way patch: %s:\n%s\n",
|
||||||
err, c.Description, original, toYAMLOrError(c.StrategicMergePatchTestCaseData))
|
err, c.Description, original, ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2087,13 +2087,13 @@ func testThreeWayPatch(t *testing.T, c StrategicMergePatchTestCase) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if !IsConflict(err) {
|
if !IsConflict(err) {
|
||||||
t.Errorf("error: %s\nin test case: %s\ncannot create three way patch:\n%s\n",
|
t.Errorf("error: %s\nin test case: %s\ncannot create three way patch:\n%s\n",
|
||||||
err, c.Description, toYAMLOrError(c.StrategicMergePatchTestCaseData))
|
err, c.Description, ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(c.Description, "conflict") {
|
if !strings.Contains(c.Description, "conflict") {
|
||||||
t.Errorf("unexpected conflict: %s\nin test case: %s\ncannot create three way patch:\n%s\n",
|
t.Errorf("unexpected conflict: %s\nin test case: %s\ncannot create three way patch:\n%s\n",
|
||||||
err, c.Description, toYAMLOrError(c.StrategicMergePatchTestCaseData))
|
err, c.Description, ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2101,7 +2101,7 @@ func testThreeWayPatch(t *testing.T, c StrategicMergePatchTestCase) {
|
|||||||
actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, true)
|
actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error: %s\nin test case: %s\ncannot force three way patch application:\n%s\n",
|
t.Errorf("error: %s\nin test case: %s\ncannot force three way patch application:\n%s\n",
|
||||||
err, c.Description, toYAMLOrError(c.StrategicMergePatchTestCaseData))
|
err, c.Description, ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2114,7 +2114,7 @@ func testThreeWayPatch(t *testing.T, c StrategicMergePatchTestCase) {
|
|||||||
|
|
||||||
if strings.Contains(c.Description, "conflict") || len(c.Result) < 1 {
|
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",
|
t.Errorf("error in test case: %s\nexpected conflict did not occur:\n%s\n",
|
||||||
c.Description, toYAMLOrError(c.StrategicMergePatchTestCaseData))
|
c.Description, ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
24
vendor/BUILD
vendored
24
vendor/BUILD
vendored
@ -14192,3 +14192,27 @@ go_library(
|
|||||||
"//vendor:k8s.io/client-go/tools/clientcmd",
|
"//vendor:k8s.io/client-go/tools/clientcmd",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "k8s.io/apimachinery/pkg/util/jsonmergepatch_test",
|
||||||
|
srcs = ["k8s.io/apimachinery/pkg/util/jsonmergepatch/patch_test.go"],
|
||||||
|
library = ":k8s.io/apimachinery/pkg/util/jsonmergepatch",
|
||||||
|
tags = ["automanaged"],
|
||||||
|
deps = [
|
||||||
|
"//vendor:github.com/davecgh/go-spew/spew",
|
||||||
|
"//vendor:github.com/evanphx/json-patch",
|
||||||
|
"//vendor:github.com/ghodss/yaml",
|
||||||
|
"//vendor:k8s.io/apimachinery/pkg/util/json",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "k8s.io/apimachinery/pkg/util/jsonmergepatch",
|
||||||
|
srcs = ["k8s.io/apimachinery/pkg/util/jsonmergepatch/patch.go"],
|
||||||
|
tags = ["automanaged"],
|
||||||
|
deps = [
|
||||||
|
"//vendor:github.com/evanphx/json-patch",
|
||||||
|
"//vendor:k8s.io/apimachinery/pkg/util/json",
|
||||||
|
"//vendor:k8s.io/apimachinery/pkg/util/strategicpatch",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user