Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 49 additions & 46 deletions pkg/builder3/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"strings"

restful "github.com/emicklei/go-restful"

"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/common/restfuladapter"
"k8s.io/kube-openapi/pkg/schemamutation"
"k8s.io/kube-openapi/pkg/spec3"
builderutil "k8s.io/kube-openapi/pkg/builder3/util"
"k8s.io/kube-openapi/pkg/util"
"k8s.io/kube-openapi/pkg/validation/spec"
)
Expand All @@ -38,7 +37,7 @@ const (
)

type openAPI struct {
config *common.Config
config *common.OpenAPIV3Config
spec *spec3.OpenAPI
definitions map[string]common.OpenAPIDefinition
}
Expand Down Expand Up @@ -85,6 +84,15 @@ func (o *openAPI) buildOperations(route common.Route, inPathCommonParamsMap map[
},
},
}
for k, v := range route.Metadata() {
if strings.HasPrefix(k, common.ExtensionPrefix) {
if ret.Extensions == nil {
ret.Extensions = spec.Extensions{}
}
ret.Extensions.Add(k, v)
}
}

var err error
if ret.OperationId, ret.Tags, err = o.config.GetOperationIDAndTagsFromRoute(route); err != nil {
return ret, err
Expand All @@ -106,9 +114,16 @@ func (o *openAPI) buildOperations(route common.Route, inPathCommonParamsMap map[
}
}

// TODO: Default response if needed. Common Response config
for code, resp := range o.config.CommonResponses {
if _, exists := ret.Responses.StatusCodeResponses[code]; !exists {
ret.Responses.StatusCodeResponses[code] = resp
}
}

if len(ret.Responses.StatusCodeResponses) == 0 {
ret.Responses.Default = o.config.DefaultResponse
}

ret.Parameters = make([]*spec3.Parameter, 0)
params := route.Parameters()
for _, param := range params {
_, isCommon := inPathCommonParamsMap[mapKeyFromParam(param)]
Expand All @@ -121,7 +136,7 @@ func (o *openAPI) buildOperations(route common.Route, inPathCommonParamsMap map[
}
}

body, err := o.buildRequestBody(params, route.RequestPayloadSample())
body, err := o.buildRequestBody(params, route.Consumes(), route.RequestPayloadSample())
if err != nil {
return nil, err
}
Expand All @@ -132,7 +147,7 @@ func (o *openAPI) buildOperations(route common.Route, inPathCommonParamsMap map[
return ret, nil
}

func (o *openAPI) buildRequestBody(parameters []common.Parameter, bodySample interface{}) (*spec3.RequestBody, error) {
func (o *openAPI) buildRequestBody(parameters []common.Parameter, consumes []string, bodySample interface{}) (*spec3.RequestBody, error) {
for _, param := range parameters {
if param.Kind() == common.BodyParameterKind && bodySample != nil {
schema, err := o.toSchema(util.GetCanonicalTypeName(bodySample))
Expand All @@ -141,15 +156,16 @@ func (o *openAPI) buildRequestBody(parameters []common.Parameter, bodySample int
}
r := &spec3.RequestBody{
RequestBodyProps: spec3.RequestBodyProps{
Content: map[string]*spec3.MediaType{
"application/json": &spec3.MediaType{
MediaTypeProps: spec3.MediaTypeProps{
Schema: schema,
},
},
},
Content: map[string]*spec3.MediaType{},
},
}
for _, consume := range consumes {
r.Content[consume] = &spec3.MediaType{
MediaTypeProps: spec3.MediaTypeProps{
Schema: schema,
},
}
}
return r, nil
}
}
Expand All @@ -158,7 +174,7 @@ func (o *openAPI) buildRequestBody(parameters []common.Parameter, bodySample int

func newOpenAPI(config *common.Config) openAPI {
o := openAPI{
config: config,
config: common.ConvertConfigToV3(config),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is common.Config meant to be used for both OpenAPIV2 and OpenAPIV3?

I am concerned that over time the OpenAPI v3 config might diverge and no longer be automatically convertible from the common.Config. It would be painful to switch the API to take a new config type at that point. We are already starting to see differences in how we want to treat GetDefinitionName of common.Config in OpenAPI v2 vs v3 (recall kubernetes/kubernetes#106778).

Are we able guarantee to our users the the OpenAPI config will always be automatically convertible from the OpenAPI v2 config?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we should have separate configs for OpenAPI V2 and V3. This should apply for structs as well (spec.Info, Schema, etc).

I think making that change would be too big given our current timeline, so as a compromise I'm starting to create the V3 config but generating that from the common Config.

spec: &spec3.OpenAPI{
Version: "3.0.0",
Info: config.Info,
Expand All @@ -170,6 +186,21 @@ func newOpenAPI(config *common.Config) openAPI {
},
},
}
if len(o.config.ResponseDefinitions) > 0 {
o.spec.Components.Responses = make(map[string]*spec3.Response)

}
for k, response := range o.config.ResponseDefinitions {
o.spec.Components.Responses[k] = response
}

if len(o.config.SecuritySchemes) > 0 {
o.spec.Components.SecuritySchemes = make(spec3.SecuritySchemes)

}
for k, securityScheme := range o.config.SecuritySchemes {
o.spec.Components.SecuritySchemes[k] = securityScheme
}

if o.config.GetOperationIDAndTagsFromRoute == nil {
// Map the deprecated handler to the common interface, if provided.
Expand Down Expand Up @@ -237,9 +268,7 @@ func (o *openAPI) buildOpenAPISpec(webServices []common.RouteContainer) error {
}

pathItem = &spec3.Path{
PathProps: spec3.PathProps{
Parameters: make([]*spec3.Parameter, 0),
},
PathProps: spec3.PathProps{},
}

// add web services's parameters as well as any parameters appears in all ops, as common parameters
Expand All @@ -251,6 +280,7 @@ func (o *openAPI) buildOpenAPISpec(webServices []common.RouteContainer) error {

for _, route := range routes {
op, _ := o.buildOperations(route, inPathCommonParamsMap)
sortParameters(op.Parameters)

switch strings.ToUpper(route.Method()) {
case "GET":
Expand Down Expand Up @@ -398,7 +428,7 @@ func (o *openAPI) buildDefinitionRecursively(name string) error {
}
// delete the embedded v2 schema if exists, otherwise no-op
delete(schema.VendorExtensible.Extensions, common.ExtensionV2Schema)
schema = wrapRefs(schema)
schema = builderutil.WrapRefs(schema)
o.spec.Components.Schemas[uniqueName] = schema
for _, v := range item.Dependencies {
if err := o.buildDefinitionRecursively(v); err != nil {
Expand Down Expand Up @@ -439,30 +469,3 @@ func (o *openAPI) toSchema(name string) (_ *spec.Schema, err error) {
}, nil
}
}

// wrapRefs wraps OpenAPI V3 Schema refs that contain sibling elements.
// AllOf is used to wrap the Ref to prevent references from having sibling elements
// Please see https://github.com/kubernetes/kubernetes/issues/106387#issuecomment-967640388
func wrapRefs(schema *spec.Schema) *spec.Schema {
walker := schemamutation.Walker{
SchemaCallback: func(schema *spec.Schema) *spec.Schema {
orig := schema
clone := func() {
if orig == schema {
schema = new(spec.Schema)
*schema = *orig
}
}
if schema.Ref.String() != "" && !reflect.DeepEqual(*schema, spec.Schema{SchemaProps: spec.SchemaProps{Ref: schema.Ref}}) {
clone()
refSchema := new(spec.Schema)
refSchema.Ref = schema.Ref
schema.Ref = spec.Ref{}
schema.AllOf = []spec.Schema{*refSchema}
}
return schema
},
RefCallback: schemamutation.RefCallbackNoop,
}
return walker.WalkSchema(schema)
}
51 changes: 51 additions & 0 deletions pkg/builder3/util/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
Copyright 2022 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 util

import (
"reflect"

"k8s.io/kube-openapi/pkg/schemamutation"
"k8s.io/kube-openapi/pkg/validation/spec"
)

// wrapRefs wraps OpenAPI V3 Schema refs that contain sibling elements.
// AllOf is used to wrap the Ref to prevent references from having sibling elements
// Please see https://github.com/kubernetes/kubernetes/issues/106387#issuecomment-967640388
func WrapRefs(schema *spec.Schema) *spec.Schema {
walker := schemamutation.Walker{
SchemaCallback: func(schema *spec.Schema) *spec.Schema {
orig := schema
clone := func() {
if orig == schema {
schema = new(spec.Schema)
*schema = *orig
}
}
if schema.Ref.String() != "" && !reflect.DeepEqual(*schema, spec.Schema{SchemaProps: spec.SchemaProps{Ref: schema.Ref}}) {
clone()
refSchema := new(spec.Schema)
refSchema.Ref = schema.Ref
schema.Ref = spec.Ref{}
schema.AllOf = []spec.Schema{*refSchema}
}
return schema
},
RefCallback: schemamutation.RefCallbackNoop,
}
return walker.WalkSchema(schema)
}
82 changes: 82 additions & 0 deletions pkg/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (

"github.com/emicklei/go-restful"

"k8s.io/kube-openapi/pkg/openapiconv"
"k8s.io/kube-openapi/pkg/spec3"
"k8s.io/kube-openapi/pkg/validation/spec"
)

Expand Down Expand Up @@ -117,6 +119,86 @@ type Config struct {
DefaultSecurity []map[string][]string
}

// OpenAPIV3Config is set of configuration for OpenAPI V3 spec generation.
type OpenAPIV3Config struct {
// Info is general information about the API.
Info *spec.Info

// DefaultResponse will be used if an operation does not have any responses listed. It
// will show up as ... "responses" : {"default" : $DefaultResponse} in the spec.
DefaultResponse *spec3.Response

// ResponseDefinitions will be added to responses component. This is an object
// that holds responses that can be used across operations.
ResponseDefinitions map[string]*spec3.Response

// CommonResponses will be added as a response to all operation specs. This is a good place to add common
// responses such as authorization failed.
CommonResponses map[int]*spec3.Response

// List of webservice's path prefixes to ignore
IgnorePrefixes []string

// 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.
GetDefinitions GetOpenAPIDefinitions

// GetOperationIDAndTags returns operation id and tags for a restful route. It is an optional function to customize operation IDs.
//
// Deprecated: GetOperationIDAndTagsFromRoute should be used instead. This cannot be specified if using the new Route
// interface set of funcs.
GetOperationIDAndTags func(r *restful.Route) (string, []string, error)

// GetOperationIDAndTagsFromRoute returns operation id and tags for a Route. It is an optional function to customize operation IDs.
GetOperationIDAndTagsFromRoute func(r 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(name string) (string, spec.Extensions)

// SecuritySchemes is list of all security schemes for OpenAPI service.
SecuritySchemes spec3.SecuritySchemes

// DefaultSecurity for all operations.
DefaultSecurity []map[string][]string
}

// ConvertConfigToV3 converts a Config object to an OpenAPIV3Config object
func ConvertConfigToV3(config *Config) *OpenAPIV3Config {
if config == nil {
return nil
}

v3Config := &OpenAPIV3Config{
Info: config.Info,
IgnorePrefixes: config.IgnorePrefixes,
GetDefinitions: config.GetDefinitions,
GetOperationIDAndTags: config.GetOperationIDAndTags,
GetOperationIDAndTagsFromRoute: config.GetOperationIDAndTagsFromRoute,
GetDefinitionName: config.GetDefinitionName,
SecuritySchemes: make(spec3.SecuritySchemes),
DefaultSecurity: config.DefaultSecurity,
DefaultResponse: openapiconv.ConvertResponse(config.DefaultResponse, []string{"application/json"}),

CommonResponses: make(map[int]*spec3.Response),
ResponseDefinitions: make(map[string]*spec3.Response),
}

if config.SecurityDefinitions != nil {
for s, securityScheme := range *config.SecurityDefinitions {
v3Config.SecuritySchemes[s] = openapiconv.ConvertSecurityScheme(securityScheme)
}
}
for k, commonResponse := range config.CommonResponses {
v3Config.CommonResponses[k] = openapiconv.ConvertResponse(&commonResponse, []string{"application/json"})
}

for k, responseDefinition := range config.ResponseDefinitions {
v3Config.ResponseDefinitions[k] = openapiconv.ConvertResponse(&responseDefinition, []string{"application/json"})
}
return v3Config
}

type typeInfo struct {
name string
format string
Expand Down
Loading