mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 14:37:00 +00:00
Merge pull request #53228 from apelisse/openapi-explain
Automatic merge from submit-queue (batch tested with PRs 53228, 53232, 53353). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Openapi explain **What this PR does / why we need it**: This rewrites `kubectl explain` but using openapi rather than swagger 1.2. Also removes the former code. **Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes #49465, fixes partially #44589, fixes partially #38637 **Special notes for your reviewer**: FYI @mbohlool **Release note**: ```release-note `kubectl explain` now uses openapi rather than swagger 1.2. ```
This commit is contained in:
commit
35ac027965
@ -91,7 +91,6 @@ go_library(
|
||||
"deployment.go",
|
||||
"doc.go",
|
||||
"env_file.go",
|
||||
"explain.go",
|
||||
"generate.go",
|
||||
"history.go",
|
||||
"interfaces.go",
|
||||
@ -118,7 +117,6 @@ go_library(
|
||||
deps = [
|
||||
"//federation/apis/federation/v1beta1:go_default_library",
|
||||
"//pkg/api:go_default_library",
|
||||
"//pkg/api/util:go_default_library",
|
||||
"//pkg/api/v1:go_default_library",
|
||||
"//pkg/api/v1/pod:go_default_library",
|
||||
"//pkg/apis/apps:go_default_library",
|
||||
@ -142,7 +140,6 @@ go_library(
|
||||
"//pkg/kubectl/util/slice:go_default_library",
|
||||
"//pkg/printers:go_default_library",
|
||||
"//pkg/printers/internalversion:go_default_library",
|
||||
"//vendor/github.com/emicklei/go-restful-swagger12:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/github.com/spf13/cobra:go_default_library",
|
||||
"//vendor/github.com/spf13/pflag:go_default_library",
|
||||
@ -197,6 +194,7 @@ filegroup(
|
||||
"//pkg/kubectl/apply:all-srcs",
|
||||
"//pkg/kubectl/apps:all-srcs",
|
||||
"//pkg/kubectl/cmd:all-srcs",
|
||||
"//pkg/kubectl/explain:all-srcs",
|
||||
"//pkg/kubectl/metricsutil:all-srcs",
|
||||
"//pkg/kubectl/plugins:all-srcs",
|
||||
"//pkg/kubectl/proxy:all-srcs",
|
||||
|
@ -84,6 +84,7 @@ go_library(
|
||||
"//pkg/kubectl/cmd/util:go_default_library",
|
||||
"//pkg/kubectl/cmd/util/editor:go_default_library",
|
||||
"//pkg/kubectl/cmd/util/openapi:go_default_library",
|
||||
"//pkg/kubectl/explain:go_default_library",
|
||||
"//pkg/kubectl/metricsutil:go_default_library",
|
||||
"//pkg/kubectl/plugins:go_default_library",
|
||||
"//pkg/kubectl/proxy:go_default_library",
|
||||
|
@ -24,9 +24,9 @@ import (
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/kubectl"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
|
||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||
"k8s.io/kubernetes/pkg/kubectl/explain"
|
||||
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
|
||||
)
|
||||
|
||||
@ -80,7 +80,7 @@ func RunExplain(f cmdutil.Factory, out, cmdErr io.Writer, cmd *cobra.Command, ar
|
||||
// TODO: After we figured out the new syntax to separate group and resource, allow
|
||||
// the users to use it in explain (kubectl explain <group><syntax><resource>).
|
||||
// Refer to issue #16039 for why we do this. Refer to PR #15808 that used "/" syntax.
|
||||
inModel, fieldsPath, err := kubectl.SplitAndParseResourceRequest(args[0], mapper)
|
||||
inModel, fieldsPath, err := explain.SplitAndParseResourceRequest(args[0], mapper)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -108,14 +108,20 @@ func RunExplain(f cmdutil.Factory, out, cmdErr io.Writer, cmd *cobra.Command, ar
|
||||
} else {
|
||||
apiVersion, err = schema.ParseGroupVersion(apiVersionString)
|
||||
if err != nil {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
}
|
||||
gvk = apiVersion.WithKind(gvk.Kind)
|
||||
|
||||
schema, err := f.SwaggerSchema(apiVersion.WithKind(gvk.Kind))
|
||||
resources, err := f.OpenAPISchema()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return kubectl.PrintModelDescription(inModel, fieldsPath, out, schema, recursive)
|
||||
schema := resources.LookupResource(gvk)
|
||||
if schema == nil {
|
||||
return fmt.Errorf("Couldn't find resource for %q", gvk)
|
||||
}
|
||||
|
||||
return explain.PrintModelDescription(fieldsPath, out, schema, recursive)
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ package openapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@ -181,6 +182,26 @@ func (k *Kind) GetName() string {
|
||||
return fmt.Sprintf("Kind(%v)", properties)
|
||||
}
|
||||
|
||||
// IsRequired returns true if `field` is a required field for this type.
|
||||
func (k *Kind) IsRequired(field string) bool {
|
||||
for _, f := range k.RequiredFields {
|
||||
if f == field {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Keys returns a alphabetically sorted list of keys.
|
||||
func (k *Kind) Keys() []string {
|
||||
keys := make([]string, 0)
|
||||
for key := range k.Fields {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// Map is an object who values must all be of the same `SubType`.
|
||||
// The key of the object is always of type `string`.
|
||||
type Map struct {
|
||||
|
@ -9,9 +9,11 @@ go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["openapi.go"],
|
||||
deps = [
|
||||
"//pkg/kubectl/cmd/util/openapi:go_default_library",
|
||||
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
|
||||
"//vendor/github.com/googleapis/gnostic/compiler:go_default_library",
|
||||
"//vendor/gopkg.in/yaml.v2:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -21,6 +21,9 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/googleapis/gnostic/OpenAPIv2"
|
||||
@ -87,3 +90,33 @@ func (f *FakeClient) OpenAPISchema() (*openapi_v2.Document, error) {
|
||||
|
||||
return f.fake.OpenAPISchema()
|
||||
}
|
||||
|
||||
// FakeResources is a wrapper to directly load the openapi schema from a
|
||||
// file, and get the schema for given GVK. This is only for test since
|
||||
// it's assuming that the file is there and everything will go fine.
|
||||
type FakeResources struct {
|
||||
fake Fake
|
||||
}
|
||||
|
||||
var _ openapi.Resources = &FakeResources{}
|
||||
|
||||
// NewFakeResources creates a new FakeResources.
|
||||
func NewFakeResources(path string) *FakeResources {
|
||||
return &FakeResources{
|
||||
fake: Fake{Path: path},
|
||||
}
|
||||
}
|
||||
|
||||
// LookupResource will read the schema, parse it and return the
|
||||
// resources. It doesn't return errors and will panic instead.
|
||||
func (f *FakeResources) LookupResource(gvk schema.GroupVersionKind) openapi.Schema {
|
||||
s, err := f.fake.OpenAPISchema()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
resources, err := openapi.NewOpenAPIData(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return resources.LookupResource(gvk)
|
||||
}
|
||||
|
@ -1,251 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 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 kubectl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/emicklei/go-restful-swagger12"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
apiutil "k8s.io/kubernetes/pkg/api/util"
|
||||
)
|
||||
|
||||
var allModels = make(map[string]*swagger.NamedModel)
|
||||
|
||||
// SplitAndParseResourceRequest separates the users input into a model and fields
|
||||
func SplitAndParseResourceRequest(inResource string, mapper meta.RESTMapper) (string, []string, error) {
|
||||
inResource, fieldsPath := splitDotNotation(inResource)
|
||||
inResource, _ = mapper.ResourceSingularizer(inResource)
|
||||
return inResource, fieldsPath, nil
|
||||
}
|
||||
|
||||
// PrintModelDescription prints the description of a specific model or dot path.
|
||||
// If recursive, all components nested within the fields of the schema will be
|
||||
// printed.
|
||||
func PrintModelDescription(inModel string, fieldsPath []string, w io.Writer, swaggerSchema *swagger.ApiDeclaration, recursive bool) error {
|
||||
apiVer := apiutil.GetVersion(swaggerSchema.ApiVersion) + "."
|
||||
|
||||
var pointedModel *swagger.NamedModel
|
||||
for i := range swaggerSchema.Models.List {
|
||||
name := swaggerSchema.Models.List[i].Name
|
||||
|
||||
allModels[name] = &swaggerSchema.Models.List[i]
|
||||
if strings.ToLower(name) == strings.ToLower(apiVer+inModel) {
|
||||
pointedModel = &swaggerSchema.Models.List[i]
|
||||
}
|
||||
}
|
||||
if pointedModel == nil {
|
||||
return fmt.Errorf("requested resource %q is not defined", inModel)
|
||||
}
|
||||
|
||||
if len(fieldsPath) == 0 {
|
||||
return printTopLevelResourceInfo(w, pointedModel, recursive)
|
||||
}
|
||||
|
||||
var pointedModelAsProp *swagger.NamedModelProperty
|
||||
for _, field := range fieldsPath {
|
||||
if prop, nextModel, isModel := getField(pointedModel, field); prop != nil {
|
||||
if isModel {
|
||||
pointedModelAsProp = prop
|
||||
pointedModel = allModels[nextModel]
|
||||
} else {
|
||||
return printPrimitive(w, prop)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("field %q does not exist", field)
|
||||
}
|
||||
}
|
||||
return printModelInfo(w, pointedModel, pointedModelAsProp, recursive)
|
||||
}
|
||||
|
||||
func splitDotNotation(model string) (string, []string) {
|
||||
var fieldsPath []string
|
||||
dotModel := strings.Split(model, ".")
|
||||
if len(dotModel) >= 1 {
|
||||
fieldsPath = dotModel[1:]
|
||||
}
|
||||
return dotModel[0], fieldsPath
|
||||
}
|
||||
|
||||
func getPointedModel(prop *swagger.ModelProperty) (string, bool) {
|
||||
if prop.Ref != nil {
|
||||
return *prop.Ref, true
|
||||
} else if *prop.Type == "array" && prop.Items.Ref != nil {
|
||||
return *prop.Items.Ref, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func getField(model *swagger.NamedModel, sField string) (*swagger.NamedModelProperty, string, bool) {
|
||||
for _, prop := range model.Model.Properties.List {
|
||||
if prop.Name == sField {
|
||||
pointedModel, isModel := getPointedModel(&prop.Property)
|
||||
return &prop, pointedModel, isModel
|
||||
}
|
||||
}
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
func printModelInfo(w io.Writer, model *swagger.NamedModel, modelProp *swagger.NamedModelProperty, recursive bool) error {
|
||||
t, _ := getFieldType(&modelProp.Property)
|
||||
fmt.Fprintf(w, "RESOURCE: %s <%s>\n\n", modelProp.Name, t)
|
||||
fieldDesc, _ := wrapAndIndentText(modelProp.Property.Description, " ", 80)
|
||||
fmt.Fprintf(w, "DESCRIPTION:\n%s\n\n%s\n", fieldDesc, indentText(model.Model.Description, " "))
|
||||
return printFields(w, model, recursive)
|
||||
}
|
||||
|
||||
func printPrimitive(w io.Writer, field *swagger.NamedModelProperty) error {
|
||||
t, _ := getFieldType(&field.Property)
|
||||
fmt.Fprintf(w, "FIELD: %s <%s>\n\n", field.Name, t)
|
||||
d, _ := wrapAndIndentText(field.Property.Description, " ", 80)
|
||||
fmt.Fprintf(w, "DESCRIPTION:\n%s\n", d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func printTopLevelResourceInfo(w io.Writer, model *swagger.NamedModel, recursive bool) error {
|
||||
fmt.Fprintf(w, "DESCRIPTION:\n%s\n", model.Model.Description)
|
||||
return printFields(w, model, recursive)
|
||||
}
|
||||
|
||||
func printFields(w io.Writer, model *swagger.NamedModel, recursive bool) error {
|
||||
fmt.Fprint(w, "\nFIELDS:\n")
|
||||
for _, field := range model.Model.Properties.List {
|
||||
fieldType, err := getFieldType(&field.Property)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if arrayContains(model.Model.Required, field.Name) {
|
||||
fmt.Fprintf(w, " %s\t<%s> -required-\n", field.Name, fieldType)
|
||||
} else {
|
||||
fmt.Fprintf(w, " %s\t<%s>\n", field.Name, fieldType)
|
||||
}
|
||||
|
||||
if recursive {
|
||||
pointedModel, isModel := getPointedModel(&field.Property)
|
||||
if isModel {
|
||||
for _, nestedField := range allModels[pointedModel].Model.Properties.List {
|
||||
t, _ := getFieldType(&nestedField.Property)
|
||||
fmt.Fprintf(w, " %s\t<%s>\n", nestedField.Name, t)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fieldDesc, _ := wrapAndIndentText(field.Property.Description, " ", 80)
|
||||
fmt.Fprintf(w, "%s\n\n", fieldDesc)
|
||||
}
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFieldType(prop *swagger.ModelProperty) (string, error) {
|
||||
if prop.Type == nil {
|
||||
return "Object", nil
|
||||
} else if *prop.Type == "any" {
|
||||
// Swagger Spec doesn't return information for maps.
|
||||
return "map[string]string", nil
|
||||
} else if *prop.Type == "array" {
|
||||
if prop.Items == nil {
|
||||
return "", fmt.Errorf("error in swagger spec. Property: %v contains an array without type", prop)
|
||||
}
|
||||
if prop.Items.Ref != nil {
|
||||
fieldType := "[]Object"
|
||||
return fieldType, nil
|
||||
}
|
||||
fieldType := "[]" + *prop.Items.Type
|
||||
return fieldType, nil
|
||||
}
|
||||
return *prop.Type, nil
|
||||
}
|
||||
|
||||
func wrapAndIndentText(desc, indent string, lim int) (string, error) {
|
||||
words := strings.Split(strings.Replace(strings.TrimSpace(desc), "\n", " ", -1), " ")
|
||||
n := len(words)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if len(words[i]) > lim {
|
||||
if strings.Contains(words[i], "/") {
|
||||
s := breakURL(words[i])
|
||||
words = append(words[:i], append(s, words[i+1:]...)...)
|
||||
i = i + len(s) - 1
|
||||
} else {
|
||||
fmt.Println(len(words[i]))
|
||||
return "", fmt.Errorf("there are words longer that the break limit is")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lines []string
|
||||
line := []string{indent}
|
||||
lineL := len(indent)
|
||||
for i := 0; i < len(words); i++ {
|
||||
w := words[i]
|
||||
|
||||
if strings.HasSuffix(w, "/") && lineL+len(w)-1 < lim {
|
||||
prev := line[len(line)-1]
|
||||
if strings.HasSuffix(prev, "/") {
|
||||
if i+1 < len(words)-1 && !strings.HasSuffix(words[i+1], "/") {
|
||||
w = strings.TrimSuffix(w, "/")
|
||||
}
|
||||
|
||||
line[len(line)-1] = prev + w
|
||||
lineL += len(w)
|
||||
} else {
|
||||
line = append(line, w)
|
||||
lineL += len(w) + 1
|
||||
}
|
||||
} else if lineL+len(w) < lim {
|
||||
line = append(line, w)
|
||||
lineL += len(w) + 1
|
||||
} else {
|
||||
lines = append(lines, strings.Join(line, " "))
|
||||
line = []string{indent, w}
|
||||
lineL = len(indent) + len(w)
|
||||
}
|
||||
}
|
||||
lines = append(lines, strings.Join(line, " "))
|
||||
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
func breakURL(url string) []string {
|
||||
var buf []string
|
||||
for _, part := range strings.Split(url, "/") {
|
||||
buf = append(buf, part+"/")
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func indentText(text, indent string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
for i := range lines {
|
||||
lines[i] = indent + lines[i]
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func arrayContains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
52
pkg/kubectl/explain/BUILD
Normal file
52
pkg/kubectl/explain/BUILD
Normal file
@ -0,0 +1,52 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"explain.go",
|
||||
"field_lookup.go",
|
||||
"fields_printer.go",
|
||||
"fields_printer_builder.go",
|
||||
"formatter.go",
|
||||
"model_printer.go",
|
||||
"recursive_fields_printer.go",
|
||||
"typename.go",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/kubectl/cmd/util/openapi:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"field_lookup_test.go",
|
||||
"fields_printer_test.go",
|
||||
"formatter_test.go",
|
||||
"model_printer_test.go",
|
||||
"recursive_fields_printer_test.go",
|
||||
"typename_test.go",
|
||||
],
|
||||
data = ["test-swagger.json"],
|
||||
library = ":go_default_library",
|
||||
deps = [
|
||||
"//pkg/kubectl/cmd/util/openapi/testing:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
],
|
||||
)
|
64
pkg/kubectl/explain/explain.go
Normal file
64
pkg/kubectl/explain/explain.go
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2017 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 explain
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
)
|
||||
|
||||
type fieldsPrinter interface {
|
||||
PrintFields(openapi.Schema) error
|
||||
}
|
||||
|
||||
func splitDotNotation(model string) (string, []string) {
|
||||
var fieldsPath []string
|
||||
dotModel := strings.Split(model, ".")
|
||||
if len(dotModel) >= 1 {
|
||||
fieldsPath = dotModel[1:]
|
||||
}
|
||||
return dotModel[0], fieldsPath
|
||||
}
|
||||
|
||||
// SplitAndParseResourceRequest separates the users input into a model and fields
|
||||
func SplitAndParseResourceRequest(inResource string, mapper meta.RESTMapper) (string, []string, error) {
|
||||
inResource, fieldsPath := splitDotNotation(inResource)
|
||||
inResource, _ = mapper.ResourceSingularizer(inResource)
|
||||
return inResource, fieldsPath, nil
|
||||
}
|
||||
|
||||
// PrintModelDescription prints the description of a specific model or dot path.
|
||||
// If recursive, all components nested within the fields of the schema will be
|
||||
// printed.
|
||||
func PrintModelDescription(fieldsPath []string, w io.Writer, schema openapi.Schema, recursive bool) error {
|
||||
fieldName := ""
|
||||
if len(fieldsPath) != 0 {
|
||||
fieldName = fieldsPath[len(fieldsPath)-1]
|
||||
}
|
||||
|
||||
// Go down the fieldsPath to find what we're trying to explain
|
||||
schema, err := LookupSchemaForField(schema, fieldsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := fieldsPrinterBuilder{Recursive: recursive}
|
||||
f := &Formatter{Writer: w, Wrap: 80}
|
||||
return PrintModel(fieldName, f, b, schema)
|
||||
}
|
107
pkg/kubectl/explain/field_lookup.go
Normal file
107
pkg/kubectl/explain/field_lookup.go
Normal file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
Copyright 2017 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 explain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
)
|
||||
|
||||
// fieldLookup walks through a schema by following a path, and returns
|
||||
// the final schema.
|
||||
type fieldLookup struct {
|
||||
// Path to walk
|
||||
Path []string
|
||||
|
||||
// Return information: Schema found, or error.
|
||||
Schema openapi.Schema
|
||||
Error error
|
||||
}
|
||||
|
||||
// SaveLeafSchema is used to detect if we are done walking the path, and
|
||||
// saves the schema as a match.
|
||||
func (f *fieldLookup) SaveLeafSchema(schema openapi.Schema) bool {
|
||||
if len(f.Path) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
f.Schema = schema
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// VisitArray is mostly a passthrough.
|
||||
func (f *fieldLookup) VisitArray(a *openapi.Array) {
|
||||
if f.SaveLeafSchema(a) {
|
||||
return
|
||||
}
|
||||
|
||||
// Passthrough arrays.
|
||||
a.SubType.Accept(f)
|
||||
}
|
||||
|
||||
// VisitMap is mostly a passthrough.
|
||||
func (f *fieldLookup) VisitMap(m *openapi.Map) {
|
||||
if f.SaveLeafSchema(m) {
|
||||
return
|
||||
}
|
||||
|
||||
// Passthrough maps.
|
||||
m.SubType.Accept(f)
|
||||
}
|
||||
|
||||
// VisitPrimitive stops the operation and returns itself as the found
|
||||
// schema, even if it had more path to walk.
|
||||
func (f *fieldLookup) VisitPrimitive(p *openapi.Primitive) {
|
||||
// Even if Path is not empty (we're not expecting a leaf),
|
||||
// return that primitive.
|
||||
f.Schema = p
|
||||
}
|
||||
|
||||
// VisitKind unstacks fields as it finds them.
|
||||
func (f *fieldLookup) VisitKind(k *openapi.Kind) {
|
||||
if f.SaveLeafSchema(k) {
|
||||
return
|
||||
}
|
||||
|
||||
subSchema, ok := k.Fields[f.Path[0]]
|
||||
if !ok {
|
||||
f.Error = fmt.Errorf("field %q does not exist", f.Path[0])
|
||||
return
|
||||
}
|
||||
|
||||
f.Path = f.Path[1:]
|
||||
subSchema.Accept(f)
|
||||
}
|
||||
|
||||
// VisitReference is mostly a passthrough.
|
||||
func (f *fieldLookup) VisitReference(r openapi.Reference) {
|
||||
if f.SaveLeafSchema(r) {
|
||||
return
|
||||
}
|
||||
|
||||
// Passthrough references.
|
||||
r.SubSchema().Accept(f)
|
||||
}
|
||||
|
||||
// LookupSchemaForField looks for the schema of a given path in a base schema.
|
||||
func LookupSchemaForField(schema openapi.Schema, path []string) (openapi.Schema, error) {
|
||||
f := &fieldLookup{Path: path}
|
||||
schema.Accept(f)
|
||||
return f.Schema, f.Error
|
||||
}
|
77
pkg/kubectl/explain/field_lookup_test.go
Normal file
77
pkg/kubectl/explain/field_lookup_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
/*
|
||||
Copyright 2017 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 explain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func TestFindField(t *testing.T) {
|
||||
schema := resources.LookupResource(schema.GroupVersionKind{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Kind: "OneKind",
|
||||
})
|
||||
if schema == nil {
|
||||
t.Fatal("Counldn't find schema v1.OneKind")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
path []string
|
||||
|
||||
err string
|
||||
expectedPath string
|
||||
}{
|
||||
{
|
||||
path: []string{},
|
||||
expectedPath: "OneKind",
|
||||
},
|
||||
{
|
||||
path: []string{"field1"},
|
||||
expectedPath: "OneKind.field1",
|
||||
},
|
||||
{
|
||||
path: []string{"field1", "array"},
|
||||
expectedPath: "OtherKind.array",
|
||||
},
|
||||
{
|
||||
path: []string{"field1", "what?"},
|
||||
err: `field "what?" does not exist`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
path, err := LookupSchemaForField(schema, test.path)
|
||||
|
||||
gotErr := ""
|
||||
if err != nil {
|
||||
gotErr = err.Error()
|
||||
}
|
||||
|
||||
gotPath := ""
|
||||
if path != nil {
|
||||
gotPath = path.GetPath().String()
|
||||
}
|
||||
|
||||
if gotErr != test.err && gotPath != test.expectedPath {
|
||||
t.Errorf("LookupSchemaForField(schema, %v) = (%v, %v), expected (%s, %v)",
|
||||
test.path, gotErr, gotPath, test.expectedPath, test.err)
|
||||
}
|
||||
}
|
||||
}
|
84
pkg/kubectl/explain/fields_printer.go
Normal file
84
pkg/kubectl/explain/fields_printer.go
Normal file
@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright 2017 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 explain
|
||||
|
||||
import (
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
)
|
||||
|
||||
// indentDesc is the level of indentation for descriptions.
|
||||
const indentDesc = 2
|
||||
|
||||
// regularFieldsPrinter prints fields with their type and description.
|
||||
type regularFieldsPrinter struct {
|
||||
Writer *Formatter
|
||||
Error error
|
||||
}
|
||||
|
||||
var _ openapi.SchemaVisitor = ®ularFieldsPrinter{}
|
||||
var _ fieldsPrinter = ®ularFieldsPrinter{}
|
||||
|
||||
// VisitArray prints a Array type. It is just a passthrough.
|
||||
func (f *regularFieldsPrinter) VisitArray(a *openapi.Array) {
|
||||
a.SubType.Accept(f)
|
||||
}
|
||||
|
||||
// VisitKind prints a Kind type. It prints each key in the kind, with
|
||||
// the type, the required flag, and the description.
|
||||
func (f *regularFieldsPrinter) VisitKind(k *openapi.Kind) {
|
||||
for _, key := range k.Keys() {
|
||||
v := k.Fields[key]
|
||||
required := ""
|
||||
if k.IsRequired(key) {
|
||||
required = " -required-"
|
||||
}
|
||||
|
||||
if err := f.Writer.Write("%s\t<%s>%s", key, GetTypeName(v), required); err != nil {
|
||||
f.Error = err
|
||||
return
|
||||
}
|
||||
if err := f.Writer.Indent(indentDesc).WriteWrapped("%s", v.GetDescription()); err != nil {
|
||||
f.Error = err
|
||||
return
|
||||
}
|
||||
if err := f.Writer.Write(""); err != nil {
|
||||
f.Error = err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VisitMap prints a Map type. It is just a passthrough.
|
||||
func (f *regularFieldsPrinter) VisitMap(m *openapi.Map) {
|
||||
m.SubType.Accept(f)
|
||||
}
|
||||
|
||||
// VisitPrimitive prints a Primitive type. It stops the recursion.
|
||||
func (f *regularFieldsPrinter) VisitPrimitive(p *openapi.Primitive) {
|
||||
// Nothing to do. Shouldn't really happen.
|
||||
}
|
||||
|
||||
// VisitReference prints a Reference type. It is just a passthrough.
|
||||
func (f *regularFieldsPrinter) VisitReference(r openapi.Reference) {
|
||||
r.SubSchema().Accept(f)
|
||||
}
|
||||
|
||||
// PrintFields will write the types from schema.
|
||||
func (f *regularFieldsPrinter) PrintFields(schema openapi.Schema) error {
|
||||
schema.Accept(f)
|
||||
return f.Error
|
||||
}
|
36
pkg/kubectl/explain/fields_printer_builder.go
Normal file
36
pkg/kubectl/explain/fields_printer_builder.go
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2017 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 explain
|
||||
|
||||
// fieldsPrinterBuilder builds either a regularFieldsPrinter or a
|
||||
// recursiveFieldsPrinter based on the argument.
|
||||
type fieldsPrinterBuilder struct {
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
// BuildFieldsPrinter builds the appropriate fieldsPrinter.
|
||||
func (f fieldsPrinterBuilder) BuildFieldsPrinter(writer *Formatter) fieldsPrinter {
|
||||
if f.Recursive {
|
||||
return &recursiveFieldsPrinter{
|
||||
Writer: writer,
|
||||
}
|
||||
}
|
||||
|
||||
return ®ularFieldsPrinter{
|
||||
Writer: writer,
|
||||
}
|
||||
}
|
66
pkg/kubectl/explain/fields_printer_test.go
Normal file
66
pkg/kubectl/explain/fields_printer_test.go
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright 2017 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 explain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func TestFields(t *testing.T) {
|
||||
schema := resources.LookupResource(schema.GroupVersionKind{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Kind: "OneKind",
|
||||
})
|
||||
if schema == nil {
|
||||
t.Fatal("Couldn't find schema v1.OneKind")
|
||||
}
|
||||
|
||||
want := `field1 <Object> -required-
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut lacus ac
|
||||
enim vulputate imperdiet ac accumsan risus. Integer vel accumsan lectus.
|
||||
Praesent tempus nulla id tortor luctus, quis varius nulla laoreet. Ut orci
|
||||
nisi, suscipit id velit sed, blandit eleifend turpis. Curabitur tempus ante at
|
||||
lectus viverra, a mattis augue euismod. Morbi quam ligula, porttitor sit amet
|
||||
lacus non, interdum pulvinar tortor. Praesent accumsan risus et ipsum dictum,
|
||||
vel ullamcorper lorem egestas.
|
||||
|
||||
field2 <[]map[string]string>
|
||||
This is an array of object of PrimitiveDef
|
||||
|
||||
`
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
f := Formatter{
|
||||
Writer: &buf,
|
||||
Wrap: 80,
|
||||
}
|
||||
s, err := LookupSchemaForField(schema, []string{})
|
||||
if err != nil {
|
||||
t.Fatalf("Invalid path %v: %v", []string{}, err)
|
||||
}
|
||||
if err := (fieldsPrinterBuilder{Recursive: false}).BuildFieldsPrinter(&f).PrintFields(s); err != nil {
|
||||
t.Fatalf("Failed to print fields: %v", err)
|
||||
}
|
||||
got := buf.String()
|
||||
if got != want {
|
||||
t.Errorf("Got:\n%v\nWant:\n%v\n", buf.String(), want)
|
||||
}
|
||||
}
|
120
pkg/kubectl/explain/formatter.go
Normal file
120
pkg/kubectl/explain/formatter.go
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
Copyright 2017 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 explain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Formatter helps you write with indentation, and can wrap text as needed.
|
||||
type Formatter struct {
|
||||
IndentLevel int
|
||||
Wrap int
|
||||
Writer io.Writer
|
||||
}
|
||||
|
||||
// Indent creates a new Formatter that will indent the code by that much more.
|
||||
func (f Formatter) Indent(indent int) *Formatter {
|
||||
f.IndentLevel = f.IndentLevel + indent
|
||||
return &f
|
||||
}
|
||||
|
||||
// Write writes a string with the indentation set for the
|
||||
// Formatter. This is not wrapping text.
|
||||
func (f *Formatter) Write(str string, a ...interface{}) error {
|
||||
// Don't indent empty lines
|
||||
if str == "" {
|
||||
_, err := io.WriteString(f.Writer, "\n")
|
||||
return err
|
||||
}
|
||||
|
||||
indent := ""
|
||||
for i := 0; i < f.IndentLevel; i++ {
|
||||
indent = indent + " "
|
||||
}
|
||||
_, err := io.WriteString(f.Writer, indent+fmt.Sprintf(str, a...)+"\n")
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteWrapped writes a string with the indentation set for the
|
||||
// Formatter, and wraps as needed.
|
||||
func (f *Formatter) WriteWrapped(str string, a ...interface{}) error {
|
||||
if f.Wrap == 0 {
|
||||
return f.Write(str, a...)
|
||||
}
|
||||
text := fmt.Sprintf(str, a...)
|
||||
strs := wrapString(text, f.Wrap-f.IndentLevel)
|
||||
for _, substr := range strs {
|
||||
if err := f.Write(substr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type line struct {
|
||||
wrap int
|
||||
words []string
|
||||
}
|
||||
|
||||
func (l *line) String() string {
|
||||
return strings.Join(l.words, " ")
|
||||
}
|
||||
|
||||
func (l *line) Empty() bool {
|
||||
return len(l.words) == 0
|
||||
}
|
||||
|
||||
func (l *line) Len() int {
|
||||
return len(l.String())
|
||||
}
|
||||
|
||||
// Add adds the word to the line, returns true if we could, false if we
|
||||
// didn't have enough room. It's always possible to add to an empty line.
|
||||
func (l *line) Add(word string) bool {
|
||||
newLine := line{
|
||||
wrap: l.wrap,
|
||||
words: append(l.words, word),
|
||||
}
|
||||
if newLine.Len() <= l.wrap || len(l.words) == 0 {
|
||||
l.words = newLine.words
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func wrapString(str string, wrap int) []string {
|
||||
words := strings.Fields(str)
|
||||
wrapped := []string{}
|
||||
l := line{wrap: wrap}
|
||||
|
||||
for _, word := range words {
|
||||
if l.Add(word) == false {
|
||||
wrapped = append(wrapped, l.String())
|
||||
l = line{wrap: wrap}
|
||||
if l.Add(word) == false {
|
||||
panic("Couldn't add to empty line.")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !l.Empty() {
|
||||
wrapped = append(wrapped, l.String())
|
||||
}
|
||||
return wrapped
|
||||
}
|
91
pkg/kubectl/explain/formatter_test.go
Normal file
91
pkg/kubectl/explain/formatter_test.go
Normal file
@ -0,0 +1,91 @@
|
||||
/*
|
||||
Copyright 2017 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 explain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatterWrite(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
f := Formatter{
|
||||
Writer: &buf,
|
||||
}
|
||||
|
||||
f.Write("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
|
||||
// Indent creates a new Formatter
|
||||
f.Indent(5).Write("Morbi at turpis faucibus, gravida dolor ut, fringilla velit.")
|
||||
// So Indent(2) doesn't indent to 7 here.
|
||||
f.Indent(2).Write("Etiam maximus urna at tellus faucibus mattis.")
|
||||
|
||||
want := `Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
Morbi at turpis faucibus, gravida dolor ut, fringilla velit.
|
||||
Etiam maximus urna at tellus faucibus mattis.
|
||||
`
|
||||
|
||||
if buf.String() != want {
|
||||
t.Errorf("Got:\n%v\nWant:\n%v\n", buf.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatterWrappedWrite(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
f := Formatter{
|
||||
Writer: &buf,
|
||||
Wrap: 50,
|
||||
}
|
||||
|
||||
f.WriteWrapped("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi at turpis faucibus, gravida dolor ut, fringilla velit.")
|
||||
f.Indent(10).WriteWrapped("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi at turpis faucibus, gravida dolor ut, fringilla velit.")
|
||||
// Test long words (especially urls) on their own line.
|
||||
f.Indent(20).WriteWrapped("Lorem ipsum dolor sit amet, consectetur adipiscing elit. ThisIsAVeryLongWordThatDoesn'tFitOnALineOnItsOwn. Morbi at turpis faucibus, gravida dolor ut, fringilla velit.")
|
||||
|
||||
want := `Lorem ipsum dolor sit amet, consectetur adipiscing
|
||||
elit. Morbi at turpis faucibus, gravida dolor ut,
|
||||
fringilla velit.
|
||||
Lorem ipsum dolor sit amet, consectetur
|
||||
adipiscing elit. Morbi at turpis
|
||||
faucibus, gravida dolor ut, fringilla
|
||||
velit.
|
||||
Lorem ipsum dolor sit amet,
|
||||
consectetur adipiscing elit.
|
||||
ThisIsAVeryLongWordThatDoesn'tFitOnALineOnItsOwn.
|
||||
Morbi at turpis faucibus,
|
||||
gravida dolor ut, fringilla
|
||||
velit.
|
||||
`
|
||||
|
||||
if buf.String() != want {
|
||||
t.Errorf("Got:\n%v\nWant:\n%v\n", buf.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultWrap(t *testing.T) {
|
||||
buf := bytes.Buffer{}
|
||||
f := Formatter{
|
||||
Writer: &buf,
|
||||
// Wrap is not set
|
||||
}
|
||||
|
||||
f.WriteWrapped("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi at turpis faucibus, gravida dolor ut, fringilla velit. Etiam maximus urna at tellus faucibus mattis.")
|
||||
want := `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi at turpis faucibus, gravida dolor ut, fringilla velit. Etiam maximus urna at tellus faucibus mattis.
|
||||
`
|
||||
if buf.String() != want {
|
||||
t.Errorf("Got:\n%v\nWant:\n%v\n", buf.String(), want)
|
||||
}
|
||||
}
|
129
pkg/kubectl/explain/model_printer.go
Normal file
129
pkg/kubectl/explain/model_printer.go
Normal file
@ -0,0 +1,129 @@
|
||||
/*
|
||||
Copyright 2017 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 explain
|
||||
|
||||
import (
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
)
|
||||
|
||||
// fieldIndentLevel is the level of indentation for fields.
|
||||
const fieldIndentLevel = 3
|
||||
|
||||
// descriptionIndentLevel is the level of indentation for the
|
||||
// description.
|
||||
const descriptionIndentLevel = 5
|
||||
|
||||
// modelPrinter prints a schema in Writer. Its "Builder" will decide if
|
||||
// it's recursive or not.
|
||||
type modelPrinter struct {
|
||||
Name string
|
||||
Type string
|
||||
Descriptions []string
|
||||
Writer *Formatter
|
||||
Builder fieldsPrinterBuilder
|
||||
Error error
|
||||
}
|
||||
|
||||
var _ openapi.SchemaVisitor = &modelPrinter{}
|
||||
|
||||
// PrintDescription prints the description for a given schema. There
|
||||
// might be multiple description, since we collect descriptions when we
|
||||
// go through references, arrays and maps.
|
||||
func (m *modelPrinter) PrintDescription(schema openapi.Schema) error {
|
||||
if err := m.Writer.Write("DESCRIPTION:"); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, desc := range append(m.Descriptions, schema.GetDescription()) {
|
||||
if desc == "" {
|
||||
continue
|
||||
}
|
||||
if i != 0 {
|
||||
if err := m.Writer.Write(""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := m.Writer.Indent(descriptionIndentLevel).WriteWrapped(desc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitArray recurses inside the subtype, while collecting the type if
|
||||
// not done yet, and the description.
|
||||
func (m *modelPrinter) VisitArray(a *openapi.Array) {
|
||||
m.Descriptions = append(m.Descriptions, a.GetDescription())
|
||||
if m.Type == "" {
|
||||
m.Type = GetTypeName(a)
|
||||
}
|
||||
a.SubType.Accept(m)
|
||||
}
|
||||
|
||||
// VisitKind prints a full resource with its fields.
|
||||
func (m *modelPrinter) VisitKind(k *openapi.Kind) {
|
||||
if m.Type == "" {
|
||||
m.Type = GetTypeName(k)
|
||||
}
|
||||
if m.Name != "" {
|
||||
m.Writer.Write("RESOURCE: %s <%s>\n", m.Name, m.Type)
|
||||
}
|
||||
|
||||
if err := m.PrintDescription(k); err != nil {
|
||||
m.Error = err
|
||||
return
|
||||
}
|
||||
if err := m.Writer.Write("\nFIELDS:"); err != nil {
|
||||
m.Error = err
|
||||
return
|
||||
}
|
||||
m.Error = m.Builder.BuildFieldsPrinter(m.Writer.Indent(fieldIndentLevel)).PrintFields(k)
|
||||
}
|
||||
|
||||
// VisitMap recurses inside the subtype, while collecting the type if
|
||||
// not done yet, and the description.
|
||||
func (m *modelPrinter) VisitMap(om *openapi.Map) {
|
||||
m.Descriptions = append(m.Descriptions, om.GetDescription())
|
||||
if m.Type == "" {
|
||||
m.Type = GetTypeName(om)
|
||||
}
|
||||
om.SubType.Accept(m)
|
||||
}
|
||||
|
||||
// VisitPrimitive prints a field type and its description.
|
||||
func (m *modelPrinter) VisitPrimitive(p *openapi.Primitive) {
|
||||
if m.Type == "" {
|
||||
m.Type = GetTypeName(p)
|
||||
}
|
||||
if err := m.Writer.Write("FIELD: %s <%s>\n", m.Name, m.Type); err != nil {
|
||||
m.Error = err
|
||||
return
|
||||
}
|
||||
m.Error = m.PrintDescription(p)
|
||||
}
|
||||
|
||||
// VisitReference recurses inside the subtype, while collecting the description.
|
||||
func (m *modelPrinter) VisitReference(r openapi.Reference) {
|
||||
m.Descriptions = append(m.Descriptions, r.GetDescription())
|
||||
r.SubSchema().Accept(m)
|
||||
}
|
||||
|
||||
// PrintModel prints the description of a schema in writer.
|
||||
func PrintModel(name string, writer *Formatter, builder fieldsPrinterBuilder, schema openapi.Schema) error {
|
||||
m := &modelPrinter{Name: name, Writer: writer, Builder: builder}
|
||||
schema.Accept(m)
|
||||
return m.Error
|
||||
}
|
122
pkg/kubectl/explain/model_printer_test.go
Normal file
122
pkg/kubectl/explain/model_printer_test.go
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
Copyright 2017 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 explain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func TestModel(t *testing.T) {
|
||||
schema := resources.LookupResource(schema.GroupVersionKind{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Kind: "OneKind",
|
||||
})
|
||||
if schema == nil {
|
||||
t.Fatal("Couldn't find schema v1.OneKind")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
path []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
want: `DESCRIPTION:
|
||||
OneKind has a short description
|
||||
|
||||
FIELDS:
|
||||
field1 <Object> -required-
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut lacus ac
|
||||
enim vulputate imperdiet ac accumsan risus. Integer vel accumsan lectus.
|
||||
Praesent tempus nulla id tortor luctus, quis varius nulla laoreet. Ut orci
|
||||
nisi, suscipit id velit sed, blandit eleifend turpis. Curabitur tempus ante
|
||||
at lectus viverra, a mattis augue euismod. Morbi quam ligula, porttitor sit
|
||||
amet lacus non, interdum pulvinar tortor. Praesent accumsan risus et ipsum
|
||||
dictum, vel ullamcorper lorem egestas.
|
||||
|
||||
field2 <[]map[string]string>
|
||||
This is an array of object of PrimitiveDef
|
||||
|
||||
`,
|
||||
path: []string{},
|
||||
},
|
||||
{
|
||||
want: `RESOURCE: field1 <Object>
|
||||
|
||||
DESCRIPTION:
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut lacus ac
|
||||
enim vulputate imperdiet ac accumsan risus. Integer vel accumsan lectus.
|
||||
Praesent tempus nulla id tortor luctus, quis varius nulla laoreet. Ut orci
|
||||
nisi, suscipit id velit sed, blandit eleifend turpis. Curabitur tempus ante
|
||||
at lectus viverra, a mattis augue euismod. Morbi quam ligula, porttitor sit
|
||||
amet lacus non, interdum pulvinar tortor. Praesent accumsan risus et ipsum
|
||||
dictum, vel ullamcorper lorem egestas.
|
||||
|
||||
This is another kind of Kind
|
||||
|
||||
FIELDS:
|
||||
array <[]integer>
|
||||
This array must be an array of int
|
||||
|
||||
int <integer>
|
||||
This int must be an int
|
||||
|
||||
object <map[string]string>
|
||||
This is an object of string
|
||||
|
||||
primitive <string>
|
||||
|
||||
string <string> -required-
|
||||
This string must be a string
|
||||
|
||||
`,
|
||||
path: []string{"field1"},
|
||||
},
|
||||
{
|
||||
want: `FIELD: string <string>
|
||||
|
||||
DESCRIPTION:
|
||||
This string must be a string
|
||||
`,
|
||||
path: []string{"field1", "string"},
|
||||
},
|
||||
{
|
||||
want: `FIELD: array <[]integer>
|
||||
|
||||
DESCRIPTION:
|
||||
This array must be an array of int
|
||||
|
||||
This is an int in an array
|
||||
`,
|
||||
path: []string{"field1", "array"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
buf := bytes.Buffer{}
|
||||
if err := PrintModelDescription(test.path, &buf, schema, false); err != nil {
|
||||
t.Fatalf("Failed to PrintModelDescription for path %v: %v", test.path, err)
|
||||
}
|
||||
got := buf.String()
|
||||
if got != test.want {
|
||||
t.Errorf("Got:\n%v\nWant:\n%v\n", buf.String(), test.want)
|
||||
}
|
||||
}
|
||||
}
|
77
pkg/kubectl/explain/recursive_fields_printer.go
Normal file
77
pkg/kubectl/explain/recursive_fields_printer.go
Normal file
@ -0,0 +1,77 @@
|
||||
/*
|
||||
Copyright 2017 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 explain
|
||||
|
||||
import (
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
)
|
||||
|
||||
// indentPerLevel is the level of indentation for each field recursion.
|
||||
const indentPerLevel = 3
|
||||
|
||||
// recursiveFieldsPrinter recursively prints all the fields for a given
|
||||
// schema.
|
||||
type recursiveFieldsPrinter struct {
|
||||
Writer *Formatter
|
||||
Error error
|
||||
}
|
||||
|
||||
var _ openapi.SchemaVisitor = &recursiveFieldsPrinter{}
|
||||
var _ fieldsPrinter = &recursiveFieldsPrinter{}
|
||||
|
||||
// VisitArray is just a passthrough.
|
||||
func (f *recursiveFieldsPrinter) VisitArray(a *openapi.Array) {
|
||||
a.SubType.Accept(f)
|
||||
}
|
||||
|
||||
// VisitKind prints all its fields with their type, and then recurses
|
||||
// inside each of these (pre-order).
|
||||
func (f *recursiveFieldsPrinter) VisitKind(k *openapi.Kind) {
|
||||
for _, key := range k.Keys() {
|
||||
v := k.Fields[key]
|
||||
f.Writer.Write("%s\t<%s>", key, GetTypeName(v))
|
||||
subFields := &recursiveFieldsPrinter{
|
||||
Writer: f.Writer.Indent(indentPerLevel),
|
||||
}
|
||||
if err := subFields.PrintFields(v); err != nil {
|
||||
f.Error = err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VisitMap is just a passthrough.
|
||||
func (f *recursiveFieldsPrinter) VisitMap(m *openapi.Map) {
|
||||
m.SubType.Accept(f)
|
||||
}
|
||||
|
||||
// VisitPrimitive does nothing, since it doesn't have sub-fields.
|
||||
func (f *recursiveFieldsPrinter) VisitPrimitive(p *openapi.Primitive) {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
// VisitReference is just a passthrough.
|
||||
func (f *recursiveFieldsPrinter) VisitReference(r openapi.Reference) {
|
||||
r.SubSchema().Accept(f)
|
||||
}
|
||||
|
||||
// PrintFields will recursively print all the fields for the given
|
||||
// schema.
|
||||
func (f *recursiveFieldsPrinter) PrintFields(schema openapi.Schema) error {
|
||||
schema.Accept(f)
|
||||
return f.Error
|
||||
}
|
61
pkg/kubectl/explain/recursive_fields_printer_test.go
Normal file
61
pkg/kubectl/explain/recursive_fields_printer_test.go
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2017 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 explain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func TestRecursiveFields(t *testing.T) {
|
||||
schema := resources.LookupResource(schema.GroupVersionKind{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Kind: "OneKind",
|
||||
})
|
||||
if schema == nil {
|
||||
t.Fatal("Couldn't find schema v1.OneKind")
|
||||
}
|
||||
|
||||
want := `field1 <Object>
|
||||
array <[]integer>
|
||||
int <integer>
|
||||
object <map[string]string>
|
||||
primitive <string>
|
||||
string <string>
|
||||
field2 <[]map[string]string>
|
||||
`
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
f := Formatter{
|
||||
Writer: &buf,
|
||||
Wrap: 80,
|
||||
}
|
||||
s, err := LookupSchemaForField(schema, []string{})
|
||||
if err != nil {
|
||||
t.Fatalf("Invalid path %v: %v", []string{}, err)
|
||||
}
|
||||
if err := (fieldsPrinterBuilder{Recursive: true}).BuildFieldsPrinter(&f).PrintFields(s); err != nil {
|
||||
t.Fatalf("Failed to print fields: %v", err)
|
||||
}
|
||||
got := buf.String()
|
||||
if got != want {
|
||||
t.Errorf("Got:\n%v\nWant:\n%v\n", buf.String(), want)
|
||||
}
|
||||
}
|
78
pkg/kubectl/explain/test-swagger.json
Normal file
78
pkg/kubectl/explain/test-swagger.json
Normal file
@ -0,0 +1,78 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "Kubernetes",
|
||||
"version": "v1.9.0"
|
||||
},
|
||||
"paths": {},
|
||||
"definitions": {
|
||||
"PrimitiveDef": {
|
||||
"type": "string"
|
||||
},
|
||||
"OneKind": {
|
||||
"description": "OneKind has a short description",
|
||||
"required": [
|
||||
"field1",
|
||||
],
|
||||
"properties": {
|
||||
"field1": {
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla ut lacus ac enim vulputate imperdiet ac accumsan risus. Integer vel accumsan lectus. Praesent tempus nulla id tortor luctus, quis varius nulla laoreet. Ut orci nisi, suscipit id velit sed, blandit eleifend turpis. Curabitur tempus ante at lectus viverra, a mattis augue euismod. Morbi quam ligula, porttitor sit amet lacus non, interdum pulvinar tortor. Praesent accumsan risus et ipsum dictum, vel ullamcorper lorem egestas.",
|
||||
"$ref": "#/definitions/OtherKind"
|
||||
},
|
||||
"field2": {
|
||||
"description": "This is an array of object of PrimitiveDef",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"description": "This is an object of PrimitiveDef",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/PrimitiveDef"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-kubernetes-group-version-kind": [
|
||||
{
|
||||
"group": "",
|
||||
"kind": "OneKind",
|
||||
"version": "v1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"OtherKind": {
|
||||
"description": "This is another kind of Kind",
|
||||
"required": [
|
||||
"string",
|
||||
],
|
||||
"properties": {
|
||||
"string": {
|
||||
"description": "This string must be a string",
|
||||
"type": "string",
|
||||
},
|
||||
"int": {
|
||||
"description": "This int must be an int",
|
||||
"type": "integer",
|
||||
},
|
||||
"array": {
|
||||
"description": "This array must be an array of int",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"description": "This is an int in an array",
|
||||
"type": "integer",
|
||||
}
|
||||
},
|
||||
"object": {
|
||||
"description": "This is an object of string",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"description": "this is a string in an object",
|
||||
"type": "string",
|
||||
}
|
||||
},
|
||||
"primitive": {
|
||||
"$ref": "#/definitions/PrimitiveDef"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
66
pkg/kubectl/explain/typename.go
Normal file
66
pkg/kubectl/explain/typename.go
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright 2017 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 explain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
)
|
||||
|
||||
// typeName finds the name of a schema
|
||||
type typeName struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
var _ openapi.SchemaVisitor = &typeName{}
|
||||
|
||||
// VisitArray adds the [] prefix and recurses.
|
||||
func (t *typeName) VisitArray(a *openapi.Array) {
|
||||
s := &typeName{}
|
||||
a.SubType.Accept(s)
|
||||
t.Name = fmt.Sprintf("[]%s", s.Name)
|
||||
}
|
||||
|
||||
// VisitKind just returns "Object".
|
||||
func (t *typeName) VisitKind(k *openapi.Kind) {
|
||||
t.Name = "Object"
|
||||
}
|
||||
|
||||
// VisitMap adds the map[string] prefix and recurses.
|
||||
func (t *typeName) VisitMap(m *openapi.Map) {
|
||||
s := &typeName{}
|
||||
m.SubType.Accept(s)
|
||||
t.Name = fmt.Sprintf("map[string]%s", s.Name)
|
||||
}
|
||||
|
||||
// VisitPrimitive returns the name of the primitive.
|
||||
func (t *typeName) VisitPrimitive(p *openapi.Primitive) {
|
||||
t.Name = p.Type
|
||||
}
|
||||
|
||||
// VisitReference is just a passthrough.
|
||||
func (t *typeName) VisitReference(r openapi.Reference) {
|
||||
r.SubSchema().Accept(t)
|
||||
}
|
||||
|
||||
// GetTypeName returns the type of a schema.
|
||||
func GetTypeName(schema openapi.Schema) string {
|
||||
t := &typeName{}
|
||||
schema.Accept(t)
|
||||
return t.Name
|
||||
}
|
80
pkg/kubectl/explain/typename_test.go
Normal file
80
pkg/kubectl/explain/typename_test.go
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
Copyright 2017 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 explain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
tst "k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi/testing"
|
||||
)
|
||||
|
||||
var resources = tst.NewFakeResources("test-swagger.json")
|
||||
|
||||
func TestReferenceTypename(t *testing.T) {
|
||||
schema := resources.LookupResource(schema.GroupVersionKind{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Kind: "OneKind",
|
||||
})
|
||||
if schema == nil {
|
||||
t.Fatal("Couldn't find schema v1.OneKind")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
path []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
// Kind is "Object"
|
||||
path: []string{},
|
||||
expected: "Object",
|
||||
},
|
||||
{
|
||||
// Reference is equal to pointed type "Object"
|
||||
path: []string{"field1"},
|
||||
expected: "Object",
|
||||
},
|
||||
{
|
||||
// Reference is equal to pointed type "string"
|
||||
path: []string{"field1", "primitive"},
|
||||
expected: "string",
|
||||
},
|
||||
{
|
||||
// Array of object of reference to string
|
||||
path: []string{"field2"},
|
||||
expected: "[]map[string]string",
|
||||
},
|
||||
{
|
||||
// Array of integer
|
||||
path: []string{"field1", "array"},
|
||||
expected: "[]integer",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
s, err := LookupSchemaForField(schema, test.path)
|
||||
if err != nil {
|
||||
t.Fatalf("Invalid test.path %v: %v", test.path, err)
|
||||
}
|
||||
got := GetTypeName(s)
|
||||
if got != test.expected {
|
||||
t.Errorf("Got %q, expected %q", got, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user