mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-09 03:57:41 +00:00
Generates OpenAPI (aka Swagger 2.0) Spec on /swagger.json path
This commit is contained in:
parent
0c51663aac
commit
c5f1d63d6f
@ -272,6 +272,7 @@ func Run(s *options.APIServer) error {
|
|||||||
genericConfig.ProxyDialer = proxyDialerFn
|
genericConfig.ProxyDialer = proxyDialerFn
|
||||||
genericConfig.ProxyTLSClientConfig = proxyTLSClientConfig
|
genericConfig.ProxyTLSClientConfig = proxyTLSClientConfig
|
||||||
genericConfig.Serializer = api.Codecs
|
genericConfig.Serializer = api.Codecs
|
||||||
|
genericConfig.OpenAPIInfo.Title = "Kubernetes"
|
||||||
|
|
||||||
config := &master.Config{
|
config := &master.Config{
|
||||||
Config: genericConfig,
|
Config: genericConfig,
|
||||||
|
@ -36,6 +36,7 @@ import (
|
|||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
"gopkg.in/natefinch/lumberjack.v2"
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
|
||||||
|
"github.com/go-openapi/spec"
|
||||||
"k8s.io/kubernetes/pkg/admission"
|
"k8s.io/kubernetes/pkg/admission"
|
||||||
"k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
"k8s.io/kubernetes/pkg/api/rest"
|
"k8s.io/kubernetes/pkg/api/rest"
|
||||||
@ -48,6 +49,7 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/auth/authorizer"
|
"k8s.io/kubernetes/pkg/auth/authorizer"
|
||||||
"k8s.io/kubernetes/pkg/auth/handlers"
|
"k8s.io/kubernetes/pkg/auth/handlers"
|
||||||
"k8s.io/kubernetes/pkg/cloudprovider"
|
"k8s.io/kubernetes/pkg/cloudprovider"
|
||||||
|
"k8s.io/kubernetes/pkg/genericapiserver/openapi"
|
||||||
"k8s.io/kubernetes/pkg/genericapiserver/options"
|
"k8s.io/kubernetes/pkg/genericapiserver/options"
|
||||||
genericvalidation "k8s.io/kubernetes/pkg/genericapiserver/validation"
|
genericvalidation "k8s.io/kubernetes/pkg/genericapiserver/validation"
|
||||||
"k8s.io/kubernetes/pkg/registry/generic"
|
"k8s.io/kubernetes/pkg/registry/generic"
|
||||||
@ -192,6 +194,15 @@ type Config struct {
|
|||||||
ExtraEndpointPorts []api.EndpointPort
|
ExtraEndpointPorts []api.EndpointPort
|
||||||
|
|
||||||
KubernetesServiceNodePort int
|
KubernetesServiceNodePort int
|
||||||
|
|
||||||
|
// EnableOpenAPISupport enables OpenAPI support. Allow downstream customers to disable OpenAPI spec.
|
||||||
|
EnableOpenAPISupport bool
|
||||||
|
|
||||||
|
// OpenAPIInfo will be directly available as Info section of Open API spec.
|
||||||
|
OpenAPIInfo spec.Info
|
||||||
|
|
||||||
|
// OpenAPIDefaultResponse will be used if an web service operation does not have any responses listed.
|
||||||
|
OpenAPIDefaultResponse spec.Response
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenericAPIServer contains state for a Kubernetes cluster api server.
|
// GenericAPIServer contains state for a Kubernetes cluster api server.
|
||||||
@ -252,6 +263,12 @@ type GenericAPIServer struct {
|
|||||||
// Map storing information about all groups to be exposed in discovery response.
|
// Map storing information about all groups to be exposed in discovery response.
|
||||||
// The map is from name to the group.
|
// The map is from name to the group.
|
||||||
apiGroupsForDiscovery map[string]unversioned.APIGroup
|
apiGroupsForDiscovery map[string]unversioned.APIGroup
|
||||||
|
|
||||||
|
// See Config.$name for documentation of these flags
|
||||||
|
|
||||||
|
enableOpenAPISupport bool
|
||||||
|
openAPIInfo spec.Info
|
||||||
|
openAPIDefaultResponse spec.Response
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *GenericAPIServer) StorageDecorator() generic.StorageDecorator {
|
func (s *GenericAPIServer) StorageDecorator() generic.StorageDecorator {
|
||||||
@ -378,6 +395,10 @@ func New(c *Config) (*GenericAPIServer, error) {
|
|||||||
|
|
||||||
KubernetesServiceNodePort: c.KubernetesServiceNodePort,
|
KubernetesServiceNodePort: c.KubernetesServiceNodePort,
|
||||||
apiGroupsForDiscovery: map[string]unversioned.APIGroup{},
|
apiGroupsForDiscovery: map[string]unversioned.APIGroup{},
|
||||||
|
|
||||||
|
enableOpenAPISupport: c.EnableOpenAPISupport,
|
||||||
|
openAPIInfo: c.OpenAPIInfo,
|
||||||
|
openAPIDefaultResponse: c.OpenAPIDefaultResponse,
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.RestfulContainer != nil {
|
if c.RestfulContainer != nil {
|
||||||
@ -579,6 +600,16 @@ func NewConfig(options *options.ServerRunOptions) *Config {
|
|||||||
ReadWritePort: options.SecurePort,
|
ReadWritePort: options.SecurePort,
|
||||||
ServiceClusterIPRange: &options.ServiceClusterIPRange,
|
ServiceClusterIPRange: &options.ServiceClusterIPRange,
|
||||||
ServiceNodePortRange: options.ServiceNodePortRange,
|
ServiceNodePortRange: options.ServiceNodePortRange,
|
||||||
|
EnableOpenAPISupport: true,
|
||||||
|
OpenAPIDefaultResponse: spec.Response{
|
||||||
|
ResponseProps: spec.ResponseProps{
|
||||||
|
Description: "Default Response."}},
|
||||||
|
OpenAPIInfo: spec.Info{
|
||||||
|
InfoProps: spec.InfoProps{
|
||||||
|
Title: "Generic API Server",
|
||||||
|
Version: "unversioned",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -632,6 +663,9 @@ func (s *GenericAPIServer) Run(options *options.ServerRunOptions) {
|
|||||||
if s.enableSwaggerSupport {
|
if s.enableSwaggerSupport {
|
||||||
s.InstallSwaggerAPI()
|
s.InstallSwaggerAPI()
|
||||||
}
|
}
|
||||||
|
if s.enableOpenAPISupport {
|
||||||
|
s.InstallOpenAPI()
|
||||||
|
}
|
||||||
// We serve on 2 ports. See docs/admin/accessing-the-api.md
|
// We serve on 2 ports. See docs/admin/accessing-the-api.md
|
||||||
secureLocation := ""
|
secureLocation := ""
|
||||||
if options.SecurePort != 0 {
|
if options.SecurePort != 0 {
|
||||||
@ -868,17 +902,12 @@ func (s *GenericAPIServer) newAPIGroupVersion(apiGroupInfo *APIGroupInfo, groupV
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallSwaggerAPI installs the /swaggerapi/ endpoint to allow schema discovery
|
// getSwaggerConfig returns swagger config shared between SwaggerAPI and OpenAPI spec generators
|
||||||
// and traversal. It is optional to allow consumers of the Kubernetes GenericAPIServer to
|
func (s *GenericAPIServer) getSwaggerConfig() *swagger.Config {
|
||||||
// register their own web services into the Kubernetes mux prior to initialization
|
|
||||||
// of swagger, so that other resource types show up in the documentation.
|
|
||||||
func (s *GenericAPIServer) InstallSwaggerAPI() {
|
|
||||||
hostAndPort := s.ExternalAddress
|
hostAndPort := s.ExternalAddress
|
||||||
protocol := "https://"
|
protocol := "https://"
|
||||||
webServicesUrl := protocol + hostAndPort
|
webServicesUrl := protocol + hostAndPort
|
||||||
|
return &swagger.Config{
|
||||||
// Enable swagger UI and discovery API
|
|
||||||
swaggerConfig := swagger.Config{
|
|
||||||
WebServicesUrl: webServicesUrl,
|
WebServicesUrl: webServicesUrl,
|
||||||
WebServices: s.HandlerContainer.RegisteredWebServices(),
|
WebServices: s.HandlerContainer.RegisteredWebServices(),
|
||||||
ApiPath: "/swaggerapi/",
|
ApiPath: "/swaggerapi/",
|
||||||
@ -892,7 +921,30 @@ func (s *GenericAPIServer) InstallSwaggerAPI() {
|
|||||||
return ""
|
return ""
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
swagger.RegisterSwaggerService(swaggerConfig, s.HandlerContainer)
|
}
|
||||||
|
|
||||||
|
// InstallSwaggerAPI installs the /swaggerapi/ endpoint to allow schema discovery
|
||||||
|
// and traversal. It is optional to allow consumers of the Kubernetes GenericAPIServer to
|
||||||
|
// register their own web services into the Kubernetes mux prior to initialization
|
||||||
|
// of swagger, so that other resource types show up in the documentation.
|
||||||
|
func (s *GenericAPIServer) InstallSwaggerAPI() {
|
||||||
|
|
||||||
|
// Enable swagger UI and discovery API
|
||||||
|
swagger.RegisterSwaggerService(*s.getSwaggerConfig(), s.HandlerContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallOpenAPI installs the /swagger.json endpoint to allow new OpenAPI schema discovery.
|
||||||
|
func (s *GenericAPIServer) InstallOpenAPI() {
|
||||||
|
openAPIConfig := openapi.Config{
|
||||||
|
SwaggerConfig: s.getSwaggerConfig(),
|
||||||
|
IgnorePrefixes: []string{"/swaggerapi"},
|
||||||
|
Info: &s.openAPIInfo,
|
||||||
|
DefaultResponse: &s.openAPIDefaultResponse,
|
||||||
|
}
|
||||||
|
err := openapi.RegisterOpenAPIService(&openAPIConfig, s.HandlerContainer)
|
||||||
|
if err != nil {
|
||||||
|
glog.Fatalf("Failed to generate open api spec: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDefaultAPIGroupInfo returns an APIGroupInfo stubbed with "normal" values
|
// NewDefaultAPIGroupInfo returns an APIGroupInfo stubbed with "normal" values
|
||||||
|
20
pkg/genericapiserver/openapi/doc.go
Normal file
20
pkg/genericapiserver/openapi/doc.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
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 contains code to generate OpenAPI discovery spec (which
|
||||||
|
// initial version of it also known as Swagger 2.0).
|
||||||
|
// For more details: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md
|
||||||
|
package openapi
|
557
pkg/genericapiserver/openapi/openapi.go
Normal file
557
pkg/genericapiserver/openapi/openapi.go
Normal file
@ -0,0 +1,557 @@
|
|||||||
|
/*
|
||||||
|
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
|
||||||
|
}
|
417
pkg/genericapiserver/openapi/openapi_test.go
Normal file
417
pkg/genericapiserver/openapi/openapi_test.go
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
/*
|
||||||
|
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
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful"
|
||||||
|
"github.com/emicklei/go-restful/swagger"
|
||||||
|
"github.com/go-openapi/spec"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setUp is a convenience function for setting up for (most) tests.
|
||||||
|
func setUp(t *testing.T, fullMethods bool) (openAPI, *assert.Assertions) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
config := Config{
|
||||||
|
SwaggerConfig: getSwaggerConfig(fullMethods),
|
||||||
|
Info: &spec.Info{
|
||||||
|
InfoProps: spec.InfoProps{
|
||||||
|
Title: "TestAPI",
|
||||||
|
Description: "Test API",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return openAPI{config: &config}, assert
|
||||||
|
}
|
||||||
|
|
||||||
|
func noOp(request *restful.Request, response *restful.Response) {}
|
||||||
|
|
||||||
|
type TestInput struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
ID int `json:"id,omitempty"`
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestOutput struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Count int `json:"count,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TestInput) SwaggerDoc() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"": "Test input",
|
||||||
|
"name": "Name of the input",
|
||||||
|
"id": "ID of the input",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TestOutput) SwaggerDoc() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"": "Test output",
|
||||||
|
"name": "Name of the output",
|
||||||
|
"count": "Number of outputs",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTestRoute(ws *restful.WebService, method string, additionalParams bool) *restful.RouteBuilder {
|
||||||
|
ret := ws.Method(method).
|
||||||
|
Path("/test/{path:*}").
|
||||||
|
Doc(fmt.Sprintf("%s test input", method)).
|
||||||
|
Operation(fmt.Sprintf("%sTestInput", method)).
|
||||||
|
Produces(restful.MIME_JSON).
|
||||||
|
Consumes(restful.MIME_JSON).
|
||||||
|
Param(ws.PathParameter("path", "path to the resource").DataType("string")).
|
||||||
|
Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
|
||||||
|
Reads(TestInput{}).
|
||||||
|
Returns(200, "OK", TestOutput{}).
|
||||||
|
Writes(TestOutput{}).
|
||||||
|
To(noOp)
|
||||||
|
if additionalParams {
|
||||||
|
ret.Param(ws.HeaderParameter("hparam", "a test head parameter").DataType("integer"))
|
||||||
|
ret.Param(ws.FormParameter("fparam", "a test form parameter").DataType("number"))
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSwaggerConfig(fullMethods bool) *swagger.Config {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
container := restful.NewContainer()
|
||||||
|
container.ServeMux = mux
|
||||||
|
ws := new(restful.WebService)
|
||||||
|
ws.Path("/foo")
|
||||||
|
ws.Route(getTestRoute(ws, "get", true))
|
||||||
|
if fullMethods {
|
||||||
|
ws.Route(getTestRoute(ws, "post", false)).
|
||||||
|
Route(getTestRoute(ws, "put", false)).
|
||||||
|
Route(getTestRoute(ws, "head", false)).
|
||||||
|
Route(getTestRoute(ws, "patch", false)).
|
||||||
|
Route(getTestRoute(ws, "options", false)).
|
||||||
|
Route(getTestRoute(ws, "delete", false))
|
||||||
|
|
||||||
|
}
|
||||||
|
ws.Path("/bar")
|
||||||
|
ws.Route(getTestRoute(ws, "get", true))
|
||||||
|
if fullMethods {
|
||||||
|
ws.Route(getTestRoute(ws, "post", false)).
|
||||||
|
Route(getTestRoute(ws, "put", false)).
|
||||||
|
Route(getTestRoute(ws, "head", false)).
|
||||||
|
Route(getTestRoute(ws, "patch", false)).
|
||||||
|
Route(getTestRoute(ws, "options", false)).
|
||||||
|
Route(getTestRoute(ws, "delete", false))
|
||||||
|
|
||||||
|
}
|
||||||
|
container.Add(ws)
|
||||||
|
return &swagger.Config{
|
||||||
|
WebServicesUrl: "https://test-server",
|
||||||
|
WebServices: container.RegisteredWebServices(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTestOperation(method string) *spec.Operation {
|
||||||
|
return &spec.Operation{
|
||||||
|
OperationProps: spec.OperationProps{
|
||||||
|
Description: fmt.Sprintf("%s test input", method),
|
||||||
|
Consumes: []string{"application/json"},
|
||||||
|
Produces: []string{"application/json"},
|
||||||
|
Schemes: []string{"https"},
|
||||||
|
Parameters: []spec.Parameter{},
|
||||||
|
Responses: getTestResponses(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTestPathItem(allMethods bool) spec.PathItem {
|
||||||
|
ret := spec.PathItem{
|
||||||
|
PathItemProps: spec.PathItemProps{
|
||||||
|
Get: getTestOperation("get"),
|
||||||
|
Parameters: getTestCommonParameters(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ret.Get.Parameters = getAdditionalTestParameters()
|
||||||
|
if allMethods {
|
||||||
|
ret.PathItemProps.Put = getTestOperation("put")
|
||||||
|
ret.PathItemProps.Post = getTestOperation("post")
|
||||||
|
ret.PathItemProps.Head = getTestOperation("head")
|
||||||
|
ret.PathItemProps.Patch = getTestOperation("patch")
|
||||||
|
ret.PathItemProps.Delete = getTestOperation("delete")
|
||||||
|
ret.PathItemProps.Options = getTestOperation("options")
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRefSchema(ref string) *spec.Schema {
|
||||||
|
return &spec.Schema{
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Ref: spec.MustCreateRef(ref),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTestResponses() *spec.Responses {
|
||||||
|
ret := spec.Responses{
|
||||||
|
ResponsesProps: spec.ResponsesProps{
|
||||||
|
StatusCodeResponses: map[int]spec.Response{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ret.StatusCodeResponses[200] = spec.Response{
|
||||||
|
ResponseProps: spec.ResponseProps{
|
||||||
|
Description: "OK",
|
||||||
|
Schema: getRefSchema("#/definitions/openapi.TestOutput"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTestCommonParameters() []spec.Parameter {
|
||||||
|
ret := make([]spec.Parameter, 3)
|
||||||
|
ret[0] = spec.Parameter{
|
||||||
|
ParamProps: spec.ParamProps{
|
||||||
|
Name: "body",
|
||||||
|
In: "body",
|
||||||
|
Required: true,
|
||||||
|
Schema: getRefSchema("#/definitions/openapi.TestInput"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ret[1] = spec.Parameter{
|
||||||
|
SimpleSchema: spec.SimpleSchema{
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
ParamProps: spec.ParamProps{
|
||||||
|
Description: "path to the resource",
|
||||||
|
Name: "path",
|
||||||
|
In: "path",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
CommonValidations: spec.CommonValidations{
|
||||||
|
UniqueItems: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ret[2] = spec.Parameter{
|
||||||
|
SimpleSchema: spec.SimpleSchema{
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
ParamProps: spec.ParamProps{
|
||||||
|
Description: "If 'true', then the output is pretty printed.",
|
||||||
|
Name: "pretty",
|
||||||
|
In: "query",
|
||||||
|
},
|
||||||
|
CommonValidations: spec.CommonValidations{
|
||||||
|
UniqueItems: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAdditionalTestParameters() []spec.Parameter {
|
||||||
|
ret := make([]spec.Parameter, 2)
|
||||||
|
ret[0] = spec.Parameter{
|
||||||
|
ParamProps: spec.ParamProps{
|
||||||
|
Name: "fparam",
|
||||||
|
Description: "a test form parameter",
|
||||||
|
In: "form",
|
||||||
|
},
|
||||||
|
SimpleSchema: spec.SimpleSchema{
|
||||||
|
Type: "number",
|
||||||
|
},
|
||||||
|
CommonValidations: spec.CommonValidations{
|
||||||
|
UniqueItems: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ret[1] = spec.Parameter{
|
||||||
|
SimpleSchema: spec.SimpleSchema{
|
||||||
|
Type: "integer",
|
||||||
|
},
|
||||||
|
ParamProps: spec.ParamProps{
|
||||||
|
Description: "a test head parameter",
|
||||||
|
Name: "hparam",
|
||||||
|
In: "header",
|
||||||
|
},
|
||||||
|
CommonValidations: spec.CommonValidations{
|
||||||
|
UniqueItems: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parameters []spec.Parameter
|
||||||
|
|
||||||
|
func (s Parameters) Len() int { return len(s) }
|
||||||
|
func (s Parameters) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
|
||||||
|
type ByName struct {
|
||||||
|
Parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ByName) Less(i, j int) bool {
|
||||||
|
return s.Parameters[i].Name < s.Parameters[j].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(mehdy): Consider sort parameters in actual spec generation for more predictable spec generation
|
||||||
|
func sortParameters(s *spec.Swagger) *spec.Swagger {
|
||||||
|
for k, p := range s.Paths.Paths {
|
||||||
|
sort.Sort(ByName{p.Parameters})
|
||||||
|
sort.Sort(ByName{p.Get.Parameters})
|
||||||
|
sort.Sort(ByName{p.Put.Parameters})
|
||||||
|
sort.Sort(ByName{p.Post.Parameters})
|
||||||
|
sort.Sort(ByName{p.Head.Parameters})
|
||||||
|
sort.Sort(ByName{p.Delete.Parameters})
|
||||||
|
sort.Sort(ByName{p.Options.Parameters})
|
||||||
|
sort.Sort(ByName{p.Patch.Parameters})
|
||||||
|
s.Paths.Paths[k] = p // Unnecessary?! Magic!!!
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTestInputDefinition() spec.Schema {
|
||||||
|
return spec.Schema{
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Description: "Test input",
|
||||||
|
Required: []string{},
|
||||||
|
Properties: map[string]spec.Schema{
|
||||||
|
"id": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Description: "ID of the input",
|
||||||
|
Type: spec.StringOrArray{"integer"},
|
||||||
|
Format: "int32",
|
||||||
|
Enum: []interface{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Description: "Name of the input",
|
||||||
|
Type: spec.StringOrArray{"string"},
|
||||||
|
Enum: []interface{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Type: spec.StringOrArray{"array"},
|
||||||
|
Enum: []interface{}{},
|
||||||
|
Items: &spec.SchemaOrArray{
|
||||||
|
Schema: &spec.Schema{
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Type: spec.StringOrArray{"string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTestOutputDefinition() spec.Schema {
|
||||||
|
return spec.Schema{
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Description: "Test output",
|
||||||
|
Required: []string{},
|
||||||
|
Properties: map[string]spec.Schema{
|
||||||
|
"count": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Description: "Number of outputs",
|
||||||
|
Type: spec.StringOrArray{"integer"},
|
||||||
|
Format: "int32",
|
||||||
|
Enum: []interface{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Description: "Name of the output",
|
||||||
|
Type: spec.StringOrArray{"string"},
|
||||||
|
Enum: []interface{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildSwaggerSpec(t *testing.T) {
|
||||||
|
o, assert := setUp(t, true)
|
||||||
|
expected := &spec.Swagger{
|
||||||
|
SwaggerProps: spec.SwaggerProps{
|
||||||
|
Info: &spec.Info{
|
||||||
|
InfoProps: spec.InfoProps{
|
||||||
|
Title: "TestAPI",
|
||||||
|
Description: "Test API",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Swagger: "2.0",
|
||||||
|
Paths: &spec.Paths{
|
||||||
|
Paths: map[string]spec.PathItem{
|
||||||
|
"/foo/test/{path}": getTestPathItem(true),
|
||||||
|
"/bar/test/{path}": getTestPathItem(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Definitions: spec.Definitions{
|
||||||
|
"openapi.TestInput": getTestInputDefinition(),
|
||||||
|
"openapi.TestOutput": getTestOutputDefinition(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := o.buildSwaggerSpec()
|
||||||
|
if assert.NoError(err) {
|
||||||
|
sortParameters(expected)
|
||||||
|
sortParameters(o.swagger)
|
||||||
|
assert.Equal(expected, o.swagger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildSwaggerSpecTwice(t *testing.T) {
|
||||||
|
o, assert := setUp(t, true)
|
||||||
|
err := o.buildSwaggerSpec()
|
||||||
|
if assert.NoError(err) {
|
||||||
|
assert.Error(o.buildSwaggerSpec(), "Swagger spec is already built. Duplicate call to buildSwaggerSpec is not allowed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
func TestBuildDefinitions(t *testing.T) {
|
||||||
|
o, assert := setUp(t, true)
|
||||||
|
expected := spec.Definitions{
|
||||||
|
"openapi.TestInput": getTestInputDefinition(),
|
||||||
|
"openapi.TestOutput": getTestOutputDefinition(),
|
||||||
|
}
|
||||||
|
def, err := o.buildDefinitions()
|
||||||
|
if assert.NoError(err) {
|
||||||
|
assert.Equal(expected, def)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildProtocolList(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
o := openAPI{config: &Config{SwaggerConfig: &swagger.Config{WebServicesUrl: "https://something"}}}
|
||||||
|
p, err := o.buildProtocolList()
|
||||||
|
if assert.NoError(err) {
|
||||||
|
assert.Equal([]string{"https"}, p)
|
||||||
|
}
|
||||||
|
o = openAPI{config: &Config{SwaggerConfig: &swagger.Config{WebServicesUrl: "http://something"}}}
|
||||||
|
p, err = o.buildProtocolList()
|
||||||
|
if assert.NoError(err) {
|
||||||
|
assert.Equal([]string{"http"}, p)
|
||||||
|
}
|
||||||
|
o = openAPI{config: &Config{SwaggerConfig: &swagger.Config{WebServicesUrl: "something"}}}
|
||||||
|
p, err = o.buildProtocolList()
|
||||||
|
if assert.NoError(err) {
|
||||||
|
assert.Equal([]string{"http"}, p)
|
||||||
|
}
|
||||||
|
}
|
@ -64,6 +64,10 @@ import (
|
|||||||
utilnet "k8s.io/kubernetes/pkg/util/net"
|
utilnet "k8s.io/kubernetes/pkg/util/net"
|
||||||
"k8s.io/kubernetes/pkg/util/sets"
|
"k8s.io/kubernetes/pkg/util/sets"
|
||||||
|
|
||||||
|
"github.com/go-openapi/loads"
|
||||||
|
"github.com/go-openapi/spec"
|
||||||
|
"github.com/go-openapi/strfmt"
|
||||||
|
"github.com/go-openapi/validate"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
@ -1205,3 +1209,54 @@ func testThirdPartyDiscovery(t *testing.T, version string) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestValidOpenAPISpec verifies that the open api is added
|
||||||
|
// at the proper endpoint and the spec is valid.
|
||||||
|
func TestValidOpenAPISpec(t *testing.T) {
|
||||||
|
_, etcdserver, config, assert := setUp(t)
|
||||||
|
defer etcdserver.Terminate(t)
|
||||||
|
|
||||||
|
config.EnableOpenAPISupport = true
|
||||||
|
config.OpenAPIInfo = spec.Info{
|
||||||
|
InfoProps: spec.InfoProps{
|
||||||
|
Title: "Kubernetes",
|
||||||
|
Version: "unversioned",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
master, err := New(&config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error in bringing up the master: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure swagger.json is not registered before calling install api.
|
||||||
|
server := httptest.NewServer(master.HandlerContainer.ServeMux)
|
||||||
|
resp, err := http.Get(server.URL + "/swagger.json")
|
||||||
|
if !assert.NoError(err) {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
assert.Equal(http.StatusNotFound, resp.StatusCode)
|
||||||
|
|
||||||
|
master.InstallOpenAPI()
|
||||||
|
resp, err = http.Get(server.URL + "/swagger.json")
|
||||||
|
if !assert.NoError(err) {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
assert.Equal(http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
// as json schema
|
||||||
|
var sch spec.Schema
|
||||||
|
if assert.NoError(decodeResponse(resp, &sch)) {
|
||||||
|
validator := validate.NewSchemaValidator(spec.MustLoadSwagger20Schema(), nil, "", strfmt.Default)
|
||||||
|
res := validator.Validate(&sch)
|
||||||
|
assert.NoError(res.AsError())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate OpenApi spec
|
||||||
|
doc, err := loads.Spec(server.URL + "/swagger.json")
|
||||||
|
if assert.NoError(err) {
|
||||||
|
// TODO(mehdy): This test is timing out on jerkin but passing locally. Enable it after debugging timeout issue.
|
||||||
|
_ = validate.NewSpecValidator(doc.Schema(), strfmt.Default)
|
||||||
|
// res, _ := validator.Validate(doc)
|
||||||
|
// assert.NoError(res.AsError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -54,6 +54,7 @@ import (
|
|||||||
utilnet "k8s.io/kubernetes/pkg/util/net"
|
utilnet "k8s.io/kubernetes/pkg/util/net"
|
||||||
"k8s.io/kubernetes/plugin/pkg/admission/admit"
|
"k8s.io/kubernetes/plugin/pkg/admission/admit"
|
||||||
|
|
||||||
|
"github.com/go-openapi/spec"
|
||||||
"github.com/pborman/uuid"
|
"github.com/pborman/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -134,6 +135,18 @@ func startMasterOrDie(masterConfig *master.Config) (*master.Master, *httptest.Se
|
|||||||
masterConfig = NewMasterConfig()
|
masterConfig = NewMasterConfig()
|
||||||
masterConfig.EnableProfiling = true
|
masterConfig.EnableProfiling = true
|
||||||
masterConfig.EnableSwaggerSupport = true
|
masterConfig.EnableSwaggerSupport = true
|
||||||
|
masterConfig.EnableOpenAPISupport = true
|
||||||
|
masterConfig.OpenAPIInfo = spec.Info{
|
||||||
|
InfoProps: spec.InfoProps{
|
||||||
|
Title: "Kubernetes",
|
||||||
|
Version: "unversioned",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
masterConfig.OpenAPIDefaultResponse = spec.Response{
|
||||||
|
ResponseProps: spec.ResponseProps{
|
||||||
|
Description: "Default Response.",
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m, err := master.New(masterConfig)
|
m, err := master.New(masterConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user