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

     1  /*
     2  Copyright 2016 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 builder
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"net/http"
    23  	"strings"
    24  
    25  	restful "github.com/emicklei/go-restful/v3"
    26  
    27  	"k8s.io/kube-openapi/pkg/common"
    28  	"k8s.io/kube-openapi/pkg/common/restfuladapter"
    29  	"k8s.io/kube-openapi/pkg/util"
    30  	"k8s.io/kube-openapi/pkg/validation/spec"
    31  )
    32  
    33  const (
    34  	OpenAPIVersion = "2.0"
    35  )
    36  
    37  type openAPI struct {
    38  	config       *common.Config
    39  	swagger      *spec.Swagger
    40  	protocolList []string
    41  	definitions  map[string]common.OpenAPIDefinition
    42  }
    43  
    44  // BuildOpenAPISpec builds OpenAPI spec given a list of route containers and common.Config to customize it.
    45  //
    46  // Deprecated: BuildOpenAPISpecFromRoutes should be used instead.
    47  func BuildOpenAPISpec(routeContainers []*restful.WebService, config *common.Config) (*spec.Swagger, error) {
    48  	return BuildOpenAPISpecFromRoutes(restfuladapter.AdaptWebServices(routeContainers), config)
    49  }
    50  
    51  // BuildOpenAPISpecFromRoutes builds OpenAPI spec given a list of route containers and common.Config to customize it.
    52  func BuildOpenAPISpecFromRoutes(routeContainers []common.RouteContainer, config *common.Config) (*spec.Swagger, error) {
    53  	o := newOpenAPI(config)
    54  	err := o.buildPaths(routeContainers)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	return o.finalizeSwagger()
    59  }
    60  
    61  // BuildOpenAPIDefinitionsForResource builds a partial OpenAPI spec given a sample object and common.Config to customize it.
    62  func BuildOpenAPIDefinitionsForResource(model interface{}, config *common.Config) (*spec.Definitions, error) {
    63  	o := newOpenAPI(config)
    64  	// We can discard the return value of toSchema because all we care about is the side effect of calling it.
    65  	// All the models created for this resource get added to o.swagger.Definitions
    66  	_, err := o.toSchema(util.GetCanonicalTypeName(model))
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  	swagger, err := o.finalizeSwagger()
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	return &swagger.Definitions, nil
    75  }
    76  
    77  // BuildOpenAPIDefinitionsForResources returns the OpenAPI spec which includes the definitions for the
    78  // passed type names.
    79  func BuildOpenAPIDefinitionsForResources(config *common.Config, names ...string) (*spec.Swagger, error) {
    80  	o := newOpenAPI(config)
    81  	// We can discard the return value of toSchema because all we care about is the side effect of calling it.
    82  	// All the models created for this resource get added to o.swagger.Definitions
    83  	for _, name := range names {
    84  		_, err := o.toSchema(name)
    85  		if err != nil {
    86  			return nil, err
    87  		}
    88  	}
    89  	return o.finalizeSwagger()
    90  }
    91  
    92  // newOpenAPI sets up the openAPI object so we can build the spec.
    93  func newOpenAPI(config *common.Config) openAPI {
    94  	o := openAPI{
    95  		config: config,
    96  		swagger: &spec.Swagger{
    97  			SwaggerProps: spec.SwaggerProps{
    98  				Swagger:     OpenAPIVersion,
    99  				Definitions: spec.Definitions{},
   100  				Responses:   config.ResponseDefinitions,
   101  				Paths:       &spec.Paths{Paths: map[string]spec.PathItem{}},
   102  				Info:        config.Info,
   103  			},
   104  		},
   105  	}
   106  
   107  	if o.config.GetOperationIDAndTagsFromRoute == nil {
   108  		// Map the deprecated handler to the common interface, if provided.
   109  		if o.config.GetOperationIDAndTags != nil {
   110  			o.config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) {
   111  				restfulRouteAdapter, ok := r.(*restfuladapter.RouteAdapter)
   112  				if !ok {
   113  					return "", nil, fmt.Errorf("config.GetOperationIDAndTags specified but route is not a restful v1 Route")
   114  				}
   115  
   116  				return o.config.GetOperationIDAndTags(restfulRouteAdapter.Route)
   117  			}
   118  		} else {
   119  			o.config.GetOperationIDAndTagsFromRoute = func(r common.Route) (string, []string, error) {
   120  				return r.OperationName(), nil, nil
   121  			}
   122  		}
   123  	}
   124  
   125  	if o.config.GetDefinitionName == nil {
   126  		o.config.GetDefinitionName = func(name string) (string, spec.Extensions) {
   127  			return name[strings.LastIndex(name, "/")+1:], nil
   128  		}
   129  	}
   130  	o.definitions = o.config.GetDefinitions(func(name string) spec.Ref {
   131  		defName, _ := o.config.GetDefinitionName(name)
   132  		return spec.MustCreateRef("#/definitions/" + common.EscapeJsonPointer(defName))
   133  	})
   134  	if o.config.CommonResponses == nil {
   135  		o.config.CommonResponses = map[int]spec.Response{}
   136  	}
   137  	return o
   138  }
   139  
   140  // finalizeSwagger is called after the spec is built and returns the final spec.
   141  // NOTE: finalizeSwagger also make changes to the final spec, as specified in the config.
   142  func (o *openAPI) finalizeSwagger() (*spec.Swagger, error) {
   143  	if o.config.SecurityDefinitions != nil {
   144  		o.swagger.SecurityDefinitions = *o.config.SecurityDefinitions
   145  		o.swagger.Security = o.config.DefaultSecurity
   146  	}
   147  	if o.config.PostProcessSpec != nil {
   148  		var err error
   149  		o.swagger, err = o.config.PostProcessSpec(o.swagger)
   150  		if err != nil {
   151  			return nil, err
   152  		}
   153  	}
   154  
   155  	return deduplicateParameters(o.swagger)
   156  }
   157  
   158  func (o *openAPI) buildDefinitionRecursively(name string) error {
   159  	uniqueName, extensions := o.config.GetDefinitionName(name)
   160  	if _, ok := o.swagger.Definitions[uniqueName]; ok {
   161  		return nil
   162  	}
   163  	if item, ok := o.definitions[name]; ok {
   164  		schema := spec.Schema{
   165  			VendorExtensible:   item.Schema.VendorExtensible,
   166  			SchemaProps:        item.Schema.SchemaProps,
   167  			SwaggerSchemaProps: item.Schema.SwaggerSchemaProps,
   168  		}
   169  		if extensions != nil {
   170  			if schema.Extensions == nil {
   171  				schema.Extensions = spec.Extensions{}
   172  			}
   173  			for k, v := range extensions {
   174  				schema.Extensions[k] = v
   175  			}
   176  		}
   177  		if v, ok := item.Schema.Extensions[common.ExtensionV2Schema]; ok {
   178  			if v2Schema, isOpenAPISchema := v.(spec.Schema); isOpenAPISchema {
   179  				schema = v2Schema
   180  			}
   181  		}
   182  		o.swagger.Definitions[uniqueName] = schema
   183  		for _, v := range item.Dependencies {
   184  			if err := o.buildDefinitionRecursively(v); err != nil {
   185  				return err
   186  			}
   187  		}
   188  	} else {
   189  		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)
   190  	}
   191  	return nil
   192  }
   193  
   194  // buildDefinitionForType build a definition for a given type and return a referable name to its definition.
   195  // This is the main function that keep track of definitions used in this spec and is depend on code generated
   196  // by k8s.io/kubernetes/cmd/libs/go2idl/openapi-gen.
   197  func (o *openAPI) buildDefinitionForType(name string) (string, error) {
   198  	if err := o.buildDefinitionRecursively(name); err != nil {
   199  		return "", err
   200  	}
   201  	defName, _ := o.config.GetDefinitionName(name)
   202  	return "#/definitions/" + common.EscapeJsonPointer(defName), nil
   203  }
   204  
   205  // buildPaths builds OpenAPI paths using go-restful's web services.
   206  func (o *openAPI) buildPaths(routeContainers []common.RouteContainer) error {
   207  	pathsToIgnore := util.NewTrie(o.config.IgnorePrefixes)
   208  	duplicateOpId := make(map[string]string)
   209  	for _, w := range routeContainers {
   210  		rootPath := w.RootPath()
   211  		if pathsToIgnore.HasPrefix(rootPath) {
   212  			continue
   213  		}
   214  		commonParams, err := o.buildParameters(w.PathParameters())
   215  		if err != nil {
   216  			return err
   217  		}
   218  		for path, routes := range groupRoutesByPath(w.Routes()) {
   219  			// go-swagger has special variable definition {$NAME:*} that can only be
   220  			// used at the end of the path and it is not recognized by OpenAPI.
   221  			if strings.HasSuffix(path, ":*}") {
   222  				path = path[:len(path)-3] + "}"
   223  			}
   224  			if pathsToIgnore.HasPrefix(path) {
   225  				continue
   226  			}
   227  			// Aggregating common parameters make API spec (and generated clients) simpler
   228  			inPathCommonParamsMap, err := o.findCommonParameters(routes)
   229  			if err != nil {
   230  				return err
   231  			}
   232  			pathItem, exists := o.swagger.Paths.Paths[path]
   233  			if exists {
   234  				return fmt.Errorf("duplicate webservice route has been found for path: %v", path)
   235  			}
   236  			pathItem = spec.PathItem{
   237  				PathItemProps: spec.PathItemProps{
   238  					Parameters: make([]spec.Parameter, 0),
   239  				},
   240  			}
   241  			// add web services's parameters as well as any parameters appears in all ops, as common parameters
   242  			pathItem.Parameters = append(pathItem.Parameters, commonParams...)
   243  			for _, p := range inPathCommonParamsMap {
   244  				pathItem.Parameters = append(pathItem.Parameters, p)
   245  			}
   246  			sortParameters(pathItem.Parameters)
   247  			for _, route := range routes {
   248  				op, err := o.buildOperations(route, inPathCommonParamsMap)
   249  				sortParameters(op.Parameters)
   250  				if err != nil {
   251  					return err
   252  				}
   253  				dpath, exists := duplicateOpId[op.ID]
   254  				if exists {
   255  					return fmt.Errorf("duplicate Operation ID %v for path %v and %v", op.ID, dpath, path)
   256  				} else {
   257  					duplicateOpId[op.ID] = path
   258  				}
   259  				switch strings.ToUpper(route.Method()) {
   260  				case "GET":
   261  					pathItem.Get = op
   262  				case "POST":
   263  					pathItem.Post = op
   264  				case "HEAD":
   265  					pathItem.Head = op
   266  				case "PUT":
   267  					pathItem.Put = op
   268  				case "DELETE":
   269  					pathItem.Delete = op
   270  				case "OPTIONS":
   271  					pathItem.Options = op
   272  				case "PATCH":
   273  					pathItem.Patch = op
   274  				}
   275  			}
   276  			o.swagger.Paths.Paths[path] = pathItem
   277  		}
   278  	}
   279  	return nil
   280  }
   281  
   282  // buildOperations builds operations for each webservice path
   283  func (o *openAPI) buildOperations(route common.Route, inPathCommonParamsMap map[interface{}]spec.Parameter) (ret *spec.Operation, err error) {
   284  	ret = &spec.Operation{
   285  		OperationProps: spec.OperationProps{
   286  			Description: route.Description(),
   287  			Consumes:    route.Consumes(),
   288  			Produces:    route.Produces(),
   289  			Schemes:     o.config.ProtocolList,
   290  			Responses: &spec.Responses{
   291  				ResponsesProps: spec.ResponsesProps{
   292  					StatusCodeResponses: make(map[int]spec.Response),
   293  				},
   294  			},
   295  		},
   296  	}
   297  	for k, v := range route.Metadata() {
   298  		if strings.HasPrefix(k, common.ExtensionPrefix) {
   299  			if ret.Extensions == nil {
   300  				ret.Extensions = spec.Extensions{}
   301  			}
   302  			ret.Extensions.Add(k, v)
   303  		}
   304  	}
   305  	if ret.ID, ret.Tags, err = o.config.GetOperationIDAndTagsFromRoute(route); err != nil {
   306  		return ret, err
   307  	}
   308  
   309  	// Build responses
   310  	for _, resp := range route.StatusCodeResponses() {
   311  		ret.Responses.StatusCodeResponses[resp.Code()], err = o.buildResponse(resp.Model(), resp.Message())
   312  		if err != nil {
   313  			return ret, err
   314  		}
   315  	}
   316  	// If there is no response but a write sample, assume that write sample is an http.StatusOK response.
   317  	if len(ret.Responses.StatusCodeResponses) == 0 && route.ResponsePayloadSample() != nil {
   318  		ret.Responses.StatusCodeResponses[http.StatusOK], err = o.buildResponse(route.ResponsePayloadSample(), "OK")
   319  		if err != nil {
   320  			return ret, err
   321  		}
   322  	}
   323  	for code, resp := range o.config.CommonResponses {
   324  		if _, exists := ret.Responses.StatusCodeResponses[code]; !exists {
   325  			ret.Responses.StatusCodeResponses[code] = resp
   326  		}
   327  	}
   328  	// If there is still no response, use default response provided.
   329  	if len(ret.Responses.StatusCodeResponses) == 0 {
   330  		ret.Responses.Default = o.config.DefaultResponse
   331  	}
   332  
   333  	// Build non-common Parameters
   334  	ret.Parameters = make([]spec.Parameter, 0)
   335  	for _, param := range route.Parameters() {
   336  		if _, isCommon := inPathCommonParamsMap[mapKeyFromParam(param)]; !isCommon {
   337  			openAPIParam, err := o.buildParameter(param, route.RequestPayloadSample())
   338  			if err != nil {
   339  				return ret, err
   340  			}
   341  			ret.Parameters = append(ret.Parameters, openAPIParam)
   342  		}
   343  	}
   344  	return ret, nil
   345  }
   346  
   347  func (o *openAPI) buildResponse(model interface{}, description string) (spec.Response, error) {
   348  	schema, err := o.toSchema(util.GetCanonicalTypeName(model))
   349  	if err != nil {
   350  		return spec.Response{}, err
   351  	}
   352  	return spec.Response{
   353  		ResponseProps: spec.ResponseProps{
   354  			Description: description,
   355  			Schema:      schema,
   356  		},
   357  	}, nil
   358  }
   359  
   360  func (o *openAPI) findCommonParameters(routes []common.Route) (map[interface{}]spec.Parameter, error) {
   361  	commonParamsMap := make(map[interface{}]spec.Parameter, 0)
   362  	paramOpsCountByName := make(map[interface{}]int, 0)
   363  	paramNameKindToDataMap := make(map[interface{}]common.Parameter, 0)
   364  	for _, route := range routes {
   365  		routeParamDuplicateMap := make(map[interface{}]bool)
   366  		s := ""
   367  		params := route.Parameters()
   368  		for _, param := range params {
   369  			m, _ := json.Marshal(param)
   370  			s += string(m) + "\n"
   371  			key := mapKeyFromParam(param)
   372  			if routeParamDuplicateMap[key] {
   373  				msg, _ := json.Marshal(params)
   374  				return commonParamsMap, fmt.Errorf("duplicate parameter %v for route %v, %v", param.Name(), string(msg), s)
   375  			}
   376  			routeParamDuplicateMap[key] = true
   377  			paramOpsCountByName[key]++
   378  			paramNameKindToDataMap[key] = param
   379  		}
   380  	}
   381  	for key, count := range paramOpsCountByName {
   382  		paramData := paramNameKindToDataMap[key]
   383  		if count == len(routes) && paramData.Kind() != common.BodyParameterKind {
   384  			openAPIParam, err := o.buildParameter(paramData, nil)
   385  			if err != nil {
   386  				return commonParamsMap, err
   387  			}
   388  			commonParamsMap[key] = openAPIParam
   389  		}
   390  	}
   391  	return commonParamsMap, nil
   392  }
   393  
   394  func (o *openAPI) toSchema(name string) (_ *spec.Schema, err error) {
   395  	if openAPIType, openAPIFormat := common.OpenAPITypeFormat(name); openAPIType != "" {
   396  		return &spec.Schema{
   397  			SchemaProps: spec.SchemaProps{
   398  				Type:   []string{openAPIType},
   399  				Format: openAPIFormat,
   400  			},
   401  		}, nil
   402  	} else {
   403  		ref, err := o.buildDefinitionForType(name)
   404  		if err != nil {
   405  			return nil, err
   406  		}
   407  		return &spec.Schema{
   408  			SchemaProps: spec.SchemaProps{
   409  				Ref: spec.MustCreateRef(ref),
   410  			},
   411  		}, nil
   412  	}
   413  }
   414  
   415  func (o *openAPI) buildParameter(restParam common.Parameter, bodySample interface{}) (ret spec.Parameter, err error) {
   416  	ret = spec.Parameter{
   417  		ParamProps: spec.ParamProps{
   418  			Name:        restParam.Name(),
   419  			Description: restParam.Description(),
   420  			Required:    restParam.Required(),
   421  		},
   422  	}
   423  	switch restParam.Kind() {
   424  	case common.BodyParameterKind:
   425  		if bodySample != nil {
   426  			ret.In = "body"
   427  			ret.Schema, err = o.toSchema(util.GetCanonicalTypeName(bodySample))
   428  			return ret, err
   429  		} else {
   430  			// There is not enough information in the body parameter to build the definition.
   431  			// Body parameter has a data type that is a short name but we need full package name
   432  			// of the type to create a definition.
   433  			return ret, fmt.Errorf("restful body parameters are not supported: %v", restParam.DataType())
   434  		}
   435  	case common.PathParameterKind:
   436  		ret.In = "path"
   437  		if !restParam.Required() {
   438  			return ret, fmt.Errorf("path parameters should be marked at required for parameter %v", restParam)
   439  		}
   440  	case common.QueryParameterKind:
   441  		ret.In = "query"
   442  	case common.HeaderParameterKind:
   443  		ret.In = "header"
   444  	case common.FormParameterKind:
   445  		ret.In = "formData"
   446  	default:
   447  		return ret, fmt.Errorf("unknown restful operation kind : %v", restParam.Kind())
   448  	}
   449  	openAPIType, openAPIFormat := common.OpenAPITypeFormat(restParam.DataType())
   450  	if openAPIType == "" {
   451  		return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", restParam.DataType())
   452  	}
   453  	ret.Type = openAPIType
   454  	ret.Format = openAPIFormat
   455  	ret.UniqueItems = !restParam.AllowMultiple()
   456  	return ret, nil
   457  }
   458  
   459  func (o *openAPI) buildParameters(restParam []common.Parameter) (ret []spec.Parameter, err error) {
   460  	ret = make([]spec.Parameter, len(restParam))
   461  	for i, v := range restParam {
   462  		ret[i], err = o.buildParameter(v, nil)
   463  		if err != nil {
   464  			return ret, err
   465  		}
   466  	}
   467  	return ret, nil
   468  }