k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/builder3/openapi.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package builder3
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"net/http"
    23  	"strings"
    24  
    25  	restful "github.com/emicklei/go-restful/v3"
    26  
    27  	builderutil "k8s.io/kube-openapi/pkg/builder3/util"
    28  	"k8s.io/kube-openapi/pkg/common"
    29  	"k8s.io/kube-openapi/pkg/common/restfuladapter"
    30  	"k8s.io/kube-openapi/pkg/spec3"
    31  	"k8s.io/kube-openapi/pkg/util"
    32  	"k8s.io/kube-openapi/pkg/validation/spec"
    33  )
    34  
    35  const (
    36  	OpenAPIVersion = "3.0"
    37  )
    38  
    39  type openAPI struct {
    40  	config      *common.OpenAPIV3Config
    41  	spec        *spec3.OpenAPI
    42  	definitions map[string]common.OpenAPIDefinition
    43  }
    44  
    45  func groupRoutesByPath(routes []common.Route) map[string][]common.Route {
    46  	pathToRoutes := make(map[string][]common.Route)
    47  	for _, r := range routes {
    48  		pathToRoutes[r.Path()] = append(pathToRoutes[r.Path()], r)
    49  	}
    50  	return pathToRoutes
    51  }
    52  
    53  func (o *openAPI) buildResponse(model interface{}, description string, content []string) (*spec3.Response, error) {
    54  	response := &spec3.Response{
    55  		ResponseProps: spec3.ResponseProps{
    56  			Description: description,
    57  			Content:     make(map[string]*spec3.MediaType),
    58  		},
    59  	}
    60  
    61  	s, err := o.toSchema(util.GetCanonicalTypeName(model))
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  
    66  	for _, contentType := range content {
    67  		response.ResponseProps.Content[contentType] = &spec3.MediaType{
    68  			MediaTypeProps: spec3.MediaTypeProps{
    69  				Schema: s,
    70  			},
    71  		}
    72  	}
    73  	return response, nil
    74  }
    75  
    76  func (o *openAPI) buildOperations(route common.Route, inPathCommonParamsMap map[interface{}]*spec3.Parameter) (*spec3.Operation, error) {
    77  	ret := &spec3.Operation{
    78  		OperationProps: spec3.OperationProps{
    79  			Description: route.Description(),
    80  			Responses: &spec3.Responses{
    81  				ResponsesProps: spec3.ResponsesProps{
    82  					StatusCodeResponses: make(map[int]*spec3.Response),
    83  				},
    84  			},
    85  		},
    86  	}
    87  	for k, v := range route.Metadata() {
    88  		if strings.HasPrefix(k, common.ExtensionPrefix) {
    89  			if ret.Extensions == nil {
    90  				ret.Extensions = spec.Extensions{}
    91  			}
    92  			ret.Extensions.Add(k, v)
    93  		}
    94  	}
    95  
    96  	var err error
    97  	if ret.OperationId, ret.Tags, err = o.config.GetOperationIDAndTagsFromRoute(route); err != nil {
    98  		return ret, err
    99  	}
   100  
   101  	// Build responses
   102  	for _, resp := range route.StatusCodeResponses() {
   103  		ret.Responses.StatusCodeResponses[resp.Code()], err = o.buildResponse(resp.Model(), resp.Message(), route.Produces())
   104  		if err != nil {
   105  			return ret, err
   106  		}
   107  	}
   108  
   109  	// If there is no response but a write sample, assume that write sample is an http.StatusOK response.
   110  	if len(ret.Responses.StatusCodeResponses) == 0 && route.ResponsePayloadSample() != nil {
   111  		ret.Responses.StatusCodeResponses[http.StatusOK], err = o.buildResponse(route.ResponsePayloadSample(), "OK", route.Produces())
   112  		if err != nil {
   113  			return ret, err
   114  		}
   115  	}
   116  
   117  	for code, resp := range o.config.CommonResponses {
   118  		if _, exists := ret.Responses.StatusCodeResponses[code]; !exists {
   119  			ret.Responses.StatusCodeResponses[code] = resp
   120  		}
   121  	}
   122  
   123  	if len(ret.Responses.StatusCodeResponses) == 0 {
   124  		ret.Responses.Default = o.config.DefaultResponse
   125  	}
   126  
   127  	params := route.Parameters()
   128  	for _, param := range params {
   129  		_, isCommon := inPathCommonParamsMap[mapKeyFromParam(param)]
   130  		if !isCommon && param.Kind() != common.BodyParameterKind {
   131  			openAPIParam, err := o.buildParameter(param)
   132  			if err != nil {
   133  				return ret, err
   134  			}
   135  			ret.Parameters = append(ret.Parameters, openAPIParam)
   136  		}
   137  	}
   138  
   139  	body, err := o.buildRequestBody(params, route.Consumes(), route.RequestPayloadSample())
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	if body != nil {
   145  		ret.RequestBody = body
   146  	}
   147  	return ret, nil
   148  }
   149  
   150  func (o *openAPI) buildRequestBody(parameters []common.Parameter, consumes []string, bodySample interface{}) (*spec3.RequestBody, error) {
   151  	for _, param := range parameters {
   152  		if param.Kind() == common.BodyParameterKind && bodySample != nil {
   153  			schema, err := o.toSchema(util.GetCanonicalTypeName(bodySample))
   154  			if err != nil {
   155  				return nil, err
   156  			}
   157  			r := &spec3.RequestBody{
   158  				RequestBodyProps: spec3.RequestBodyProps{
   159  					Content:     map[string]*spec3.MediaType{},
   160  					Description: param.Description(),
   161  					Required:    param.Required(),
   162  				},
   163  			}
   164  			for _, consume := range consumes {
   165  				r.Content[consume] = &spec3.MediaType{
   166  					MediaTypeProps: spec3.MediaTypeProps{
   167  						Schema: schema,
   168  					},
   169  				}
   170  			}
   171  			return r, nil
   172  		}
   173  	}
   174  	return nil, nil
   175  }
   176  
   177  func newOpenAPI(config *common.OpenAPIV3Config) openAPI {
   178  	o := openAPI{
   179  		config: config,
   180  		spec: &spec3.OpenAPI{
   181  			Version: "3.0.0",
   182  			Info:    config.Info,
   183  			Paths: &spec3.Paths{
   184  				Paths: map[string]*spec3.Path{},
   185  			},
   186  			Components: &spec3.Components{
   187  				Schemas: map[string]*spec.Schema{},
   188  			},
   189  		},
   190  	}
   191  	if len(o.config.ResponseDefinitions) > 0 {
   192  		o.spec.Components.Responses = make(map[string]*spec3.Response)
   193  
   194  	}
   195  	for k, response := range o.config.ResponseDefinitions {
   196  		o.spec.Components.Responses[k] = response
   197  	}
   198  
   199  	if len(o.config.SecuritySchemes) > 0 {
   200  		o.spec.Components.SecuritySchemes = make(spec3.SecuritySchemes)
   201  
   202  	}
   203  	for k, securityScheme := range o.config.SecuritySchemes {
   204  		o.spec.Components.SecuritySchemes[k] = securityScheme
   205  	}
   206  
   207  	if o.config.GetOperationIDAndTagsFromRoute == nil {
   208  		// Map the deprecated handler to the common interface, if provided.
   209  		if o.config.GetOperationIDAndTags != nil {
   210  			o.config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) {
   211  				restfulRouteAdapter, ok := r.(*restfuladapter.RouteAdapter)
   212  				if !ok {
   213  					return "", nil, fmt.Errorf("config.GetOperationIDAndTags specified but route is not a restful v1 Route")
   214  				}
   215  
   216  				return o.config.GetOperationIDAndTags(restfulRouteAdapter.Route)
   217  			}
   218  		} else {
   219  			o.config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) {
   220  				return r.OperationName(), nil, nil
   221  			}
   222  		}
   223  	}
   224  
   225  	if o.config.GetDefinitionName == nil {
   226  		o.config.GetDefinitionName = func(name string) (string, spec.Extensions) {
   227  			return name[strings.LastIndex(name, "/")+1:], nil
   228  		}
   229  	}
   230  
   231  	if o.config.Definitions != nil {
   232  		o.definitions = o.config.Definitions
   233  	} else {
   234  		o.definitions = o.config.GetDefinitions(func(name string) spec.Ref {
   235  			defName, _ := o.config.GetDefinitionName(name)
   236  			return spec.MustCreateRef("#/components/schemas/" + common.EscapeJsonPointer(defName))
   237  		})
   238  	}
   239  
   240  	return o
   241  }
   242  
   243  func (o *openAPI) buildOpenAPISpec(webServices []common.RouteContainer) error {
   244  	pathsToIgnore := util.NewTrie(o.config.IgnorePrefixes)
   245  	for _, w := range webServices {
   246  		rootPath := w.RootPath()
   247  		if pathsToIgnore.HasPrefix(rootPath) {
   248  			continue
   249  		}
   250  
   251  		commonParams, err := o.buildParameters(w.PathParameters())
   252  		if err != nil {
   253  			return err
   254  		}
   255  
   256  		for path, routes := range groupRoutesByPath(w.Routes()) {
   257  			// go-swagger has special variable definition {$NAME:*} that can only be
   258  			// used at the end of the path and it is not recognized by OpenAPI.
   259  			if strings.HasSuffix(path, ":*}") {
   260  				path = path[:len(path)-3] + "}"
   261  			}
   262  			if pathsToIgnore.HasPrefix(path) {
   263  				continue
   264  			}
   265  
   266  			// Aggregating common parameters make API spec (and generated clients) simpler
   267  			inPathCommonParamsMap, err := o.findCommonParameters(routes)
   268  			if err != nil {
   269  				return err
   270  			}
   271  			pathItem, exists := o.spec.Paths.Paths[path]
   272  			if exists {
   273  				return fmt.Errorf("duplicate webservice route has been found for path: %v", path)
   274  			}
   275  
   276  			pathItem = &spec3.Path{
   277  				PathProps: spec3.PathProps{},
   278  			}
   279  
   280  			// add web services's parameters as well as any parameters appears in all ops, as common parameters
   281  			pathItem.Parameters = append(pathItem.Parameters, commonParams...)
   282  			for _, p := range inPathCommonParamsMap {
   283  				pathItem.Parameters = append(pathItem.Parameters, p)
   284  			}
   285  			sortParameters(pathItem.Parameters)
   286  
   287  			for _, route := range routes {
   288  				op, _ := o.buildOperations(route, inPathCommonParamsMap)
   289  				sortParameters(op.Parameters)
   290  
   291  				switch strings.ToUpper(route.Method()) {
   292  				case "GET":
   293  					pathItem.Get = op
   294  				case "POST":
   295  					pathItem.Post = op
   296  				case "HEAD":
   297  					pathItem.Head = op
   298  				case "PUT":
   299  					pathItem.Put = op
   300  				case "DELETE":
   301  					pathItem.Delete = op
   302  				case "OPTIONS":
   303  					pathItem.Options = op
   304  				case "PATCH":
   305  					pathItem.Patch = op
   306  				}
   307  
   308  			}
   309  			o.spec.Paths.Paths[path] = pathItem
   310  		}
   311  	}
   312  	return nil
   313  }
   314  
   315  // BuildOpenAPISpec builds OpenAPI v3 spec given a list of route containers and common.Config to customize it.
   316  //
   317  // Deprecated: BuildOpenAPISpecFromRoutes should be used instead.
   318  func BuildOpenAPISpec(webServices []*restful.WebService, config *common.OpenAPIV3Config) (*spec3.OpenAPI, error) {
   319  	return BuildOpenAPISpecFromRoutes(restfuladapter.AdaptWebServices(webServices), config)
   320  }
   321  
   322  // BuildOpenAPISpecFromRoutes builds OpenAPI v3 spec given a list of route containers and common.Config to customize it.
   323  func BuildOpenAPISpecFromRoutes(webServices []common.RouteContainer, config *common.OpenAPIV3Config) (*spec3.OpenAPI, error) {
   324  	a := newOpenAPI(config)
   325  	err := a.buildOpenAPISpec(webServices)
   326  	if err != nil {
   327  		return nil, err
   328  	}
   329  	if config.PostProcessSpec != nil {
   330  		return config.PostProcessSpec(a.spec)
   331  	}
   332  	return a.spec, nil
   333  }
   334  
   335  // BuildOpenAPIDefinitionsForResource builds a partial OpenAPI spec given a sample object and common.Config to customize it.
   336  // BuildOpenAPIDefinitionsForResources returns the OpenAPI spec which includes the definitions for the
   337  // passed type names.
   338  func BuildOpenAPIDefinitionsForResources(config *common.OpenAPIV3Config, names ...string) (map[string]*spec.Schema, error) {
   339  	o := newOpenAPI(config)
   340  	// We can discard the return value of toSchema because all we care about is the side effect of calling it.
   341  	// All the models created for this resource get added to o.swagger.Definitions
   342  	for _, name := range names {
   343  		_, err := o.toSchema(name)
   344  		if err != nil {
   345  			return nil, err
   346  		}
   347  	}
   348  	return o.spec.Components.Schemas, nil
   349  }
   350  func (o *openAPI) findCommonParameters(routes []common.Route) (map[interface{}]*spec3.Parameter, error) {
   351  	commonParamsMap := make(map[interface{}]*spec3.Parameter, 0)
   352  	paramOpsCountByName := make(map[interface{}]int, 0)
   353  	paramNameKindToDataMap := make(map[interface{}]common.Parameter, 0)
   354  	for _, route := range routes {
   355  		routeParamDuplicateMap := make(map[interface{}]bool)
   356  		s := ""
   357  		params := route.Parameters()
   358  		for _, param := range params {
   359  			m, _ := json.Marshal(param)
   360  			s += string(m) + "\n"
   361  			key := mapKeyFromParam(param)
   362  			if routeParamDuplicateMap[key] {
   363  				msg, _ := json.Marshal(params)
   364  				return commonParamsMap, fmt.Errorf("duplicate parameter %v for route %v, %v", param.Name(), string(msg), s)
   365  			}
   366  			routeParamDuplicateMap[key] = true
   367  			paramOpsCountByName[key]++
   368  			paramNameKindToDataMap[key] = param
   369  		}
   370  	}
   371  	for key, count := range paramOpsCountByName {
   372  		paramData := paramNameKindToDataMap[key]
   373  		if count == len(routes) && paramData.Kind() != common.BodyParameterKind {
   374  			openAPIParam, err := o.buildParameter(paramData)
   375  			if err != nil {
   376  				return commonParamsMap, err
   377  			}
   378  			commonParamsMap[key] = openAPIParam
   379  		}
   380  	}
   381  	return commonParamsMap, nil
   382  }
   383  
   384  func (o *openAPI) buildParameters(restParam []common.Parameter) (ret []*spec3.Parameter, err error) {
   385  	ret = make([]*spec3.Parameter, len(restParam))
   386  	for i, v := range restParam {
   387  		ret[i], err = o.buildParameter(v)
   388  		if err != nil {
   389  			return ret, err
   390  		}
   391  	}
   392  	return ret, nil
   393  }
   394  
   395  func (o *openAPI) buildParameter(restParam common.Parameter) (ret *spec3.Parameter, err error) {
   396  	ret = &spec3.Parameter{
   397  		ParameterProps: spec3.ParameterProps{
   398  			Name:        restParam.Name(),
   399  			Description: restParam.Description(),
   400  			Required:    restParam.Required(),
   401  		},
   402  	}
   403  	switch restParam.Kind() {
   404  	case common.BodyParameterKind:
   405  		return nil, nil
   406  	case common.PathParameterKind:
   407  		ret.In = "path"
   408  		if !restParam.Required() {
   409  			return ret, fmt.Errorf("path parameters should be marked as required for parameter %v", restParam)
   410  		}
   411  	case common.QueryParameterKind:
   412  		ret.In = "query"
   413  	case common.HeaderParameterKind:
   414  		ret.In = "header"
   415  	/* TODO: add support for the cookie param */
   416  	default:
   417  		return ret, fmt.Errorf("unsupported restful parameter kind : %v", restParam.Kind())
   418  	}
   419  	openAPIType, openAPIFormat := common.OpenAPITypeFormat(restParam.DataType())
   420  	if openAPIType == "" {
   421  		return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", restParam.DataType())
   422  	}
   423  
   424  	ret.Schema = &spec.Schema{
   425  		SchemaProps: spec.SchemaProps{
   426  			Type:        []string{openAPIType},
   427  			Format:      openAPIFormat,
   428  			UniqueItems: !restParam.AllowMultiple(),
   429  		},
   430  	}
   431  	return ret, nil
   432  }
   433  
   434  func (o *openAPI) buildDefinitionRecursively(name string) error {
   435  	uniqueName, extensions := o.config.GetDefinitionName(name)
   436  	if _, ok := o.spec.Components.Schemas[uniqueName]; ok {
   437  		return nil
   438  	}
   439  	if item, ok := o.definitions[name]; ok {
   440  		schema := &spec.Schema{
   441  			VendorExtensible:   item.Schema.VendorExtensible,
   442  			SchemaProps:        item.Schema.SchemaProps,
   443  			SwaggerSchemaProps: item.Schema.SwaggerSchemaProps,
   444  		}
   445  		if extensions != nil {
   446  			if schema.Extensions == nil {
   447  				schema.Extensions = spec.Extensions{}
   448  			}
   449  			for k, v := range extensions {
   450  				schema.Extensions[k] = v
   451  			}
   452  		}
   453  		// delete the embedded v2 schema if exists, otherwise no-op
   454  		delete(schema.VendorExtensible.Extensions, common.ExtensionV2Schema)
   455  		schema = builderutil.WrapRefs(schema)
   456  		o.spec.Components.Schemas[uniqueName] = schema
   457  		for _, v := range item.Dependencies {
   458  			if err := o.buildDefinitionRecursively(v); err != nil {
   459  				return err
   460  			}
   461  		}
   462  	} else {
   463  		return fmt.Errorf("cannot find model definition for %v. If you added a new type, you may need to add +k8s:openapi-gen=true to the package or type and run code-gen again", name)
   464  	}
   465  	return nil
   466  }
   467  
   468  func (o *openAPI) buildDefinitionForType(name string) (string, error) {
   469  	if err := o.buildDefinitionRecursively(name); err != nil {
   470  		return "", err
   471  	}
   472  	defName, _ := o.config.GetDefinitionName(name)
   473  	return "#/components/schemas/" + common.EscapeJsonPointer(defName), nil
   474  }
   475  
   476  func (o *openAPI) toSchema(name string) (_ *spec.Schema, err error) {
   477  	if openAPIType, openAPIFormat := common.OpenAPITypeFormat(name); openAPIType != "" {
   478  		return &spec.Schema{
   479  			SchemaProps: spec.SchemaProps{
   480  				Type:   []string{openAPIType},
   481  				Format: openAPIFormat,
   482  			},
   483  		}, nil
   484  	} else {
   485  		ref, err := o.buildDefinitionForType(name)
   486  		if err != nil {
   487  			return nil, err
   488  		}
   489  		return &spec.Schema{
   490  			SchemaProps: spec.SchemaProps{
   491  				Ref: spec.MustCreateRef(ref),
   492  			},
   493  		}, nil
   494  	}
   495  }