diff --git a/pkg/util/strategicpatch/meta.go b/pkg/util/strategicpatch/meta.go index df305b712..85b0cfc07 100644 --- a/pkg/util/strategicpatch/meta.go +++ b/pkg/util/strategicpatch/meta.go @@ -20,12 +20,17 @@ import ( "errors" "fmt" "reflect" + "strings" "k8s.io/apimachinery/pkg/util/mergepatch" forkedjson "k8s.io/apimachinery/third_party/forked/golang/json" openapi "k8s.io/kube-openapi/pkg/util/proto" + "k8s.io/kube-openapi/pkg/validation/spec" ) +const patchMergeKey = "x-kubernetes-patch-merge-key" +const patchStrategy = "x-kubernetes-patch-strategy" + type PatchMeta struct { patchStrategies []string patchMergeKey string @@ -148,6 +153,90 @@ func GetTagStructTypeOrDie(dataStruct interface{}) reflect.Type { return t } +type PatchMetaFromOpenAPIV3 struct { + // SchemaList is required to resolve OpenAPI V3 references + SchemaList map[string]*spec.Schema + Schema *spec.Schema +} + +func (s PatchMetaFromOpenAPIV3) traverse(key string) (PatchMetaFromOpenAPIV3, error) { + if s.Schema == nil { + return PatchMetaFromOpenAPIV3{}, nil + } + if len(s.Schema.Properties) == 0 { + return PatchMetaFromOpenAPIV3{}, fmt.Errorf("unable to find api field \"%s\"", key) + } + subschema, ok := s.Schema.Properties[key] + if !ok { + return PatchMetaFromOpenAPIV3{}, fmt.Errorf("unable to find api field \"%s\"", key) + } + return PatchMetaFromOpenAPIV3{SchemaList: s.SchemaList, Schema: &subschema}, nil +} + +func resolve(l *PatchMetaFromOpenAPIV3) error { + if len(l.Schema.AllOf) > 0 { + l.Schema = &l.Schema.AllOf[0] + } + if refString := l.Schema.Ref.String(); refString != "" { + str := strings.TrimPrefix(refString, "#/components/schemas/") + sch, ok := l.SchemaList[str] + if ok { + l.Schema = sch + } else { + return fmt.Errorf("unable to resolve %s in OpenAPI V3", refString) + } + } + return nil +} + +func (s PatchMetaFromOpenAPIV3) LookupPatchMetadataForStruct(key string) (LookupPatchMeta, PatchMeta, error) { + l, err := s.traverse(key) + if err != nil { + return l, PatchMeta{}, err + } + p := PatchMeta{} + f, ok := l.Schema.Extensions[patchMergeKey] + if ok { + p.SetPatchMergeKey(f.(string)) + } + g, ok := l.Schema.Extensions[patchStrategy] + if ok { + p.SetPatchStrategies(strings.Split(g.(string), ",")) + } + + err = resolve(&l) + return l, p, err +} + +func (s PatchMetaFromOpenAPIV3) LookupPatchMetadataForSlice(key string) (LookupPatchMeta, PatchMeta, error) { + l, err := s.traverse(key) + if err != nil { + return l, PatchMeta{}, err + } + p := PatchMeta{} + f, ok := l.Schema.Extensions[patchMergeKey] + if ok { + p.SetPatchMergeKey(f.(string)) + } + g, ok := l.Schema.Extensions[patchStrategy] + if ok { + p.SetPatchStrategies(strings.Split(g.(string), ",")) + } + if l.Schema.Items != nil { + l.Schema = l.Schema.Items.Schema + } + err = resolve(&l) + return l, p, err +} + +func (s PatchMetaFromOpenAPIV3) Name() string { + schema := s.Schema + if len(schema.Type) > 0 { + return strings.Join(schema.Type, "") + } + return "Struct" +} + type PatchMetaFromOpenAPI struct { Schema openapi.Schema } diff --git a/pkg/util/strategicpatch/patch_test.go b/pkg/util/strategicpatch/patch_test.go index 80b8c30ab..0192a2e93 100644 --- a/pkg/util/strategicpatch/patch_test.go +++ b/pkg/util/strategicpatch/patch_test.go @@ -36,6 +36,9 @@ import ( var ( fakeMergeItemSchema = sptest.Fake{Path: filepath.Join("testdata", "swagger-merge-item.json")} fakePrecisionItemSchema = sptest.Fake{Path: filepath.Join("testdata", "swagger-precision-item.json")} + + fakeMergeItemV3Schema = sptest.OpenAPIV3Getter{Path: filepath.Join("testdata", "swagger-merge-item-v3.json")} + fakePrecisionItemV3Schema = sptest.OpenAPIV3Getter{Path: filepath.Join("testdata", "swagger-precision-item-v3.json")} ) type SortMergeListTestCases struct { @@ -284,9 +287,14 @@ func TestSortMergeLists(t *testing.T) { mergeItemOpenapiSchema := PatchMetaFromOpenAPI{ Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"), } + mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{ + SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas, + Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"], + } schemas := []LookupPatchMeta{ mergeItemStructSchema, mergeItemOpenapiSchema, + mergeItemOpenapiV3Schema, } tc := SortMergeListTestCases{} @@ -766,9 +774,14 @@ func TestCustomStrategicMergePatch(t *testing.T) { mergeItemOpenapiSchema := PatchMetaFromOpenAPI{ Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"), } + mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{ + SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas, + Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"], + } schemas := []LookupPatchMeta{ mergeItemStructSchema, mergeItemOpenapiSchema, + mergeItemOpenapiV3Schema, } tc := StrategicMergePatchTestCases{} @@ -6169,9 +6182,14 @@ func TestStrategicMergePatch(t *testing.T) { mergeItemOpenapiSchema := PatchMetaFromOpenAPI{ Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"), } + mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{ + SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas, + Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"], + } schemas := []LookupPatchMeta{ mergeItemStructSchema, mergeItemOpenapiSchema, + mergeItemOpenapiV3Schema, } tc := StrategicMergePatchTestCases{} @@ -6564,9 +6582,14 @@ func TestNumberConversion(t *testing.T) { precisionItemOpenapiSchema := PatchMetaFromOpenAPI{ Schema: sptest.GetSchemaOrDie(&fakePrecisionItemSchema, "precisionItem"), } + precisionItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{ + SchemaList: fakePrecisionItemV3Schema.SchemaOrDie().Components.Schemas, + Schema: fakePrecisionItemV3Schema.SchemaOrDie().Components.Schemas["precisionItem"], + } precisionItemSchemas := []LookupPatchMeta{ precisionItemStructSchema, precisionItemOpenapiSchema, + precisionItemOpenapiV3Schema, } for _, schema := range precisionItemSchemas { @@ -6774,9 +6797,14 @@ func TestReplaceWithRawExtension(t *testing.T) { mergeItemOpenapiSchema := PatchMetaFromOpenAPI{ Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"), } + mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{ + SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas, + Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"], + } schemas := []LookupPatchMeta{ mergeItemStructSchema, mergeItemOpenapiSchema, + mergeItemOpenapiV3Schema, } for _, schema := range schemas { @@ -6946,9 +6974,14 @@ func TestUnknownField(t *testing.T) { mergeItemOpenapiSchema := PatchMetaFromOpenAPI{ Schema: sptest.GetSchemaOrDie(&fakeMergeItemSchema, "mergeItem"), } + mergeItemOpenapiV3Schema := PatchMetaFromOpenAPIV3{ + SchemaList: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas, + Schema: fakeMergeItemV3Schema.SchemaOrDie().Components.Schemas["mergeItem"], + } schemas := []LookupPatchMeta{ mergeItemStructSchema, mergeItemOpenapiSchema, + mergeItemOpenapiV3Schema, } for _, k := range sets.StringKeySet(testcases).List() { diff --git a/pkg/util/strategicpatch/testdata/swagger-merge-item-v3.json b/pkg/util/strategicpatch/testdata/swagger-merge-item-v3.json new file mode 100644 index 000000000..7c66ba813 --- /dev/null +++ b/pkg/util/strategicpatch/testdata/swagger-merge-item-v3.json @@ -0,0 +1,180 @@ +{ + "openapi": "3.0", + "info": { + "title": "StrategicMergePatchTestingMergeItem", + "version": "v3.0" + }, + "paths": {}, + "components": { + "schemas": { + "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": { + "default": {}, + "allOf": [ + {"$ref": "#/components/schemas/mergeItem"} + ] + }, + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "nonMergingList": { + "description": "NonMergingList field.", + "type": "array", + "items": { + "$ref": "#/components/schemas/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.", + "allOf": [ + {"$ref": "#/components/schemas/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.", + "allOf": [ + {"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.runtime.RawExtension"} + ], + "x-kubernetes-patch-strategy": "replace" + }, + "retainKeysMap": { + "description": "RetainKeysMap field.", + "allOf": [ + {"$ref": "#/components/schemas/retainKeysMergeItem"} + ], + "x-kubernetes-patch-strategy": "retainKeys" + }, + "retainKeysMergingList": { + "description": "RetainKeysMergingList field.", + "type": "array", + "items": { + "$ref": "#/components/schemas/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.", + "items": { + "type": "string" + } + }, + "mergingList": { + "description": "MergingList field.", + "type": "array", + "items": { + "$ref": "#/components/schemas/mergeItem" + }, + "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge" + }, + "nonMergingList": { + "description": "NonMergingList field.", + "type": "array", + "items": { + "$ref": "#/components/schemas/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" + } + } + } + } + } +} diff --git a/pkg/util/strategicpatch/testdata/swagger-precision-item-v3.json b/pkg/util/strategicpatch/testdata/swagger-precision-item-v3.json new file mode 100644 index 000000000..0101c4102 --- /dev/null +++ b/pkg/util/strategicpatch/testdata/swagger-precision-item-v3.json @@ -0,0 +1,49 @@ +{ + "openapi": "3.0", + "info": { + "title": "StrategicMergePatchTestingPrecisionItem", + "version": "v1.9.0" + }, + "paths": {}, + "components": { + "schemas": { + "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" + } + ] + } + } + } +} \ No newline at end of file diff --git a/pkg/util/strategicpatch/testing/openapi3.go b/pkg/util/strategicpatch/testing/openapi3.go new file mode 100644 index 000000000..895522b31 --- /dev/null +++ b/pkg/util/strategicpatch/testing/openapi3.go @@ -0,0 +1,65 @@ +/* +Copyright 2023 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 ( + "os" + "sync" + + "k8s.io/kube-openapi/pkg/spec3" +) + +type OpenAPIV3Getter struct { + Path string + once sync.Once + bytes []byte + openapiv3 spec3.OpenAPI +} + +func (f *OpenAPIV3Getter) SchemaBytesOrDie() []byte { + f.once.Do(func() { + _, err := os.Stat(f.Path) + if err != nil { + panic(err) + } + spec, err := os.ReadFile(f.Path) + if err != nil { + panic(err) + } + f.bytes = spec + }) + return f.bytes +} + +func (f *OpenAPIV3Getter) SchemaOrDie() *spec3.OpenAPI { + f.once.Do(func() { + _, err := os.Stat(f.Path) + if err != nil { + panic(err) + } + spec, err := os.ReadFile(f.Path) + if err != nil { + panic(err) + } + + err = f.openapiv3.UnmarshalJSON(spec) + if err != nil { + panic(err) + } + }) + return &f.openapiv3 +}