mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 19:31:44 +00:00
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:
parent
8865f5d007
commit
54fee8c253
@ -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
|
||||
|
4
cmd/libs/go2idl/openapi-gen/README
Normal file
4
cmd/libs/go2idl/openapi-gen/README
Normal 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.
|
105
cmd/libs/go2idl/openapi-gen/generators/common/common.go
Normal file
105
cmd/libs/go2idl/openapi-gen/generators/common/common.go
Normal 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]
|
||||
}
|
18
cmd/libs/go2idl/openapi-gen/generators/common/doc.go
Normal file
18
cmd/libs/go2idl/openapi-gen/generators/common/doc.go
Normal 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
|
519
cmd/libs/go2idl/openapi-gen/generators/openapi.go
Normal file
519
cmd/libs/go2idl/openapi-gen/generators/openapi.go
Normal 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
|
||||
}
|
359
cmd/libs/go2idl/openapi-gen/generators/openapi_test.go
Normal file
359
cmd/libs/go2idl/openapi-gen/generators/openapi_test.go
Normal 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())
|
||||
}
|
44
cmd/libs/go2idl/openapi-gen/main.go
Normal file
44
cmd/libs/go2idl/openapi-gen/main.go
Normal 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.")
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user