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