1
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
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
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
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
258
259 if strings.HasSuffix(path, ":*}") {
260 path = path[:len(path)-3] + "}"
261 }
262 if pathsToIgnore.HasPrefix(path) {
263 continue
264 }
265
266
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
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
316
317
318 func BuildOpenAPISpec(webServices []*restful.WebService, config *common.OpenAPIV3Config) (*spec3.OpenAPI, error) {
319 return BuildOpenAPISpecFromRoutes(restfuladapter.AdaptWebServices(webServices), config)
320 }
321
322
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
336
337
338 func BuildOpenAPIDefinitionsForResources(config *common.OpenAPIV3Config, names ...string) (map[string]*spec.Schema, error) {
339 o := newOpenAPI(config)
340
341
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
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
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 }
496
View as plain text