diff --git a/pkg/generators/extension.go b/pkg/generators/extension.go index 14eab18f6..9c3b5c4fd 100644 --- a/pkg/generators/extension.go +++ b/pkg/generators/extension.go @@ -54,6 +54,16 @@ var tagToExtension = map[string]extensionAttributes{ kind: types.Slice, allowedValues: sets.NewString("atomic", "set", "map"), }, + "mapType": { + xName: "x-kubernetes-map-type", + kind: types.Map, + allowedValues: sets.NewString("atomic", "granular"), + }, + "structType": { + xName: "x-kubernetes-map-type", + kind: types.Struct, + allowedValues: sets.NewString("atomic", "granular"), + }, } // Extension encapsulates information necessary to generate an OpenAPI extension. diff --git a/pkg/generators/extension_test.go b/pkg/generators/extension_test.go index d1214bd9a..7c2c0dff1 100644 --- a/pkg/generators/extension_test.go +++ b/pkg/generators/extension_test.go @@ -58,6 +58,12 @@ func TestSingleTagExtension(t *testing.T) { extensionName: "x-kubernetes-list-map-keys", extensionValues: []string{"port"}, }, + { + comments: []string{"+mapType=granular"}, + extensionTag: "mapType", + extensionName: "x-kubernetes-map-type", + extensionValues: []string{"granular"}, + }, { comments: []string{"+k8s:openapi-gen=x-kubernetes-member-tag:member_test"}, extensionTag: "k8s:openapi-gen", @@ -205,6 +211,18 @@ func TestExtensionAllowedValues(t *testing.T) { }, allowedValues: nil, }, + { + e: extension{ + idlTag: "mapType", + }, + allowedValues: sets.NewString("atomic", "granular"), + }, + { + e: extension{ + idlTag: "structType", + }, + allowedValues: sets.NewString("atomic", "granular"), + }, { e: extension{ idlTag: "k8s:openapi-gen", @@ -259,6 +277,20 @@ func TestExtensionAllowedValues(t *testing.T) { values: []string{"atomic"}, }, }, + { + e: extension{ + idlTag: "mapType", + xName: "x-kubernetes-map-type", + values: []string{"atomic"}, + }, + }, + { + e: extension{ + idlTag: "structType", + xName: "x-kubernetes-map-type", + values: []string{"granular"}, + }, + }, } for _, test := range successTests { actualErr := test.e.validateAllowedValues() @@ -292,6 +324,20 @@ func TestExtensionAllowedValues(t *testing.T) { values: []string{"not-allowed"}, }, }, + { + e: extension{ + idlTag: "mapType", + xName: "x-kubernetes-map-type", + values: []string{"something-pretty-wrong"}, + }, + }, + { + e: extension{ + idlTag: "structType", + xName: "x-kubernetes-map-type", + values: []string{"not-quite-right"}, + }, + }, } for _, test := range failureTests { actualErr := test.e.validateAllowedValues() @@ -326,6 +372,18 @@ func TestExtensionKind(t *testing.T) { }, kind: types.Slice, }, + { + e: extension{ + idlTag: "mapType", + }, + kind: types.Map, + }, + { + e: extension{ + idlTag: "structType", + }, + kind: types.Struct, + }, { e: extension{ idlTag: "listMapKey", diff --git a/pkg/idl/doc.go b/pkg/idl/doc.go index 9d59aaf0e..f870405be 100644 --- a/pkg/idl/doc.go +++ b/pkg/idl/doc.go @@ -79,13 +79,14 @@ type ListType string type ListMapKey string // MapType annotates a map to further describe its topology. It may -// have only one value: "atomic". Atomic means that the entire map is -// considered as a whole, rather than as distinct values. +// have one of two values: `atomic` or `granular`. `atomic` means that the entire map is +// considered as a whole; actors that wish to update the map can only +// entirely replace it. `granular` means that specific values in the map can be +// updated separately from other fields. // // By default, a map will be considered as a set of distinct values that -// can be updated individually. This default WILL NOT generate any -// openapi extension, as this will also be interpreted as the default -// behavior in the openapi definition. +// can be updated individually (i.e. the equivalent of `granular`). +// This default will still generate an OpenAPI extension with key: "x-kubernetes-map-type". // // This tag MUST only be used on maps, or the generation step will fail. // @@ -114,13 +115,14 @@ type PatchMergeKey string type PatchStrategy string // StructType annotates a struct to further describe its topology. It may -// have only one value: "atomic". Atomic means that the entire struct is -// considered as a whole, rather than as distinct values. +// have one of two values: `atomic` or `granular`. `atomic` means that the entire struct is +// considered as a whole; actors that wish to update the struct can only +// entirely replace it. `granular` means that specific fields in the struct can be +// updated separately from other fields. // // By default, a struct will be considered as a set of distinct values that -// can be updated individually. This default WILL NOT generate any -// openapi extension, as this will also be interpreted as the default -// behavior in the openapi definition. +// can be updated individually (`granular`). +// This default will still generate an OpenAPI extension with key: "x-kubernetes-map-type". // // This tag MUST only be used on structs, or the generation step will fail. // @@ -133,7 +135,7 @@ type PatchStrategy string // used on any struct. // // Using this tag will generate the following OpenAPI extension: -// "x-kubernetes-struct-type": "atomic" +// "x-kubernetes-map-type": "atomic" type StructType string // Union is TBD. diff --git a/test/integration/README.md b/test/integration/README.md index 0f72f96d7..d2d1fe90f 100644 --- a/test/integration/README.md +++ b/test/integration/README.md @@ -12,27 +12,27 @@ $ go test -v . First, run the generator to create `openapi_generated.go` file which specifies the `OpenAPIDefinition` for each type, and generate the golden API rule -violation report file . Note that if you do not pass a report -filename (`./testdata/golden.report` in the command below) to let the generator +violation report file. Note that if you do not pass a report +filename (`./testdata/golden.v2.report` in the command below) to let the generator to print API rule violations to the file, the generator will return error to stderr on API rule violations. ```bash $ go run ../../cmd/openapi-gen/openapi-gen.go \ - -i "k8s.io/kube-openapi/test/integration/testdata/listtype,k8s.io/kube-openapi/test/integration/testdata/dummytype,k8s.io/kube-openapi/test/integration/testdata/uniontype" \ + -i "k8s.io/kube-openapi/test/integration/testdata/custom,k8s.io/kube-openapi/test/integration/testdata/listtype,k8s.io/kube-openapi/test/integration/testdata/maptype,k8s.io/kube-openapi/test/integration/testdata/structtype,k8s.io/kube-openapi/test/integration/testdata/dummytype,k8s.io/kube-openapi/test/integration/testdata/uniontype" \ -o pkg \ -p generated \ -O openapi_generated \ - -r ./testdata/golden.report + -r ./testdata/golden.v2.report ``` The generated file `pkg/generated/openapi_generated.go` should have been created. Next, run the OpenAPI builder to create the Swagger file which includes -the definitions. The output file named `golden.json` will be output in +the definitions. The output file named `golden.v2.json` will be output in the current directory. ```bash -$ go run builder/main.go testdata/golden.json +$ go run builder/main.go testdata/golden.v2.json ``` After the golden spec is generated, please clean up the generated file diff --git a/test/integration/builder/main.go b/test/integration/builder/main.go index 355c88590..7c531ff9a 100644 --- a/test/integration/builder/main.go +++ b/test/integration/builder/main.go @@ -115,6 +115,10 @@ func createWebServices() []*restful.WebService { w.Route(buildRouteForType(w, "custom", "Bak")) w.Route(buildRouteForType(w, "custom", "Bac")) w.Route(buildRouteForType(w, "custom", "Bah")) + w.Route(buildRouteForType(w, "maptype", "GranularMap")) + w.Route(buildRouteForType(w, "maptype", "AtomicMap")) + w.Route(buildRouteForType(w, "structtype", "GranularStruct")) + w.Route(buildRouteForType(w, "structtype", "AtomicStruct")) return []*restful.WebService{w} } diff --git a/test/integration/integration_suite_test.go b/test/integration/integration_suite_test.go index 5b6a8f72f..cc1f0afd3 100644 --- a/test/integration/integration_suite_test.go +++ b/test/integration/integration_suite_test.go @@ -32,6 +32,8 @@ const ( testdataDir = "./testdata" testPkgDir = "k8s.io/kube-openapi/test/integration/testdata" inputDir = testPkgDir + "/listtype" + + "," + testPkgDir + "/maptype" + + "," + testPkgDir + "/structtype" + "," + testPkgDir + "/dummytype" + "," + testPkgDir + "/uniontype" + "," + testPkgDir + "/custom" diff --git a/test/integration/testdata/golden.v2.json b/test/integration/testdata/golden.v2.json index c541c6f97..ac53978a2 100644 --- a/test/integration/testdata/golden.v2.json +++ b/test/integration/testdata/golden.v2.json @@ -214,6 +214,82 @@ } } }, + "/test/maptype/atomicmap": { + "get": { + "schemes": [ + "https" + ], + "operationId": "func15", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/maptype.AtomicMap" + } + }, + "404": { + "$ref": "#/responses/NotFound" + } + } + } + }, + "/test/maptype/granularmap": { + "get": { + "schemes": [ + "https" + ], + "operationId": "func14", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/maptype.GranularMap" + } + }, + "404": { + "$ref": "#/responses/NotFound" + } + } + } + }, + "/test/structtype/atomicstruct": { + "get": { + "schemes": [ + "https" + ], + "operationId": "func17", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/structtype.AtomicStruct" + } + }, + "404": { + "$ref": "#/responses/NotFound" + } + } + } + }, + "/test/structtype/granularstruct": { + "get": { + "schemes": [ + "https" + ], + "operationId": "func16", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/structtype.GranularStruct" + } + }, + "404": { + "$ref": "#/responses/NotFound" + } + } + } + }, "/test/uniontype/inlinedunion": { "get": { "schemes": [ @@ -403,6 +479,73 @@ } } }, + "maptype.AtomicMap": { + "type": "object", + "required": [ + "KeyValue" + ], + "properties": { + "KeyValue": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-kubernetes-map-type": "atomic" + } + } + }, + "maptype.GranularMap": { + "type": "object", + "required": [ + "KeyValue" + ], + "properties": { + "KeyValue": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-kubernetes-map-type": "granular" + } + } + }, + "structtype.AtomicStruct": { + "type": "object", + "required": [ + "Field", + "OtherField" + ], + "properties": { + "Field": { + "x-kubernetes-map-type": "atomic", + "$ref": "#/definitions/structtype.ContainedStruct" + }, + "OtherField": { + "type": "integer", + "format": "int32" + } + } + }, + "structtype.ContainedStruct": { + "type": "object" + }, + "structtype.GranularStruct": { + "type": "object", + "required": [ + "Field", + "OtherField" + ], + "properties": { + "Field": { + "x-kubernetes-map-type": "granular", + "$ref": "#/definitions/structtype.ContainedStruct" + }, + "OtherField": { + "type": "integer", + "format": "int32" + } + } + }, "uniontype.InlinedUnion": { "type": "object", "required": [ diff --git a/test/integration/testdata/golden.v2.report b/test/integration/testdata/golden.v2.report index 85735aa23..cfed82e35 100644 --- a/test/integration/testdata/golden.v2.report +++ b/test/integration/testdata/golden.v2.report @@ -13,4 +13,10 @@ API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/li API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/listtype,MapList,Field API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/listtype,SetList,Field API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/listtype,UntypedList,Field +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/maptype,AtomicMap,KeyValue +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/maptype,GranularMap,KeyValue +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/structtype,AtomicStruct,Field +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/structtype,AtomicStruct,OtherField +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/structtype,GranularStruct,Field +API rule violation: names_match,k8s.io/kube-openapi/test/integration/testdata/structtype,GranularStruct,OtherField API rule violation: omitempty_match_case,k8s.io/kube-openapi/test/integration/testdata/listtype,Item,C diff --git a/test/integration/testdata/maptype/atomic-map.go b/test/integration/testdata/maptype/atomic-map.go new file mode 100644 index 000000000..7447eb2ba --- /dev/null +++ b/test/integration/testdata/maptype/atomic-map.go @@ -0,0 +1,7 @@ +package maptype + +// +k8s:openapi-gen=true +type AtomicMap struct { + // +mapType=atomic + KeyValue map[string]string +} diff --git a/test/integration/testdata/maptype/granular-map.go b/test/integration/testdata/maptype/granular-map.go new file mode 100644 index 000000000..422d95a2c --- /dev/null +++ b/test/integration/testdata/maptype/granular-map.go @@ -0,0 +1,7 @@ +package maptype + +// +k8s:openapi-gen=true +type GranularMap struct { + // +mapType=granular + KeyValue map[string]string +} diff --git a/test/integration/testdata/structtype/atomic-struct.go b/test/integration/testdata/structtype/atomic-struct.go new file mode 100644 index 000000000..41ebe388e --- /dev/null +++ b/test/integration/testdata/structtype/atomic-struct.go @@ -0,0 +1,11 @@ +package structtype + +// +k8s:openapi-gen=true +type AtomicStruct struct { + // +structType=atomic + Field ContainedStruct + OtherField int +} + +// +k8s:openapi-gen=true +type ContainedStruct struct{} diff --git a/test/integration/testdata/structtype/granular-struct.go b/test/integration/testdata/structtype/granular-struct.go new file mode 100644 index 000000000..c31b325de --- /dev/null +++ b/test/integration/testdata/structtype/granular-struct.go @@ -0,0 +1,8 @@ +package structtype + +// +k8s:openapi-gen=true +type GranularStruct struct { + // +structType=granular + Field ContainedStruct + OtherField int +}