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
This commit is contained in:
mbohlool 2016-09-01 16:39:10 -07:00
parent 8865f5d007
commit 54fee8c253
14 changed files with 1540 additions and 364 deletions

View File

@ -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))
@ -285,6 +285,107 @@ $(DEEPCOPY_GEN):
hack/make-rules/build.sh cmd/libs/go2idl/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
#
@ -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

View File

@ -0,0 +1,4 @@
# 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.

View File

@ -0,0 +1,105 @@
/*
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]
}

View File

@ -0,0 +1,18 @@
/*
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

View File

@ -0,0 +1,519 @@
/*
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"
"io"
"path/filepath"
"reflect"
"sort"
"strings"
"k8s.io/kubernetes/cmd/libs/go2idl/args"
"k8s.io/kubernetes/cmd/libs/go2idl/generator"
"k8s.io/kubernetes/cmd/libs/go2idl/namer"
"k8s.io/kubernetes/cmd/libs/go2idl/openapi-gen/generators/common"
"k8s.io/kubernetes/cmd/libs/go2idl/types"
"k8s.io/kubernetes/pkg/util/sets"
"github.com/golang/glog"
)
// This is the comment tag that carries parameters for open API generation.
const tagName = "k8s:openapi-gen"
// Known values for the tag.
const (
tagValueTrue = "true"
tagValueFalse = "false"
// Should only be used only for test
tagTargetType = "target"
)
func hasOpenAPITagValue(comments []string, value string) bool {
tagValues := types.ExtractCommentTags("+", comments)[tagName]
if tagValues == nil {
return false
}
for _, val := range tagValues {
if val == value {
return true
}
}
return false
}
// NameSystems returns the name system used by the generators in this package.
func NameSystems() namer.NameSystems {
return namer.NameSystems{
"raw": namer.NewRawNamer("", nil),
}
}
// DefaultNameSystem returns the default name system for ordering the types to be
// processed by the generators in this package.
func DefaultNameSystem() string {
return "raw"
}
func Packages(context *generator.Context, arguments *args.GeneratorArgs) generator.Packages {
boilerplate, err := arguments.LoadGoBoilerplate()
if err != nil {
glog.Fatalf("Failed loading boilerplate: %v", err)
}
inputs := sets.NewString(context.Inputs...)
header := append([]byte(fmt.Sprintf("// +build !%s\n\n", arguments.GeneratedBuildTag)), boilerplate...)
header = append(header, []byte(
`
// This file was autogenerated by openapi-gen. Do not edit it manually!
`)...)
targets := []*types.Type{}
for i := range inputs {
glog.V(5).Infof("considering pkg %q", i)
pkg, ok := context.Universe[i]
if !ok {
// If the input had no Go files, for example.
continue
}
for _, t := range pkg.Types {
if hasOpenAPITagValue(t.CommentLines, tagTargetType) {
glog.V(5).Infof("target type : %q", t)
targets = append(targets, t)
}
}
}
switch len(targets) {
case 0:
// If no target package found, that means the generated file in target package is up to date
// and build excluded the target package.
return generator.Packages{}
case 1:
pkg := context.Universe[targets[0].Name.Package]
return generator.Packages{&generator.DefaultPackage{
PackageName: strings.Split(filepath.Base(pkg.Path), ".")[0],
PackagePath: pkg.Path,
HeaderText: header,
GeneratorFunc: func(c *generator.Context) (generators []generator.Generator) {
return []generator.Generator{NewOpenAPIGen(arguments.OutputFileBaseName, targets[0], context)}
},
FilterFunc: func(c *generator.Context, t *types.Type) bool {
// There is a conflict between this codegen and codecgen, we should avoid types generated for codecgen
if strings.HasPrefix(t.Name.Name, "codecSelfer") {
return false
}
pkg := context.Universe.Package(t.Name.Package)
if hasOpenAPITagValue(pkg.Comments, tagValueTrue) {
return !hasOpenAPITagValue(t.CommentLines, tagValueFalse)
}
if hasOpenAPITagValue(t.CommentLines, tagValueTrue) {
return true
}
return false
},
},
}
default:
glog.Fatalf("Duplicate target type found: %v", targets)
}
return generator.Packages{}
}
const (
specPackagePath = "github.com/go-openapi/spec"
openAPICommonPackagePath = "k8s.io/kubernetes/cmd/libs/go2idl/openapi-gen/generators/common"
)
// openApiGen produces a file with auto-generated OpenAPI functions.
type openAPIGen struct {
generator.DefaultGen
// TargetType is the type that will get OpenAPIDefinitions method returning all definitions.
targetType *types.Type
imports namer.ImportTracker
context *generator.Context
}
func NewOpenAPIGen(sanitizedName string, targetType *types.Type, context *generator.Context) generator.Generator {
return &openAPIGen{
DefaultGen: generator.DefaultGen{
OptionalName: sanitizedName,
},
imports: generator.NewImportTracker(),
targetType: targetType,
context: context,
}
}
func (g *openAPIGen) Namers(c *generator.Context) namer.NameSystems {
// Have the raw namer for this file track what it imports.
return namer.NameSystems{
"raw": namer.NewRawNamer(g.targetType.Name.Package, g.imports),
}
}
func (g *openAPIGen) Filter(c *generator.Context, t *types.Type) bool {
// There is a conflict between this codegen and codecgen, we should avoid types generated for codecgen
if strings.HasPrefix(t.Name.Name, "codecSelfer") {
return false
}
return true
}
func (g *openAPIGen) isOtherPackage(pkg string) bool {
if pkg == g.targetType.Name.Package {
return false
}
if strings.HasSuffix(pkg, "\""+g.targetType.Name.Package+"\"") {
return false
}
return true
}
func (g *openAPIGen) Imports(c *generator.Context) []string {
importLines := []string{}
for _, singleImport := range g.imports.ImportLines() {
importLines = append(importLines, singleImport)
}
return importLines
}
func argsFromType(t *types.Type) generator.Args {
return generator.Args{
"type": t,
"OpenAPIDefinitions": types.Ref(openAPICommonPackagePath, "OpenAPIDefinitions"),
"OpenAPIDefinition": types.Ref(openAPICommonPackagePath, "OpenAPIDefinition"),
"SpecSchemaType": types.Ref(specPackagePath, "Schema"),
}
}
func (g *openAPIGen) Init(c *generator.Context, w io.Writer) error {
sw := generator.NewSnippetWriter(w, c, "$", "$")
sw.Do("func (_ $.type|raw$) OpenAPIDefinitions() *$.OpenAPIDefinitions|raw$ {\n", argsFromType(g.targetType))
sw.Do("return &$.OpenAPIDefinitions|raw${\n", argsFromType(nil))
return sw.Error()
}
func (g *openAPIGen) Finalize(c *generator.Context, w io.Writer) error {
sw := generator.NewSnippetWriter(w, c, "$", "$")
sw.Do("}\n}\n", nil)
return sw.Error()
}
func (g *openAPIGen) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
glog.V(5).Infof("generating for type %v", t)
sw := generator.NewSnippetWriter(w, c, "$", "$")
err := newOpenAPITypeWriter(sw).generate(t)
if err != nil {
return err
}
return sw.Error()
}
func getJsonTags(m *types.Member) []string {
jsonTag := reflect.StructTag(m.Tags).Get("json")
if jsonTag == "" {
return []string{}
}
return strings.Split(jsonTag, ",")
}
func getReferableName(m *types.Member) string {
jsonTags := getJsonTags(m)
if len(jsonTags) > 0 {
if jsonTags[0] == "-" {
return ""
} else {
return jsonTags[0]
}
} else {
return m.Name
}
}
func optionIndex(s, optionName string) int {
ret := 0
for s != "" {
var next string
i := strings.Index(s, ",")
if i >= 0 {
s, next = s[:i], s[i+1:]
}
if s == optionName {
return ret
}
s = next
ret++
}
return -1
}
func isPropertyRequired(m *types.Member) bool {
// A property is required if it does not have omitempty value in its json tag (documentation and implementation
// of json package requires omitempty to be at location 1 or higher.
// TODO: Move optional field definition from tags to comments.
return optionIndex(reflect.StructTag(m.Tags).Get("json"), "omitempty") < 1
}
type openAPITypeWriter struct {
*generator.SnippetWriter
refTypes map[string]*types.Type
GetDefinitionInterface *types.Type
}
func newOpenAPITypeWriter(sw *generator.SnippetWriter) openAPITypeWriter {
return openAPITypeWriter{
SnippetWriter: sw,
refTypes: map[string]*types.Type{},
}
}
func hasOpenAPIDefinitionMethod(t *types.Type) bool {
for mn, mt := range t.Methods {
if mn != "OpenAPIDefinition" {
continue
}
if len(mt.Signature.Parameters) != 0 || len(mt.Signature.Results) != 1 {
return false
}
r := mt.Signature.Results[0]
if r.Name.Name != "OpenAPIDefinition" || r.Name.Package != openAPICommonPackagePath {
return false
}
return true
}
return false
}
// typeShortName returns short package name (e.g. the name x appears in package x definition) dot type name.
func typeShortName(t *types.Type) string {
return filepath.Base(t.Name.Package) + "." + t.Name.Name
}
func (g openAPITypeWriter) generate(t *types.Type) error {
// Only generate for struct type and ignore the rest
switch t.Kind {
case types.Struct:
args := argsFromType(t)
g.Do("\"$.$\": ", typeShortName(t))
if hasOpenAPIDefinitionMethod(t) {
g.Do("$.type|raw${}.OpenAPIDefinition(),", args)
return nil
}
g.Do("{\nSchema: spec.Schema{\nSchemaProps: spec.SchemaProps{\n", nil)
g.generateDescription(t.CommentLines)
g.Do("Properties: map[string]$.SpecSchemaType|raw${\n", args)
required := []string{}
for _, m := range t.Members {
if hasOpenAPITagValue(m.CommentLines, tagValueFalse) {
continue
}
name := getReferableName(&m)
if name == "" {
continue
}
if isPropertyRequired(&m) {
required = append(required, name)
}
if err := g.generateProperty(&m); err != nil {
return err
}
}
g.Do("},\n", nil)
if len(required) > 0 {
g.Do("Required: []string{\"$.$\"},\n", strings.Join(required, "\",\""))
}
g.Do("},\n},\n", nil)
g.Do("Dependencies: []string{\n", args)
// Map order is undefined, sort them or we may get a different file generated each time.
keys := []string{}
for k := range g.refTypes {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := g.refTypes[k]
if t, _ := common.GetOpenAPITypeFormat(v.String()); t != "" {
// This is a known type, we do not need a reference to it
// Will eliminate special case of time.Time
continue
}
g.Do("\"$.$\",", k)
}
g.Do("},\n},\n", nil)
}
return nil
}
func (g openAPITypeWriter) generateDescription(CommentLines []string) {
var buffer bytes.Buffer
delPrevChar := func() {
if buffer.Len() > 0 {
buffer.Truncate(buffer.Len() - 1) // Delete the last " " or "\n"
}
}
for _, line := range CommentLines {
// Ignore all lines after ---
if line == "---" {
break
}
line = strings.TrimRight(line, " ")
leading := strings.TrimLeft(line, " ")
switch {
case len(line) == 0: // Keep paragraphs
delPrevChar()
buffer.WriteString("\n\n")
case strings.HasPrefix(leading, "TODO"): // Ignore one line TODOs
case strings.HasPrefix(leading, "+"): // Ignore instructions to go2idl
default:
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
delPrevChar()
line = "\n" + line + "\n" // Replace it with newline. This is useful when we have a line with: "Example:\n\tJSON-someting..."
} else {
line += " "
}
buffer.WriteString(line)
}
}
postDoc := strings.TrimRight(buffer.String(), "\n")
postDoc = strings.Replace(postDoc, "\\\"", "\"", -1) // replace user's \" to "
postDoc = strings.Replace(postDoc, "\"", "\\\"", -1) // Escape "
postDoc = strings.Replace(postDoc, "\n", "\\n", -1)
postDoc = strings.Replace(postDoc, "\t", "\\t", -1)
postDoc = strings.Trim(postDoc, " ")
if postDoc != "" {
g.Do("Description: \"$.$\",\n", postDoc)
}
}
func (g openAPITypeWriter) generateProperty(m *types.Member) error {
name := getReferableName(m)
if name == "" {
return nil
}
g.Do("\"$.$\": {\n", name)
g.Do("SchemaProps: spec.SchemaProps{\n", nil)
g.generateDescription(m.CommentLines)
jsonTags := getJsonTags(m)
if len(jsonTags) > 1 && jsonTags[1] == "string" {
g.generateSimpleProperty("string", "")
g.Do("},\n},\n", nil)
return nil
}
t := resolveAliasAndPtrType(m.Type)
// If we can get a openAPI type and format for this type, we consider it to be simple property
typeString, format := common.GetOpenAPITypeFormat(t.String())
if typeString != "" {
g.generateSimpleProperty(typeString, format)
g.Do("},\n},\n", nil)
return nil
}
switch t.Kind {
case types.Builtin:
return fmt.Errorf("please add type %v to getOpenAPITypeFormat function.", t)
case types.Map:
if err := g.generateMapProperty(t); err != nil {
return err
}
case types.Slice, types.Array:
if err := g.generateSliceProperty(t); err != nil {
return err
}
case types.Struct, types.Interface:
g.generateReferenceProperty(t)
default:
return fmt.Errorf("cannot generate spec for type %v.", t)
}
g.Do("},\n},\n", nil)
return g.Error()
}
func (g openAPITypeWriter) generateSimpleProperty(typeString, format string) {
g.Do("Type: []string{\"$.$\"},\n", typeString)
g.Do("Format: \"$.$\",\n", format)
}
func (g openAPITypeWriter) generateReferenceProperty(t *types.Type) {
var name string
if t.Name.Package == "" {
name = t.Name.Name
} else {
name = filepath.Base(t.Name.Package) + "." + t.Name.Name
}
g.refTypes[name] = t
g.Do("Ref: spec.MustCreateRef(\"#/definitions/$.$\"),\n", name)
}
func resolveAliasAndPtrType(t *types.Type) *types.Type {
var prev *types.Type
for prev != t {
prev = t
if t.Kind == types.Alias {
t = t.Underlying
}
if t.Kind == types.Pointer {
t = t.Elem
}
}
return t
}
func (g openAPITypeWriter) generateMapProperty(t *types.Type) error {
keyType := resolveAliasAndPtrType(t.Key)
elemType := resolveAliasAndPtrType(t.Elem)
// According to OpenAPI examples, only map from string is supported
if keyType.Name.Name != "string" {
return fmt.Errorf("map with non-string keys are not supported by OpenAPI in %v", t)
}
g.Do("Type: []string{\"object\"},\n", nil)
g.Do("AdditionalProperties: &spec.SchemaOrBool{\nSchema: &spec.Schema{\nSchemaProps: spec.SchemaProps{\n", nil)
switch elemType.Kind {
case types.Builtin:
typeString, format := common.GetOpenAPITypeFormat(elemType.String())
g.generateSimpleProperty(typeString, format)
case types.Struct:
g.generateReferenceProperty(t.Elem)
case types.Slice, types.Array:
g.generateSliceProperty(elemType)
default:
return fmt.Errorf("map Element kind %v is not supported in %v", elemType.Kind, t.Name)
}
g.Do("},\n},\n},\n", nil)
return nil
}
func (g openAPITypeWriter) generateSliceProperty(t *types.Type) error {
elemType := resolveAliasAndPtrType(t.Elem)
g.Do("Type: []string{\"array\"},\n", nil)
g.Do("Items: &spec.SchemaOrArray{\nSchema: &spec.Schema{\nSchemaProps: spec.SchemaProps{\n", nil)
switch elemType.Kind {
case types.Builtin:
typeString, format := common.GetOpenAPITypeFormat(elemType.String())
g.generateSimpleProperty(typeString, format)
case types.Struct:
g.generateReferenceProperty(t.Elem)
default:
return fmt.Errorf("slice Element kind %v is not supported in %v", elemType.Kind, t)
}
g.Do("},\n},\n},\n", nil)
return nil
}

View File

@ -0,0 +1,359 @@
/*
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())
}

View File

@ -0,0 +1,44 @@
/*
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.")
}

View File

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

View File

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

View File

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

View File

@ -16,213 +16,133 @@ 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/cmd/libs/go2idl/openapi-gen/generators/common"
"k8s.io/kubernetes/pkg/util/json"
)
const (
// By convention, the Swagger specification file is named swagger.json
OpenAPIServePath = "/swagger.json"
OpenAPIVersion = "2.0"
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
// Path to the spec file. by convention, it should name [.*/]*/swagger.json
OpenAPIServePath string
// List of web services for this API spec
WebServices []*restful.WebService
// List of supported protocols such as https, http, etc.
ProtocolList []string
// 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.
// will show up as ... "responses" : {"default" : $DefaultResponse} in the spec.
DefaultResponse *spec.Response
// List of webservice's path prefixes to ignore
IgnorePrefixes []string
}
// +k8s:openapi-gen=target
type openAPI struct {
config *Config
swagger *spec.Swagger
protocolList []string
config *Config
swagger *spec.Swagger
protocolList []string
openAPIDefinitions *common.OpenAPIDefinitions
}
// 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,
swagger: &spec.Swagger{
SwaggerProps: spec.SwaggerProps{
Swagger: OpenAPIVersion,
Definitions: spec.Definitions{},
Paths: &spec.Paths{Paths: map[string]spec.PathItem{}},
Info: config.Info,
},
},
}
err = o.buildSwaggerSpec()
err = o.init()
if err != nil {
return err
}
containers.ServeMux.HandleFunc(OpenAPIServePath, func(w http.ResponseWriter, r *http.Request) {
containers.ServeMux.HandleFunc(config.OpenAPIServePath, func(w http.ResponseWriter, r *http.Request) {
resp := restful.NewResponse(w)
if r.URL.Path != OpenAPIServePath {
if r.URL.Path != config.OpenAPIServePath {
resp.WriteErrorString(http.StatusNotFound, "Path not found!")
}
// TODO: we can cache json string and return it here.
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.")
func (o *openAPI) init() error {
if o.openAPIDefinitions == nil {
// Compilation error here means the code generator need to run first.
o.openAPIDefinitions = o.OpenAPIDefinitions()
}
o.protocolList, err = o.buildProtocolList()
err := o.buildPaths()
if err != nil {
return err
}
definitions, err := o.buildDefinitions()
if err != nil {
return err
// no need to the keep type list in memory
o.openAPIDefinitions = nil
return nil
}
func (o *openAPI) buildDefinitionRecursively(name string) error {
if _, ok := o.swagger.Definitions[name]; ok {
return nil
}
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,
},
if item, ok := (*o.openAPIDefinitions)[name]; ok {
o.swagger.Definitions[name] = item.Schema
for _, v := range item.Dependencies {
if err := o.buildDefinitionRecursively(v); err != nil {
return err
}
}
} else {
return fmt.Errorf("cannot find model definition for %v. If you added a new type, you may need to add +k8s:openapi-gen=true to the package or type and run code-gen again.", name)
}
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
}
}
// buildDefinitionForType build a definition for a given type and return a referable name to it's definition.
// This is the main function that keep track of definitions used in this spec and is depend on code generated
// by k8s.io/kubernetes/cmd/libs/go2idl/openapi-gen.
func (o *openAPI) buildDefinitionForType(sample interface{}) (string, error) {
t := reflect.TypeOf(sample)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
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,
},
name := t.String()
if err := o.buildDefinitionRecursively(name); err != nil {
return "", err
}
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
return "#/definitions/" + name, 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),
}
func (o *openAPI) buildPaths() error {
pathsToIgnore := createTrie(o.config.IgnorePrefixes)
duplicateOpId := make(map[string]bool)
// Find duplicate operation IDs.
for _, service := range o.config.SwaggerConfig.WebServices {
for _, service := range o.config.WebServices {
if pathsToIgnore.HasPrefix(service.RootPath()) {
continue
}
@ -231,28 +151,32 @@ func (o *openAPI) buildPaths() (spec.Paths, error) {
duplicateOpId[route.Operation] = exists
}
}
for _, w := range o.config.SwaggerConfig.WebServices {
for _, w := range o.config.WebServices {
rootPath := w.RootPath()
if pathsToIgnore.HasPrefix(rootPath) {
continue
}
commonParams, err := buildParameters(w.PathParameters())
commonParams, err := o.buildParameters(w.PathParameters())
if err != nil {
return paths, err
return err
}
for path, routes := range groupRoutesByPath(w.Routes()) {
// go-swagger has special variable difinition {$NAME:*} that can only be
// go-swagger has special variable definition {$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
if pathsToIgnore.HasPrefix(path) {
continue
}
pathItem, exists := paths.Paths[path]
// Aggregating common parameters make API spec (and generated clients) simpler
inPathCommonParamsMap, err := o.findCommonParameters(routes)
if err != nil {
return err
}
pathItem, exists := o.swagger.Paths.Paths[path]
if exists {
return paths, fmt.Errorf("Duplicate webservice route has been found for path: %v", path)
return fmt.Errorf("duplicate webservice route has been found for path: %v", path)
}
pathItem = spec.PathItem{
PathItemProps: spec.PathItemProps{
@ -260,16 +184,14 @@ func (o *openAPI) buildPaths() (spec.Paths, error) {
},
}
// 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)
}
pathItem.Parameters = append(pathItem.Parameters, commonParams...)
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
return err
}
if duplicateOpId[op.ID] {
// Repeated Operation IDs are not allowed in OpenAPI spec but if
@ -294,36 +216,21 @@ func (o *openAPI) buildPaths() (spec.Paths, error) {
pathItem.Patch = op
}
}
paths.Paths[path] = pathItem
o.swagger.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
}
return 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{
func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map[interface{}]spec.Parameter) (ret *spec.Operation, err error) {
ret = &spec.Operation{
OperationProps: spec.OperationProps{
Description: route.Doc,
Consumes: route.Consumes,
Produces: route.Produces,
ID: route.Operation,
Schemes: o.protocolList,
Schemes: o.config.ProtocolList,
Responses: &spec.Responses{
ResponsesProps: spec.ResponsesProps{
StatusCodeResponses: make(map[int]spec.Response),
@ -331,26 +238,37 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map
},
},
}
// Build responses
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()),
},
},
},
ret.Responses.StatusCodeResponses[resp.Code], err = o.buildResponse(resp.Model, resp.Message)
if err != nil {
return ret, err
}
}
// If there is no response but a write sample, assume that write sample is an http.StatusOK response.
if len(ret.Responses.StatusCodeResponses) == 0 && route.WriteSample != nil {
ret.Responses.StatusCodeResponses[http.StatusOK], err = o.buildResponse(route.WriteSample, "OK")
if err != nil {
return ret, err
}
}
// If there is still no response, use default response provided.
if len(ret.Responses.StatusCodeResponses) == 0 {
ret.Responses.Default = o.config.DefaultResponse
}
// If there is a read sample, there will be a body param referring to it.
if route.ReadSample != nil {
if _, err := o.toSchema(reflect.TypeOf(route.ReadSample).String(), route.ReadSample); err != nil {
return ret, err
}
}
// Build non-common Parameters
ret.Parameters = make([]spec.Parameter, 0)
for _, param := range route.ParameterDocs {
_, isCommon := inPathCommonParamsMap[mapKeyFromParam(param)]
if !isCommon {
openAPIParam, err := buildParameter(param.Data())
if _, isCommon := inPathCommonParamsMap[mapKeyFromParam(param)]; !isCommon {
openAPIParam, err := o.buildParameter(param.Data())
if err != nil {
return ret, err
}
@ -360,6 +278,20 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map
return ret, nil
}
func (o *openAPI) buildResponse(model interface{}, description string) (spec.Response, error) {
typeName := reflect.TypeOf(model).String()
schema, err := o.toSchema(typeName, model)
if err != nil {
return spec.Response{}, err
}
return spec.Response{
ResponseProps: spec.ResponseProps{
Description: description,
Schema: schema,
},
}, nil
}
func groupRoutesByPath(routes []restful.Route) (ret map[string][]restful.Route) {
ret = make(map[string][]restful.Route)
for _, r := range routes {
@ -382,7 +314,7 @@ func mapKeyFromParam(param *restful.Parameter) interface{} {
}
}
func findCommonParameters(routes []restful.Route) (map[interface{}]spec.Parameter, error) {
func (o *openAPI) 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)
@ -395,7 +327,7 @@ func findCommonParameters(routes []restful.Route) (map[interface{}]spec.Paramete
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)
return commonParamsMap, fmt.Errorf("duplicate parameter %v for route %v, %v.", param.Data().Name, string(msg), s)
}
routeParamDuplicateMap[key] = true
paramOpsCountByName[key]++
@ -404,7 +336,7 @@ func findCommonParameters(routes []restful.Route) (map[interface{}]spec.Paramete
}
for key, count := range paramOpsCountByName {
if count == len(routes) {
openAPIParam, err := buildParameter(paramNameKindToDataMap[key])
openAPIParam, err := o.buildParameter(paramNameKindToDataMap[key])
if err != nil {
return commonParamsMap, err
}
@ -414,7 +346,31 @@ func findCommonParameters(routes []restful.Route) (map[interface{}]spec.Paramete
return commonParamsMap, nil
}
func buildParameter(restParam restful.ParameterData) (ret spec.Parameter, err error) {
func (o *openAPI) toSchema(typeName string, model interface{}) (_ *spec.Schema, err error) {
if openAPIType, openAPIFormat := common.GetOpenAPITypeFormat(typeName); openAPIType != "" {
return &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{openAPIType},
Format: openAPIFormat,
},
}, nil
} else {
ref := "#/definitions/" + typeName
if model != nil {
ref, err = o.buildDefinitionForType(model)
if err != nil {
return nil, err
}
}
return &spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: spec.MustCreateRef(ref),
},
}, nil
}
}
func (o *openAPI) buildParameter(restParam restful.ParameterData) (ret spec.Parameter, err error) {
ret = spec.Parameter{
ParamProps: spec.ParamProps{
Name: restParam.Name,
@ -425,16 +381,12 @@ func buildParameter(restParam restful.ParameterData) (ret spec.Parameter, err er
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
ret.Schema, err = o.toSchema(restParam.DataType, nil)
return ret, err
case restful.PathParameterKind:
ret.In = "path"
if !restParam.Required {
return ret, fmt.Errorf("Path parameters should be marked at required for parameter %v", restParam)
return ret, fmt.Errorf("path parameters should be marked at required for parameter %v", restParam)
}
case restful.QueryParameterKind:
ret.In = "query"
@ -443,26 +395,22 @@ func buildParameter(restParam restful.ParameterData) (ret spec.Parameter, err er
case restful.FormParameterKind:
ret.In = "form"
default:
return ret, fmt.Errorf("Unknown restful operation kind : %v", restParam.Kind)
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)
openAPIType, openAPIFormat := common.GetOpenAPITypeFormat(restParam.DataType)
if openAPIType == "" {
return ret, fmt.Errorf("non-body Restful parameter type should be a simple type, but got : %v", restParam.DataType)
}
ret.Type = restParam.DataType
ret.Format = restParam.DataFormat
ret.Type = openAPIType
ret.Format = openAPIFormat
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) {
func (o *openAPI) 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())
ret[i], err = o.buildParameter(v.Data())
if err != nil {
return ret, err
}
@ -470,60 +418,16 @@ func buildParameters(restParam []*restful.Parameter) (ret []spec.Parameter, 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
wordTail bool
}
func createTrie(list []string) trie {
ret := trie{
children: make(map[byte]*trie),
wordTail: false,
}
for _, v := range list {
ret.Add(v)
@ -536,22 +440,31 @@ func (t *trie) Add(v string) {
for _, b := range []byte(v) {
child, exists := root.children[b]
if !exists {
child = new(trie)
child.children = make(map[byte]*trie)
child = &trie{
children: make(map[byte]*trie),
wordTail: false,
}
root.children[b] = child
}
root = child
}
root.wordTail = true
}
func (t *trie) HasPrefix(v string) bool {
root := t
if root.wordTail {
return true
}
for _, b := range []byte(v) {
child, exists := root.children[b]
if !exists {
return false
}
if child.wordTail {
return true
}
root = child
}
return true
return false
}

View File

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

View File

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

View File

@ -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.")
}
}
}
}
}