From 249caa95b500c90d1b095b46ba400d73d8dac61b Mon Sep 17 00:00:00 2001 From: Antoine Pelisse Date: Sun, 24 Sep 2017 20:40:33 -0700 Subject: [PATCH] Rewrite `kubectl explain` to use openapi This removes all dependencies on swagger 1.2 for explain. --- pkg/kubectl/BUILD | 4 +- pkg/kubectl/cmd/BUILD | 1 + pkg/kubectl/cmd/explain.go | 16 +- pkg/kubectl/cmd/util/openapi/openapi.go | 21 ++ pkg/kubectl/explain.go | 251 ------------------ pkg/kubectl/explain/BUILD | 34 +++ pkg/kubectl/explain/explain.go | 64 +++++ pkg/kubectl/explain/field_lookup.go | 107 ++++++++ pkg/kubectl/explain/fields_printer.go | 84 ++++++ pkg/kubectl/explain/fields_printer_builder.go | 36 +++ pkg/kubectl/explain/formatter.go | 120 +++++++++ pkg/kubectl/explain/model_printer.go | 129 +++++++++ .../explain/recursive_fields_printer.go | 77 ++++++ pkg/kubectl/explain/typename.go | 66 +++++ 14 files changed, 751 insertions(+), 259 deletions(-) delete mode 100644 pkg/kubectl/explain.go create mode 100644 pkg/kubectl/explain/BUILD create mode 100644 pkg/kubectl/explain/explain.go create mode 100644 pkg/kubectl/explain/field_lookup.go create mode 100644 pkg/kubectl/explain/fields_printer.go create mode 100644 pkg/kubectl/explain/fields_printer_builder.go create mode 100644 pkg/kubectl/explain/formatter.go create mode 100644 pkg/kubectl/explain/model_printer.go create mode 100644 pkg/kubectl/explain/recursive_fields_printer.go create mode 100644 pkg/kubectl/explain/typename.go diff --git a/pkg/kubectl/BUILD b/pkg/kubectl/BUILD index 5a846e5f788..e80fb2a593d 100644 --- a/pkg/kubectl/BUILD +++ b/pkg/kubectl/BUILD @@ -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", @@ -196,6 +193,7 @@ filegroup( ":package-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", diff --git a/pkg/kubectl/cmd/BUILD b/pkg/kubectl/cmd/BUILD index 3f60d639fc7..10477403998 100644 --- a/pkg/kubectl/cmd/BUILD +++ b/pkg/kubectl/cmd/BUILD @@ -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", diff --git a/pkg/kubectl/cmd/explain.go b/pkg/kubectl/cmd/explain.go index 5f10c14a5b5..55d095690b5 100644 --- a/pkg/kubectl/cmd/explain.go +++ b/pkg/kubectl/cmd/explain.go @@ -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 ). // 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) } diff --git a/pkg/kubectl/cmd/util/openapi/openapi.go b/pkg/kubectl/cmd/util/openapi/openapi.go index 8e6cf0639f4..bc479f66b52 100644 --- a/pkg/kubectl/cmd/util/openapi/openapi.go +++ b/pkg/kubectl/cmd/util/openapi/openapi.go @@ -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 { diff --git a/pkg/kubectl/explain.go b/pkg/kubectl/explain.go deleted file mode 100644 index 06190ab9ecd..00000000000 --- a/pkg/kubectl/explain.go +++ /dev/null @@ -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 -} diff --git a/pkg/kubectl/explain/BUILD b/pkg/kubectl/explain/BUILD new file mode 100644 index 00000000000..457ce2dd853 --- /dev/null +++ b/pkg/kubectl/explain/BUILD @@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +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"], +) diff --git a/pkg/kubectl/explain/explain.go b/pkg/kubectl/explain/explain.go new file mode 100644 index 00000000000..d794ea70e3d --- /dev/null +++ b/pkg/kubectl/explain/explain.go @@ -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) +} diff --git a/pkg/kubectl/explain/field_lookup.go b/pkg/kubectl/explain/field_lookup.go new file mode 100644 index 00000000000..b766fbf3b40 --- /dev/null +++ b/pkg/kubectl/explain/field_lookup.go @@ -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 +} diff --git a/pkg/kubectl/explain/fields_printer.go b/pkg/kubectl/explain/fields_printer.go new file mode 100644 index 00000000000..8da066085bc --- /dev/null +++ b/pkg/kubectl/explain/fields_printer.go @@ -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 +} diff --git a/pkg/kubectl/explain/fields_printer_builder.go b/pkg/kubectl/explain/fields_printer_builder.go new file mode 100644 index 00000000000..8a2fc045eba --- /dev/null +++ b/pkg/kubectl/explain/fields_printer_builder.go @@ -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, + } +} diff --git a/pkg/kubectl/explain/formatter.go b/pkg/kubectl/explain/formatter.go new file mode 100644 index 00000000000..5543dbf1bb9 --- /dev/null +++ b/pkg/kubectl/explain/formatter.go @@ -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 +} diff --git a/pkg/kubectl/explain/model_printer.go b/pkg/kubectl/explain/model_printer.go new file mode 100644 index 00000000000..1f8d9208695 --- /dev/null +++ b/pkg/kubectl/explain/model_printer.go @@ -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 +} diff --git a/pkg/kubectl/explain/recursive_fields_printer.go b/pkg/kubectl/explain/recursive_fields_printer.go new file mode 100644 index 00000000000..dfa37931c05 --- /dev/null +++ b/pkg/kubectl/explain/recursive_fields_printer.go @@ -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 +} diff --git a/pkg/kubectl/explain/typename.go b/pkg/kubectl/explain/typename.go new file mode 100644 index 00000000000..b90580e614f --- /dev/null +++ b/pkg/kubectl/explain/typename.go @@ -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 +}