From 239169a5a45d94b7f404c080743953fcc26f50b6 Mon Sep 17 00:00:00 2001 From: mbohlool Date: Thu, 19 Jan 2017 09:34:22 -0800 Subject: [PATCH] Use full package path as definition name in OpenAPI --- cmd/kube-apiserver/app/server.go | 2 +- .../go2idl/openapi-gen/generators/openapi.go | 29 +++--- .../openapi-gen/generators/openapi_test.go | 8 +- .../cmd/federation-apiserver/app/server.go | 2 +- .../endpoints/openapi/openapi.go | 83 +++++++++++++++++ .../endpoints/openapi/openapi_test.go | 86 ++++++++++++++++++ pkg/genericapiserver/server/config.go | 6 +- .../server/genericapiserver_test.go | 2 +- .../server/openapi/openapi.go | 89 +++++++++++++------ .../server/openapi/openapi_test.go | 77 +++++++++++----- pkg/master/master_openapi_test.go | 2 +- .../k8s.io/apimachinery/pkg/openapi/common.go | 18 +++- test/integration/framework/master_utils.go | 4 +- 13 files changed, 326 insertions(+), 82 deletions(-) create mode 100644 pkg/genericapiserver/endpoints/openapi/openapi_test.go diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index 96a4c12b830..d5f0ac6b18d 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -295,7 +295,7 @@ func Run(s *options.ServerRunOptions) error { genericConfig.Authenticator = apiAuthenticator genericConfig.Authorizer = apiAuthorizer genericConfig.AdmissionControl = admissionController - genericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(generatedopenapi.OpenAPIDefinitions) + genericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(generatedopenapi.GetOpenAPIDefinitions) genericConfig.OpenAPIConfig.SecurityDefinitions = securityDefinitions genericConfig.OpenAPIConfig.Info.Title = "Kubernetes" genericConfig.SwaggerConfig = genericapiserver.DefaultSwaggerConfig() diff --git a/cmd/libs/go2idl/openapi-gen/generators/openapi.go b/cmd/libs/go2idl/openapi-gen/generators/openapi.go index 8fc4f0184e3..7cb35bc77c3 100644 --- a/cmd/libs/go2idl/openapi-gen/generators/openapi.go +++ b/cmd/libs/go2idl/openapi-gen/generators/openapi.go @@ -133,7 +133,7 @@ const ( // openApiGen produces a file with auto-generated OpenAPI functions. type openAPIGen struct { generator.DefaultGen - // TargetPackage is the package that will get OpenAPIDefinitions variable contains all open API definitions. + // TargetPackage is the package that will get GetOpenAPIDefinitions function returns all open API definitions. targetPackage *types.Package imports namer.ImportTracker context *generator.Context @@ -185,23 +185,24 @@ func (g *openAPIGen) Imports(c *generator.Context) []string { func argsFromType(t *types.Type) generator.Args { return generator.Args{ - "type": t, - "OpenAPIDefinitions": types.Ref(openAPICommonPackagePath, "OpenAPIDefinitions"), - "OpenAPIDefinition": types.Ref(openAPICommonPackagePath, "OpenAPIDefinition"), - "SpecSchemaType": types.Ref(specPackagePath, "Schema"), + "type": t, + "ReferenceCallback": types.Ref(openAPICommonPackagePath, "ReferenceCallback"), + "OpenAPIDefinition": types.Ref(openAPICommonPackagePath, "OpenAPIDefinition"), + "SpecSchemaType": types.Ref(specPackagePath, "Schema"), } } func (g *openAPIGen) Init(c *generator.Context, w io.Writer) error { sw := generator.NewSnippetWriter(w, c, "$", "$") - sw.Do("var OpenAPIDefinitions *$.OpenAPIDefinitions|raw$ = ", argsFromType(nil)) - sw.Do("&$.OpenAPIDefinitions|raw${\n", argsFromType(nil)) + sw.Do("func GetOpenAPIDefinitions(ref $.ReferenceCallback|raw$) map[string]$.OpenAPIDefinition|raw$ {\n", argsFromType(nil)) + sw.Do("return map[string]$.OpenAPIDefinition|raw${\n", argsFromType(nil)) return sw.Error() } func (g *openAPIGen) Finalize(c *generator.Context, w io.Writer) error { sw := generator.NewSnippetWriter(w, c, "$", "$") sw.Do("}\n", nil) + sw.Do("}\n", nil) return sw.Error() } @@ -308,9 +309,9 @@ func (g openAPITypeWriter) generate(t *types.Type) error { switch t.Kind { case types.Struct: args := argsFromType(t) - g.Do("\"$.$\": ", typeShortName(t)) + g.Do("\"$.$\": ", t.Name) if hasOpenAPIDefinitionMethod(t) { - g.Do("$.type|raw${}.OpenAPIDefinition(),", args) + g.Do("$.type|raw${}.OpenAPIDefinition(),\n", args) return nil } g.Do("{\nSchema: spec.Schema{\nSchemaProps: spec.SchemaProps{\n", nil) @@ -437,14 +438,8 @@ func (g openAPITypeWriter) generateSimpleProperty(typeString, format string) { } func (g openAPITypeWriter) generateReferenceProperty(t *types.Type) { - var name string - if t.Name.Package == "" { - name = t.Name.Name - } else { - name = filepath.Base(t.Name.Package) + "." + t.Name.Name - } - g.refTypes[name] = t - g.Do("Ref: spec.MustCreateRef(\"#/definitions/$.$\"),\n", name) + g.refTypes[t.Name.String()] = t + g.Do("Ref: ref(\"$.$\"),\n", t.Name.String()) } func resolveAliasAndPtrType(t *types.Type) *types.Type { diff --git a/cmd/libs/go2idl/openapi-gen/generators/openapi_test.go b/cmd/libs/go2idl/openapi-gen/generators/openapi_test.go index 3ae467ffa18..0f12addf503 100644 --- a/cmd/libs/go2idl/openapi-gen/generators/openapi_test.go +++ b/cmd/libs/go2idl/openapi-gen/generators/openapi_test.go @@ -112,7 +112,7 @@ type Blah struct { if err != nil { t.Fatal(err) } - assert.Equal(`"foo.Blah": { + assert.Equal(`"base/foo.Blah": { Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ Description: "Blah is a test.", @@ -280,7 +280,7 @@ type Blah struct { if err != nil { t.Fatal(err) } - assert.Equal(`"foo.Blah": { + assert.Equal(`"base/foo.Blah": { Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ Description: "PointerSample demonstrate pointer's properties", @@ -295,7 +295,7 @@ Format: "", "StructPointer": { SchemaProps: spec.SchemaProps{ Description: "A struct pointer", -Ref: spec.MustCreateRef("#/definitions/foo.Blah"), +Ref: ref("base/foo.Blah"), }, }, "SlicePointer": { @@ -331,7 +331,7 @@ Required: []string{"StringPointer","StructPointer","SlicePointer","MapPointer"}, }, }, Dependencies: []string{ -"foo.Blah",}, +"base/foo.Blah",}, }, `, buffer.String()) } diff --git a/federation/cmd/federation-apiserver/app/server.go b/federation/cmd/federation-apiserver/app/server.go index e63ecb81f77..20620188232 100644 --- a/federation/cmd/federation-apiserver/app/server.go +++ b/federation/cmd/federation-apiserver/app/server.go @@ -175,7 +175,7 @@ func Run(s *options.ServerRunOptions) error { genericConfig.Authenticator = apiAuthenticator genericConfig.Authorizer = apiAuthorizer genericConfig.AdmissionControl = admissionController - genericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(openapi.OpenAPIDefinitions) + genericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(openapi.GetOpenAPIDefinitions) genericConfig.OpenAPIConfig.SecurityDefinitions = securityDefinitions genericConfig.SwaggerConfig = genericapiserver.DefaultSwaggerConfig() genericConfig.LongRunningFunc = filters.BasicLongRunningRequestCheck( diff --git a/pkg/genericapiserver/endpoints/openapi/openapi.go b/pkg/genericapiserver/endpoints/openapi/openapi.go index 92196b3777e..c4913755801 100644 --- a/pkg/genericapiserver/endpoints/openapi/openapi.go +++ b/pkg/genericapiserver/endpoints/openapi/openapi.go @@ -19,16 +19,25 @@ package openapi import ( "bytes" "fmt" + "reflect" "strings" "unicode" "github.com/emicklei/go-restful" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/util/trie" + "sort" ) var verbs = trie.New([]string{"get", "log", "read", "replace", "patch", "delete", "deletecollection", "watch", "connect", "proxy", "list", "create", "patch"}) +const ( + extensionGVK = "x-kubernetes-group-version-kind" +) + // ToValidOperationID makes an string a valid op ID (e.g. removing punctuations and whitespaces and make it camel case) func ToValidOperationID(s string, capitalizeFirstLetter bool) string { var buffer bytes.Buffer @@ -85,3 +94,77 @@ func GetOperationIDAndTags(servePath string, r *restful.Route) (string, []string return op, tags, nil } } + +type groupVersionKinds []v1.GroupVersionKind + +func (s groupVersionKinds) Len() int { + return len(s) +} + +func (s groupVersionKinds) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s groupVersionKinds) Less(i, j int) bool { + if s[i].Group == s[j].Group { + if s[i].Version == s[j].Version { + return s[i].Kind < s[j].Kind + } + return s[i].Version < s[j].Version + } + return s[i].Group < s[j].Group +} + +// DefinitionNamer is the type to customize OpenAPI definition name. +type DefinitionNamer struct { + typeGroupVersionKinds map[string]groupVersionKinds +} + +func gvkConvert(gvk schema.GroupVersionKind) v1.GroupVersionKind { + return v1.GroupVersionKind{ + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func friendlyName(name string) string { + nameParts := strings.Split(name, "/") + // Reverse first part. e.g., io.k8s... instead of k8s.io... + if len(nameParts) > 0 && strings.Contains(nameParts[0], ".") { + parts := strings.Split(nameParts[0], ".") + for i, j := 0, len(parts)-1; i < j; i, j = i+1, j-1 { + parts[i], parts[j] = parts[j], parts[i] + } + nameParts[0] = strings.Join(parts, ".") + } + return strings.Join(nameParts, ".") +} + +func typeName(t reflect.Type) string { + return fmt.Sprintf("%s.%s", t.PkgPath(), t.Name()) +} + +// NewDefinitionNamer constructs a new DefinitionNamer to be used to customize OpenAPI spec. +func NewDefinitionNamer(s *runtime.Scheme) DefinitionNamer { + ret := DefinitionNamer{ + typeGroupVersionKinds: map[string]groupVersionKinds{}, + } + for gvk, rtype := range s.AllKnownTypes() { + ret.typeGroupVersionKinds[typeName(rtype)] = append(ret.typeGroupVersionKinds[typeName(rtype)], gvkConvert(gvk)) + } + for _, gvk := range ret.typeGroupVersionKinds { + sort.Sort(gvk) + } + return ret +} + +// GetDefinitionName returns the name and tags for a given definition +func (d *DefinitionNamer) GetDefinitionName(servePath string, name string) (string, spec.Extensions) { + if groupVersionKinds, ok := d.typeGroupVersionKinds[name]; ok { + return friendlyName(name), spec.Extensions{ + extensionGVK: []v1.GroupVersionKind(groupVersionKinds), + } + } + return friendlyName(name), nil +} diff --git a/pkg/genericapiserver/endpoints/openapi/openapi_test.go b/pkg/genericapiserver/endpoints/openapi/openapi_test.go new file mode 100644 index 00000000000..9541eeb5b72 --- /dev/null +++ b/pkg/genericapiserver/endpoints/openapi/openapi_test.go @@ -0,0 +1,86 @@ +/* +Copyright 2016 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 openapi + +import ( + "reflect" + "strings" + "testing" + + "github.com/go-openapi/spec" + + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type TestType struct { +} + +func (t TestType) GetObjectKind() schema.ObjectKind { + return t +} + +func (t TestType) SetGroupVersionKind(kind schema.GroupVersionKind) { +} + +func (t TestType) GroupVersionKind() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: "test", + Version: "v1", + Kind: "TestType", + } +} + +func assertEqual(t *testing.T, expected, actual interface{}) { + var equal bool + if expected == nil || actual == nil { + equal = expected == actual + } else { + equal = reflect.DeepEqual(expected, actual) + } + if !equal { + t.Errorf("%v != %v", expected, actual) + } +} + +func TestGetDefinitionName(t *testing.T) { + testType := TestType{} + typePkgName := "k8s.io/kubernetes/pkg/genericapiserver/endpoints/openapi.TestType" + typeFriendlyName := "io.k8s.kubernetes.pkg.genericapiserver.endpoints.openapi.TestType" + if strings.HasSuffix(reflect.TypeOf(testType).PkgPath(), "go_default_test") { + // the test is running inside bazel where the package name is changed and + // "go_default_test" will add to package path. + typePkgName = "k8s.io/kubernetes/pkg/genericapiserver/endpoints/openapi/go_default_test.TestType" + typeFriendlyName = "io.k8s.kubernetes.pkg.genericapiserver.endpoints.openapi.go_default_test.TestType" + } + s := runtime.NewScheme() + s.AddKnownTypeWithName(testType.GroupVersionKind(), &testType) + namer := NewDefinitionNamer(s) + n, e := namer.GetDefinitionName("", typePkgName) + assertEqual(t, typeFriendlyName, n) + assertEqual(t, e["x-kubernetes-group-version-kind"], []v1.GroupVersionKind{ + { + Group: "test", + Version: "v1", + Kind: "TestType", + }, + }) + n, e2 := namer.GetDefinitionName("", "test.com/another.Type") + assertEqual(t, "com.test.another.Type", n) + assertEqual(t, e2, spec.Extensions(nil)) +} diff --git a/pkg/genericapiserver/server/config.go b/pkg/genericapiserver/server/config.go index 27dbaf1624f..c62e17ad021 100644 --- a/pkg/genericapiserver/server/config.go +++ b/pkg/genericapiserver/server/config.go @@ -213,7 +213,8 @@ func NewConfig() *Config { return config.ApplyOptions(defaultOptions) } -func DefaultOpenAPIConfig(definitions *openapicommon.OpenAPIDefinitions) *openapicommon.Config { +func DefaultOpenAPIConfig(getDefinitions openapicommon.GetOpenAPIDefinitions) *openapicommon.Config { + defNamer := apiopenapi.NewDefinitionNamer(api.Scheme) return &openapicommon.Config{ ProtocolList: []string{"https"}, IgnorePrefixes: []string{"/swaggerapi"}, @@ -228,7 +229,8 @@ func DefaultOpenAPIConfig(definitions *openapicommon.OpenAPIDefinitions) *openap }, }, GetOperationIDAndTags: apiopenapi.GetOperationIDAndTags, - Definitions: definitions, + GetDefinitionName: defNamer.GetDefinitionName, + GetDefinitions: getDefinitions, } } diff --git a/pkg/genericapiserver/server/genericapiserver_test.go b/pkg/genericapiserver/server/genericapiserver_test.go index 26e9bf2a215..d53b0b63f03 100644 --- a/pkg/genericapiserver/server/genericapiserver_test.go +++ b/pkg/genericapiserver/server/genericapiserver_test.go @@ -61,7 +61,7 @@ func setUp(t *testing.T) (*etcdtesting.EtcdTestServer, Config, *assert.Assertion config.RequestContextMapper = genericapirequest.NewRequestContextMapper() config.LegacyAPIGroupPrefixes = sets.NewString("/api") - config.OpenAPIConfig = DefaultOpenAPIConfig(openapigen.OpenAPIDefinitions) + config.OpenAPIConfig = DefaultOpenAPIConfig(openapigen.GetOpenAPIDefinitions) config.OpenAPIConfig.Info = &spec.Info{ InfoProps: spec.InfoProps{ Title: "Kubernetes", diff --git a/pkg/genericapiserver/server/openapi/openapi.go b/pkg/genericapiserver/server/openapi/openapi.go index e8531ced269..15ef46cde0c 100644 --- a/pkg/genericapiserver/server/openapi/openapi.go +++ b/pkg/genericapiserver/server/openapi/openapi.go @@ -40,6 +40,7 @@ type openAPI struct { swagger *spec.Swagger protocolList []string servePath string + definitions map[string]openapi.OpenAPIDefinition } // RegisterOpenAPIService registers a handler to provides standard OpenAPI specification. @@ -79,6 +80,15 @@ func (o *openAPI) init(webServices []*restful.WebService) error { return r.Operation, nil, nil } } + if o.config.GetDefinitionName == nil { + o.config.GetDefinitionName = func(_, name string) (string, spec.Extensions) { + return name[strings.LastIndex(name, "/")+1:], nil + } + } + o.definitions = o.config.GetDefinitions(func(name string) spec.Ref { + defName, _ := o.config.GetDefinitionName(o.servePath, name) + return spec.MustCreateRef("#/definitions/" + openapi.EscapeJsonPointer(defName)) + }) if o.config.CommonResponses == nil { o.config.CommonResponses = map[int]spec.Response{} } @@ -93,12 +103,34 @@ func (o *openAPI) init(webServices []*restful.WebService) error { return nil } +func getCanonicalizeTypeName(t reflect.Type) string { + if t.PkgPath() == "" { + return t.Name() + } + path := t.PkgPath() + if strings.Contains(path, "/vendor/") { + path = path[strings.Index(path, "/vendor/")+len("/vendor/"):] + } + return path + "." + t.Name() +} + func (o *openAPI) buildDefinitionRecursively(name string) error { - if _, ok := o.swagger.Definitions[name]; ok { + uniqueName, extensions := o.config.GetDefinitionName(o.servePath, name) + if _, ok := o.swagger.Definitions[uniqueName]; ok { return nil } - if item, ok := (*o.config.Definitions)[name]; ok { - o.swagger.Definitions[name] = item.Schema + if item, ok := o.definitions[name]; ok { + schema := spec.Schema{ + SchemaProps: item.Schema.SchemaProps, + SwaggerSchemaProps: item.Schema.SwaggerSchemaProps, + } + if extensions != nil { + schema.Extensions = spec.Extensions{} + for k, v := range extensions { + schema.Extensions[k] = v + } + } + o.swagger.Definitions[uniqueName] = schema for _, v := range item.Dependencies { if err := o.buildDefinitionRecursively(v); err != nil { return err @@ -118,11 +150,12 @@ func (o *openAPI) buildDefinitionForType(sample interface{}) (string, error) { if t.Kind() == reflect.Ptr { t = t.Elem() } - name := t.String() + name := getCanonicalizeTypeName(t) if err := o.buildDefinitionRecursively(name); err != nil { return "", err } - return "#/definitions/" + name, nil + defName, _ := o.config.GetDefinitionName(o.servePath, name) + return "#/definitions/" + openapi.EscapeJsonPointer(defName), nil } // buildPaths builds OpenAPI paths using go-restful's web services. @@ -244,18 +277,12 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map if len(ret.Responses.StatusCodeResponses) == 0 { ret.Responses.Default = o.config.DefaultResponse } - // If there is a read sample, there will be a body param referring to it. - if route.ReadSample != nil { - if _, err := o.toSchema(reflect.TypeOf(route.ReadSample).String(), route.ReadSample); err != nil { - return ret, err - } - } // Build non-common Parameters ret.Parameters = make([]spec.Parameter, 0) for _, param := range route.ParameterDocs { if _, isCommon := inPathCommonParamsMap[mapKeyFromParam(param)]; !isCommon { - openAPIParam, err := o.buildParameter(param.Data()) + openAPIParam, err := o.buildParameter(param.Data(), route.ReadSample) if err != nil { return ret, err } @@ -266,8 +293,7 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map } func (o *openAPI) buildResponse(model interface{}, description string) (spec.Response, error) { - typeName := reflect.TypeOf(model).String() - schema, err := o.toSchema(typeName, model) + schema, err := o.toSchema(model) if err != nil { return spec.Response{}, err } @@ -300,8 +326,9 @@ func (o *openAPI) findCommonParameters(routes []restful.Route) (map[interface{}] } } for key, count := range paramOpsCountByName { - if count == len(routes) { - openAPIParam, err := o.buildParameter(paramNameKindToDataMap[key]) + paramData := paramNameKindToDataMap[key] + if count == len(routes) && paramData.Kind != restful.BodyParameterKind { + openAPIParam, err := o.buildParameter(paramData, nil) if err != nil { return commonParamsMap, err } @@ -311,8 +338,8 @@ func (o *openAPI) findCommonParameters(routes []restful.Route) (map[interface{}] return commonParamsMap, nil } -func (o *openAPI) toSchema(typeName string, model interface{}) (_ *spec.Schema, err error) { - if openAPIType, openAPIFormat := openapi.GetOpenAPITypeFormat(typeName); openAPIType != "" { +func (o *openAPI) toSchema(model interface{}) (_ *spec.Schema, err error) { + if openAPIType, openAPIFormat := openapi.GetOpenAPITypeFormat(getCanonicalizeTypeName(reflect.TypeOf(model))); openAPIType != "" { return &spec.Schema{ SchemaProps: spec.SchemaProps{ Type: []string{openAPIType}, @@ -320,12 +347,9 @@ func (o *openAPI) toSchema(typeName string, model interface{}) (_ *spec.Schema, }, }, nil } else { - ref := "#/definitions/" + typeName - if model != nil { - ref, err = o.buildDefinitionForType(model) - if err != nil { - return nil, err - } + ref, err := o.buildDefinitionForType(model) + if err != nil { + return nil, err } return &spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -335,7 +359,7 @@ func (o *openAPI) toSchema(typeName string, model interface{}) (_ *spec.Schema, } } -func (o *openAPI) buildParameter(restParam restful.ParameterData) (ret spec.Parameter, err error) { +func (o *openAPI) buildParameter(restParam restful.ParameterData, bodySample interface{}) (ret spec.Parameter, err error) { ret = spec.Parameter{ ParamProps: spec.ParamProps{ Name: restParam.Name, @@ -345,9 +369,16 @@ func (o *openAPI) buildParameter(restParam restful.ParameterData) (ret spec.Para } switch restParam.Kind { case restful.BodyParameterKind: - ret.In = "body" - ret.Schema, err = o.toSchema(restParam.DataType, nil) - return ret, err + if bodySample != nil { + ret.In = "body" + ret.Schema, err = o.toSchema(bodySample) + return ret, err + } else { + // There is not enough information in the body parameter to build the definition. + // Body parameter has a data type that is a short name but we need full package name + // of the type to create a definition. + return ret, fmt.Errorf("restful body parameters are not supported: %v", restParam.DataType) + } case restful.PathParameterKind: ret.In = "path" if !restParam.Required { @@ -375,7 +406,7 @@ func (o *openAPI) buildParameter(restParam restful.ParameterData) (ret spec.Para func (o *openAPI) buildParameters(restParam []*restful.Parameter) (ret []spec.Parameter, err error) { ret = make([]spec.Parameter, len(restParam)) for i, v := range restParam { - ret[i], err = o.buildParameter(v.Data()) + ret[i], err = o.buildParameter(v.Data(), nil) if err != nil { return ret, err } diff --git a/pkg/genericapiserver/server/openapi/openapi_test.go b/pkg/genericapiserver/server/openapi/openapi_test.go index 8048c4489b2..dedf7c4b84f 100644 --- a/pkg/genericapiserver/server/openapi/openapi_test.go +++ b/pkg/genericapiserver/server/openapi/openapi_test.go @@ -19,6 +19,7 @@ package openapi import ( "fmt" "net/http" + "strings" "testing" "github.com/emicklei/go-restful" @@ -186,9 +187,22 @@ func getConfig(fullMethods bool) (*openapi.Config, *restful.Container) { Description: "Test API", }, }, - Definitions: &openapi.OpenAPIDefinitions{ - "openapi.TestInput": *TestInput{}.OpenAPIDefinition(), - "openapi.TestOutput": *TestOutput{}.OpenAPIDefinition(), + GetDefinitions: func(_ openapi.ReferenceCallback) map[string]openapi.OpenAPIDefinition { + return map[string]openapi.OpenAPIDefinition{ + "k8s.io/kubernetes/pkg/genericapiserver/server/openapi.TestInput": *TestInput{}.OpenAPIDefinition(), + "k8s.io/kubernetes/pkg/genericapiserver/server/openapi.TestOutput": *TestOutput{}.OpenAPIDefinition(), + // Bazel changes the package name, this is ok for testing, but we need to fix it if it happened + // in the main code. + "k8s.io/kubernetes/pkg/genericapiserver/server/openapi/go_default_test.TestInput": *TestInput{}.OpenAPIDefinition(), + "k8s.io/kubernetes/pkg/genericapiserver/server/openapi/go_default_test.TestOutput": *TestOutput{}.OpenAPIDefinition(), + } + }, + GetDefinitionName: func(_ string, name string) (string, spec.Extensions) { + friendlyName := name[strings.LastIndex(name, "/")+1:] + if strings.HasPrefix(friendlyName, "go_default_test") { + friendlyName = "openapi" + friendlyName[len("go_default_test"):] + } + return friendlyName, nil }, }, container } @@ -216,12 +230,18 @@ func getTestPathItem(allMethods bool, opPrefix string) spec.PathItem { } ret.Get.Parameters = getAdditionalTestParameters() if allMethods { - ret.PathItemProps.Put = getTestOperation("put", opPrefix) - ret.PathItemProps.Post = getTestOperation("post", opPrefix) - ret.PathItemProps.Head = getTestOperation("head", opPrefix) - ret.PathItemProps.Patch = getTestOperation("patch", opPrefix) - ret.PathItemProps.Delete = getTestOperation("delete", opPrefix) - ret.PathItemProps.Options = getTestOperation("options", opPrefix) + ret.Put = getTestOperation("put", opPrefix) + ret.Put.Parameters = getTestParameters() + ret.Post = getTestOperation("post", opPrefix) + ret.Post.Parameters = getTestParameters() + ret.Head = getTestOperation("head", opPrefix) + ret.Head.Parameters = getTestParameters() + ret.Patch = getTestOperation("patch", opPrefix) + ret.Patch.Parameters = getTestParameters() + ret.Delete = getTestOperation("delete", opPrefix) + ret.Delete.Parameters = getTestParameters() + ret.Options = getTestOperation("options", opPrefix) + ret.Options.Parameters = getTestParameters() } return ret } @@ -250,16 +270,8 @@ func getTestResponses() *spec.Responses { } func getTestCommonParameters() []spec.Parameter { - ret := make([]spec.Parameter, 3) + ret := make([]spec.Parameter, 2) ret[0] = spec.Parameter{ - ParamProps: spec.ParamProps{ - Name: "body", - In: "body", - Required: true, - Schema: getRefSchema("#/definitions/openapi.TestInput"), - }, - } - ret[1] = spec.Parameter{ SimpleSchema: spec.SimpleSchema{ Type: "string", }, @@ -273,7 +285,7 @@ func getTestCommonParameters() []spec.Parameter { UniqueItems: true, }, } - ret[2] = spec.Parameter{ + ret[1] = spec.Parameter{ SimpleSchema: spec.SimpleSchema{ Type: "string", }, @@ -289,9 +301,30 @@ func getTestCommonParameters() []spec.Parameter { return ret } -func getAdditionalTestParameters() []spec.Parameter { - ret := make([]spec.Parameter, 2) +func getTestParameters() []spec.Parameter { + ret := make([]spec.Parameter, 1) ret[0] = spec.Parameter{ + ParamProps: spec.ParamProps{ + Name: "body", + In: "body", + Required: true, + Schema: getRefSchema("#/definitions/openapi.TestInput"), + }, + } + return ret +} + +func getAdditionalTestParameters() []spec.Parameter { + ret := make([]spec.Parameter, 3) + ret[0] = spec.Parameter{ + ParamProps: spec.ParamProps{ + Name: "body", + In: "body", + Required: true, + Schema: getRefSchema("#/definitions/openapi.TestInput"), + }, + } + ret[1] = spec.Parameter{ ParamProps: spec.ParamProps{ Name: "fparam", Description: "a test form parameter", @@ -304,7 +337,7 @@ func getAdditionalTestParameters() []spec.Parameter { UniqueItems: true, }, } - ret[1] = spec.Parameter{ + ret[2] = spec.Parameter{ SimpleSchema: spec.SimpleSchema{ Type: "integer", }, diff --git a/pkg/master/master_openapi_test.go b/pkg/master/master_openapi_test.go index d4a557e7f82..921099e37cf 100644 --- a/pkg/master/master_openapi_test.go +++ b/pkg/master/master_openapi_test.go @@ -43,7 +43,7 @@ func TestValidOpenAPISpec(t *testing.T) { defer etcdserver.Terminate(t) config.GenericConfig.EnableIndex = true - config.GenericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(openapigen.OpenAPIDefinitions) + config.GenericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(openapigen.GetOpenAPIDefinitions) config.GenericConfig.OpenAPIConfig.Info = &spec.Info{ InfoProps: spec.InfoProps{ Title: "Kubernetes", diff --git a/staging/src/k8s.io/apimachinery/pkg/openapi/common.go b/staging/src/k8s.io/apimachinery/pkg/openapi/common.go index e81dac9ed20..a16cb7f9ae4 100644 --- a/staging/src/k8s.io/apimachinery/pkg/openapi/common.go +++ b/staging/src/k8s.io/apimachinery/pkg/openapi/common.go @@ -19,6 +19,7 @@ package openapi import ( "github.com/emicklei/go-restful" "github.com/go-openapi/spec" + "strings" ) // OpenAPIDefinition describes single type. Normally these definitions are auto-generated using gen-openapi. @@ -27,8 +28,10 @@ type OpenAPIDefinition struct { Dependencies []string } +type ReferenceCallback func(path string) spec.Ref + // OpenAPIDefinitions is collection of all definitions. -type OpenAPIDefinitions map[string]OpenAPIDefinition +type GetOpenAPIDefinitions func(ReferenceCallback) map[string]OpenAPIDefinition // OpenAPIDefinitionGetter gets openAPI definitions for a given type. If a type implements this interface, // the definition returned by it will be used, otherwise the auto-generated definitions will be used. See @@ -59,11 +62,15 @@ type Config struct { // OpenAPIDefinitions should provide definition for all models used by routes. Failure to provide this map // or any of the models will result in spec generation failure. - Definitions *OpenAPIDefinitions + GetDefinitions GetOpenAPIDefinitions // GetOperationIDAndTags returns operation id and tags for a restful route. It is an optional function to customize operation IDs. GetOperationIDAndTags func(servePath string, r *restful.Route) (string, []string, error) + // GetDefinitionName returns a friendly name for a definition base on the serving path. parameter `name` is the full name of the definition. + // It is an optional function to customize model names. + GetDefinitionName func(servePath string, name string) (string, spec.Extensions) + // SecurityDefinitions is list of all security definitions for OpenAPI service. If this is not nil, the user of config // is responsible to provide DefaultSecurity and (maybe) add unauthorized response to CommonResponses. SecurityDefinitions *spec.SecurityDefinitions @@ -141,3 +148,10 @@ func GetOpenAPITypeFormat(typeName string) (string, string) { } return mapped[0], mapped[1] } + +func EscapeJsonPointer(p string) string { + // Escaping reference name using rfc6901 + p = strings.Replace(p, "~", "~0", -1) + p = strings.Replace(p, "/", "~1", -1) + return p +} diff --git a/test/integration/framework/master_utils.go b/test/integration/framework/master_utils.go index 4c0a1981b62..8b3061f8996 100644 --- a/test/integration/framework/master_utils.go +++ b/test/integration/framework/master_utils.go @@ -183,7 +183,7 @@ func startMasterOrDie(masterConfig *master.Config, incomingServer *httptest.Serv masterConfig = NewMasterConfig() masterConfig.GenericConfig.EnableProfiling = true masterConfig.GenericConfig.EnableMetrics = true - masterConfig.GenericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(openapi.OpenAPIDefinitions) + masterConfig.GenericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(openapi.GetOpenAPIDefinitions) masterConfig.GenericConfig.OpenAPIConfig.Info = &spec.Info{ InfoProps: spec.InfoProps{ Title: "Kubernetes", @@ -195,7 +195,7 @@ func startMasterOrDie(masterConfig *master.Config, incomingServer *httptest.Serv Description: "Default Response.", }, } - masterConfig.GenericConfig.OpenAPIConfig.Definitions = openapi.OpenAPIDefinitions + masterConfig.GenericConfig.OpenAPIConfig.GetDefinitions = openapi.GetOpenAPIDefinitions masterConfig.GenericConfig.SwaggerConfig = genericapiserver.DefaultSwaggerConfig() }