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 }