k8s.io/kube-openapi@v0.0.0-20240826222958-65a50c78dec5/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, err := o.buildOperations(route, inPathCommonParamsMap)
   289  				if err != nil {
   290  					return err
   291  				}
   292  				sortParameters(op.Parameters)
   293  
   294  				switch strings.ToUpper(route.Method()) {
   295  				case "GET":
   296  					pathItem.Get = op
   297  				case "POST":
   298  					pathItem.Post = op
   299  				case "HEAD":
   300  					pathItem.Head = op
   301  				case "PUT":
   302  					pathItem.Put = op
   303  				case "DELETE":
   304  					pathItem.Delete = op
   305  				case "OPTIONS":
   306  					pathItem.Options = op
   307  				case "PATCH":
   308  					pathItem.Patch = op
   309  				}
   310  
   311  			}
   312  			o.spec.Paths.Paths[path] = pathItem
   313  		}
   314  	}
   315  	return nil
   316  }
   317  
   318  // BuildOpenAPISpec builds OpenAPI v3 spec given a list of route containers and common.Config to customize it.
   319  //
   320  // Deprecated: BuildOpenAPISpecFromRoutes should be used instead.
   321  func BuildOpenAPISpec(webServices []*restful.WebService, config *common.OpenAPIV3Config) (*spec3.OpenAPI, error) {
   322  	return BuildOpenAPISpecFromRoutes(restfuladapter.AdaptWebServices(webServices), config)
   323  }
   324  
   325  // BuildOpenAPISpecFromRoutes builds OpenAPI v3 spec given a list of route containers and common.Config to customize it.
   326  func BuildOpenAPISpecFromRoutes(webServices []common.RouteContainer, config *common.OpenAPIV3Config) (*spec3.OpenAPI, error) {
   327  	a := newOpenAPI(config)
   328  	err := a.buildOpenAPISpec(webServices)
   329  	if err != nil {
   330  		return nil, err
   331  	}
   332  	if config.PostProcessSpec != nil {
   333  		return config.PostProcessSpec(a.spec)
   334  	}
   335  	return a.spec, nil
   336  }
   337  
   338  // BuildOpenAPIDefinitionsForResource builds a partial OpenAPI spec given a sample object and common.Config to customize it.
   339  // BuildOpenAPIDefinitionsForResources returns the OpenAPI spec which includes the definitions for the
   340  // passed type names.
   341  func BuildOpenAPIDefinitionsForResources(config *common.OpenAPIV3Config, names ...string) (map[string]*spec.Schema, error) {
   342  	o := newOpenAPI(config)
   343  	// We can discard the return value of toSchema because all we care about is the side effect of calling it.
   344  	// All the models created for this resource get added to o.swagger.Definitions
   345  	for _, name := range names {
   346  		_, err := o.toSchema(name)
   347  		if err != nil {
   348  			return nil, err
   349  		}
   350  	}
   351  	return o.spec.Components.Schemas, nil
   352  }
   353  func (o *openAPI) findCommonParameters(routes []common.Route) (map[interface{}]*spec3.Parameter, error) {
   354  	commonParamsMap := make(map[interface{}]*spec3.Parameter, 0)
   355  	paramOpsCountByName := make(map[interface{}]int, 0)
   356  	paramNameKindToDataMap := make(map[interface{}]common.Parameter, 0)
   357  	for _, route := range routes {
   358  		routeParamDuplicateMap := make(map[interface{}]bool)
   359  		s := ""
   360  		params := route.Parameters()
   361  		for _, param := range params {
   362  			m, _ := json.Marshal(param)
   363  			s += string(m) + "\n"
   364  			key := mapKeyFromParam(param)
   365  			if routeParamDuplicateMap[key] {
   366  				msg, _ := json.Marshal(params)
   367  				return commonParamsMap, fmt.Errorf("duplicate parameter %v for route %v, %v", param.Name(), string(msg), s)
   368  			}
   369  			routeParamDuplicateMap[key] = true
   370  			paramOpsCountByName[key]++
   371  			paramNameKindToDataMap[key] = param
   372  		}
   373  	}
   374  	for key, count := range paramOpsCountByName {
   375  		paramData := paramNameKindToDataMap[key]
   376  		if count == len(routes) && paramData.Kind() != common.BodyParameterKind {
   377  			openAPIParam, err := o.buildParameter(paramData)
   378  			if err != nil {
   379  				return commonParamsMap, err
   380  			}
   381  			commonParamsMap[key] = openAPIParam
   382  		}
   383  	}
   384  	return commonParamsMap, nil
   385  }
   386  
   387  func (o *openAPI) buildParameters(restParam []common.Parameter) (ret []*spec3.Parameter, err error) {
   388  	ret = make([]*spec3.Parameter, len(restParam))
   389  	for i, v := range restParam {
   390  		ret[i], err = o.buildParameter(v)
   391  		if err != nil {
   392  			return ret, err
   393  		}
   394  	}
   395  	return ret, nil
   396  }
   397  
   398  func (o *openAPI) buildParameter(restParam common.Parameter) (ret *spec3.Parameter, err error) {
   399  	ret = &spec3.Parameter{
   400  		ParameterProps: spec3.ParameterProps{
   401  			Name:        restParam.Name(),
   402  			Description: restParam.Description(),
   403  			Required:    restParam.Required(),
   404  		},
   405  	}
   406  	switch restParam.Kind() {
   407  	case common.BodyParameterKind:
   408  		return nil, nil
   409  	case common.PathParameterKind:
   410  		ret.In = "path"
   411  		if !restParam.Required() {
   412  			return ret, fmt.Errorf("path parameters should be marked as required for parameter %v", restParam)
   413  		}
   414  	case common.QueryParameterKind:
   415  		ret.In = "query"
   416  	case common.HeaderParameterKind:
   417  		ret.In = "header"
   418  	/* TODO: add support for the cookie param */
   419  	default:
   420  		return ret, fmt.Errorf("unsupported restful parameter kind : %v", restParam.Kind())
   421  	}
   422  	openAPIType, openAPIFormat := common.OpenAPITypeFormat(restParam.DataType())
   423  	if openAPIType == "" {
   424  		return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", restParam.DataType())
   425  	}
   426  
   427  	ret.Schema = &spec.Schema{
   428  		SchemaProps: spec.SchemaProps{
   429  			Type:        []string{openAPIType},
   430  			Format:      openAPIFormat,
   431  			UniqueItems: !restParam.AllowMultiple(),
   432  		},
   433  	}
   434  	return ret, nil
   435  }
   436  
   437  func (o *openAPI) buildDefinitionRecursively(name string) error {
   438  	uniqueName, extensions := o.config.GetDefinitionName(name)
   439  	if _, ok := o.spec.Components.Schemas[uniqueName]; ok {
   440  		return nil
   441  	}
   442  	if item, ok := o.definitions[name]; ok {
   443  		schema := &spec.Schema{
   444  			VendorExtensible:   item.Schema.VendorExtensible,
   445  			SchemaProps:        item.Schema.SchemaProps,
   446  			SwaggerSchemaProps: item.Schema.SwaggerSchemaProps,
   447  		}
   448  		if extensions != nil {
   449  			if schema.Extensions == nil {
   450  				schema.Extensions = spec.Extensions{}
   451  			}
   452  			for k, v := range extensions {
   453  				schema.Extensions[k] = v
   454  			}
   455  		}
   456  		// delete the embedded v2 schema if exists, otherwise no-op
   457  		delete(schema.VendorExtensible.Extensions, common.ExtensionV2Schema)
   458  		schema = builderutil.WrapRefs(schema)
   459  		o.spec.Components.Schemas[uniqueName] = schema
   460  		for _, v := range item.Dependencies {
   461  			if err := o.buildDefinitionRecursively(v); err != nil {
   462  				return err
   463  			}
   464  		}
   465  	} else {
   466  		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)
   467  	}
   468  	return nil
   469  }
   470  
   471  func (o *openAPI) buildDefinitionForType(name string) (string, error) {
   472  	if err := o.buildDefinitionRecursively(name); err != nil {
   473  		return "", err
   474  	}
   475  	defName, _ := o.config.GetDefinitionName(name)
   476  	return "#/components/schemas/" + common.EscapeJsonPointer(defName), nil
   477  }
   478  
   479  func (o *openAPI) toSchema(name string) (_ *spec.Schema, err error) {
   480  	if openAPIType, openAPIFormat := common.OpenAPITypeFormat(name); openAPIType != "" {
   481  		return &spec.Schema{
   482  			SchemaProps: spec.SchemaProps{
   483  				Type:   []string{openAPIType},
   484  				Format: openAPIFormat,
   485  			},
   486  		}, nil
   487  	} else {
   488  		ref, err := o.buildDefinitionForType(name)
   489  		if err != nil {
   490  			return nil, err
   491  		}
   492  		return &spec.Schema{
   493  			SchemaProps: spec.SchemaProps{
   494  				Ref: spec.MustCreateRef(ref),
   495  			},
   496  		}, nil
   497  	}
   498  }