diff --git a/Makefile.generated_files b/Makefile.generated_files index b564c476715..83835b4c2f0 100644 --- a/Makefile.generated_files +++ b/Makefile.generated_files @@ -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=' $$@.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 diff --git a/cmd/libs/go2idl/openapi-gen/README b/cmd/libs/go2idl/openapi-gen/README new file mode 100644 index 00000000000..8f2225fba70 --- /dev/null +++ b/cmd/libs/go2idl/openapi-gen/README @@ -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. diff --git a/cmd/libs/go2idl/openapi-gen/generators/common/common.go b/cmd/libs/go2idl/openapi-gen/generators/common/common.go new file mode 100644 index 00000000000..48c4f9c0191 --- /dev/null +++ b/cmd/libs/go2idl/openapi-gen/generators/common/common.go @@ -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] +} diff --git a/cmd/libs/go2idl/openapi-gen/generators/common/doc.go b/cmd/libs/go2idl/openapi-gen/generators/common/doc.go new file mode 100644 index 00000000000..0308d58b4db --- /dev/null +++ b/cmd/libs/go2idl/openapi-gen/generators/common/doc.go @@ -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 diff --git a/cmd/libs/go2idl/openapi-gen/generators/openapi.go b/cmd/libs/go2idl/openapi-gen/generators/openapi.go new file mode 100644 index 00000000000..21181a4335f --- /dev/null +++ b/cmd/libs/go2idl/openapi-gen/generators/openapi.go @@ -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 +} diff --git a/cmd/libs/go2idl/openapi-gen/generators/openapi_test.go b/cmd/libs/go2idl/openapi-gen/generators/openapi_test.go new file mode 100644 index 00000000000..7586fc256d1 --- /dev/null +++ b/cmd/libs/go2idl/openapi-gen/generators/openapi_test.go @@ -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()) +} diff --git a/cmd/libs/go2idl/openapi-gen/main.go b/cmd/libs/go2idl/openapi-gen/main.go new file mode 100644 index 00000000000..cc749443d60 --- /dev/null +++ b/cmd/libs/go2idl/openapi-gen/main.go @@ -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.") +} diff --git a/hack/.linted_packages b/hack/.linted_packages index 0b790f454b8..8a9dfd37e99 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -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 diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 0e1530eeb85..3ea03de6d67 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -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) diff --git a/pkg/genericapiserver/genericapiserver.go b/pkg/genericapiserver/genericapiserver.go index 43ddb090b27..949e268f757 100644 --- a/pkg/genericapiserver/genericapiserver.go +++ b/pkg/genericapiserver/genericapiserver.go @@ -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) } } diff --git a/pkg/genericapiserver/openapi/openapi.go b/pkg/genericapiserver/openapi/openapi.go index d1550918941..e064c2c28c4 100644 --- a/pkg/genericapiserver/openapi/openapi.go +++ b/pkg/genericapiserver/openapi/openapi.go @@ -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 } diff --git a/pkg/genericapiserver/openapi/openapi_test.go b/pkg/genericapiserver/openapi/openapi_test.go index fb5c6df81ba..c6696fd8358 100644 --- a/pkg/genericapiserver/openapi/openapi_test.go +++ b/pkg/genericapiserver/openapi/openapi_test.go @@ -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) - } -} diff --git a/pkg/kubelet/server/server.go b/pkg/kubelet/server/server.go index 38b4dad0b0f..fb54a4e24d9 100644 --- a/pkg/kubelet/server/server.go +++ b/pkg/kubelet/server/server.go @@ -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) diff --git a/pkg/master/master_test.go b/pkg/master/master_test.go index bccb987a50c..24911d43b1a 100644 --- a/pkg/master/master_test.go +++ b/pkg/master/master_test.go @@ -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.") + } + } + + } + } + }