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 }