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 }