mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-11 21:12:07 +00:00
support openapi in strategic merge patch
This commit is contained in:
parent
db4134d03f
commit
f1ad84a2c3
@ -9,26 +9,38 @@ load(
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["patch_test.go"],
|
||||
data = [
|
||||
"testdata/swagger-merge-item.json",
|
||||
"testdata/swagger-precision-item.json",
|
||||
],
|
||||
importpath = "k8s.io/apimachinery/pkg/util/strategicpatch",
|
||||
library = ":go_default_library",
|
||||
deps = [
|
||||
"//vendor/github.com/davecgh/go-spew/spew:go_default_library",
|
||||
"//vendor/github.com/ghodss/yaml:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/mergepatch:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/strategicpatch/testing:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["patch.go"],
|
||||
srcs = [
|
||||
"errors.go",
|
||||
"meta.go",
|
||||
"patch.go",
|
||||
"types.go",
|
||||
],
|
||||
importpath = "k8s.io/apimachinery/pkg/util/strategicpatch",
|
||||
deps = [
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/mergepatch:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/third_party/forked/golang/json:go_default_library",
|
||||
"//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@ -41,6 +53,9 @@ filegroup(
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/testing:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
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 strategicpatch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type LookupPatchMetaError struct {
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e LookupPatchMetaError) Error() string {
|
||||
return fmt.Sprintf("LookupPatchMetaError(%s): %v", e.Path, e.Err)
|
||||
}
|
||||
|
||||
type FieldNotFoundError struct {
|
||||
Path string
|
||||
Field string
|
||||
}
|
||||
|
||||
func (e FieldNotFoundError) Error() string {
|
||||
return fmt.Sprintf("unable to find api field %q in %s", e.Field, e.Path)
|
||||
}
|
||||
|
||||
type InvalidTypeError struct {
|
||||
Path string
|
||||
Expected string
|
||||
Actual string
|
||||
}
|
||||
|
||||
func (e InvalidTypeError) Error() string {
|
||||
return fmt.Sprintf("invalid type for %s: got %q, expected %q", e.Path, e.Actual, e.Expected)
|
||||
}
|
194
staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/meta.go
Normal file
194
staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/meta.go
Normal file
@ -0,0 +1,194 @@
|
||||
/*
|
||||
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 strategicpatch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||
forkedjson "k8s.io/apimachinery/third_party/forked/golang/json"
|
||||
openapi "k8s.io/kube-openapi/pkg/util/proto"
|
||||
)
|
||||
|
||||
type PatchMeta struct {
|
||||
patchStrategies []string
|
||||
patchMergeKey string
|
||||
}
|
||||
|
||||
func (pm PatchMeta) GetPatchStrategies() []string {
|
||||
if pm.patchStrategies == nil {
|
||||
return []string{}
|
||||
}
|
||||
return pm.patchStrategies
|
||||
}
|
||||
|
||||
func (pm PatchMeta) SetPatchStrategies(ps []string) {
|
||||
pm.patchStrategies = ps
|
||||
}
|
||||
|
||||
func (pm PatchMeta) GetPatchMergeKey() string {
|
||||
return pm.patchMergeKey
|
||||
}
|
||||
|
||||
func (pm PatchMeta) SetPatchMergeKey(pmk string) {
|
||||
pm.patchMergeKey = pmk
|
||||
}
|
||||
|
||||
type LookupPatchMeta interface {
|
||||
// LookupPatchMetadataForStruct gets subschema and the patch metadata (e.g. patch strategy and merge key) for map.
|
||||
LookupPatchMetadataForStruct(key string) (LookupPatchMeta, PatchMeta, error)
|
||||
// LookupPatchMetadataForSlice get subschema and the patch metadata for slice.
|
||||
LookupPatchMetadataForSlice(key string) (LookupPatchMeta, PatchMeta, error)
|
||||
// Get the type name of the field
|
||||
Name() string
|
||||
}
|
||||
|
||||
type PatchMetaFromStruct struct {
|
||||
T reflect.Type
|
||||
}
|
||||
|
||||
func NewPatchMetaFromStruct(dataStruct interface{}) (PatchMetaFromStruct, error) {
|
||||
t, err := getTagStructType(dataStruct)
|
||||
return PatchMetaFromStruct{T: t}, err
|
||||
}
|
||||
|
||||
var _ LookupPatchMeta = PatchMetaFromStruct{}
|
||||
|
||||
func (s PatchMetaFromStruct) LookupPatchMetadataForStruct(key string) (LookupPatchMeta, PatchMeta, error) {
|
||||
fieldType, fieldPatchStrategies, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadataForStruct(s.T, key)
|
||||
if err != nil {
|
||||
return nil, PatchMeta{}, err
|
||||
}
|
||||
|
||||
return PatchMetaFromStruct{T: fieldType},
|
||||
PatchMeta{
|
||||
patchStrategies: fieldPatchStrategies,
|
||||
patchMergeKey: fieldPatchMergeKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s PatchMetaFromStruct) LookupPatchMetadataForSlice(key string) (LookupPatchMeta, PatchMeta, error) {
|
||||
subschema, patchMeta, err := s.LookupPatchMetadataForStruct(key)
|
||||
if err != nil {
|
||||
return nil, PatchMeta{}, err
|
||||
}
|
||||
elemPatchMetaFromStruct := subschema.(PatchMetaFromStruct)
|
||||
t := elemPatchMetaFromStruct.T
|
||||
|
||||
var elemType reflect.Type
|
||||
switch t.Kind() {
|
||||
// If t is an array or a slice, get the element type.
|
||||
// If element is still an array or a slice, return an error.
|
||||
// Otherwise, return element type.
|
||||
case reflect.Array, reflect.Slice:
|
||||
elemType = t.Elem()
|
||||
if elemType.Kind() == reflect.Array || elemType.Kind() == reflect.Slice {
|
||||
return nil, PatchMeta{}, errors.New("unexpected slice of slice")
|
||||
}
|
||||
// If t is an pointer, get the underlying element.
|
||||
// If the underlying element is neither an array nor a slice, the pointer is pointing to a slice,
|
||||
// e.g. https://github.com/kubernetes/kubernetes/blob/bc22e206c79282487ea0bf5696d5ccec7e839a76/staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/patch_test.go#L2782-L2822
|
||||
// If the underlying element is either an array or a slice, return its element type.
|
||||
case reflect.Ptr:
|
||||
t = t.Elem()
|
||||
if t.Kind() == reflect.Array || t.Kind() == reflect.Slice {
|
||||
t = t.Elem()
|
||||
}
|
||||
elemType = t
|
||||
default:
|
||||
return nil, PatchMeta{}, fmt.Errorf("expected slice or array type, but got: %s", s.T.Kind().String())
|
||||
}
|
||||
|
||||
return PatchMetaFromStruct{T: elemType}, patchMeta, nil
|
||||
}
|
||||
|
||||
func (s PatchMetaFromStruct) Name() string {
|
||||
return s.T.Kind().String()
|
||||
}
|
||||
|
||||
func getTagStructType(dataStruct interface{}) (reflect.Type, error) {
|
||||
if dataStruct == nil {
|
||||
return nil, mergepatch.ErrBadArgKind(struct{}{}, nil)
|
||||
}
|
||||
|
||||
t := reflect.TypeOf(dataStruct)
|
||||
// Get the underlying type for pointers
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
if t.Kind() != reflect.Struct {
|
||||
return nil, mergepatch.ErrBadArgKind(struct{}{}, dataStruct)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func GetTagStructTypeOrDie(dataStruct interface{}) reflect.Type {
|
||||
t, err := getTagStructType(dataStruct)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
type PatchMetaFromOpenAPI struct {
|
||||
Schema openapi.Schema
|
||||
}
|
||||
|
||||
func NewPatchMetaFromOpenAPI(s openapi.Schema) PatchMetaFromOpenAPI {
|
||||
return PatchMetaFromOpenAPI{Schema: s}
|
||||
}
|
||||
|
||||
var _ LookupPatchMeta = PatchMetaFromOpenAPI{}
|
||||
|
||||
func (s PatchMetaFromOpenAPI) LookupPatchMetadataForStruct(key string) (LookupPatchMeta, PatchMeta, error) {
|
||||
if s.Schema == nil {
|
||||
return nil, PatchMeta{}, nil
|
||||
}
|
||||
kindItem := NewKindItem(key, s.Schema.GetPath())
|
||||
s.Schema.Accept(kindItem)
|
||||
|
||||
err := kindItem.Error()
|
||||
if err != nil {
|
||||
return nil, PatchMeta{}, err
|
||||
}
|
||||
return PatchMetaFromOpenAPI{Schema: kindItem.subschema},
|
||||
kindItem.patchmeta, nil
|
||||
}
|
||||
|
||||
func (s PatchMetaFromOpenAPI) LookupPatchMetadataForSlice(key string) (LookupPatchMeta, PatchMeta, error) {
|
||||
if s.Schema == nil {
|
||||
return nil, PatchMeta{}, nil
|
||||
}
|
||||
sliceItem := NewSliceItem(key, s.Schema.GetPath())
|
||||
s.Schema.Accept(sliceItem)
|
||||
|
||||
err := sliceItem.Error()
|
||||
if err != nil {
|
||||
return nil, PatchMeta{}, err
|
||||
}
|
||||
return PatchMetaFromOpenAPI{Schema: sliceItem.subschema},
|
||||
sliceItem.patchmeta, nil
|
||||
}
|
||||
|
||||
func (s PatchMetaFromOpenAPI) Name() string {
|
||||
schema := s.Schema
|
||||
return schema.GetName()
|
||||
}
|
@ -25,7 +25,6 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||
forkedjson "k8s.io/apimachinery/third_party/forked/golang/json"
|
||||
)
|
||||
|
||||
// An alternate implementation of JSON Merge Patch
|
||||
@ -93,6 +92,16 @@ type MergeOptions struct {
|
||||
// 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 ...mergepatch.PreconditionFunc) ([]byte, error) {
|
||||
schema, err := NewPatchMetaFromStruct(dataStruct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return CreateTwoWayMergePatchUsingLookupPatchMeta(original, modified, schema, fns...)
|
||||
}
|
||||
|
||||
func CreateTwoWayMergePatchUsingLookupPatchMeta(
|
||||
original, modified []byte, schema LookupPatchMeta, fns ...mergepatch.PreconditionFunc) ([]byte, error) {
|
||||
originalMap := map[string]interface{}{}
|
||||
if len(original) > 0 {
|
||||
if err := json.Unmarshal(original, &originalMap); err != nil {
|
||||
@ -107,7 +116,7 @@ func CreateTwoWayMergePatch(original, modified []byte, dataStruct interface{}, f
|
||||
}
|
||||
}
|
||||
|
||||
patchMap, err := CreateTwoWayMergeMapPatch(originalMap, modifiedMap, dataStruct, fns...)
|
||||
patchMap, err := CreateTwoWayMergeMapPatchUsingLookupPatchMeta(originalMap, modifiedMap, schema, fns...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -119,15 +128,19 @@ func CreateTwoWayMergePatch(original, modified []byte, dataStruct interface{}, f
|
||||
// encoded JSONMap.
|
||||
// The serialized version of the map can then be passed to StrategicMergeMapPatch.
|
||||
func CreateTwoWayMergeMapPatch(original, modified JSONMap, dataStruct interface{}, fns ...mergepatch.PreconditionFunc) (JSONMap, error) {
|
||||
t, err := getTagStructType(dataStruct)
|
||||
schema, err := NewPatchMetaFromStruct(dataStruct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return CreateTwoWayMergeMapPatchUsingLookupPatchMeta(original, modified, schema, fns...)
|
||||
}
|
||||
|
||||
func CreateTwoWayMergeMapPatchUsingLookupPatchMeta(original, modified JSONMap, schema LookupPatchMeta, fns ...mergepatch.PreconditionFunc) (JSONMap, error) {
|
||||
diffOptions := DiffOptions{
|
||||
SetElementOrder: true,
|
||||
}
|
||||
patchMap, err := diffMaps(original, modified, t, diffOptions)
|
||||
patchMap, err := diffMaps(original, modified, schema, diffOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -152,12 +165,9 @@ func CreateTwoWayMergeMapPatch(original, modified JSONMap, dataStruct interface{
|
||||
// - IFF list of primitives && merge strategy - use parallel deletion list
|
||||
// - IFF list of maps or primitives with replace strategy (default) - set patch value to the value in modified
|
||||
// - Build $retainKeys directive for fields with retainKeys patch strategy
|
||||
func diffMaps(original, modified map[string]interface{}, t reflect.Type, diffOptions DiffOptions) (map[string]interface{}, error) {
|
||||
func diffMaps(original, modified map[string]interface{}, schema LookupPatchMeta, diffOptions DiffOptions) (map[string]interface{}, error) {
|
||||
patch := map[string]interface{}{}
|
||||
// Get the underlying type for pointers
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
// This will be used to build the $retainKeys directive sent in the patch
|
||||
retainKeysList := make([]interface{}, 0, len(modified))
|
||||
|
||||
@ -199,10 +209,10 @@ func diffMaps(original, modified map[string]interface{}, t reflect.Type, diffOpt
|
||||
switch originalValueTyped := originalValue.(type) {
|
||||
case map[string]interface{}:
|
||||
modifiedValueTyped := modifiedValue.(map[string]interface{})
|
||||
err = handleMapDiff(key, originalValueTyped, modifiedValueTyped, patch, t, diffOptions)
|
||||
err = handleMapDiff(key, originalValueTyped, modifiedValueTyped, patch, schema, diffOptions)
|
||||
case []interface{}:
|
||||
modifiedValueTyped := modifiedValue.([]interface{})
|
||||
err = handleSliceDiff(key, originalValueTyped, modifiedValueTyped, patch, t, diffOptions)
|
||||
err = handleSliceDiff(key, originalValueTyped, modifiedValueTyped, patch, schema, diffOptions)
|
||||
default:
|
||||
replacePatchFieldIfNotEqual(key, originalValue, modifiedValue, patch, diffOptions)
|
||||
}
|
||||
@ -249,8 +259,9 @@ func handleDirectiveMarker(key string, originalValue, modifiedValue interface{},
|
||||
// patch is the patch map that contains key and the updated value, and it is the parent of originalValue, modifiedValue
|
||||
// diffOptions contains multiple options to control how we do the diff.
|
||||
func handleMapDiff(key string, originalValue, modifiedValue, patch map[string]interface{},
|
||||
t reflect.Type, diffOptions DiffOptions) error {
|
||||
fieldType, fieldPatchStrategies, _, err := forkedjson.LookupPatchMetadata(t, key)
|
||||
schema LookupPatchMeta, diffOptions DiffOptions) error {
|
||||
subschema, patchMeta, err := schema.LookupPatchMetadataForStruct(key)
|
||||
|
||||
if err != nil {
|
||||
// We couldn't look up metadata for the field
|
||||
// If the values are identical, this doesn't matter, no patch is needed
|
||||
@ -260,7 +271,7 @@ func handleMapDiff(key string, originalValue, modifiedValue, patch map[string]in
|
||||
// Otherwise, return the error
|
||||
return err
|
||||
}
|
||||
retainKeys, patchStrategy, err := extractRetainKeysPatchStrategy(fieldPatchStrategies)
|
||||
retainKeys, patchStrategy, err := extractRetainKeysPatchStrategy(patchMeta.GetPatchStrategies())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -272,7 +283,7 @@ func handleMapDiff(key string, originalValue, modifiedValue, patch map[string]in
|
||||
patch[key] = modifiedValue
|
||||
}
|
||||
default:
|
||||
patchValue, err := diffMaps(originalValue, modifiedValue, fieldType, diffOptions)
|
||||
patchValue, err := diffMaps(originalValue, modifiedValue, subschema, diffOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -291,8 +302,8 @@ func handleMapDiff(key string, originalValue, modifiedValue, patch map[string]in
|
||||
// patch is the patch map that contains key and the updated value, and it is the parent of originalValue, modifiedValue
|
||||
// diffOptions contains multiple options to control how we do the diff.
|
||||
func handleSliceDiff(key string, originalValue, modifiedValue []interface{}, patch map[string]interface{},
|
||||
t reflect.Type, diffOptions DiffOptions) error {
|
||||
fieldType, fieldPatchStrategies, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, key)
|
||||
schema LookupPatchMeta, diffOptions DiffOptions) error {
|
||||
subschema, patchMeta, err := schema.LookupPatchMetadataForSlice(key)
|
||||
if err != nil {
|
||||
// We couldn't look up metadata for the field
|
||||
// If the values are identical, this doesn't matter, no patch is needed
|
||||
@ -302,7 +313,7 @@ func handleSliceDiff(key string, originalValue, modifiedValue []interface{}, pat
|
||||
// Otherwise, return the error
|
||||
return err
|
||||
}
|
||||
retainKeys, patchStrategy, err := extractRetainKeysPatchStrategy(fieldPatchStrategies)
|
||||
retainKeys, patchStrategy, err := extractRetainKeysPatchStrategy(patchMeta.GetPatchStrategies())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -310,7 +321,7 @@ func handleSliceDiff(key string, originalValue, modifiedValue []interface{}, pat
|
||||
// Merge the 2 slices using mergePatchKey
|
||||
case mergeDirective:
|
||||
diffOptions.BuildRetainKeysDirective = retainKeys
|
||||
addList, deletionList, setOrderList, err := diffLists(originalValue, modifiedValue, fieldType.Elem(), fieldPatchMergeKey, diffOptions)
|
||||
addList, deletionList, setOrderList, err := diffLists(originalValue, modifiedValue, subschema, patchMeta.GetPatchMergeKey(), diffOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -537,7 +548,7 @@ func normalizeSliceOrder(toSort, order []interface{}, mergeKey string, kind refl
|
||||
// another list to set the order of the list
|
||||
// Only list of primitives with merge strategy will generate a parallel deletion list.
|
||||
// These two lists should yield modified when applied to original, for lists with merge semantics.
|
||||
func diffLists(original, modified []interface{}, t reflect.Type, mergeKey string, diffOptions DiffOptions) ([]interface{}, []interface{}, []interface{}, error) {
|
||||
func diffLists(original, modified []interface{}, schema LookupPatchMeta, mergeKey string, diffOptions DiffOptions) ([]interface{}, []interface{}, []interface{}, error) {
|
||||
if len(original) == 0 {
|
||||
// Both slices are empty - do nothing
|
||||
if len(modified) == 0 || diffOptions.IgnoreChangesAndAdditions {
|
||||
@ -557,7 +568,7 @@ func diffLists(original, modified []interface{}, t reflect.Type, mergeKey string
|
||||
kind := elementType.Kind()
|
||||
switch kind {
|
||||
case reflect.Map:
|
||||
patchList, deleteList, err = diffListsOfMaps(original, modified, t, mergeKey, diffOptions)
|
||||
patchList, deleteList, err = diffListsOfMaps(original, modified, schema, mergeKey, diffOptions)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@ -703,15 +714,15 @@ func compareListValuesAtIndex(list1Inbounds, list2Inbounds bool, list1Value, lis
|
||||
// diffListsOfMaps takes a pair of lists and
|
||||
// returns a (recursive) strategic merge patch list contains additions and changes and
|
||||
// a deletion list contains deletions
|
||||
func diffListsOfMaps(original, modified []interface{}, t reflect.Type, mergeKey string, diffOptions DiffOptions) ([]interface{}, []interface{}, error) {
|
||||
func diffListsOfMaps(original, modified []interface{}, schema LookupPatchMeta, mergeKey string, diffOptions DiffOptions) ([]interface{}, []interface{}, error) {
|
||||
patch := make([]interface{}, 0, len(modified))
|
||||
deletionList := make([]interface{}, 0, len(original))
|
||||
|
||||
originalSorted, err := sortMergeListsByNameArray(original, t, mergeKey, false)
|
||||
originalSorted, err := sortMergeListsByNameArray(original, schema, mergeKey, false)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
modifiedSorted, err := sortMergeListsByNameArray(modified, t, mergeKey, false)
|
||||
modifiedSorted, err := sortMergeListsByNameArray(modified, schema, mergeKey, false)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@ -746,7 +757,7 @@ func diffListsOfMaps(original, modified []interface{}, t reflect.Type, mergeKey
|
||||
switch {
|
||||
case bothInBounds && ItemMatchesOriginalAndModifiedSlice(originalElementMergeKeyValueString, modifiedElementMergeKeyValueString):
|
||||
// Merge key values are equal, so recurse
|
||||
patchValue, err := diffMaps(originalElement, modifiedElement, t, diffOptions)
|
||||
patchValue, err := diffMaps(originalElement, modifiedElement, schema, diffOptions)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@ -799,6 +810,15 @@ func getMapAndMergeKeyValueByIndex(index int, mergeKey string, listOfMaps []inte
|
||||
// must be json encoded content. A patch can be created from an original and a modified document
|
||||
// by calling CreateStrategicMergePatch.
|
||||
func StrategicMergePatch(original, patch []byte, dataStruct interface{}) ([]byte, error) {
|
||||
schema, err := NewPatchMetaFromStruct(dataStruct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return StrategicMergePatchUsingLookupPatchMeta(original, patch, schema)
|
||||
}
|
||||
|
||||
func StrategicMergePatchUsingLookupPatchMeta(original, patch []byte, schema LookupPatchMeta) ([]byte, error) {
|
||||
originalMap, err := handleUnmarshal(original)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -808,7 +828,7 @@ func StrategicMergePatch(original, patch []byte, dataStruct interface{}) ([]byte
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := StrategicMergeMapPatch(originalMap, patchMap, dataStruct)
|
||||
result, err := StrategicMergeMapPatchUsingLookupPatchMeta(originalMap, patchMap, schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -834,7 +854,7 @@ func handleUnmarshal(j []byte) (map[string]interface{}, error) {
|
||||
// calling CreateTwoWayMergeMapPatch.
|
||||
// Warning: the original and patch JSONMap objects are mutated by this function and should not be reused.
|
||||
func StrategicMergeMapPatch(original, patch JSONMap, dataStruct interface{}) (JSONMap, error) {
|
||||
t, err := getTagStructType(dataStruct)
|
||||
schema, err := NewPatchMetaFromStruct(dataStruct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -849,29 +869,15 @@ func StrategicMergeMapPatch(original, patch JSONMap, dataStruct interface{}) (JS
|
||||
return nil, mergepatch.ErrUnsupportedStrategicMergePatchFormat
|
||||
}
|
||||
|
||||
return StrategicMergeMapPatchUsingLookupPatchMeta(original, patch, schema)
|
||||
}
|
||||
|
||||
func StrategicMergeMapPatchUsingLookupPatchMeta(original, patch JSONMap, schema LookupPatchMeta) (JSONMap, error) {
|
||||
mergeOptions := MergeOptions{
|
||||
MergeParallelList: true,
|
||||
IgnoreUnmatchedNulls: true,
|
||||
}
|
||||
return mergeMap(original, patch, t, mergeOptions)
|
||||
}
|
||||
|
||||
func getTagStructType(dataStruct interface{}) (reflect.Type, error) {
|
||||
if dataStruct == nil {
|
||||
return nil, mergepatch.ErrBadArgKind(struct{}{}, nil)
|
||||
}
|
||||
|
||||
t := reflect.TypeOf(dataStruct)
|
||||
// Get the underlying type for pointers
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
if t.Kind() != reflect.Struct {
|
||||
return nil, mergepatch.ErrBadArgKind(struct{}{}, dataStruct)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
return mergeMap(original, patch, schema, mergeOptions)
|
||||
}
|
||||
|
||||
// handleDirectiveInMergeMap handles the patch directive when merging 2 maps.
|
||||
@ -1079,7 +1085,7 @@ func applyRetainKeysDirective(original, patch map[string]interface{}, options Me
|
||||
// The precedence is $setElementOrder > order in patch list > order in live list.
|
||||
// This function will delete the item after merging it to prevent process it again in the future.
|
||||
// Ref: https://git.k8s.io/community/contributors/design-proposals/cli/preserve-order-in-strategic-merge-patch.md
|
||||
func mergePatchIntoOriginal(original, patch map[string]interface{}, t reflect.Type, mergeOptions MergeOptions) error {
|
||||
func mergePatchIntoOriginal(original, patch map[string]interface{}, schema LookupPatchMeta, mergeOptions MergeOptions) error {
|
||||
for key, patchV := range patch {
|
||||
// Do nothing if there is no ordering directive
|
||||
if !strings.HasPrefix(key, setElementOrderDirectivePrefix) {
|
||||
@ -1106,9 +1112,9 @@ func mergePatchIntoOriginal(original, patch map[string]interface{}, t reflect.Ty
|
||||
var (
|
||||
ok bool
|
||||
originalFieldValue, patchFieldValue, merged []interface{}
|
||||
patchStrategy, mergeKey string
|
||||
patchStrategies []string
|
||||
fieldType reflect.Type
|
||||
patchStrategy string
|
||||
patchMeta PatchMeta
|
||||
subschema LookupPatchMeta
|
||||
)
|
||||
typedSetElementOrderList, ok := setElementOrderInPatch.([]interface{})
|
||||
if !ok {
|
||||
@ -1134,16 +1140,16 @@ func mergePatchIntoOriginal(original, patch map[string]interface{}, t reflect.Ty
|
||||
return mergepatch.ErrBadArgType(patchFieldValue, patchList)
|
||||
}
|
||||
}
|
||||
fieldType, patchStrategies, mergeKey, err = forkedjson.LookupPatchMetadata(t, originalKey)
|
||||
subschema, patchMeta, err = schema.LookupPatchMetadataForSlice(originalKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, patchStrategy, err = extractRetainKeysPatchStrategy(patchStrategies)
|
||||
_, patchStrategy, err = extractRetainKeysPatchStrategy(patchMeta.GetPatchStrategies())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Check for consistency between the element order list and the field it applies to
|
||||
err = validatePatchWithSetOrderList(patchFieldValue, typedSetElementOrderList, mergeKey)
|
||||
err = validatePatchWithSetOrderList(patchFieldValue, typedSetElementOrderList, patchMeta.GetPatchMergeKey())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -1156,8 +1162,8 @@ func mergePatchIntoOriginal(original, patch map[string]interface{}, t reflect.Ty
|
||||
// list was added
|
||||
merged = patchFieldValue
|
||||
case foundOriginal && foundPatch:
|
||||
merged, err = mergeSliceHandler(originalList, patchList, fieldType,
|
||||
patchStrategy, mergeKey, false, mergeOptions)
|
||||
merged, err = mergeSliceHandler(originalList, patchList, subschema,
|
||||
patchStrategy, patchMeta.GetPatchMergeKey(), false, mergeOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -1167,13 +1173,13 @@ func mergePatchIntoOriginal(original, patch map[string]interface{}, t reflect.Ty
|
||||
|
||||
// Split all items into patch items and server-only items and then enforce the order.
|
||||
var patchItems, serverOnlyItems []interface{}
|
||||
if len(mergeKey) == 0 {
|
||||
if len(patchMeta.GetPatchMergeKey()) == 0 {
|
||||
// Primitives doesn't need merge key to do partitioning.
|
||||
patchItems, serverOnlyItems = partitionPrimitivesByPresentInList(merged, typedSetElementOrderList)
|
||||
|
||||
} else {
|
||||
// Maps need merge key to do partitioning.
|
||||
patchItems, serverOnlyItems, err = partitionMapsByPresentInList(merged, typedSetElementOrderList, mergeKey)
|
||||
patchItems, serverOnlyItems, err = partitionMapsByPresentInList(merged, typedSetElementOrderList, patchMeta.GetPatchMergeKey())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -1187,7 +1193,7 @@ func mergePatchIntoOriginal(original, patch map[string]interface{}, t reflect.Ty
|
||||
// normalize merged list
|
||||
// typedSetElementOrderList contains all the relative order in typedPatchList,
|
||||
// so don't need to use typedPatchList
|
||||
both, err := normalizeElementOrder(patchItems, serverOnlyItems, typedSetElementOrderList, originalFieldValue, mergeKey, kind)
|
||||
both, err := normalizeElementOrder(patchItems, serverOnlyItems, typedSetElementOrderList, originalFieldValue, patchMeta.GetPatchMergeKey(), kind)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -1249,7 +1255,7 @@ func partitionMapsByPresentInList(original, partitionBy []interface{}, mergeKey
|
||||
// If patch contains any null field (e.g. field_1: null) that is not
|
||||
// present in original, then to propagate it to the end result use
|
||||
// mergeOptions.IgnoreUnmatchedNulls == false.
|
||||
func mergeMap(original, patch map[string]interface{}, t reflect.Type, mergeOptions MergeOptions) (map[string]interface{}, error) {
|
||||
func mergeMap(original, patch map[string]interface{}, schema LookupPatchMeta, mergeOptions MergeOptions) (map[string]interface{}, error) {
|
||||
if v, ok := patch[directiveMarker]; ok {
|
||||
return handleDirectiveInMergeMap(v, patch)
|
||||
}
|
||||
@ -1269,7 +1275,7 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type, mergeOptio
|
||||
// When not merging the directive, it will make sure $setElementOrder list exist only in original.
|
||||
// When merging the directive, it will process $setElementOrder and its patch list together.
|
||||
// This function will delete the merged elements from patch so they will not be reprocessed
|
||||
err = mergePatchIntoOriginal(original, patch, t, mergeOptions)
|
||||
err = mergePatchIntoOriginal(original, patch, schema, mergeOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1307,11 +1313,6 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type, mergeOptio
|
||||
continue
|
||||
}
|
||||
|
||||
// If the data type is a pointer, resolve the element.
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
originalType := reflect.TypeOf(original[k])
|
||||
patchType := reflect.TypeOf(patchV)
|
||||
if originalType != patchType {
|
||||
@ -1319,22 +1320,27 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type, mergeOptio
|
||||
continue
|
||||
}
|
||||
// If they're both maps or lists, recurse into the value.
|
||||
// First find the fieldPatchStrategy and fieldPatchMergeKey.
|
||||
fieldType, fieldPatchStrategies, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, patchStrategy, err := extractRetainKeysPatchStrategy(fieldPatchStrategies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch originalType.Kind() {
|
||||
case reflect.Map:
|
||||
|
||||
original[k], err = mergeMapHandler(original[k], patchV, fieldType, patchStrategy, mergeOptions)
|
||||
subschema, patchMeta, err := schema.LookupPatchMetadataForStruct(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, patchStrategy, err := extractRetainKeysPatchStrategy(patchMeta.GetPatchStrategies())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
original[k], err = mergeMapHandler(original[k], patchV, subschema, patchStrategy, mergeOptions)
|
||||
case reflect.Slice:
|
||||
original[k], err = mergeSliceHandler(original[k], patchV, fieldType, patchStrategy, fieldPatchMergeKey, isDeleteList, mergeOptions)
|
||||
subschema, patchMeta, err := schema.LookupPatchMetadataForSlice(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, patchStrategy, err := extractRetainKeysPatchStrategy(patchMeta.GetPatchStrategies())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
original[k], err = mergeSliceHandler(original[k], patchV, subschema, patchStrategy, patchMeta.GetPatchMergeKey(), isDeleteList, mergeOptions)
|
||||
default:
|
||||
original[k] = patchV
|
||||
}
|
||||
@ -1347,7 +1353,7 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type, mergeOptio
|
||||
|
||||
// mergeMapHandler handles how to merge `patchV` whose key is `key` with `original` respecting
|
||||
// fieldPatchStrategy and mergeOptions.
|
||||
func mergeMapHandler(original, patch interface{}, fieldType reflect.Type,
|
||||
func mergeMapHandler(original, patch interface{}, schema LookupPatchMeta,
|
||||
fieldPatchStrategy string, mergeOptions MergeOptions) (map[string]interface{}, error) {
|
||||
typedOriginal, typedPatch, err := mapTypeAssertion(original, patch)
|
||||
if err != nil {
|
||||
@ -1355,7 +1361,7 @@ func mergeMapHandler(original, patch interface{}, fieldType reflect.Type,
|
||||
}
|
||||
|
||||
if fieldPatchStrategy != replaceDirective {
|
||||
return mergeMap(typedOriginal, typedPatch, fieldType, mergeOptions)
|
||||
return mergeMap(typedOriginal, typedPatch, schema, mergeOptions)
|
||||
} else {
|
||||
return typedPatch, nil
|
||||
}
|
||||
@ -1363,7 +1369,7 @@ func mergeMapHandler(original, patch interface{}, fieldType reflect.Type,
|
||||
|
||||
// mergeSliceHandler handles how to merge `patchV` whose key is `key` with `original` respecting
|
||||
// fieldPatchStrategy, fieldPatchMergeKey, isDeleteList and mergeOptions.
|
||||
func mergeSliceHandler(original, patch interface{}, fieldType reflect.Type,
|
||||
func mergeSliceHandler(original, patch interface{}, schema LookupPatchMeta,
|
||||
fieldPatchStrategy, fieldPatchMergeKey string, isDeleteList bool, mergeOptions MergeOptions) ([]interface{}, error) {
|
||||
typedOriginal, typedPatch, err := sliceTypeAssertion(original, patch)
|
||||
if err != nil {
|
||||
@ -1371,8 +1377,7 @@ func mergeSliceHandler(original, patch interface{}, fieldType reflect.Type,
|
||||
}
|
||||
|
||||
if fieldPatchStrategy == mergeDirective {
|
||||
elemType := fieldType.Elem()
|
||||
return mergeSlice(typedOriginal, typedPatch, elemType, fieldPatchMergeKey, mergeOptions, isDeleteList)
|
||||
return mergeSlice(typedOriginal, typedPatch, schema, fieldPatchMergeKey, mergeOptions, isDeleteList)
|
||||
} else {
|
||||
return typedPatch, nil
|
||||
}
|
||||
@ -1381,7 +1386,7 @@ func mergeSliceHandler(original, patch interface{}, fieldType reflect.Type,
|
||||
// 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, mergeOptions MergeOptions, isDeleteList bool) ([]interface{}, error) {
|
||||
func mergeSlice(original, patch []interface{}, schema LookupPatchMeta, mergeKey string, mergeOptions MergeOptions, isDeleteList bool) ([]interface{}, error) {
|
||||
if len(original) == 0 && len(patch) == 0 {
|
||||
return original, nil
|
||||
}
|
||||
@ -1406,7 +1411,7 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
|
||||
|
||||
} else {
|
||||
if mergeKey == "" {
|
||||
return nil, fmt.Errorf("cannot merge lists without merge key for type %s", elemType.Kind().String())
|
||||
return nil, fmt.Errorf("cannot merge lists without merge key for %s", schema.Name())
|
||||
}
|
||||
|
||||
original, patch, err = mergeSliceWithSpecialElements(original, patch, mergeKey)
|
||||
@ -1414,7 +1419,7 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
|
||||
return nil, err
|
||||
}
|
||||
|
||||
merged, err = mergeSliceWithoutSpecialElements(original, patch, mergeKey, elemType, mergeOptions)
|
||||
merged, err = mergeSliceWithoutSpecialElements(original, patch, mergeKey, schema, mergeOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1492,7 +1497,7 @@ func deleteMatchingEntries(original []interface{}, mergeKey string, mergeValue i
|
||||
|
||||
// mergeSliceWithoutSpecialElements merges slices with non-special elements.
|
||||
// original and patch must be slices of maps, they should be checked before calling this function.
|
||||
func mergeSliceWithoutSpecialElements(original, patch []interface{}, mergeKey string, elemType reflect.Type, mergeOptions MergeOptions) ([]interface{}, error) {
|
||||
func mergeSliceWithoutSpecialElements(original, patch []interface{}, mergeKey string, schema LookupPatchMeta, mergeOptions MergeOptions) ([]interface{}, error) {
|
||||
for _, v := range patch {
|
||||
typedV := v.(map[string]interface{})
|
||||
mergeValue, ok := typedV[mergeKey]
|
||||
@ -1511,7 +1516,7 @@ func mergeSliceWithoutSpecialElements(original, patch []interface{}, mergeKey st
|
||||
var mergedMaps interface{}
|
||||
var err error
|
||||
// Merge into original.
|
||||
mergedMaps, err = mergeMap(originalMap, typedV, elemType, mergeOptions)
|
||||
mergedMaps, err = mergeMap(originalMap, typedV, schema, mergeOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1560,14 +1565,14 @@ func findMapInSliceBasedOnKeyValue(m []interface{}, key string, value interface{
|
||||
// by key. This is needed by tests because in JSON, list order is significant,
|
||||
// but in Strategic Merge Patch, merge lists do not have significant order.
|
||||
// Sorting the lists allows for order-insensitive comparison of patched maps.
|
||||
func sortMergeListsByName(mapJSON []byte, dataStruct interface{}) ([]byte, error) {
|
||||
func sortMergeListsByName(mapJSON []byte, schema LookupPatchMeta) ([]byte, error) {
|
||||
var m map[string]interface{}
|
||||
err := json.Unmarshal(mapJSON, &m)
|
||||
if err != nil {
|
||||
return nil, mergepatch.ErrBadJSONDoc
|
||||
}
|
||||
|
||||
newM, err := sortMergeListsByNameMap(m, reflect.TypeOf(dataStruct))
|
||||
newM, err := sortMergeListsByNameMap(m, schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1576,7 +1581,7 @@ func sortMergeListsByName(mapJSON []byte, dataStruct interface{}) ([]byte, error
|
||||
}
|
||||
|
||||
// Function sortMergeListsByNameMap recursively sorts the merge lists by its mergeKey in a map.
|
||||
func sortMergeListsByNameMap(s map[string]interface{}, t reflect.Type) (map[string]interface{}, error) {
|
||||
func sortMergeListsByNameMap(s map[string]interface{}, schema LookupPatchMeta) (map[string]interface{}, error) {
|
||||
newS := map[string]interface{}{}
|
||||
for k, v := range s {
|
||||
if k == retainKeysDirective {
|
||||
@ -1597,26 +1602,29 @@ func sortMergeListsByNameMap(s map[string]interface{}, t reflect.Type) (map[stri
|
||||
return nil, mergepatch.ErrBadPatchFormatForSetElementOrderList
|
||||
}
|
||||
} else if k != directiveMarker {
|
||||
fieldType, fieldPatchStrategies, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, patchStrategy, err := extractRetainKeysPatchStrategy(fieldPatchStrategies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If v is a map or a merge slice, recurse.
|
||||
if typedV, ok := v.(map[string]interface{}); ok {
|
||||
var err error
|
||||
v, err = sortMergeListsByNameMap(typedV, fieldType)
|
||||
// recurse for map and slice.
|
||||
switch typedV := v.(type) {
|
||||
case map[string]interface{}:
|
||||
subschema, _, err := schema.LookupPatchMetadataForStruct(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err = sortMergeListsByNameMap(typedV, subschema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case []interface{}:
|
||||
subschema, patchMeta, err := schema.LookupPatchMetadataForSlice(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, patchStrategy, err := extractRetainKeysPatchStrategy(patchMeta.GetPatchStrategies())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if typedV, ok := v.([]interface{}); ok {
|
||||
if patchStrategy == mergeDirective {
|
||||
var err error
|
||||
v, err = sortMergeListsByNameArray(typedV, fieldType.Elem(), fieldPatchMergeKey, true)
|
||||
v, err = sortMergeListsByNameArray(typedV, subschema, patchMeta.GetPatchMergeKey(), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1631,7 +1639,7 @@ func sortMergeListsByNameMap(s map[string]interface{}, t reflect.Type) (map[stri
|
||||
}
|
||||
|
||||
// Function sortMergeListsByNameMap recursively sorts the merge lists by its mergeKey in an array.
|
||||
func sortMergeListsByNameArray(s []interface{}, elemType reflect.Type, mergeKey string, recurse bool) ([]interface{}, error) {
|
||||
func sortMergeListsByNameArray(s []interface{}, schema LookupPatchMeta, mergeKey string, recurse bool) ([]interface{}, error) {
|
||||
if len(s) == 0 {
|
||||
return s, nil
|
||||
}
|
||||
@ -1654,7 +1662,7 @@ func sortMergeListsByNameArray(s []interface{}, elemType reflect.Type, mergeKey
|
||||
for _, elem := range s {
|
||||
if recurse {
|
||||
typedElem := elem.(map[string]interface{})
|
||||
newElem, err := sortMergeListsByNameMap(typedElem, elemType)
|
||||
newElem, err := sortMergeListsByNameMap(typedElem, schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1800,18 +1808,13 @@ func sliceElementType(slices ...[]interface{}) (reflect.Type, error) {
|
||||
// objects overlap with different values in any key. All keys are required to be
|
||||
// strings. Since patches of the same Type have congruent keys, this is valid
|
||||
// for multiple patch types. This method supports strategic merge patch semantics.
|
||||
func MergingMapsHaveConflicts(left, right map[string]interface{}, dataStruct interface{}) (bool, error) {
|
||||
t, err := getTagStructType(dataStruct)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
return mergingMapFieldsHaveConflicts(left, right, t, "", "")
|
||||
func MergingMapsHaveConflicts(left, right map[string]interface{}, schema LookupPatchMeta) (bool, error) {
|
||||
return mergingMapFieldsHaveConflicts(left, right, schema, "", "")
|
||||
}
|
||||
|
||||
func mergingMapFieldsHaveConflicts(
|
||||
left, right interface{},
|
||||
fieldType reflect.Type,
|
||||
schema LookupPatchMeta,
|
||||
fieldPatchStrategy, fieldPatchMergeKey string,
|
||||
) (bool, error) {
|
||||
switch leftType := left.(type) {
|
||||
@ -1842,15 +1845,14 @@ func mergingMapFieldsHaveConflicts(
|
||||
return false, nil
|
||||
}
|
||||
// Check the individual keys.
|
||||
return mapsHaveConflicts(leftType, rightType, fieldType)
|
||||
return mapsHaveConflicts(leftType, rightType, schema)
|
||||
|
||||
case []interface{}:
|
||||
rightType, ok := right.([]interface{})
|
||||
if !ok {
|
||||
return true, nil
|
||||
}
|
||||
return slicesHaveConflicts(leftType, rightType, fieldType, fieldPatchStrategy, fieldPatchMergeKey)
|
||||
|
||||
return slicesHaveConflicts(leftType, rightType, schema, fieldPatchStrategy, fieldPatchMergeKey)
|
||||
case string, float64, bool, int, int64, nil:
|
||||
return !reflect.DeepEqual(left, right), nil
|
||||
default:
|
||||
@ -1858,21 +1860,37 @@ func mergingMapFieldsHaveConflicts(
|
||||
}
|
||||
}
|
||||
|
||||
func mapsHaveConflicts(typedLeft, typedRight map[string]interface{}, structType reflect.Type) (bool, error) {
|
||||
func mapsHaveConflicts(typedLeft, typedRight map[string]interface{}, schema LookupPatchMeta) (bool, error) {
|
||||
for key, leftValue := range typedLeft {
|
||||
if key != directiveMarker && key != retainKeysDirective {
|
||||
if rightValue, ok := typedRight[key]; ok {
|
||||
fieldType, fieldPatchStrategies, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(structType, key)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
_, patchStrategy, err := extractRetainKeysPatchStrategy(fieldPatchStrategies)
|
||||
if err != nil {
|
||||
return true, err
|
||||
var subschema LookupPatchMeta
|
||||
var patchMeta PatchMeta
|
||||
var patchStrategy string
|
||||
var err error
|
||||
switch leftValue.(type) {
|
||||
case []interface{}:
|
||||
subschema, patchMeta, err = schema.LookupPatchMetadataForSlice(key)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
_, patchStrategy, err = extractRetainKeysPatchStrategy(patchMeta.patchStrategies)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
case map[string]interface{}:
|
||||
subschema, patchMeta, err = schema.LookupPatchMetadataForStruct(key)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
_, patchStrategy, err = extractRetainKeysPatchStrategy(patchMeta.patchStrategies)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
|
||||
if hasConflicts, err := mergingMapFieldsHaveConflicts(leftValue, rightValue,
|
||||
fieldType, patchStrategy, fieldPatchMergeKey); hasConflicts {
|
||||
subschema, patchStrategy, patchMeta.GetPatchMergeKey()); hasConflicts {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
@ -1884,7 +1902,7 @@ func mapsHaveConflicts(typedLeft, typedRight map[string]interface{}, structType
|
||||
|
||||
func slicesHaveConflicts(
|
||||
typedLeft, typedRight []interface{},
|
||||
fieldType reflect.Type,
|
||||
schema LookupPatchMeta,
|
||||
fieldPatchStrategy, fieldPatchMergeKey string,
|
||||
) (bool, error) {
|
||||
elementType, err := sliceElementType(typedLeft, typedRight)
|
||||
@ -1892,7 +1910,6 @@ func slicesHaveConflicts(
|
||||
return true, err
|
||||
}
|
||||
|
||||
valueType := fieldType.Elem()
|
||||
if fieldPatchStrategy == mergeDirective {
|
||||
// Merging lists of scalars have no conflicts by definition
|
||||
// So we only need to check further if the elements are maps
|
||||
@ -1911,7 +1928,7 @@ func slicesHaveConflicts(
|
||||
return true, err
|
||||
}
|
||||
|
||||
return mapsOfMapsHaveConflicts(leftMap, rightMap, valueType)
|
||||
return mapsOfMapsHaveConflicts(leftMap, rightMap, schema)
|
||||
}
|
||||
|
||||
// Either we don't have type information, or these are non-merging lists
|
||||
@ -1929,7 +1946,7 @@ func slicesHaveConflicts(
|
||||
// Compare the slices element by element in order
|
||||
// This test will fail if the slices are not sorted
|
||||
for i := range typedLeft {
|
||||
if hasConflicts, err := mergingMapFieldsHaveConflicts(typedLeft[i], typedRight[i], valueType, "", ""); hasConflicts {
|
||||
if hasConflicts, err := mergingMapFieldsHaveConflicts(typedLeft[i], typedRight[i], schema, "", ""); hasConflicts {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
@ -1956,10 +1973,10 @@ func sliceOfMapsToMapOfMaps(slice []interface{}, mergeKey string) (map[string]in
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func mapsOfMapsHaveConflicts(typedLeft, typedRight map[string]interface{}, structType reflect.Type) (bool, error) {
|
||||
func mapsOfMapsHaveConflicts(typedLeft, typedRight map[string]interface{}, schema LookupPatchMeta) (bool, error) {
|
||||
for key, leftValue := range typedLeft {
|
||||
if rightValue, ok := typedRight[key]; ok {
|
||||
if hasConflicts, err := mergingMapFieldsHaveConflicts(leftValue, rightValue, structType, "", ""); hasConflicts {
|
||||
if hasConflicts, err := mergingMapFieldsHaveConflicts(leftValue, rightValue, schema, "", ""); hasConflicts {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
@ -1979,7 +1996,7 @@ func mapsOfMapsHaveConflicts(typedLeft, typedRight map[string]interface{}, struc
|
||||
// in a way that is different from how it is changed in current (e.g., deleting it, changing its
|
||||
// value). We also propagate values fields that do not exist in original but are explicitly
|
||||
// defined in modified.
|
||||
func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct interface{}, overwrite bool, fns ...mergepatch.PreconditionFunc) ([]byte, error) {
|
||||
func CreateThreeWayMergePatch(original, modified, current []byte, schema LookupPatchMeta, overwrite bool, fns ...mergepatch.PreconditionFunc) ([]byte, error) {
|
||||
originalMap := map[string]interface{}{}
|
||||
if len(original) > 0 {
|
||||
if err := json.Unmarshal(original, &originalMap); err != nil {
|
||||
@ -2001,11 +2018,6 @@ func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct int
|
||||
}
|
||||
}
|
||||
|
||||
t, err := getTagStructType(dataStruct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The patch is the difference from current to modified without deletions, plus deletions
|
||||
// from original to modified. To find it, we compute deletions, which are the deletions from
|
||||
// original to modified, and delta, which is the difference from current to modified without
|
||||
@ -2014,7 +2026,7 @@ func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct int
|
||||
IgnoreDeletions: true,
|
||||
SetElementOrder: true,
|
||||
}
|
||||
deltaMap, err := diffMaps(currentMap, modifiedMap, t, deltaMapDiffOptions)
|
||||
deltaMap, err := diffMaps(currentMap, modifiedMap, schema, deltaMapDiffOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -2022,13 +2034,13 @@ func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct int
|
||||
SetElementOrder: true,
|
||||
IgnoreChangesAndAdditions: true,
|
||||
}
|
||||
deletionsMap, err := diffMaps(originalMap, modifiedMap, t, deletionsMapDiffOptions)
|
||||
deletionsMap, err := diffMaps(originalMap, modifiedMap, schema, deletionsMapDiffOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mergeOptions := MergeOptions{}
|
||||
patchMap, err := mergeMap(deletionsMap, deltaMap, t, mergeOptions)
|
||||
patchMap, err := mergeMap(deletionsMap, deltaMap, schema, mergeOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -2044,12 +2056,12 @@ func CreateThreeWayMergePatch(original, modified, current []byte, dataStruct int
|
||||
// then return a conflict error.
|
||||
if !overwrite {
|
||||
changeMapDiffOptions := DiffOptions{}
|
||||
changedMap, err := diffMaps(originalMap, currentMap, t, changeMapDiffOptions)
|
||||
changedMap, err := diffMaps(originalMap, currentMap, schema, changeMapDiffOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasConflicts, err := MergingMapsHaveConflicts(patchMap, changedMap, dataStruct)
|
||||
hasConflicts, err := MergingMapsHaveConflicts(patchMap, changedMap, schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -17,17 +17,25 @@ limitations under the License.
|
||||
package strategicpatch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/ghodss/yaml"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
sptest "k8s.io/apimachinery/pkg/util/strategicpatch/testing"
|
||||
)
|
||||
|
||||
var (
|
||||
fakeMergeItemSchema = sptest.Fake{Path: filepath.Join("testdata", "swagger-merge-item.json")}
|
||||
fakePrecisionItemSchema = sptest.Fake{Path: filepath.Join("testdata", "swagger-precision-item.json")}
|
||||
)
|
||||
|
||||
type SortMergeListTestCases struct {
|
||||
@ -86,31 +94,34 @@ type StrategicMergePatchRawTestCaseData struct {
|
||||
}
|
||||
|
||||
type MergeItem struct {
|
||||
Name string
|
||||
Value string
|
||||
Other string
|
||||
MergingList []MergeItem `patchStrategy:"merge" patchMergeKey:"name"`
|
||||
NonMergingList []MergeItem
|
||||
MergingIntList []int `patchStrategy:"merge"`
|
||||
NonMergingIntList []int
|
||||
MergeItemPtr *MergeItem `patchStrategy:"merge" patchMergeKey:"name"`
|
||||
SimpleMap map[string]string
|
||||
ReplacingItem runtime.RawExtension `patchStrategy:"replace"`
|
||||
RetainKeysMap RetainKeysMergeItem `patchStrategy:"retainKeys"`
|
||||
RetainKeysMergingList []MergeItem `patchStrategy:"merge,retainKeys" patchMergeKey:"name"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Other string `json:"other,omitempty"`
|
||||
MergingList []MergeItem `json:"mergingList,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
|
||||
NonMergingList []MergeItem `json:"nonMergingList,omitempty"`
|
||||
MergingIntList []int `json:"mergingIntList,omitempty" patchStrategy:"merge"`
|
||||
NonMergingIntList []int `json:"nonMergingIntList,omitempty"`
|
||||
MergeItemPtr *MergeItem `json:"mergeItemPtr,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
|
||||
SimpleMap map[string]string `json:"simpleMap,omitempty"`
|
||||
ReplacingItem runtime.RawExtension `json:"replacingItem,omitempty" patchStrategy:"replace"`
|
||||
RetainKeysMap RetainKeysMergeItem `json:"retainKeysMap,omitempty" patchStrategy:"retainKeys"`
|
||||
RetainKeysMergingList []MergeItem `json:"retainKeysMergingList,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"name"`
|
||||
}
|
||||
|
||||
type RetainKeysMergeItem struct {
|
||||
Name string
|
||||
Value string
|
||||
Other string
|
||||
SimpleMap map[string]string
|
||||
MergingIntList []int `patchStrategy:"merge"`
|
||||
MergingList []MergeItem `patchStrategy:"merge" patchMergeKey:"name"`
|
||||
NonMergingList []MergeItem
|
||||
Name string `json:"name,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Other string `json:"other,omitempty"`
|
||||
SimpleMap map[string]string `json:"simpleMap,omitempty"`
|
||||
MergingIntList []int `json:"mergingIntList,omitempty" patchStrategy:"merge"`
|
||||
MergingList []MergeItem `json:"mergingList,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
|
||||
NonMergingList []MergeItem `json:"nonMergingList,omitempty"`
|
||||
}
|
||||
|
||||
var mergeItem MergeItem
|
||||
var (
|
||||
mergeItem MergeItem
|
||||
mergeItemStructSchema = PatchMetaFromStruct{T: GetTagStructTypeOrDie(mergeItem)}
|
||||
)
|
||||
|
||||
// These are test cases for SortMergeList, used to assert that it (recursively)
|
||||
// sorts both merging and non merging lists correctly.
|
||||
@ -151,7 +162,6 @@ testCases:
|
||||
- name: 3
|
||||
- name: 2
|
||||
- description: sort lists of maps and nested lists of maps
|
||||
fieldTypes:
|
||||
original:
|
||||
mergingList:
|
||||
- name: 2
|
||||
@ -271,6 +281,14 @@ testCases:
|
||||
`)
|
||||
|
||||
func TestSortMergeLists(t *testing.T) {
|
||||
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
|
||||
Schema: sptest.GetSchemaOrDie(fakeMergeItemSchema, "mergeItem"),
|
||||
}
|
||||
schemas := []LookupPatchMeta{
|
||||
mergeItemStructSchema,
|
||||
mergeItemOpenapiSchema,
|
||||
}
|
||||
|
||||
tc := SortMergeListTestCases{}
|
||||
err := yaml.Unmarshal(sortMergeListTestCaseData, &tc)
|
||||
if err != nil {
|
||||
@ -278,12 +296,15 @@ func TestSortMergeLists(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, c := range tc.TestCases {
|
||||
got := sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Original), c.Description)
|
||||
expected := testObjectToJSONOrFail(t, c.Sorted)
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("error in test case: %s\ncannot sort object:\n%s\nexpected:\n%s\ngot:\n%s\n",
|
||||
c.Description, mergepatch.ToYAMLOrError(c.Original), mergepatch.ToYAMLOrError(c.Sorted), jsonToYAMLOrError(got))
|
||||
for _, schema := range schemas {
|
||||
for _, c := range tc.TestCases {
|
||||
temp := testObjectToJSONOrFail(t, c.Original)
|
||||
got := sortJsonOrFail(t, temp, c.Description, schema)
|
||||
expected := testObjectToJSONOrFail(t, c.Sorted)
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("using %s error in test case: %s\ncannot sort object:\n%s\nexpected:\n%s\ngot:\n%s\n",
|
||||
getSchemaType(schema), c.Description, mergepatch.ToYAMLOrError(c.Original), mergepatch.ToYAMLOrError(c.Sorted), jsonToYAMLOrError(got))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -636,6 +657,14 @@ mergingIntList:
|
||||
}
|
||||
|
||||
func TestCustomStrategicMergePatch(t *testing.T) {
|
||||
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
|
||||
Schema: sptest.GetSchemaOrDie(fakeMergeItemSchema, "mergeItem"),
|
||||
}
|
||||
schemas := []LookupPatchMeta{
|
||||
mergeItemStructSchema,
|
||||
mergeItemOpenapiSchema,
|
||||
}
|
||||
|
||||
tc := StrategicMergePatchTestCases{}
|
||||
err := yaml.Unmarshal(customStrategicMergePatchTestCaseData, &tc)
|
||||
if err != nil {
|
||||
@ -643,14 +672,16 @@ func TestCustomStrategicMergePatch(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, c := range tc.TestCases {
|
||||
original, expectedTwoWayPatch, _, expectedResult := twoWayTestCaseToJSONOrFail(t, c)
|
||||
testPatchApplication(t, original, expectedTwoWayPatch, expectedResult, c.Description, "")
|
||||
}
|
||||
for _, schema := range schemas {
|
||||
for _, c := range tc.TestCases {
|
||||
original, expectedTwoWayPatch, _, expectedResult := twoWayTestCaseToJSONOrFail(t, c, schema)
|
||||
testPatchApplication(t, original, expectedTwoWayPatch, expectedResult, c.Description, "", schema)
|
||||
}
|
||||
|
||||
for _, c := range customStrategicMergePatchRawTestCases {
|
||||
original, expectedTwoWayPatch, _, expectedResult := twoWayRawTestCaseToJSONOrFail(t, c)
|
||||
testPatchApplication(t, original, expectedTwoWayPatch, expectedResult, c.Description, c.ExpectedError)
|
||||
for _, c := range customStrategicMergePatchRawTestCases {
|
||||
original, expectedTwoWayPatch, _, expectedResult := twoWayRawTestCaseToJSONOrFail(t, c)
|
||||
testPatchApplication(t, original, expectedTwoWayPatch, expectedResult, c.Description, c.ExpectedError, schema)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2350,68 +2381,6 @@ mergingList:
|
||||
value: 1
|
||||
- name: 2
|
||||
other: b
|
||||
`),
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "replace non merging list nested in merging list with value conflict",
|
||||
StrategicMergePatchRawTestCaseData: StrategicMergePatchRawTestCaseData{
|
||||
Original: []byte(`
|
||||
mergingList:
|
||||
- name: 1
|
||||
nonMergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
value: 2
|
||||
- name: 2
|
||||
`),
|
||||
TwoWay: []byte(`
|
||||
$setElementOrder/mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
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(`
|
||||
$setElementOrder/mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
mergingList:
|
||||
- name: 1
|
||||
nonMergingList:
|
||||
- name: 1
|
||||
value: 1
|
||||
`),
|
||||
Result: []byte(`
|
||||
mergingList:
|
||||
- name: 1
|
||||
other: a
|
||||
nonMergingList:
|
||||
- name: 1
|
||||
value: 1
|
||||
- name: 2
|
||||
other: b
|
||||
`),
|
||||
},
|
||||
},
|
||||
@ -6041,14 +6010,16 @@ mergeItemPtr:
|
||||
}
|
||||
|
||||
func TestStrategicMergePatch(t *testing.T) {
|
||||
testStrategicMergePatchWithCustomArguments(t, "bad original",
|
||||
"<THIS IS NOT JSON>", "{}", mergeItem, mergepatch.ErrBadJSONDoc)
|
||||
testStrategicMergePatchWithCustomArguments(t, "bad patch",
|
||||
"{}", "<THIS IS NOT JSON>", mergeItem, mergepatch.ErrBadJSONDoc)
|
||||
testStrategicMergePatchWithCustomArguments(t, "bad struct",
|
||||
testStrategicMergePatchWithCustomArgumentsUsingStruct(t, "bad struct",
|
||||
"{}", "{}", []byte("<THIS IS NOT A STRUCT>"), mergepatch.ErrBadArgKind(struct{}{}, []byte{}))
|
||||
testStrategicMergePatchWithCustomArguments(t, "nil struct",
|
||||
"{}", "{}", nil, mergepatch.ErrBadArgKind(struct{}{}, nil))
|
||||
|
||||
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
|
||||
Schema: sptest.GetSchemaOrDie(fakeMergeItemSchema, "mergeItem"),
|
||||
}
|
||||
schemas := []LookupPatchMeta{
|
||||
mergeItemStructSchema,
|
||||
mergeItemOpenapiSchema,
|
||||
}
|
||||
|
||||
tc := StrategicMergePatchTestCases{}
|
||||
err := yaml.Unmarshal(createStrategicMergePatchTestCaseData, &tc)
|
||||
@ -6057,53 +6028,76 @@ func TestStrategicMergePatch(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, c := range tc.TestCases {
|
||||
testTwoWayPatch(t, c)
|
||||
testThreeWayPatch(t, c)
|
||||
}
|
||||
for _, schema := range schemas {
|
||||
testStrategicMergePatchWithCustomArguments(t, "bad original",
|
||||
"<THIS IS NOT JSON>", "{}", schema, mergepatch.ErrBadJSONDoc)
|
||||
testStrategicMergePatchWithCustomArguments(t, "bad patch",
|
||||
"{}", "<THIS IS NOT JSON>", schema, mergepatch.ErrBadJSONDoc)
|
||||
testStrategicMergePatchWithCustomArguments(t, "nil struct",
|
||||
"{}", "{}", nil, mergepatch.ErrBadArgKind(struct{}{}, nil))
|
||||
|
||||
// run multiple times to exercise different map traversal orders
|
||||
for i := 0; i < 10; i++ {
|
||||
for _, c := range strategicMergePatchRawTestCases {
|
||||
testTwoWayPatchForRawTestCase(t, c)
|
||||
testThreeWayPatchForRawTestCase(t, c)
|
||||
for _, c := range tc.TestCases {
|
||||
testTwoWayPatch(t, c, schema)
|
||||
testThreeWayPatch(t, c, schema)
|
||||
}
|
||||
|
||||
// run multiple times to exercise different map traversal orders
|
||||
for i := 0; i < 10; i++ {
|
||||
for _, c := range strategicMergePatchRawTestCases {
|
||||
testTwoWayPatchForRawTestCase(t, c, schema)
|
||||
testThreeWayPatchForRawTestCase(t, c, schema)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testStrategicMergePatchWithCustomArguments(t *testing.T, description, original, patch string, dataStruct interface{}, err error) {
|
||||
_, err2 := StrategicMergePatch([]byte(original), []byte(patch), dataStruct)
|
||||
if err2 != err {
|
||||
if err2 == nil {
|
||||
t.Errorf("expected error: %s\ndid not occur in test case: %s", err, description)
|
||||
func testStrategicMergePatchWithCustomArgumentsUsingStruct(t *testing.T, description, original, patch string, dataStruct interface{}, expected error) {
|
||||
schema, actual := NewPatchMetaFromStruct(dataStruct)
|
||||
// If actual is not nil, check error. If errors match, return.
|
||||
if actual != nil {
|
||||
checkErrorsEqual(t, description, expected, actual, schema)
|
||||
return
|
||||
}
|
||||
testStrategicMergePatchWithCustomArguments(t, description, original, patch, schema, expected)
|
||||
}
|
||||
|
||||
func testStrategicMergePatchWithCustomArguments(t *testing.T, description, original, patch string, schema LookupPatchMeta, expected error) {
|
||||
_, actual := StrategicMergePatch([]byte(original), []byte(patch), schema)
|
||||
checkErrorsEqual(t, description, expected, actual, schema)
|
||||
}
|
||||
|
||||
func checkErrorsEqual(t *testing.T, description string, expected, actual error, schema LookupPatchMeta) {
|
||||
if actual != expected {
|
||||
if actual == nil {
|
||||
t.Errorf("using %s expected error: %s\ndid not occur in test case: %s", getSchemaType(schema), expected, description)
|
||||
return
|
||||
}
|
||||
|
||||
if err == nil || err2.Error() != err.Error() {
|
||||
t.Errorf("unexpected error: %s\noccurred in test case: %s", err2, description)
|
||||
if expected == nil || actual.Error() != expected.Error() {
|
||||
t.Errorf("using %s unexpected error: %s\noccurred in test case: %s", getSchemaType(schema), actual, description)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testTwoWayPatch(t *testing.T, c StrategicMergePatchTestCase) {
|
||||
original, expectedPatch, modified, expectedResult := twoWayTestCaseToJSONOrFail(t, c)
|
||||
func testTwoWayPatch(t *testing.T, c StrategicMergePatchTestCase, schema LookupPatchMeta) {
|
||||
original, expectedPatch, modified, expectedResult := twoWayTestCaseToJSONOrFail(t, c, schema)
|
||||
|
||||
actualPatch, err := CreateTwoWayMergePatch(original, modified, mergeItem)
|
||||
actualPatch, err := CreateTwoWayMergePatchUsingLookupPatchMeta(original, modified, schema)
|
||||
if err != nil {
|
||||
t.Errorf("error: %s\nin test case: %s\ncannot create two way patch: %s:\n%s\n",
|
||||
err, c.Description, original, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||
t.Errorf("using %s error: %s\nin test case: %s\ncannot create two way patch: %s:\n%s\n",
|
||||
getSchemaType(schema), err, c.Description, original, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||
return
|
||||
}
|
||||
|
||||
testPatchCreation(t, expectedPatch, actualPatch, c.Description)
|
||||
testPatchApplication(t, original, actualPatch, expectedResult, c.Description, "")
|
||||
testPatchApplication(t, original, actualPatch, expectedResult, c.Description, "", schema)
|
||||
}
|
||||
|
||||
func testTwoWayPatchForRawTestCase(t *testing.T, c StrategicMergePatchRawTestCase) {
|
||||
func testTwoWayPatchForRawTestCase(t *testing.T, c StrategicMergePatchRawTestCase, schema LookupPatchMeta) {
|
||||
original, expectedPatch, modified, expectedResult := twoWayRawTestCaseToJSONOrFail(t, c)
|
||||
|
||||
actualPatch, err := CreateTwoWayMergePatch(original, modified, mergeItem)
|
||||
actualPatch, err := CreateTwoWayMergePatchUsingLookupPatchMeta(original, modified, schema)
|
||||
if err != nil {
|
||||
t.Errorf("error: %s\nin test case: %s\ncannot create two way patch:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
|
||||
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
|
||||
@ -6111,18 +6105,18 @@ func testTwoWayPatchForRawTestCase(t *testing.T, c StrategicMergePatchRawTestCas
|
||||
}
|
||||
|
||||
testPatchCreation(t, expectedPatch, actualPatch, c.Description)
|
||||
testPatchApplication(t, original, actualPatch, expectedResult, c.Description, c.ExpectedError)
|
||||
testPatchApplication(t, original, actualPatch, expectedResult, c.Description, c.ExpectedError, schema)
|
||||
}
|
||||
|
||||
func twoWayTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchTestCase) ([]byte, []byte, []byte, []byte) {
|
||||
func twoWayTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchTestCase, schema LookupPatchMeta) ([]byte, []byte, []byte, []byte) {
|
||||
expectedResult := c.TwoWayResult
|
||||
if expectedResult == nil {
|
||||
expectedResult = c.Modified
|
||||
}
|
||||
return sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Original), c.Description),
|
||||
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.TwoWay), c.Description),
|
||||
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Modified), c.Description),
|
||||
sortJsonOrFail(t, testObjectToJSONOrFail(t, expectedResult), c.Description)
|
||||
return sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Original), c.Description, schema),
|
||||
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.TwoWay), c.Description, schema),
|
||||
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Modified), c.Description, schema),
|
||||
sortJsonOrFail(t, testObjectToJSONOrFail(t, expectedResult), c.Description, schema)
|
||||
}
|
||||
|
||||
func twoWayRawTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchRawTestCase) ([]byte, []byte, []byte, []byte) {
|
||||
@ -6136,94 +6130,94 @@ func twoWayRawTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchRawTestCas
|
||||
yamlToJSONOrError(t, expectedResult)
|
||||
}
|
||||
|
||||
func testThreeWayPatch(t *testing.T, c StrategicMergePatchTestCase) {
|
||||
original, modified, current, expected, result := threeWayTestCaseToJSONOrFail(t, c)
|
||||
actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, false)
|
||||
func testThreeWayPatch(t *testing.T, c StrategicMergePatchTestCase, schema LookupPatchMeta) {
|
||||
original, modified, current, expected, result := threeWayTestCaseToJSONOrFail(t, c, schema)
|
||||
actual, err := CreateThreeWayMergePatch(original, modified, current, schema, false)
|
||||
if err != nil {
|
||||
if !mergepatch.IsConflict(err) {
|
||||
t.Errorf("error: %s\nin test case: %s\ncannot create three way patch:\n%s\n",
|
||||
err, c.Description, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||
t.Errorf("using %s error: %s\nin test case: %s\ncannot create three way patch:\n%s\n",
|
||||
getSchemaType(schema), err, c.Description, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||
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, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||
t.Errorf("using %s unexpected conflict: %s\nin test case: %s\ncannot create three way patch:\n%s\n",
|
||||
getSchemaType(schema), err, c.Description, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||
return
|
||||
}
|
||||
|
||||
if len(c.Result) > 0 {
|
||||
actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, true)
|
||||
actual, err := CreateThreeWayMergePatch(original, modified, current, schema, true)
|
||||
if err != nil {
|
||||
t.Errorf("error: %s\nin test case: %s\ncannot force three way patch application:\n%s\n",
|
||||
err, c.Description, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||
t.Errorf("using %s error: %s\nin test case: %s\ncannot force three way patch application:\n%s\n",
|
||||
getSchemaType(schema), err, c.Description, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||
return
|
||||
}
|
||||
|
||||
testPatchCreation(t, expected, actual, c.Description)
|
||||
testPatchApplication(t, current, actual, result, c.Description, "")
|
||||
testPatchApplication(t, current, actual, result, c.Description, "", schema)
|
||||
}
|
||||
|
||||
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, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||
t.Errorf("using %s error in test case: %s\nexpected conflict did not occur:\n%s\n",
|
||||
getSchemaType(schema), c.Description, mergepatch.ToYAMLOrError(c.StrategicMergePatchTestCaseData))
|
||||
return
|
||||
}
|
||||
|
||||
testPatchCreation(t, expected, actual, c.Description)
|
||||
testPatchApplication(t, current, actual, result, c.Description, "")
|
||||
testPatchApplication(t, current, actual, result, c.Description, "", schema)
|
||||
}
|
||||
|
||||
func testThreeWayPatchForRawTestCase(t *testing.T, c StrategicMergePatchRawTestCase) {
|
||||
func testThreeWayPatchForRawTestCase(t *testing.T, c StrategicMergePatchRawTestCase, schema LookupPatchMeta) {
|
||||
original, modified, current, expected, result := threeWayRawTestCaseToJSONOrFail(t, c)
|
||||
actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, false)
|
||||
actual, err := CreateThreeWayMergePatch(original, modified, current, schema, false)
|
||||
if err != nil {
|
||||
if !mergepatch.IsConflict(err) {
|
||||
t.Errorf("error: %s\nin test case: %s\ncannot create three way patch:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
|
||||
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
|
||||
t.Errorf("using %s error: %s\nin test case: %s\ncannot create three way patch:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
|
||||
getSchemaType(schema), err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(c.Description, "conflict") {
|
||||
t.Errorf("unexpected conflict: %s\nin test case: %s\ncannot create three way patch:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
|
||||
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
|
||||
t.Errorf("using %s 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",
|
||||
getSchemaType(schema), err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
|
||||
return
|
||||
}
|
||||
|
||||
if len(c.Result) > 0 {
|
||||
actual, err := CreateThreeWayMergePatch(original, modified, current, mergeItem, true)
|
||||
actual, err := CreateThreeWayMergePatch(original, modified, current, schema, true)
|
||||
if err != nil {
|
||||
t.Errorf("error: %s\nin test case: %s\ncannot force three way patch application:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
|
||||
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
|
||||
t.Errorf("using %s 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",
|
||||
getSchemaType(schema), 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, c.ExpectedError)
|
||||
testPatchApplication(t, current, actual, result, c.Description, c.ExpectedError, schema)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(c.Description, "conflict") || len(c.Result) < 1 {
|
||||
t.Errorf("error: %s\nin test case: %s\nexpected conflict did not occur:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
|
||||
err, c.Description, c.Original, c.TwoWay, c.Modified, c.Current, c.ThreeWay, c.Result)
|
||||
t.Errorf("using %s error: %s\nin test case: %s\nexpected conflict did not occur:\noriginal:%s\ntwoWay:%s\nmodified:%s\ncurrent:%s\nthreeWay:%s\nresult:%s\n",
|
||||
getSchemaType(schema), 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, c.ExpectedError)
|
||||
testPatchApplication(t, current, actual, result, c.Description, c.ExpectedError, schema)
|
||||
}
|
||||
|
||||
func threeWayTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchTestCase) ([]byte, []byte, []byte, []byte, []byte) {
|
||||
return sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Original), c.Description),
|
||||
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Modified), c.Description),
|
||||
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Current), c.Description),
|
||||
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.ThreeWay), c.Description),
|
||||
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Result), c.Description)
|
||||
func threeWayTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchTestCase, schema LookupPatchMeta) ([]byte, []byte, []byte, []byte, []byte) {
|
||||
return sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Original), c.Description, schema),
|
||||
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Modified), c.Description, schema),
|
||||
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Current), c.Description, schema),
|
||||
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.ThreeWay), c.Description, schema),
|
||||
sortJsonOrFail(t, testObjectToJSONOrFail(t, c.Result), c.Description, schema)
|
||||
}
|
||||
|
||||
func threeWayRawTestCaseToJSONOrFail(t *testing.T, c StrategicMergePatchRawTestCase) ([]byte, []byte, []byte, []byte, []byte) {
|
||||
@ -6242,22 +6236,22 @@ func testPatchCreation(t *testing.T, expected, actual []byte, description string
|
||||
}
|
||||
}
|
||||
|
||||
func testPatchApplication(t *testing.T, original, patch, expected []byte, description, expectedError string) {
|
||||
result, err := StrategicMergePatch(original, patch, mergeItem)
|
||||
func testPatchApplication(t *testing.T, original, patch, expected []byte, description, expectedError string, schema LookupPatchMeta) {
|
||||
result, err := StrategicMergePatchUsingLookupPatchMeta(original, patch, schema)
|
||||
if len(expectedError) != 0 {
|
||||
if err != nil && strings.Contains(err.Error(), expectedError) {
|
||||
return
|
||||
}
|
||||
t.Errorf("expected error should contain:\n%s\nin test case: %s\nbut got:\n%s\n", expectedError, description, err)
|
||||
t.Errorf("using %s expected error should contain:\n%s\nin test case: %s\nbut got:\n%s\n", getSchemaType(schema), expectedError, description, err)
|
||||
}
|
||||
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))
|
||||
t.Errorf("using %s error: %s\nin test case: %s\ncannot apply patch:\n%s\nto original:\n%s\n",
|
||||
getSchemaType(schema), 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"
|
||||
format := "using 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))
|
||||
@ -6277,19 +6271,23 @@ func testObjectToJSONOrFail(t *testing.T, o map[string]interface{}) []byte {
|
||||
return j
|
||||
}
|
||||
|
||||
func sortJsonOrFail(t *testing.T, j []byte, description string) []byte {
|
||||
func sortJsonOrFail(t *testing.T, j []byte, description string, schema LookupPatchMeta) []byte {
|
||||
if j == nil {
|
||||
return nil
|
||||
}
|
||||
r, err := sortMergeListsByName(j, mergeItem)
|
||||
r, err := sortMergeListsByName(j, schema)
|
||||
if err != nil {
|
||||
t.Errorf("error: %s\nin test case: %s\ncannot sort object:\n%s\n", err, description, j)
|
||||
t.Errorf("using %s error: %s\n in test case: %s\ncannot sort object:\n%s\n", getSchemaType(schema), err, description, j)
|
||||
return nil
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func getSchemaType(schema LookupPatchMeta) string {
|
||||
return reflect.TypeOf(schema).String()
|
||||
}
|
||||
|
||||
func jsonToYAMLOrError(j []byte) string {
|
||||
y, err := jsonToYAML(j)
|
||||
if err != nil {
|
||||
@ -6336,14 +6334,17 @@ func yamlToJSONOrError(t *testing.T, y []byte) []byte {
|
||||
}
|
||||
|
||||
type PrecisionItem struct {
|
||||
Name string
|
||||
Int32 int32
|
||||
Int64 int64
|
||||
Float32 float32
|
||||
Float64 float64
|
||||
Name string `json:"name,omitempty"`
|
||||
Int32 int32 `json:"int32,omitempty"`
|
||||
Int64 int64 `json:"int64,omitempty"`
|
||||
Float32 float32 `json:"float32,omitempty"`
|
||||
Float64 float64 `json:"float64,omitempty"`
|
||||
}
|
||||
|
||||
var precisionItem PrecisionItem
|
||||
var (
|
||||
precisionItem PrecisionItem
|
||||
precisionItemStructSchema = PatchMetaFromStruct{T: GetTagStructTypeOrDie(precisionItem)}
|
||||
)
|
||||
|
||||
func TestNumberConversion(t *testing.T) {
|
||||
testcases := map[string]struct {
|
||||
@ -6396,25 +6397,35 @@ func TestNumberConversion(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for k, tc := range testcases {
|
||||
patch, err := CreateTwoWayMergePatch([]byte(tc.Old), []byte(tc.New), precisionItem)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error %v", k, err)
|
||||
continue
|
||||
}
|
||||
if tc.ExpectedPatch != string(patch) {
|
||||
t.Errorf("%s: expected %s, got %s", k, tc.ExpectedPatch, string(patch))
|
||||
continue
|
||||
}
|
||||
precisionItemOpenapiSchema := PatchMetaFromOpenAPI{
|
||||
Schema: sptest.GetSchemaOrDie(fakePrecisionItemSchema, "precisionItem"),
|
||||
}
|
||||
precisionItemSchemas := []LookupPatchMeta{
|
||||
precisionItemStructSchema,
|
||||
precisionItemOpenapiSchema,
|
||||
}
|
||||
|
||||
result, err := StrategicMergePatch([]byte(tc.Old), patch, precisionItem)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error %v", k, err)
|
||||
continue
|
||||
}
|
||||
if tc.ExpectedResult != string(result) {
|
||||
t.Errorf("%s: expected %s, got %s", k, tc.ExpectedResult, string(result))
|
||||
continue
|
||||
for _, schema := range precisionItemSchemas {
|
||||
for k, tc := range testcases {
|
||||
patch, err := CreateTwoWayMergePatchUsingLookupPatchMeta([]byte(tc.Old), []byte(tc.New), schema)
|
||||
if err != nil {
|
||||
t.Errorf("using %s in testcase %s: unexpected error %v", getSchemaType(schema), k, err)
|
||||
continue
|
||||
}
|
||||
if tc.ExpectedPatch != string(patch) {
|
||||
t.Errorf("using %s in testcase %s: expected %s, got %s", getSchemaType(schema), k, tc.ExpectedPatch, string(patch))
|
||||
continue
|
||||
}
|
||||
|
||||
result, err := StrategicMergePatchUsingLookupPatchMeta([]byte(tc.Old), patch, schema)
|
||||
if err != nil {
|
||||
t.Errorf("using %s in testcase %s: unexpected error %v", getSchemaType(schema), k, err)
|
||||
continue
|
||||
}
|
||||
if tc.ExpectedResult != string(result) {
|
||||
t.Errorf("using %s in testcase %s: expected %s, got %s", getSchemaType(schema), k, tc.ExpectedResult, string(result))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6437,7 +6448,7 @@ replacingItem:
|
||||
name: my-object
|
||||
value: some-value
|
||||
other: current-other
|
||||
merginglist:
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
- name: 3
|
||||
@ -6451,7 +6462,7 @@ replacingItem:
|
||||
name: my-object
|
||||
value: some-value
|
||||
other: current-other
|
||||
merginglist:
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
- name: 3
|
||||
@ -6461,7 +6472,7 @@ replacingItem:
|
||||
The: RawExtension
|
||||
`),
|
||||
TwoWay: []byte(`
|
||||
merginglist:
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
- name: 3
|
||||
@ -6474,7 +6485,7 @@ replacingItem:
|
||||
name: my-object
|
||||
value: some-value
|
||||
other: current-other
|
||||
merginglist:
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
- name: 3
|
||||
@ -6493,7 +6504,7 @@ replacingItem:
|
||||
name: my-object
|
||||
value: some-value
|
||||
other: current-other
|
||||
merginglist:
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
- name: 3
|
||||
@ -6511,7 +6522,7 @@ replacingItem:
|
||||
name: my-object
|
||||
value: some-value
|
||||
other: current-other
|
||||
merginglist:
|
||||
mergingList:
|
||||
- name: 1
|
||||
replacingItem:
|
||||
Some: Generic
|
||||
@ -6523,7 +6534,7 @@ replacingItem:
|
||||
name: my-object
|
||||
value: some-value
|
||||
other: current-other
|
||||
merginglist:
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 3
|
||||
replacingItem:
|
||||
@ -6536,7 +6547,7 @@ replacingItem:
|
||||
name: my-object
|
||||
value: some-value
|
||||
other: current-other
|
||||
merginglist:
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
replacingItem:
|
||||
@ -6545,10 +6556,10 @@ replacingItem:
|
||||
The: RawExtension
|
||||
`),
|
||||
TwoWay: []byte(`
|
||||
$setElementOrder/merginglist:
|
||||
$setElementOrder/mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
merginglist:
|
||||
mergingList:
|
||||
- name: 2
|
||||
replacingItem:
|
||||
Newly: Modified
|
||||
@ -6559,7 +6570,7 @@ replacingItem:
|
||||
name: my-object
|
||||
value: some-value
|
||||
other: current-other
|
||||
merginglist:
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
replacingItem:
|
||||
@ -6568,10 +6579,10 @@ replacingItem:
|
||||
The: RawExtension
|
||||
`),
|
||||
ThreeWay: []byte(`
|
||||
$setElementOrder/merginglist:
|
||||
$setElementOrder/mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
merginglist:
|
||||
mergingList:
|
||||
- name: 2
|
||||
replacingItem:
|
||||
Newly: Modified
|
||||
@ -6582,7 +6593,7 @@ replacingItem:
|
||||
name: my-object
|
||||
value: some-value
|
||||
other: current-other
|
||||
merginglist:
|
||||
mergingList:
|
||||
- name: 1
|
||||
- name: 2
|
||||
- name: 3
|
||||
@ -6596,9 +6607,19 @@ replacingItem:
|
||||
}
|
||||
|
||||
func TestReplaceWithRawExtension(t *testing.T) {
|
||||
for _, c := range replaceRawExtensionPatchTestCases {
|
||||
testTwoWayPatchForRawTestCase(t, c)
|
||||
testThreeWayPatchForRawTestCase(t, c)
|
||||
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
|
||||
Schema: sptest.GetSchemaOrDie(fakeMergeItemSchema, "mergeItem"),
|
||||
}
|
||||
schemas := []LookupPatchMeta{
|
||||
mergeItemStructSchema,
|
||||
mergeItemOpenapiSchema,
|
||||
}
|
||||
|
||||
for _, schema := range schemas {
|
||||
for _, c := range replaceRawExtensionPatchTestCases {
|
||||
testTwoWayPatchForRawTestCase(t, c, schema)
|
||||
testThreeWayPatchForRawTestCase(t, c, schema)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6658,60 +6679,70 @@ func TestUnknownField(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
mergeItemOpenapiSchema := PatchMetaFromOpenAPI{
|
||||
Schema: sptest.GetSchemaOrDie(fakeMergeItemSchema, "mergeItem"),
|
||||
}
|
||||
schemas := []LookupPatchMeta{
|
||||
mergeItemStructSchema,
|
||||
mergeItemOpenapiSchema,
|
||||
}
|
||||
|
||||
for _, k := range sets.StringKeySet(testcases).List() {
|
||||
tc := testcases[k]
|
||||
func() {
|
||||
twoWay, err := CreateTwoWayMergePatch([]byte(tc.Original), []byte(tc.Modified), &MergeItem{})
|
||||
if err != nil {
|
||||
if len(tc.ExpectedTwoWayErr) == 0 {
|
||||
t.Errorf("%s: error making two-way patch: %v", k, err)
|
||||
for _, schema := range schemas {
|
||||
func() {
|
||||
twoWay, err := CreateTwoWayMergePatchUsingLookupPatchMeta([]byte(tc.Original), []byte(tc.Modified), schema)
|
||||
if err != nil {
|
||||
if len(tc.ExpectedTwoWayErr) == 0 {
|
||||
t.Errorf("using %s in testcase %s: error making two-way patch: %v", getSchemaType(schema), k, err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.ExpectedTwoWayErr) {
|
||||
t.Errorf("using %s in testcase %s: expected error making two-way patch to contain '%s', got %s", getSchemaType(schema), k, tc.ExpectedTwoWayErr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.ExpectedTwoWayErr) {
|
||||
t.Errorf("%s: expected error making two-way patch to contain '%s', got %s", k, tc.ExpectedTwoWayErr, err)
|
||||
|
||||
if string(twoWay) != tc.ExpectedTwoWay {
|
||||
t.Errorf("using %s in testcase %s: expected two-way patch:\n\t%s\ngot\n\t%s", getSchemaType(schema), k, string(tc.ExpectedTwoWay), string(twoWay))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if string(twoWay) != tc.ExpectedTwoWay {
|
||||
t.Errorf("%s: expected two-way patch:\n\t%s\ngot\n\t%s", k, string(tc.ExpectedTwoWay), string(twoWay))
|
||||
return
|
||||
}
|
||||
|
||||
twoWayResult, err := StrategicMergePatch([]byte(tc.Original), twoWay, MergeItem{})
|
||||
if err != nil {
|
||||
t.Errorf("%s: error applying two-way patch: %v", k, err)
|
||||
return
|
||||
}
|
||||
if string(twoWayResult) != tc.ExpectedTwoWayResult {
|
||||
t.Errorf("%s: expected two-way result:\n\t%s\ngot\n\t%s", k, string(tc.ExpectedTwoWayResult), string(twoWayResult))
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
func() {
|
||||
threeWay, err := CreateThreeWayMergePatch([]byte(tc.Original), []byte(tc.Modified), []byte(tc.Current), &MergeItem{}, false)
|
||||
if err != nil {
|
||||
if len(tc.ExpectedThreeWayErr) == 0 {
|
||||
t.Errorf("%s: error making three-way patch: %v", k, err)
|
||||
} else if !strings.Contains(err.Error(), tc.ExpectedThreeWayErr) {
|
||||
t.Errorf("%s: expected error making three-way patch to contain '%s', got %s", k, tc.ExpectedThreeWayErr, err)
|
||||
twoWayResult, err := StrategicMergePatchUsingLookupPatchMeta([]byte(tc.Original), twoWay, schema)
|
||||
if err != nil {
|
||||
t.Errorf("using %s in testcase %s: error applying two-way patch: %v", getSchemaType(schema), k, err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if string(twoWayResult) != tc.ExpectedTwoWayResult {
|
||||
t.Errorf("using %s in testcase %s: expected two-way result:\n\t%s\ngot\n\t%s", getSchemaType(schema), k, string(tc.ExpectedTwoWayResult), string(twoWayResult))
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
if string(threeWay) != tc.ExpectedThreeWay {
|
||||
t.Errorf("%s: expected three-way patch:\n\t%s\ngot\n\t%s", k, string(tc.ExpectedThreeWay), string(threeWay))
|
||||
return
|
||||
}
|
||||
func() {
|
||||
threeWay, err := CreateThreeWayMergePatch([]byte(tc.Original), []byte(tc.Modified), []byte(tc.Current), schema, false)
|
||||
if err != nil {
|
||||
if len(tc.ExpectedThreeWayErr) == 0 {
|
||||
t.Errorf("using %s in testcase %s: error making three-way patch: %v", getSchemaType(schema), k, err)
|
||||
} else if !strings.Contains(err.Error(), tc.ExpectedThreeWayErr) {
|
||||
t.Errorf("using %s in testcase %s: expected error making three-way patch to contain '%s', got %s", getSchemaType(schema), k, tc.ExpectedThreeWayErr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
threeWayResult, err := StrategicMergePatch([]byte(tc.Current), threeWay, MergeItem{})
|
||||
if err != nil {
|
||||
t.Errorf("%s: error applying three-way patch: %v", k, err)
|
||||
return
|
||||
} else if string(threeWayResult) != tc.ExpectedThreeWayResult {
|
||||
t.Errorf("%s: expected three-way result:\n\t%s\ngot\n\t%s", k, string(tc.ExpectedThreeWayResult), string(threeWayResult))
|
||||
return
|
||||
}
|
||||
}()
|
||||
if string(threeWay) != tc.ExpectedThreeWay {
|
||||
t.Errorf("using %s in testcase %s: expected three-way patch:\n\t%s\ngot\n\t%s", getSchemaType(schema), k, string(tc.ExpectedThreeWay), string(threeWay))
|
||||
return
|
||||
}
|
||||
|
||||
threeWayResult, err := StrategicMergePatch([]byte(tc.Current), threeWay, schema)
|
||||
if err != nil {
|
||||
t.Errorf("using %s in testcase %s: error applying three-way patch: %v", getSchemaType(schema), k, err)
|
||||
return
|
||||
} else if string(threeWayResult) != tc.ExpectedThreeWayResult {
|
||||
t.Errorf("using %s in testcase %s: expected three-way result:\n\t%s\ngot\n\t%s", getSchemaType(schema), k, string(tc.ExpectedThreeWayResult), string(threeWayResult))
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
170
staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/testdata/swagger-merge-item.json
vendored
Normal file
170
staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/testdata/swagger-merge-item.json
vendored
Normal file
@ -0,0 +1,170 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "StrategicMergePatchTestingMergeItem",
|
||||
"version": "v1.9.0"
|
||||
},
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"mergeItem": {
|
||||
"description": "MergeItem is type definition for testing strategic merge.",
|
||||
"required": [],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name field.",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"description": "Value field.",
|
||||
"type": "string"
|
||||
},
|
||||
"other": {
|
||||
"description": "Other field.",
|
||||
"type": "string"
|
||||
},
|
||||
"mergingList": {
|
||||
"description": "MergingList field.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/mergeItem"
|
||||
},
|
||||
"x-kubernetes-patch-merge-key": "name",
|
||||
"x-kubernetes-patch-strategy": "merge"
|
||||
},
|
||||
"nonMergingList": {
|
||||
"description": "NonMergingList field.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/mergeItem"
|
||||
}
|
||||
},
|
||||
"mergingIntList": {
|
||||
"description": "MergingIntList field.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"x-kubernetes-patch-strategy": "merge"
|
||||
},
|
||||
"nonMergingIntList": {
|
||||
"description": "NonMergingIntList field.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
},
|
||||
"mergeItemPtr": {
|
||||
"description": "MergeItemPtr field.",
|
||||
"$ref": "#/definitions/mergeItem",
|
||||
"x-kubernetes-patch-merge-key": "name",
|
||||
"x-kubernetes-patch-strategy": "merge"
|
||||
},
|
||||
"simpleMap": {
|
||||
"description": "SimpleMap field.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"replacingItem": {
|
||||
"description": "ReplacingItem field.",
|
||||
"$ref": "#/definitions/io.k8s.apimachinery.pkg.runtime.RawExtension",
|
||||
"x-kubernetes-patch-strategy": "replace"
|
||||
},
|
||||
"retainKeysMap": {
|
||||
"description": "RetainKeysMap field.",
|
||||
"$ref": "#/definitions/retainKeysMergeItem",
|
||||
"x-kubernetes-patch-strategy": "retainKeys"
|
||||
},
|
||||
"retainKeysMergingList": {
|
||||
"description": "RetainKeysMergingList field.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/mergeItem"
|
||||
},
|
||||
"x-kubernetes-patch-merge-key": "name",
|
||||
"x-kubernetes-patch-strategy": "merge,retainKeys"
|
||||
}
|
||||
},
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "fake-group",
|
||||
"kind": "mergeItem",
|
||||
"version": "some-version"
|
||||
}
|
||||
]
|
||||
},
|
||||
"retainKeysMergeItem": {
|
||||
"description": "RetainKeysMergeItem is type definition for testing strategic merge.",
|
||||
"required": [],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name field.",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"description": "Value field.",
|
||||
"type": "string"
|
||||
},
|
||||
"other": {
|
||||
"description": "Other field.",
|
||||
"type": "string"
|
||||
},
|
||||
"simpleMap": {
|
||||
"description": "SimpleMap field.",
|
||||
"additionalProperties": "object",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"mergingList": {
|
||||
"description": "MergingList field.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/mergeItem"
|
||||
},
|
||||
"x-kubernetes-patch-merge-key": "name",
|
||||
"x-kubernetes-patch-strategy": "merge"
|
||||
},
|
||||
"nonMergingList": {
|
||||
"description": "NonMergingList field.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/mergeItem"
|
||||
}
|
||||
},
|
||||
"mergingIntList": {
|
||||
"description": "MergingIntList field.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"x-kubernetes-patch-strategy": "merge"
|
||||
}
|
||||
},
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "fake-group",
|
||||
"kind": "retainKeysMergeItem",
|
||||
"version": "some-version"
|
||||
}
|
||||
]
|
||||
},
|
||||
"io.k8s.apimachinery.pkg.runtime.RawExtension": {
|
||||
"description": "RawExtension is used to hold extensions in external versions.",
|
||||
"required": [
|
||||
"Raw"
|
||||
],
|
||||
"properties": {
|
||||
"Raw": {
|
||||
"description": "Raw is the underlying serialization of this object.",
|
||||
"type": "string",
|
||||
"format": "byte"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
47
staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/testdata/swagger-precision-item.json
vendored
Normal file
47
staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/testdata/swagger-precision-item.json
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "StrategicMergePatchTestingPrecisionItem",
|
||||
"version": "v1.9.0"
|
||||
},
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"precisionItem": {
|
||||
"description": "PrecisionItem is type definition for testing strategic merge.",
|
||||
"required": [],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name field.",
|
||||
"type": "string"
|
||||
},
|
||||
"int32": {
|
||||
"description": "Int32 field.",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"int64": {
|
||||
"description": "Int64 field.",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"float32": {
|
||||
"description": "Float32 field.",
|
||||
"type": "number",
|
||||
"format": "float32"
|
||||
},
|
||||
"float64": {
|
||||
"description": "Float64 field.",
|
||||
"type": "number",
|
||||
"format": "float64"
|
||||
}
|
||||
},
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "fake-group",
|
||||
"kind": "precisionItem",
|
||||
"version": "some-version"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["openapi.go"],
|
||||
importpath = "k8s.io/apimachinery/pkg/util/strategicpatch/testing",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
|
||||
"//vendor/github.com/googleapis/gnostic/compiler:go_default_library",
|
||||
"//vendor/gopkg.in/yaml.v2:go_default_library",
|
||||
"//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
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 testing
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/googleapis/gnostic/OpenAPIv2"
|
||||
"github.com/googleapis/gnostic/compiler"
|
||||
openapi "k8s.io/kube-openapi/pkg/util/proto"
|
||||
)
|
||||
|
||||
// Fake opens and returns a openapi swagger from a file Path. It will
|
||||
// parse only once and then return the same copy everytime.
|
||||
type Fake struct {
|
||||
Path string
|
||||
|
||||
once sync.Once
|
||||
document *openapi_v2.Document
|
||||
err error
|
||||
}
|
||||
|
||||
// OpenAPISchema returns the openapi document and a potential error.
|
||||
func (f *Fake) OpenAPISchema() (*openapi_v2.Document, error) {
|
||||
f.once.Do(func() {
|
||||
_, err := os.Stat(f.Path)
|
||||
if err != nil {
|
||||
f.err = err
|
||||
return
|
||||
}
|
||||
spec, err := ioutil.ReadFile(f.Path)
|
||||
if err != nil {
|
||||
f.err = err
|
||||
return
|
||||
}
|
||||
var info yaml.MapSlice
|
||||
err = yaml.Unmarshal(spec, &info)
|
||||
if err != nil {
|
||||
f.err = err
|
||||
return
|
||||
}
|
||||
f.document, f.err = openapi_v2.NewDocument(info, compiler.NewContext("$root", nil))
|
||||
})
|
||||
return f.document, f.err
|
||||
}
|
||||
|
||||
func getSchema(f Fake, model string) (openapi.Schema, error) {
|
||||
s, err := f.OpenAPISchema()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m, err := openapi.NewOpenAPIData(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.LookupModel(model), nil
|
||||
}
|
||||
|
||||
// GetSchemaOrDie returns returns the openapi schema.
|
||||
func GetSchemaOrDie(f Fake, model string) openapi.Schema {
|
||||
s, err := getSchema(f, model)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s
|
||||
}
|
193
staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/types.go
Normal file
193
staging/src/k8s.io/apimachinery/pkg/util/strategicpatch/types.go
Normal file
@ -0,0 +1,193 @@
|
||||
/*
|
||||
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 strategicpatch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||
openapi "k8s.io/kube-openapi/pkg/util/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
patchStrategyOpenapiextensionKey = "x-kubernetes-patch-strategy"
|
||||
patchMergeKeyOpenapiextensionKey = "x-kubernetes-patch-merge-key"
|
||||
)
|
||||
|
||||
type LookupPatchItem interface {
|
||||
openapi.SchemaVisitor
|
||||
|
||||
Error() error
|
||||
Path() *openapi.Path
|
||||
}
|
||||
|
||||
type kindItem struct {
|
||||
key string
|
||||
path *openapi.Path
|
||||
err error
|
||||
patchmeta PatchMeta
|
||||
subschema openapi.Schema
|
||||
hasVisitKind bool
|
||||
}
|
||||
|
||||
func NewKindItem(key string, path *openapi.Path) *kindItem {
|
||||
return &kindItem{
|
||||
key: key,
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
var _ LookupPatchItem = &kindItem{}
|
||||
|
||||
func (item *kindItem) Error() error {
|
||||
return item.err
|
||||
}
|
||||
|
||||
func (item *kindItem) Path() *openapi.Path {
|
||||
return item.path
|
||||
}
|
||||
|
||||
func (item *kindItem) VisitPrimitive(schema *openapi.Primitive) {
|
||||
item.err = errors.New("expected kind, but got primitive")
|
||||
}
|
||||
|
||||
func (item *kindItem) VisitArray(schema *openapi.Array) {
|
||||
item.err = errors.New("expected kind, but got slice")
|
||||
}
|
||||
|
||||
func (item *kindItem) VisitMap(schema *openapi.Map) {
|
||||
item.err = errors.New("expected kind, but got map")
|
||||
}
|
||||
|
||||
func (item *kindItem) VisitReference(schema openapi.Reference) {
|
||||
if !item.hasVisitKind {
|
||||
schema.SubSchema().Accept(item)
|
||||
}
|
||||
}
|
||||
|
||||
func (item *kindItem) VisitKind(schema *openapi.Kind) {
|
||||
subschema, ok := schema.Fields[item.key]
|
||||
if !ok {
|
||||
item.err = FieldNotFoundError{Path: schema.GetPath().String(), Field: item.key}
|
||||
return
|
||||
}
|
||||
|
||||
mergeKey, patchStrategies, err := parsePatchMetadata(subschema.GetExtensions())
|
||||
if err != nil {
|
||||
item.err = err
|
||||
return
|
||||
}
|
||||
item.patchmeta = PatchMeta{
|
||||
patchStrategies: patchStrategies,
|
||||
patchMergeKey: mergeKey,
|
||||
}
|
||||
item.subschema = subschema
|
||||
}
|
||||
|
||||
type sliceItem struct {
|
||||
key string
|
||||
path *openapi.Path
|
||||
err error
|
||||
patchmeta PatchMeta
|
||||
subschema openapi.Schema
|
||||
hasVisitKind bool
|
||||
}
|
||||
|
||||
func NewSliceItem(key string, path *openapi.Path) *sliceItem {
|
||||
return &sliceItem{
|
||||
key: key,
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
var _ LookupPatchItem = &sliceItem{}
|
||||
|
||||
func (item *sliceItem) Error() error {
|
||||
return item.err
|
||||
}
|
||||
|
||||
func (item *sliceItem) Path() *openapi.Path {
|
||||
return item.path
|
||||
}
|
||||
|
||||
func (item *sliceItem) VisitPrimitive(schema *openapi.Primitive) {
|
||||
item.err = errors.New("expected slice, but got primitive")
|
||||
}
|
||||
|
||||
func (item *sliceItem) VisitArray(schema *openapi.Array) {
|
||||
if !item.hasVisitKind {
|
||||
item.err = errors.New("expected visit kind first, then visit array")
|
||||
}
|
||||
subschema := schema.SubType
|
||||
item.subschema = subschema
|
||||
}
|
||||
|
||||
func (item *sliceItem) VisitMap(schema *openapi.Map) {
|
||||
item.err = errors.New("expected slice, but got map")
|
||||
}
|
||||
|
||||
func (item *sliceItem) VisitReference(schema openapi.Reference) {
|
||||
if !item.hasVisitKind {
|
||||
schema.SubSchema().Accept(item)
|
||||
} else {
|
||||
item.subschema = schema.SubSchema()
|
||||
}
|
||||
}
|
||||
|
||||
func (item *sliceItem) VisitKind(schema *openapi.Kind) {
|
||||
subschema, ok := schema.Fields[item.key]
|
||||
if !ok {
|
||||
item.err = FieldNotFoundError{Path: schema.GetPath().String(), Field: item.key}
|
||||
return
|
||||
}
|
||||
|
||||
mergeKey, patchStrategies, err := parsePatchMetadata(subschema.GetExtensions())
|
||||
if err != nil {
|
||||
item.err = err
|
||||
return
|
||||
}
|
||||
item.patchmeta = PatchMeta{
|
||||
patchStrategies: patchStrategies,
|
||||
patchMergeKey: mergeKey,
|
||||
}
|
||||
item.hasVisitKind = true
|
||||
subschema.Accept(item)
|
||||
}
|
||||
|
||||
func parsePatchMetadata(extensions map[string]interface{}) (string, []string, error) {
|
||||
ps, foundPS := extensions[patchStrategyOpenapiextensionKey]
|
||||
var patchStrategies []string
|
||||
var mergeKey, patchStrategy string
|
||||
var ok bool
|
||||
if foundPS {
|
||||
patchStrategy, ok = ps.(string)
|
||||
if ok {
|
||||
patchStrategies = strings.Split(patchStrategy, ",")
|
||||
} else {
|
||||
return "", nil, mergepatch.ErrBadArgType(patchStrategy, ps)
|
||||
}
|
||||
}
|
||||
mk, foundMK := extensions[patchMergeKeyOpenapiextensionKey]
|
||||
if foundMK {
|
||||
mergeKey, ok = mk.(string)
|
||||
if !ok {
|
||||
return "", nil, mergepatch.ErrBadArgType(mergeKey, mk)
|
||||
}
|
||||
}
|
||||
return mergeKey, patchStrategies, nil
|
||||
}
|
@ -26,17 +26,14 @@ const (
|
||||
// struct field given the struct type and the JSON name of the field.
|
||||
// It returns field type, a slice of patch strategies, merge key and error.
|
||||
// TODO: fix the returned errors to be introspectable.
|
||||
func LookupPatchMetadata(t reflect.Type, jsonField string) (
|
||||
func LookupPatchMetadataForStruct(t reflect.Type, jsonField string) (
|
||||
elemType reflect.Type, patchStrategies []string, patchMergeKey string, e error) {
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t.Kind() == reflect.Map {
|
||||
elemType = t.Elem()
|
||||
return
|
||||
}
|
||||
|
||||
if t.Kind() != reflect.Struct {
|
||||
e = fmt.Errorf("merging an object in json but data type is not map or struct, instead is: %s",
|
||||
e = fmt.Errorf("merging an object in json but data type is not struct, instead is: %s",
|
||||
t.Kind().String())
|
||||
return
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ func TestLookupPtrToStruct(t *testing.T) {
|
||||
Inner []Elem `json:"inner" patchStrategy:"merge" patchMergeKey:"key"`
|
||||
}
|
||||
outer := &Outer{}
|
||||
elemType, patchStrategies, patchMergeKey, err := LookupPatchMetadata(reflect.TypeOf(outer), "inner")
|
||||
elemType, patchStrategies, patchMergeKey, err := LookupPatchMetadataForStruct(reflect.TypeOf(outer), "inner")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user