Commit 54fee8c2 authored by mbohlool's avatar mbohlool

Improvements on OpenAPI spec generation:

- Generating models using go2idl library (no reflection anymore) - Remove dependencies on go-restful/swagger - Generate one swagger.json file for each web-service - Bugfix: fixed a bug in trie implementation
parent 8865f5d0
......@@ -35,7 +35,7 @@ SHELL := /bin/bash
# This rule collects all the generated file sets into a single rule. Other
# rules should depend on this to ensure generated files are rebuilt.
.PHONY: generated_files
generated_files: gen_deepcopy gen_conversion
generated_files: gen_deepcopy gen_conversion gen_openapi
# Code-generation logic.
#
......@@ -202,10 +202,10 @@ DEEPCOPY_GEN := $(BIN_DIR)/deepcopy-gen
ifeq ($(DBG_MAKEFILE),1)
$(warning ***** finding all +k8s:deepcopy-gen tags)
endif
DEEPCOPY_DIRS := $(shell \
DEEPCOPY_DIRS := $(shell \
grep --color=never -l '+k8s:deepcopy-gen=' $(ALL_K8S_TAG_FILES) \
| xargs -n1 dirname \
| sort -u \
| xargs -n1 dirname \
| sort -u \
)
DEEPCOPY_FILES := $(addsuffix /$(DEEPCOPY_FILENAME), $(DEEPCOPY_DIRS))
......@@ -286,6 +286,107 @@ $(DEEPCOPY_GEN):
touch $@
#
# Open-api generation
#
# Any package that wants open-api functions generated must include a
# comment-tag in column 0 of one file of the form:
# // +k8s:openapi-gen=true
#
# The result file, in each pkg, of open-api generation.
OPENAPI_BASENAME := $(GENERATED_FILE_PREFIX)openapi
OPENAPI_FILENAME := $(OPENAPI_BASENAME).go
# The tool used to generate open apis.
OPENAPI_GEN := $(BIN_DIR)/openapi-gen
# Find all the directories that request open-api generation.
ifeq ($(DBG_MAKEFILE),1)
$(warning ***** finding all +k8s:openapi-gen tags)
endif
OPENAPI_DIRS := $(shell \
grep --color=never -l '+k8s:openapi-gen=' $(ALL_K8S_TAG_FILES) \
| xargs -n1 dirname \
| sort -u \
)
OPENAPI_FILES := $(addsuffix /$(OPENAPI_FILENAME), $(OPENAPI_DIRS))
# This rule aggregates the set of files to generate and then generates them all
# in a single run of the tool.
.PHONY: gen_openapi
gen_openapi: $(OPENAPI_FILES)
if [[ -f $(META_DIR)/$(OPENAPI_GEN).todo ]]; then \
./hack/run-in-gopath.sh $(OPENAPI_GEN) \
--v $(KUBE_VERBOSE) \
-i $$(cat $(META_DIR)/$(OPENAPI_GEN).todo | paste -sd, -) \
-O $(OPENAPI_BASENAME); \
fi
# For each dir in OPENAPI_DIRS, this establishes a dependency between the
# output file and the input files that should trigger a rebuild.
#
# Note that this is a deps-only statement, not a full rule (see below). This
# has to be done in a distinct step because wildcards don't work in static
# pattern rules.
#
# The '$(eval)' is needed because this has a different RHS for each LHS, and
# would otherwise produce results that make can't parse.
#
# We depend on the $(GOFILES_META).stamp to detect when the set of input files
# has changed. This allows us to detect deleted input files.
$(foreach dir, $(OPENAPI_DIRS), $(eval \
$(dir)/$(OPENAPI_FILENAME): $(META_DIR)/$(dir)/$(GOFILES_META).stamp \
$(gofiles__$(dir)) \
))
# Unilaterally remove any leftovers from previous runs.
$(shell rm -f $(META_DIR)/$(OPENAPI_GEN)*.todo)
# How to regenerate open-api code. We need to collect these up and trigger one
# single run to generate definition for all types.
$(OPENAPI_FILES): $(OPENAPI_GEN)
mkdir -p $$(dirname $(META_DIR)/$(OPENAPI_GEN))
echo $(PRJ_SRC_PATH)/$(@D) >> $(META_DIR)/$(OPENAPI_GEN).todo
# This calculates the dependencies for the generator tool, so we only rebuild
# it when needed. It is PHONY so that it always runs, but it only updates the
# file if the contents have actually changed. We 'sinclude' this later.
.PHONY: $(META_DIR)/$(OPENAPI_GEN).mk
$(META_DIR)/$(OPENAPI_GEN).mk:
mkdir -p $(@D); \
(echo -n "$(OPENAPI_GEN): "; \
DIRECT=$$(go list -f '{{.Dir}} {{.Dir}}/*.go' \
./cmd/libs/go2idl/openapi-gen); \
INDIRECT=$$(go list \
-f '{{range .Deps}}{{.}}{{"\n"}}{{end}}' \
./cmd/libs/go2idl/openapi-gen \
| grep --color=never "^$(PRJ_SRC_PATH)" \
| sed 's|^$(PRJ_SRC_PATH)|./|' \
| xargs go list -f '{{.Dir}} {{.Dir}}/*.go'); \
echo $$DIRECT $$INDIRECT \
| sed 's/ / \\=,/g' \
| tr '=,' '\n\t'; \
) | sed "s|$$(pwd -P)/||" > $@.tmp; \
cmp -s $@.tmp $@ || cat $@.tmp > $@ && rm -f $@.tmp
# Include dependency info for the generator tool. This will cause the rule of
# the same name to be considered and if it is updated, make will restart.
sinclude $(META_DIR)/$(OPENAPI_GEN).mk
# How to build the generator tool. The deps for this are defined in
# the $(OPENAPI_GEN).mk, above.
#
# A word on the need to touch: This rule might trigger if, for example, a
# non-Go file was added or deleted from a directory on which this depends.
# This target needs to be reconsidered, but Go realizes it doesn't actually
# have to be rebuilt. In that case, make will forever see the dependency as
# newer than the binary, and try to rebuild it over and over. So we touch it,
# and make is happy.
$(OPENAPI_GEN):
hack/make-rules/build.sh cmd/libs/go2idl/openapi-gen
touch $@
#
# Conversion generation
#
# Any package that wants conversion functions generated must include one or
......@@ -315,11 +416,11 @@ CONVERSIONS_META := conversions.mk
ifeq ($(DBG_MAKEFILE),1)
$(warning ***** finding all +k8s:conversion-gen tags)
endif
CONVERSION_DIRS := $(shell \
CONVERSION_DIRS := $(shell \
grep --color=never '^// *+k8s:conversion-gen=' $(ALL_K8S_TAG_FILES) \
| cut -f1 -d: \
| xargs -n1 dirname \
| sort -u \
| cut -f1 -d: \
| xargs -n1 dirname \
| sort -u \
)
CONVERSION_FILES := $(addsuffix /$(CONVERSION_FILENAME), $(CONVERSION_DIRS))
......@@ -362,11 +463,11 @@ $(foreach dir, $(CONVERSION_DIRS), $(eval \
$(foreach dir, $(CONVERSION_DIRS), \
$(META_DIR)/$(dir)/$(CONVERSIONS_META)):
TAGS=$$(grep --color=never -h '^// *+k8s:conversion-gen=' $</*.go \
| cut -f2- -d= \
| sed 's|$(PRJ_SRC_PATH)/||'); \
mkdir -p $(@D); \
echo "conversions__$< := $$(echo $${TAGS})" >$@.tmp; \
cmp -s $@.tmp $@ || touch $@.stamp; \
| cut -f2- -d= \
| sed 's|$(PRJ_SRC_PATH)/||'); \
mkdir -p $(@D); \
echo "conversions__$< := $$(echo $${TAGS})" >$@.tmp; \
cmp -s $@.tmp $@ || touch $@.stamp; \
mv $@.tmp $@
# Include any deps files as additional Makefile rules. This triggers make to
......
# Generate OpenAPI definitions
- To generate definition for a specific type or package add "+k8s:openapi-gen=true" tag to the type/package comment lines.
- To exclude a type or a member from a tagged package/type, add "+k8s:openapi-gen=false" tag to the comment lines.
/*
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 common
import "github.com/go-openapi/spec"
// OpenAPIDefinition describes single type. Normally these definitions are auto-generated using gen-openapi.
type OpenAPIDefinition struct {
Schema spec.Schema
Dependencies []string
}
// OpenAPIDefinitions is collection of all definitions.
type OpenAPIDefinitions map[string]OpenAPIDefinition
// OpenAPIDefinitionGetter gets openAPI definitions for a given type. If a type implements this interface,
// the definition returned by it will be used, otherwise the auto-generated definitions will be used. See
// GetOpenAPITypeFormat for more information about trade-offs of using this interface or GetOpenAPITypeFormat method when
// possible.
type OpenAPIDefinitionGetter interface {
OpenAPIDefinition() *OpenAPIDefinition
}
// This function is a reference for converting go (or any custom type) to a simple open API type,format pair. There are
// two ways to customize spec for a type. If you add it here, a type will be converted to a simple type and the type
// comment (the comment that is added before type definition) will be lost. The spec will still have the property
// comment. The second way is to implement OpenAPIDefinitionGetter interface. That function can customize the spec (so
// the spec does not need to be simple type,format) or can even return a simple type,format (e.g. IntOrString). For simple
// type formats, the benefit of adding OpenAPIDefinitionGetter interface is to keep both type and property documentation.
// Example:
// type Sample struct {
// ...
// // port of the server
// port IntOrString
// ...
// }
// // IntOrString documentation...
// type IntOrString { ... }
//
// Adding IntOrString to this function:
// "port" : {
// format: "string",
// type: "int-or-string",
// Description: "port of the server"
// }
//
// Implement OpenAPIDefinitionGetter for IntOrString:
//
// "port" : {
// $Ref: "#/definitions/IntOrString"
// Description: "port of the server"
// }
// ...
// definitions:
// {
// "IntOrString": {
// format: "string",
// type: "int-or-string",
// Description: "IntOrString documentation..." // new
// }
// }
//
func GetOpenAPITypeFormat(typeName string) (string, string) {
schemaTypeFormatMap := map[string][]string{
"uint": {"integer", "int32"},
"uint8": {"integer", "byte"},
"uint16": {"integer", "int32"},
"uint32": {"integer", "int64"},
"uint64": {"integer", "int64"},
"int": {"integer", "int32"},
"int8": {"integer", "byte"},
"int16": {"integer", "int32"},
"int32": {"integer", "int32"},
"int64": {"integer", "int64"},
"byte": {"integer", "byte"},
"float64": {"number", "double"},
"float32": {"number", "float"},
"bool": {"boolean", ""},
"time.Time": {"string", "date-time"},
"string": {"string", ""},
"integer": {"integer", ""},
"number": {"number", ""},
"boolean": {"boolean", ""},
"[]byte": {"string", "byte"}, // base64 encoded characters
}
mapped, ok := schemaTypeFormatMap[typeName]
if !ok {
return "", ""
}
return mapped[0], mapped[1]
}
/*
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 common holds shared codes and types between open API code generator and spec generator.
package common
/*
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 generators
import (
"bytes"
"fmt"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/kubernetes/cmd/libs/go2idl/generator"
"k8s.io/kubernetes/cmd/libs/go2idl/namer"
"k8s.io/kubernetes/cmd/libs/go2idl/parser"
"k8s.io/kubernetes/cmd/libs/go2idl/types"
)
func construct(t *testing.T, files map[string]string, testNamer namer.Namer) (*parser.Builder, types.Universe, []*types.Type) {
b := parser.New()
for name, src := range files {
if err := b.AddFile(filepath.Dir(name), name, []byte(src)); err != nil {
t.Fatal(err)
}
}
u, err := b.FindTypes()
if err != nil {
t.Fatal(err)
}
orderer := namer.Orderer{Namer: testNamer}
o := orderer.OrderUniverse(u)
return b, u, o
}
func testOpenAPITypeWritter(t *testing.T, code string) (error, *assert.Assertions, *bytes.Buffer) {
assert := assert.New(t)
var testFiles = map[string]string{
"base/foo/bar.go": code,
}
rawNamer := namer.NewRawNamer("o", nil)
namers := namer.NameSystems{
"raw": namer.NewRawNamer("", nil),
}
builder, universe, _ := construct(t, testFiles, rawNamer)
context, err := generator.NewContext(builder, namers, "raw")
if err != nil {
t.Fatal(err)
}
buffer := &bytes.Buffer{}
sw := generator.NewSnippetWriter(buffer, context, "$", "$")
blahT := universe.Type(types.Name{Package: "base/foo", Name: "Blah"})
return newOpenAPITypeWriter(sw).generate(blahT), assert, buffer
}
func TestSimple(t *testing.T) {
err, assert, buffer := testOpenAPITypeWritter(t, `
package foo
import (
"time"
"k8s.io/kubernetes/pkg/util/intstr"
)
// Blah is a test.
// +k8s:openapi=true
type Blah struct {
// A simple string
String string
// A simple int
Int int `+"`"+`json:",omitempty"`+"`"+`
// An int considered string simple int
IntString int `+"`"+`json:",string"`+"`"+`
// A simple int64
Int64 int64
// A simple int32
Int32 int32
// A simple int16
Int16 int16
// A simple int8
Int8 int8
// A simple int
Uint uint
// A simple int64
Uint64 uint64
// A simple int32
Uint32 uint32
// A simple int16
Uint16 uint16
// A simple int8
Uint8 uint8
// A simple byte
Byte byte
// A simple boolean
Bool bool
// A simple float64
Float64 float64
// A simple float32
Float32 float32
// A simple time
Time time.Time
// a base64 encoded characters
ByteArray []byte
// an int or string type
IntOrString intstr.IntOrString
}
`)
if err != nil {
t.Fatal(err)
}
assert.Equal(`"foo.Blah": {
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Blah is a test.",
Properties: map[string]spec.Schema{
"String": {
SchemaProps: spec.SchemaProps{
Description: "A simple string",
Type: []string{"string"},
Format: "",
},
},
"Int64": {
SchemaProps: spec.SchemaProps{
Description: "A simple int64",
Type: []string{"integer"},
Format: "int64",
},
},
"Int32": {
SchemaProps: spec.SchemaProps{
Description: "A simple int32",
Type: []string{"integer"},
Format: "int32",
},
},
"Int16": {
SchemaProps: spec.SchemaProps{
Description: "A simple int16",
Type: []string{"integer"},
Format: "int32",
},
},
"Int8": {
SchemaProps: spec.SchemaProps{
Description: "A simple int8",
Type: []string{"integer"},
Format: "byte",
},
},
"Uint": {
SchemaProps: spec.SchemaProps{
Description: "A simple int",
Type: []string{"integer"},
Format: "int32",
},
},
"Uint64": {
SchemaProps: spec.SchemaProps{
Description: "A simple int64",
Type: []string{"integer"},
Format: "int64",
},
},
"Uint32": {
SchemaProps: spec.SchemaProps{
Description: "A simple int32",
Type: []string{"integer"},
Format: "int64",
},
},
"Uint16": {
SchemaProps: spec.SchemaProps{
Description: "A simple int16",
Type: []string{"integer"},
Format: "int32",
},
},
"Uint8": {
SchemaProps: spec.SchemaProps{
Description: "A simple int8",
Type: []string{"integer"},
Format: "byte",
},
},
"Byte": {
SchemaProps: spec.SchemaProps{
Description: "A simple byte",
Type: []string{"integer"},
Format: "byte",
},
},
"Bool": {
SchemaProps: spec.SchemaProps{
Description: "A simple boolean",
Type: []string{"boolean"},
Format: "",
},
},
"Float64": {
SchemaProps: spec.SchemaProps{
Description: "A simple float64",
Type: []string{"number"},
Format: "double",
},
},
"Float32": {
SchemaProps: spec.SchemaProps{
Description: "A simple float32",
Type: []string{"number"},
Format: "float",
},
},
"Time": {
SchemaProps: spec.SchemaProps{
Description: "A simple time",
Type: []string{"string"},
Format: "date-time",
},
},
"ByteArray": {
SchemaProps: spec.SchemaProps{
Description: "a base64 encoded characters",
Type: []string{"string"},
Format: "byte",
},
},
"IntOrString": {
SchemaProps: spec.SchemaProps{
Description: "an int or string type",
Ref: spec.MustCreateRef("#/definitions/intstr.IntOrString"),
},
},
},
Required: []string{"String","Int64","Int32","Int16","Int8","Uint","Uint64","Uint32","Uint16","Uint8","Byte","Bool","Float64","Float32","Time","ByteArray","IntOrString"},
},
},
Dependencies: []string{
"intstr.IntOrString",},
},
`, buffer.String())
}
func TestFailingSample1(t *testing.T) {
err, assert, _ := testOpenAPITypeWritter(t, `
package foo
// Map sample tests openAPIGen.generateMapProperty method.
type Blah struct {
// A sample String to String map
StringToArray map[string]map[string]string
}
`)
if assert.Error(err, "An error was expected") {
assert.Equal(err, fmt.Errorf("map Element kind Map is not supported in map[string]map[string]string"))
}
}
func TestFailingSample2(t *testing.T) {
err, assert, _ := testOpenAPITypeWritter(t, `
package foo
// Map sample tests openAPIGen.generateMapProperty method.
type Blah struct {
// A sample String to String map
StringToArray map[int]string
} `)
if assert.Error(err, "An error was expected") {
assert.Equal(err, fmt.Errorf("map with non-string keys are not supported by OpenAPI in map[int]string"))
}
}
func TestPointer(t *testing.T) {
err, assert, buffer := testOpenAPITypeWritter(t, `
package foo
// PointerSample demonstrate pointer's properties
type Blah struct {
// A string pointer
StringPointer *string
// A struct pointer
StructPointer *Blah
// A slice pointer
SlicePointer *[]string
// A map pointer
MapPointer *map[string]string
}
`)
if err != nil {
t.Fatal(err)
}
assert.Equal(`"foo.Blah": {
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "PointerSample demonstrate pointer's properties",
Properties: map[string]spec.Schema{
"StringPointer": {
SchemaProps: spec.SchemaProps{
Description: "A string pointer",
Type: []string{"string"},
Format: "",
},
},
"StructPointer": {
SchemaProps: spec.SchemaProps{
Description: "A struct pointer",
Ref: spec.MustCreateRef("#/definitions/foo.Blah"),
},
},
"SlicePointer": {
SchemaProps: spec.SchemaProps{
Description: "A slice pointer",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
},
},
"MapPointer": {
SchemaProps: spec.SchemaProps{
Description: "A map pointer",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
Required: []string{"StringPointer","StructPointer","SlicePointer","MapPointer"},
},
},
Dependencies: []string{
"foo.Blah",},
},
`, buffer.String())
}
/*
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.
*/
// This package generates openAPI definition file to be used in open API spec generation on API servers. To generate
// definition for a specific type or package add "+k8s:openapi-gen=true" tag to the type/package comment lines. To
// exclude a type from a tagged package, add "+k8s:openapi-gen=false" tag to the type comment lines.
package main
import (
"k8s.io/kubernetes/cmd/libs/go2idl/args"
"k8s.io/kubernetes/cmd/libs/go2idl/openapi-gen/generators"
"github.com/golang/glog"
)
func main() {
arguments := args.Default()
// Override defaults.
arguments.OutputFileBaseName = "openapi_generated"
// Run it.
if err := arguments.Execute(
generators.NameSystems(),
generators.DefaultNameSystem(),
generators.Packages,
); err != nil {
glog.Fatalf("Error: %v", err)
}
glog.V(2).Info("Completed successfully.")
}
......@@ -24,6 +24,7 @@ cmd/libs/go2idl/generator
cmd/libs/go2idl/go-to-protobuf
cmd/libs/go2idl/go-to-protobuf/protoc-gen-gogo
cmd/libs/go2idl/import-boss
cmd/libs/go2idl/openapi-gen
cmd/libs/go2idl/parser
cmd/libs/go2idl/set-gen
cmd/libs/go2idl/set-gen/generators
......
......@@ -215,7 +215,7 @@ func InstallLogsSupport(mux Mux, container *restful.Container) {
ws := new(restful.WebService)
ws.Path("/logs")
ws.Doc("get log files")
ws.Route(ws.GET("/{logpath:*}").To(logFileHandler))
ws.Route(ws.GET("/{logpath:*}").To(logFileHandler).Param(ws.PathParameter("logpath", "path to the log").DataType("string")))
ws.Route(ws.GET("/").To(logFileListHandler))
container.Add(ws)
......
......@@ -522,17 +522,38 @@ func (s *GenericAPIServer) InstallSwaggerAPI() {
swagger.RegisterSwaggerService(*s.getSwaggerConfig(), s.HandlerContainer)
}
// InstallOpenAPI installs the /swagger.json endpoint to allow new OpenAPI schema discovery.
// InstallOpenAPI installs spec endpoints for each web service.
func (s *GenericAPIServer) InstallOpenAPI() {
openAPIConfig := openapi.Config{
SwaggerConfig: s.getSwaggerConfig(),
IgnorePrefixes: []string{"/swaggerapi"},
Info: &s.openAPIInfo,
DefaultResponse: &s.openAPIDefaultResponse,
// Install one spec per web service, an ideal client will have a ClientSet containing one client
// per each of these specs.
for _, w := range s.HandlerContainer.RegisteredWebServices() {
if w.RootPath() == "/swaggerapi" {
continue
}
info := s.openAPIInfo
info.Title = info.Title + " " + w.RootPath()
err := openapi.RegisterOpenAPIService(&openapi.Config{
OpenAPIServePath: w.RootPath() + "/swagger.json",
WebServices: []*restful.WebService{w},
ProtocolList: []string{"https"},
IgnorePrefixes: []string{"/swaggerapi"},
Info: &info,
DefaultResponse: &s.openAPIDefaultResponse,
}, s.HandlerContainer)
if err != nil {
glog.Fatalf("Failed to register open api spec for %v: %v", w.RootPath(), err)
}
}
err := openapi.RegisterOpenAPIService(&openAPIConfig, s.HandlerContainer)
err := openapi.RegisterOpenAPIService(&openapi.Config{
OpenAPIServePath: "/swagger.json",
WebServices: s.HandlerContainer.RegisteredWebServices(),
ProtocolList: []string{"https"},
IgnorePrefixes: []string{"/swaggerapi"},
Info: &s.openAPIInfo,
DefaultResponse: &s.openAPIDefaultResponse,
}, s.HandlerContainer)
if err != nil {
glog.Fatalf("Failed to generate open api spec: %v", err)
glog.Fatalf("Failed to register open api spec for root: %v", err)
}
}
......
......@@ -22,56 +22,119 @@ import (
"testing"
"github.com/emicklei/go-restful"
"github.com/emicklei/go-restful/swagger"
"github.com/go-openapi/spec"
"github.com/stretchr/testify/assert"
"k8s.io/kubernetes/cmd/libs/go2idl/openapi-gen/generators/common"
"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",
config := getConfig(fullMethods)
return openAPI{
config: config,
swagger: &spec.Swagger{
SwaggerProps: spec.SwaggerProps{
Swagger: OpenAPIVersion,
Definitions: spec.Definitions{},
Paths: &spec.Paths{Paths: map[string]spec.PathItem{}},
Info: config.Info,
},
},
}
return openAPI{config: &config}, assert
openAPIDefinitions: &common.OpenAPIDefinitions{
"openapi.TestInput": *TestInput{}.OpenAPIDefinition(),
"openapi.TestOutput": *TestOutput{}.OpenAPIDefinition(),
},
}, assert
}
func noOp(request *restful.Request, response *restful.Response) {}
// Test input
type TestInput struct {
Name string `json:"name,omitempty"`
// Name of the input
Name string `json:"name,omitempty"`
// ID of the input
ID int `json:"id,omitempty"`
Tags []string `json:"tags,omitempty"`
}
// Test output
type TestOutput struct {
Name string `json:"name,omitempty"`
Count int `json:"count,omitempty"`
// Name of the output
Name string `json:"name,omitempty"`
// Number of outputs
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 (_ TestInput) OpenAPIDefinition() *common.OpenAPIDefinition {
schema := spec.Schema{}
schema.Description = "Test input"
schema.Properties = map[string]spec.Schema{
"name": {
SchemaProps: spec.SchemaProps{
Description: "Name of the input",
Type: []string{"string"},
Format: "",
},
},
"id": {
SchemaProps: spec.SchemaProps{
Description: "ID of the input",
Type: []string{"integer"},
Format: "int32",
},
},
"tags": {
SchemaProps: spec.SchemaProps{
Description: "",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
},
},
}
return &common.OpenAPIDefinition{
Schema: schema,
Dependencies: []string{},
}
}
func (t TestOutput) SwaggerDoc() map[string]string {
return map[string]string{
"": "Test output",
"name": "Name of the output",
"count": "Number of outputs",
func (_ TestOutput) OpenAPIDefinition() *common.OpenAPIDefinition {
schema := spec.Schema{}
schema.Description = "Test output"
schema.Properties = map[string]spec.Schema{
"name": {
SchemaProps: spec.SchemaProps{
Description: "Name of the output",
Type: []string{"string"},
Format: "",
},
},
"count": {
SchemaProps: spec.SchemaProps{
Description: "Number of outputs",
Type: []string{"integer"},
Format: "int32",
},
},
}
return &common.OpenAPIDefinition{
Schema: schema,
Dependencies: []string{},
}
}
var _ common.OpenAPIDefinitionGetter = TestInput{}
var _ common.OpenAPIDefinitionGetter = TestOutput{}
func getTestRoute(ws *restful.WebService, method string, additionalParams bool) *restful.RouteBuilder {
ret := ws.Method(method).
Path("/test/{path:*}").
......@@ -92,7 +155,7 @@ func getTestRoute(ws *restful.WebService, method string, additionalParams bool)
return ret
}
func getSwaggerConfig(fullMethods bool) *swagger.Config {
func getConfig(fullMethods bool) *Config {
mux := http.NewServeMux()
container := restful.NewContainer()
container.ServeMux = mux
......@@ -120,9 +183,16 @@ func getSwaggerConfig(fullMethods bool) *swagger.Config {
}
container.Add(ws)
return &swagger.Config{
WebServicesUrl: "https://test-server",
WebServices: container.RegisteredWebServices(),
return &Config{
WebServices: container.RegisteredWebServices(),
ProtocolList: []string{"https"},
OpenAPIServePath: "/swagger.json",
Info: &spec.Info{
InfoProps: spec.InfoProps{
Title: "TestAPI",
Description: "Test API",
},
},
}
}
......@@ -285,27 +355,23 @@ 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{
......@@ -324,21 +390,18 @@ 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{}{},
},
},
},
......@@ -369,49 +432,10 @@ func TestBuildSwaggerSpec(t *testing.T) {
},
},
}
err := o.buildSwaggerSpec()
err := o.init()
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)
}
}
......@@ -342,7 +342,8 @@ func (s *Server) InstallDebuggingHandlers() {
Operation("getLogs"))
ws.Route(ws.GET("/{logpath:*}").
To(s.getLogs).
Operation("getLogs"))
Operation("getLogs").
Param(ws.PathParameter("logpath", "path to the log").DataType("string")))
s.restfulCont.Add(ws)
ws = new(restful.WebService)
......
......@@ -821,6 +821,16 @@ func decodeResponse(resp *http.Response, obj interface{}) error {
return nil
}
func writeResponseToFile(resp *http.Response, filename string) error {
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return ioutil.WriteFile(filename, data, 0755)
}
func TestInstallThirdPartyAPIGet(t *testing.T) {
for _, version := range versionsToTest {
testInstallThirdPartyAPIGetVersion(t, version)
......@@ -1228,6 +1238,7 @@ func TestValidOpenAPISpec(t *testing.T) {
defer etcdserver.Terminate(t)
config.EnableOpenAPISupport = true
config.EnableIndex = true
config.OpenAPIInfo = spec.Info{
InfoProps: spec.InfoProps{
Title: "Kubernetes",
......@@ -1262,12 +1273,67 @@ func TestValidOpenAPISpec(t *testing.T) {
assert.NoError(res.AsError())
}
// TODO(mehdy): The actual validation part of these tests are timing out on jerkin but passing locally. Enable it after debugging timeout issue.
disableValidation := true
// Saving specs to a temporary folder is a good way to debug spec generation without bringing up an actual
// api server.
saveSwaggerSpecs := false
// 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())
validator := validate.NewSpecValidator(doc.Schema(), strfmt.Default)
if !disableValidation {
res, warns := validator.Validate(doc)
assert.NoError(res.AsError())
if !warns.IsValid() {
t.Logf("Open API spec on root has some warnings : %v", warns)
}
} else {
t.Logf("Validation is disabled because it is timing out on jenkins put passing locally.")
}
}
// validate specs on each end-point
resp, err = http.Get(server.URL)
if !assert.NoError(err) {
t.Errorf("unexpected error: %v", err)
}
assert.Equal(http.StatusOK, resp.StatusCode)
var list unversioned.RootPaths
if assert.NoError(decodeResponse(resp, &list)) {
for _, path := range list.Paths {
if !strings.HasPrefix(path, "/api") {
continue
}
t.Logf("Validating open API spec on %v ...", path)
if saveSwaggerSpecs {
resp, err = http.Get(server.URL + path + "/swagger.json")
if !assert.NoError(err) {
t.Errorf("unexpected error: %v", err)
}
assert.Equal(http.StatusOK, resp.StatusCode)
assert.NoError(writeResponseToFile(resp, "/tmp/swagger_"+strings.Replace(path, "/", "_", -1)+".json"))
}
// Validate OpenApi spec on path
doc, err := loads.Spec(server.URL + path + "/swagger.json")
if assert.NoError(err) {
validator := validate.NewSpecValidator(doc.Schema(), strfmt.Default)
if !disableValidation {
res, warns := validator.Validate(doc)
assert.NoError(res.AsError())
if !warns.IsValid() {
t.Logf("Open API spec on %v has some warnings : %v", path, warns)
}
} else {
t.Logf("Validation is disabled because it is timing out on jenkins put passing locally.")
}
}
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment