558 lines
16 KiB
Go
558 lines
16 KiB
Go
/*
|
|
Copyright 2016 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package openapi
|
|
|
|
// Note: Any reference to swagger in this document is to swagger 1.2 spec.
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/emicklei/go-restful"
|
|
"github.com/emicklei/go-restful/swagger"
|
|
"github.com/go-openapi/loads"
|
|
"github.com/go-openapi/spec"
|
|
"github.com/go-openapi/strfmt"
|
|
"github.com/go-openapi/validate"
|
|
"k8s.io/kubernetes/pkg/util/json"
|
|
)
|
|
|
|
const (
|
|
// By convention, the Swagger specification file is named swagger.json
|
|
OpenAPIServePath = "/swagger.json"
|
|
OpenAPIVersion = "2.0"
|
|
)
|
|
|
|
// Config is set of configuration for openAPI spec generation.
|
|
type Config struct {
|
|
// SwaggerConfig is set of configuration for go-restful swagger spec generation. Currently
|
|
// openAPI implementation depends on go-restful to generate models.
|
|
SwaggerConfig *swagger.Config
|
|
// Info is general information about the API.
|
|
Info *spec.Info
|
|
// DefaultResponse will be used if an operation does not have any responses listed. It
|
|
// will show up as ... "responses" : {"default" : $DefaultResponse} in swagger spec.
|
|
DefaultResponse *spec.Response
|
|
// List of webservice's path prefixes to ignore
|
|
IgnorePrefixes []string
|
|
}
|
|
|
|
type openAPI struct {
|
|
config *Config
|
|
swagger *spec.Swagger
|
|
protocolList []string
|
|
}
|
|
|
|
// RegisterOpenAPIService registers a handler to provides standard OpenAPI specification.
|
|
func RegisterOpenAPIService(config *Config, containers *restful.Container) (err error) {
|
|
var _ = loads.Spec
|
|
var _ = strfmt.ParseDuration
|
|
var _ = validate.FormatOf
|
|
o := openAPI{
|
|
config: config,
|
|
}
|
|
err = o.buildSwaggerSpec()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
containers.ServeMux.HandleFunc(OpenAPIServePath, func(w http.ResponseWriter, r *http.Request) {
|
|
resp := restful.NewResponse(w)
|
|
if r.URL.Path != OpenAPIServePath {
|
|
resp.WriteErrorString(http.StatusNotFound, "Path not found!")
|
|
}
|
|
resp.WriteAsJson(o.swagger)
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (o *openAPI) buildSwaggerSpec() (err error) {
|
|
if o.swagger != nil {
|
|
return fmt.Errorf("Swagger spec is already built. Duplicate call to buildSwaggerSpec is not allowed.")
|
|
}
|
|
o.protocolList, err = o.buildProtocolList()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
definitions, err := o.buildDefinitions()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
paths, err := o.buildPaths()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.swagger = &spec.Swagger{
|
|
SwaggerProps: spec.SwaggerProps{
|
|
Swagger: OpenAPIVersion,
|
|
Definitions: definitions,
|
|
Paths: &paths,
|
|
Info: o.config.Info,
|
|
},
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// buildDefinitions construct OpenAPI definitions using go-restful's swagger 1.2 generated models.
|
|
func (o *openAPI) buildDefinitions() (definitions spec.Definitions, err error) {
|
|
definitions = spec.Definitions{}
|
|
for _, decl := range swagger.NewSwaggerBuilder(*o.config.SwaggerConfig).ProduceAllDeclarations() {
|
|
for _, swaggerModel := range decl.Models.List {
|
|
_, ok := definitions[swaggerModel.Name]
|
|
if ok {
|
|
// TODO(mbohlool): decide what to do with repeated models
|
|
// The best way is to make sure they have the same content and
|
|
// fail otherwise.
|
|
continue
|
|
}
|
|
definitions[swaggerModel.Name], err = buildModel(swaggerModel.Model)
|
|
if err != nil {
|
|
return definitions, err
|
|
}
|
|
}
|
|
}
|
|
return definitions, nil
|
|
}
|
|
|
|
func buildModel(swaggerModel swagger.Model) (ret spec.Schema, err error) {
|
|
ret = spec.Schema{
|
|
// SchemaProps.SubTypes is not used in go-restful, ignoring.
|
|
SchemaProps: spec.SchemaProps{
|
|
Description: swaggerModel.Description,
|
|
Required: swaggerModel.Required,
|
|
Properties: make(map[string]spec.Schema),
|
|
},
|
|
SwaggerSchemaProps: spec.SwaggerSchemaProps{
|
|
Discriminator: swaggerModel.Discriminator,
|
|
},
|
|
}
|
|
for _, swaggerProp := range swaggerModel.Properties.List {
|
|
if _, ok := ret.Properties[swaggerProp.Name]; ok {
|
|
return ret, fmt.Errorf("Duplicate property in swagger 1.2 spec: %v", swaggerProp.Name)
|
|
}
|
|
ret.Properties[swaggerProp.Name], err = buildProperty(swaggerProp)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// buildProperty converts a swagger 1.2 property to an open API property.
|
|
func buildProperty(swaggerProperty swagger.NamedModelProperty) (openAPIProperty spec.Schema, err error) {
|
|
if swaggerProperty.Property.Ref != nil {
|
|
return spec.Schema{
|
|
SchemaProps: spec.SchemaProps{
|
|
Ref: spec.MustCreateRef("#/definitions/" + *swaggerProperty.Property.Ref),
|
|
},
|
|
}, nil
|
|
}
|
|
openAPIProperty = spec.Schema{
|
|
SchemaProps: spec.SchemaProps{
|
|
Description: swaggerProperty.Property.Description,
|
|
Default: getDefaultValue(swaggerProperty.Property.DefaultValue),
|
|
Enum: make([]interface{}, len(swaggerProperty.Property.Enum)),
|
|
},
|
|
}
|
|
for i, e := range swaggerProperty.Property.Enum {
|
|
openAPIProperty.Enum[i] = e
|
|
}
|
|
openAPIProperty.Minimum, err = getFloat64OrNil(swaggerProperty.Property.Minimum)
|
|
if err != nil {
|
|
return spec.Schema{}, err
|
|
}
|
|
openAPIProperty.Maximum, err = getFloat64OrNil(swaggerProperty.Property.Maximum)
|
|
if err != nil {
|
|
return spec.Schema{}, err
|
|
}
|
|
if swaggerProperty.Property.UniqueItems != nil {
|
|
openAPIProperty.UniqueItems = *swaggerProperty.Property.UniqueItems
|
|
}
|
|
|
|
if swaggerProperty.Property.Items != nil {
|
|
if swaggerProperty.Property.Items.Ref != nil {
|
|
openAPIProperty.Items = &spec.SchemaOrArray{
|
|
Schema: &spec.Schema{
|
|
SchemaProps: spec.SchemaProps{
|
|
Ref: spec.MustCreateRef("#/definitions/" + *swaggerProperty.Property.Items.Ref),
|
|
},
|
|
},
|
|
}
|
|
} else {
|
|
openAPIProperty.Items = &spec.SchemaOrArray{
|
|
Schema: &spec.Schema{},
|
|
}
|
|
openAPIProperty.Items.Schema.Type, openAPIProperty.Items.Schema.Format, err =
|
|
buildType(swaggerProperty.Property.Items.Type, swaggerProperty.Property.Items.Format)
|
|
if err != nil {
|
|
return spec.Schema{}, err
|
|
}
|
|
}
|
|
}
|
|
openAPIProperty.Type, openAPIProperty.Format, err =
|
|
buildType(swaggerProperty.Property.Type, swaggerProperty.Property.Format)
|
|
if err != nil {
|
|
return spec.Schema{}, err
|
|
}
|
|
return openAPIProperty, nil
|
|
}
|
|
|
|
// buildPaths builds OpenAPI paths using go-restful's web services.
|
|
func (o *openAPI) buildPaths() (spec.Paths, error) {
|
|
paths := spec.Paths{
|
|
Paths: make(map[string]spec.PathItem),
|
|
}
|
|
pathsToIgnore := createTrie(o.config.IgnorePrefixes)
|
|
duplicateOpId := make(map[string]bool)
|
|
// Find duplicate operation IDs.
|
|
for _, service := range o.config.SwaggerConfig.WebServices {
|
|
if pathsToIgnore.HasPrefix(service.RootPath()) {
|
|
continue
|
|
}
|
|
for _, route := range service.Routes() {
|
|
_, exists := duplicateOpId[route.Operation]
|
|
duplicateOpId[route.Operation] = exists
|
|
}
|
|
}
|
|
for _, w := range o.config.SwaggerConfig.WebServices {
|
|
rootPath := w.RootPath()
|
|
if pathsToIgnore.HasPrefix(rootPath) {
|
|
continue
|
|
}
|
|
commonParams, err := buildParameters(w.PathParameters())
|
|
if err != nil {
|
|
return paths, err
|
|
}
|
|
for path, routes := range groupRoutesByPath(w.Routes()) {
|
|
// go-swagger has special variable difinition {$NAME:*} that can only be
|
|
// used at the end of the path and it is not recognized by OpenAPI.
|
|
if strings.HasSuffix(path, ":*}") {
|
|
path = path[:len(path)-3] + "}"
|
|
}
|
|
inPathCommonParamsMap, err := findCommonParameters(routes)
|
|
if err != nil {
|
|
return paths, err
|
|
}
|
|
pathItem, exists := paths.Paths[path]
|
|
if exists {
|
|
return paths, fmt.Errorf("Duplicate webservice route has been found for path: %v", path)
|
|
}
|
|
pathItem = spec.PathItem{
|
|
PathItemProps: spec.PathItemProps{
|
|
Parameters: make([]spec.Parameter, 0),
|
|
},
|
|
}
|
|
// add web services's parameters as well as any parameters appears in all ops, as common parameters
|
|
for _, p := range commonParams {
|
|
pathItem.Parameters = append(pathItem.Parameters, p)
|
|
}
|
|
for _, p := range inPathCommonParamsMap {
|
|
pathItem.Parameters = append(pathItem.Parameters, p)
|
|
}
|
|
for _, route := range routes {
|
|
op, err := o.buildOperations(route, inPathCommonParamsMap)
|
|
if err != nil {
|
|
return paths, err
|
|
}
|
|
if duplicateOpId[op.ID] {
|
|
// Repeated Operation IDs are not allowed in OpenAPI spec but if
|
|
// an OperationID is empty, client generators will infer the ID
|
|
// from the path and method of operation.
|
|
op.ID = ""
|
|
}
|
|
switch strings.ToUpper(route.Method) {
|
|
case "GET":
|
|
pathItem.Get = op
|
|
case "POST":
|
|
pathItem.Post = op
|
|
case "HEAD":
|
|
pathItem.Head = op
|
|
case "PUT":
|
|
pathItem.Put = op
|
|
case "DELETE":
|
|
pathItem.Delete = op
|
|
case "OPTIONS":
|
|
pathItem.Options = op
|
|
case "PATCH":
|
|
pathItem.Patch = op
|
|
}
|
|
}
|
|
paths.Paths[path] = pathItem
|
|
}
|
|
}
|
|
|
|
return paths, nil
|
|
}
|
|
|
|
// buildProtocolList returns list of accepted protocols for this web service. If web service url has no protocol, it
|
|
// will default to http.
|
|
func (o *openAPI) buildProtocolList() ([]string, error) {
|
|
uri, err := url.Parse(o.config.SwaggerConfig.WebServicesUrl)
|
|
if err != nil {
|
|
return []string{}, err
|
|
}
|
|
if uri.Scheme != "" {
|
|
return []string{uri.Scheme}, nil
|
|
} else {
|
|
return []string{"http"}, nil
|
|
}
|
|
}
|
|
|
|
// buildOperations builds operations for each webservice path
|
|
func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map[interface{}]spec.Parameter) (*spec.Operation, error) {
|
|
ret := &spec.Operation{
|
|
OperationProps: spec.OperationProps{
|
|
Description: route.Doc,
|
|
Consumes: route.Consumes,
|
|
Produces: route.Produces,
|
|
ID: route.Operation,
|
|
Schemes: o.protocolList,
|
|
Responses: &spec.Responses{
|
|
ResponsesProps: spec.ResponsesProps{
|
|
StatusCodeResponses: make(map[int]spec.Response),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, resp := range route.ResponseErrors {
|
|
ret.Responses.StatusCodeResponses[resp.Code] = spec.Response{
|
|
ResponseProps: spec.ResponseProps{
|
|
Description: resp.Message,
|
|
Schema: &spec.Schema{
|
|
SchemaProps: spec.SchemaProps{
|
|
Ref: spec.MustCreateRef("#/definitions/" + reflect.TypeOf(resp.Model).String()),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
if len(ret.Responses.StatusCodeResponses) == 0 {
|
|
ret.Responses.Default = o.config.DefaultResponse
|
|
}
|
|
ret.Parameters = make([]spec.Parameter, 0)
|
|
for _, param := range route.ParameterDocs {
|
|
_, isCommon := inPathCommonParamsMap[mapKeyFromParam(param)]
|
|
if !isCommon {
|
|
openAPIParam, err := buildParameter(param.Data())
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
ret.Parameters = append(ret.Parameters, openAPIParam)
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func groupRoutesByPath(routes []restful.Route) (ret map[string][]restful.Route) {
|
|
ret = make(map[string][]restful.Route)
|
|
for _, r := range routes {
|
|
route, exists := ret[r.Path]
|
|
if !exists {
|
|
route = make([]restful.Route, 0, 1)
|
|
}
|
|
ret[r.Path] = append(route, r)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func mapKeyFromParam(param *restful.Parameter) interface{} {
|
|
return struct {
|
|
Name string
|
|
Kind int
|
|
}{
|
|
Name: param.Data().Name,
|
|
Kind: param.Data().Kind,
|
|
}
|
|
}
|
|
|
|
func findCommonParameters(routes []restful.Route) (map[interface{}]spec.Parameter, error) {
|
|
commonParamsMap := make(map[interface{}]spec.Parameter, 0)
|
|
paramOpsCountByName := make(map[interface{}]int, 0)
|
|
paramNameKindToDataMap := make(map[interface{}]restful.ParameterData, 0)
|
|
for _, route := range routes {
|
|
routeParamDuplicateMap := make(map[interface{}]bool)
|
|
s := ""
|
|
for _, param := range route.ParameterDocs {
|
|
m, _ := json.Marshal(param.Data())
|
|
s += string(m) + "\n"
|
|
key := mapKeyFromParam(param)
|
|
if routeParamDuplicateMap[key] {
|
|
msg, _ := json.Marshal(route.ParameterDocs)
|
|
return commonParamsMap, fmt.Errorf("Duplicate parameter %v for route %v, %v.", param.Data().Name, string(msg), s)
|
|
}
|
|
routeParamDuplicateMap[key] = true
|
|
paramOpsCountByName[key]++
|
|
paramNameKindToDataMap[key] = param.Data()
|
|
}
|
|
}
|
|
for key, count := range paramOpsCountByName {
|
|
if count == len(routes) {
|
|
openAPIParam, err := buildParameter(paramNameKindToDataMap[key])
|
|
if err != nil {
|
|
return commonParamsMap, err
|
|
}
|
|
commonParamsMap[key] = openAPIParam
|
|
}
|
|
}
|
|
return commonParamsMap, nil
|
|
}
|
|
|
|
func buildParameter(restParam restful.ParameterData) (ret spec.Parameter, err error) {
|
|
ret = spec.Parameter{
|
|
ParamProps: spec.ParamProps{
|
|
Name: restParam.Name,
|
|
Description: restParam.Description,
|
|
Required: restParam.Required,
|
|
},
|
|
}
|
|
switch restParam.Kind {
|
|
case restful.BodyParameterKind:
|
|
ret.In = "body"
|
|
ret.Schema = &spec.Schema{
|
|
SchemaProps: spec.SchemaProps{
|
|
Ref: spec.MustCreateRef("#/definitions/" + restParam.DataType),
|
|
},
|
|
}
|
|
return ret, nil
|
|
case restful.PathParameterKind:
|
|
ret.In = "path"
|
|
if !restParam.Required {
|
|
return ret, fmt.Errorf("Path parameters should be marked at required for parameter %v", restParam)
|
|
}
|
|
case restful.QueryParameterKind:
|
|
ret.In = "query"
|
|
case restful.HeaderParameterKind:
|
|
ret.In = "header"
|
|
case restful.FormParameterKind:
|
|
ret.In = "form"
|
|
default:
|
|
return ret, fmt.Errorf("Unknown restful operation kind : %v", restParam.Kind)
|
|
}
|
|
if !isSimpleDataType(restParam.DataType) {
|
|
return ret, fmt.Errorf("Restful DataType should be a simple type, but got : %v", restParam.DataType)
|
|
}
|
|
ret.Type = restParam.DataType
|
|
ret.Format = restParam.DataFormat
|
|
ret.UniqueItems = !restParam.AllowMultiple
|
|
// TODO(mbohlool): make sure the type of default value matches Type
|
|
if restParam.DefaultValue != "" {
|
|
ret.Default = restParam.DefaultValue
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func buildParameters(restParam []*restful.Parameter) (ret []spec.Parameter, err error) {
|
|
ret = make([]spec.Parameter, len(restParam))
|
|
for i, v := range restParam {
|
|
ret[i], err = buildParameter(v.Data())
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func isSimpleDataType(typeName string) bool {
|
|
switch typeName {
|
|
// Note that "file" intentionally kept out of this list as it is not being used.
|
|
// "file" type has more requirements.
|
|
case "string", "number", "integer", "boolean", "array":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getFloat64OrNil(str string) (*float64, error) {
|
|
if len(str) > 0 {
|
|
num, err := strconv.ParseFloat(str, 64)
|
|
return &num, err
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// TODO(mbohlool): Convert default value type to the type of parameter
|
|
func getDefaultValue(str swagger.Special) interface{} {
|
|
if len(str) > 0 {
|
|
return str
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildType(swaggerType *string, swaggerFormat string) ([]string, string, error) {
|
|
if swaggerType == nil {
|
|
return []string{}, "", nil
|
|
}
|
|
switch *swaggerType {
|
|
case "integer", "number", "string", "boolean", "array", "object", "file":
|
|
return []string{*swaggerType}, swaggerFormat, nil
|
|
case "int":
|
|
return []string{"integer"}, "int32", nil
|
|
case "long":
|
|
return []string{"integer"}, "int64", nil
|
|
case "float", "double":
|
|
return []string{"number"}, *swaggerType, nil
|
|
case "byte", "date", "datetime", "date-time":
|
|
return []string{"string"}, *swaggerType, nil
|
|
default:
|
|
return []string{}, "", fmt.Errorf("Unrecognized swagger 1.2 type : %v, %v", swaggerType, swaggerFormat)
|
|
}
|
|
}
|
|
|
|
// A simple trie implementation with Add an HasPrefix methods only.
|
|
type trie struct {
|
|
children map[byte]*trie
|
|
}
|
|
|
|
func createTrie(list []string) trie {
|
|
ret := trie{
|
|
children: make(map[byte]*trie),
|
|
}
|
|
for _, v := range list {
|
|
ret.Add(v)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (t *trie) Add(v string) {
|
|
root := t
|
|
for _, b := range []byte(v) {
|
|
child, exists := root.children[b]
|
|
if !exists {
|
|
child = new(trie)
|
|
child.children = make(map[byte]*trie)
|
|
root.children[b] = child
|
|
}
|
|
root = child
|
|
}
|
|
}
|
|
|
|
func (t *trie) HasPrefix(v string) bool {
|
|
root := t
|
|
for _, b := range []byte(v) {
|
|
child, exists := root.children[b]
|
|
if !exists {
|
|
return false
|
|
}
|
|
root = child
|
|
}
|
|
return true
|
|
}
|