diff --git a/explore/explainer.go b/explore/explainer.go index f8cd4c0..e6ab629 100644 --- a/explore/explainer.go +++ b/explore/explainer.go @@ -1,7 +1,6 @@ package explore import ( - "errors" "fmt" "io" "strings" @@ -12,22 +11,27 @@ import ( ) type explainer struct { - gvr schema.GroupVersionResource - openAPIV3Client openapiclient.Client - enablePrintPath bool + gvr schema.GroupVersionResource + openAPIV3Client openapiclient.Client + enablePrintPath bool + enablePrintBrackets bool } -func (e explainer) explain(w io.Writer, path string) error { - if path == "" { - return errors.New("path must be provided") +func (e explainer) explain(w io.Writer, path path) error { + if path.isEmpty() { + return fmt.Errorf("path must not be empty: %#v", path) } - fields := strings.Split(path, ".") + fields := strings.Split(path.original, ".") if len(fields) > 0 { // Remove resource name fields = fields[1:] } if e.enablePrintPath { - w.Write([]byte(fmt.Sprintf("PATH: %s\n", path))) + if e.enablePrintBrackets { + w.Write([]byte(fmt.Sprintf("PATH: %s\n", path.withBrackets))) + } else { + w.Write([]byte(fmt.Sprintf("PATH: %s\n", path.original))) + } } return explainv2.PrintModelDescription( fields, diff --git a/explore/export_test.go b/explore/export_test.go new file mode 100644 index 0000000..c546aed --- /dev/null +++ b/explore/export_test.go @@ -0,0 +1,9 @@ +package explore + +func SetDisablePrintPath(o *Options, b bool) { + o.disablePrintPath = b +} + +func SetShowBrackets(o *Options, b bool) { + o.showBrackets = b +} diff --git a/explore/options.go b/explore/options.go index 585ef40..2df21d3 100644 --- a/explore/options.go +++ b/explore/options.go @@ -27,6 +27,7 @@ type Options struct { apiVersion string inputFieldPath string disablePrintPath bool + showBrackets bool // After completion inputFieldPathRegex *regexp.Regexp @@ -69,6 +70,7 @@ kubectl explore --context=onecontext } cmd.Flags().StringVar(&o.apiVersion, "api-version", o.apiVersion, "Get different explanations for particular API version (API group/version)") cmd.Flags().BoolVar(&o.disablePrintPath, "disable-print-path", o.disablePrintPath, "Disable printing the path to explain") + cmd.Flags().BoolVar(&o.showBrackets, "show-brackets", o.showBrackets, "Enable showing brackets for fields that are arrays") kubeConfigFlags := defaultConfigFlags().WithWarningPrinter(o.IOStreams) flags := cmd.PersistentFlags() kubeConfigFlags.AddFlags(flags) @@ -194,13 +196,16 @@ func (o *Options) Complete(f cmdutil.Factory, args []string) error { } func (o *Options) Run() error { - pathExplainers := make(map[string]explainer) - var paths []string + pathExplainers := make(map[path]explainer) + var paths []path for _, gvr := range o.gvrs { visitor := &schemaVisitor{ - pathSchema: make(map[string]proto.Schema), - prevPath: strings.ToLower(gvr.Resource), - err: nil, + pathSchema: make(map[path]proto.Schema), + prevPath: path{ + original: strings.ToLower(gvr.Resource), + withBrackets: strings.ToLower(gvr.Resource), + }, + err: nil, } gvk, err := o.mapper.KindFor(gvr) if err != nil { @@ -214,14 +219,15 @@ func (o *Options) Run() error { if visitor.err != nil { return visitor.err } - filteredPaths := visitor.listPaths(func(s string) bool { - return o.inputFieldPathRegex.MatchString(s) + filteredPaths := visitor.listPaths(func(s path) bool { + return o.inputFieldPathRegex.MatchString(s.original) }) for _, p := range filteredPaths { pathExplainers[p] = explainer{ - gvr: gvr, - openAPIV3Client: o.cachedOpenAPIV3Client, - enablePrintPath: !o.disablePrintPath, + gvr: gvr, + openAPIV3Client: o.cachedOpenAPIV3Client, + enablePrintPath: !o.disablePrintPath, + enablePrintBrackets: o.showBrackets, } paths = append(paths, p) } @@ -232,10 +238,12 @@ func (o *Options) Run() error { if len(paths) == 1 { return pathExplainers[paths[0]].explain(o.Out, paths[0]) } - sort.Strings(paths) + sort.SliceStable(paths, func(i, j int) bool { + return paths[i].original < paths[j].original + }) idx, err := fuzzyfinder.Find( paths, - func(i int) string { return paths[i] }, + func(i int) string { return paths[i].original }, fuzzyfinder.WithPreviewWindow(func(i, _, _ int) string { if i < 0 { return "" diff --git a/explore/options_test.go b/explore/options_test.go index 3f4af9e..0de2040 100644 --- a/explore/options_test.go +++ b/explore/options_test.go @@ -213,13 +213,18 @@ func Test_Run(t *testing.T) { }, } tests := []struct { - inputFieldPath string - expectRunError bool - expectKeywords []string + inputFieldPath string + disablePrintPath bool + showBrackets bool + expectRunError bool + expectKeywords []string + unexpectKeywords []string }{ { - inputFieldPath: "no.*pro", - expectRunError: false, + inputFieldPath: "no.*pro", + disablePrintPath: false, + showBrackets: false, + expectRunError: false, expectKeywords: []string{ "Node", "providerID", @@ -227,8 +232,10 @@ func Test_Run(t *testing.T) { }, }, { - inputFieldPath: "node.*pro", - expectRunError: false, + inputFieldPath: "node.*pro", + disablePrintPath: false, + showBrackets: false, + expectRunError: false, expectKeywords: []string{ "Node", "providerID", @@ -236,8 +243,10 @@ func Test_Run(t *testing.T) { }, }, { - inputFieldPath: "nodes.*pro", - expectRunError: false, + inputFieldPath: "nodes.*pro", + disablePrintPath: false, + showBrackets: false, + expectRunError: false, expectKeywords: []string{ "Node", "providerID", @@ -245,8 +254,10 @@ func Test_Run(t *testing.T) { }, }, { - inputFieldPath: "providerID", - expectRunError: false, + inputFieldPath: "providerID", + disablePrintPath: false, + showBrackets: false, + expectRunError: false, expectKeywords: []string{ "Node", "providerID", @@ -254,8 +265,10 @@ func Test_Run(t *testing.T) { }, }, { - inputFieldPath: "hpa.*own.*id", - expectRunError: false, + inputFieldPath: "hpa.*own.*id", + disablePrintPath: false, + showBrackets: false, + expectRunError: false, expectKeywords: []string{ "autoscaling", "HorizontalPodAutoscaler", @@ -264,8 +277,10 @@ func Test_Run(t *testing.T) { }, }, { - inputFieldPath: "horizontalpodautoscalers.*own.*id", - expectRunError: false, + inputFieldPath: "horizontalpodautoscalers.*own.*id", + disablePrintPath: false, + showBrackets: false, + expectRunError: false, expectKeywords: []string{ "autoscaling", "HorizontalPodAutoscaler", @@ -274,8 +289,10 @@ func Test_Run(t *testing.T) { }, }, { - inputFieldPath: "horizontalpodautoscaler.*own.*id", - expectRunError: false, + inputFieldPath: "horizontalpodautoscaler.*own.*id", + disablePrintPath: false, + showBrackets: false, + expectRunError: false, expectKeywords: []string{ "autoscaling", "HorizontalPodAutoscaler", @@ -284,8 +301,10 @@ func Test_Run(t *testing.T) { }, }, { - inputFieldPath: "csistoragecapacity.maximumVolumeSize", - expectRunError: false, + inputFieldPath: "csistoragecapacity.maximumVolumeSize", + disablePrintPath: false, + showBrackets: false, + expectRunError: false, expectKeywords: []string{ "CSIStorageCapacity", "storage.k8s.io", @@ -294,8 +313,10 @@ func Test_Run(t *testing.T) { }, }, { - inputFieldPath: "csistoragecapacities.maximumVolumeSize", - expectRunError: false, + inputFieldPath: "csistoragecapacities.maximumVolumeSize", + disablePrintPath: false, + showBrackets: false, + expectRunError: false, expectKeywords: []string{ "CSIStorageCapacity", "storage.k8s.io", @@ -304,8 +325,10 @@ func Test_Run(t *testing.T) { }, }, { - inputFieldPath: "CSIStorageCapacity.*VolumeSize", - expectRunError: false, + inputFieldPath: "CSIStorageCapacity.*VolumeSize", + disablePrintPath: false, + showBrackets: false, + expectRunError: false, expectKeywords: []string{ "CSIStorageCapacity", "storage.k8s.io", @@ -313,10 +336,41 @@ func Test_Run(t *testing.T) { "PATH: csistoragecapacities.maximumVolumeSize", }, }, + { + inputFieldPath: "nodes.status.conditions.type", + disablePrintPath: false, + showBrackets: true, + expectRunError: false, + expectKeywords: []string{ + "Node", + "type", + "PATH: nodes.status.conditions[].type", + }, + }, + { + inputFieldPath: "nodes.status.conditions.type", + disablePrintPath: true, + showBrackets: true, + expectRunError: false, + expectKeywords: []string{ + "Node", + "type", + }, + unexpectKeywords: []string{ + "PATH: nodes.status.conditions[].type", + "PATH: nodes.status.conditions.type", + }, + }, } for _, tt := range tests { for _, version := range k8sVersions { - t.Run(fmt.Sprintf("version: %s inputFieldPath: %s", version, tt.inputFieldPath), func(t *testing.T) { + t.Run(fmt.Sprintf( + "version: %s inputFieldPath: %s, disablePrintPath: %v, showBrackets: %v", + version, + tt.inputFieldPath, + tt.disablePrintPath, + tt.showBrackets, + ), func(t *testing.T) { fakeServer := fakeServers[version] fakeDiscoveryClient := discovery.NewDiscoveryClientForConfigOrDie(&rest.Config{Host: fakeServer.HttpServer.URL}) tf := cmdtesting.NewTestFactory() @@ -338,6 +392,8 @@ func Test_Run(t *testing.T) { Out: &stdout, ErrOut: &errout, }) + explore.SetDisablePrintPath(opts, tt.disablePrintPath) + explore.SetShowBrackets(opts, tt.showBrackets) require.NoError(t, opts.Complete(tf, []string{tt.inputFieldPath})) err := opts.Run() if tt.expectRunError { @@ -348,6 +404,9 @@ func Test_Run(t *testing.T) { for _, keyword := range tt.expectKeywords { require.Contains(t, stdout.String(), keyword) } + for _, keyword := range tt.unexpectKeywords { + require.NotContains(t, stdout.String(), keyword) + } }) } } diff --git a/explore/schema_visitor.go b/explore/schema_visitor.go index e6b4fac..6231250 100644 --- a/explore/schema_visitor.go +++ b/explore/schema_visitor.go @@ -8,9 +8,18 @@ import ( "k8s.io/kubectl/pkg/explain" ) +type path struct { + original string + withBrackets string +} + +func (p path) isEmpty() bool { + return p.original == "" && p.withBrackets == "" +} + type schemaVisitor struct { - prevPath string - pathSchema map[string]proto.Schema + prevPath path + pathSchema map[path]proto.Schema err error } @@ -18,9 +27,12 @@ var _ proto.SchemaVisitor = (*schemaVisitor)(nil) func (v *schemaVisitor) VisitKind(k *proto.Kind) { keys := k.Keys() - paths := make([]string, len(keys)) + paths := make([]path, len(keys)) for i, key := range keys { - paths[i] = strings.Join([]string{v.prevPath, key}, ".") + paths[i] = path{ + original: strings.Join([]string{v.prevPath.original, key}, "."), + withBrackets: strings.Join([]string{v.prevPath.withBrackets, key}, "."), + } } for i, key := range keys { schema, err := explain.LookupSchemaForField(k, []string{key}) @@ -28,6 +40,9 @@ func (v *schemaVisitor) VisitKind(k *proto.Kind) { v.err = err return } + if _, ok := schema.(*proto.Array); ok { + paths[i].withBrackets += "[]" + } v.pathSchema[paths[i]] = schema v.prevPath = paths[i] schema.Accept(v) @@ -57,13 +72,15 @@ func (v *schemaVisitor) VisitMap(m *proto.Map) { m.SubType.Accept(v) } -func (v *schemaVisitor) listPaths(filter func(string) bool) []string { - paths := make([]string, 0, len(v.pathSchema)) +func (v *schemaVisitor) listPaths(filter func(path) bool) []path { + paths := make([]path, 0, len(v.pathSchema)) for path := range v.pathSchema { if filter(path) { paths = append(paths, path) } } - sort.Strings(paths) + sort.SliceStable(paths, func(i, j int) bool { + return paths[i].original < paths[j].original + }) return paths }