Generates OpenAPI (aka Swagger 2.0) Spec on /swagger.json path

This commit is contained in:
mbohlool 2016-08-18 08:12:26 -07:00
parent 0c51663aac
commit c5f1d63d6f
7 changed files with 1124 additions and 9 deletions

View File

@ -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,

View File

@ -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

View 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

View 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
}

View 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)
}
}

View File

@ -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())
}
}

View File

@ -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 {