Skip to content

Commit 3f586a4

Browse files
authored
Merge pull request #22 from keisku/issue16
Show all resources which contain a particular field
2 parents ee94484 + 01f448e commit 3f586a4

File tree

6 files changed

+103
-157
lines changed

6 files changed

+103
-157
lines changed

README.md

+13-6
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,36 @@ Usage:
3131
3232
Examples:
3333
34-
# Fuzzy-find the field explanation from supported API resources.
34+
# Fuzzy-find the field to explain from all API resources.
3535
kubectl explore
3636
37-
# Fuzzy-find the field explanation from "pod"
37+
# Fuzzy-find the field to explain from fields matching the regex.
38+
kubectl explore pod.*node
39+
kubectl explore spec.*containers
40+
kubectl explore lifecycle
41+
42+
# Fuzzy-find the field to explain from all API resources in the selected cluster.
43+
kubectl explore --context=onecontext
44+
45+
# Fuzzy-find the field to explain from fields under "pod".
3846
kubectl explore pod
3947
40-
# Fuzzy-find the field explanation from "pod.spec.containers"
48+
# Fuzzy-find the field to explain from fields under "pod.spec.containers".
4149
kubectl explore pod.spec.containers
4250
43-
# Fuzzy-find the field explanation from supported API resources in the selected cluster.
44-
kubectl explore --context=onecontext
4551
4652
Flags:
4753
--api-version string Get different explanations for particular API version (API group/version)
4854
--as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace.
4955
--as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups.
5056
--as-uid string UID to impersonate for the operation.
51-
--cache-dir string Default cache directory (default "/Users/keisukeumegaki/.kube/cache")
57+
--cache-dir string Default cache directory (default "/root/.kube/cache")
5258
--certificate-authority string Path to a cert file for the certificate authority
5359
--client-certificate string Path to a client certificate file for TLS
5460
--client-key string Path to a client key file for TLS
5561
--cluster string The name of the kubeconfig cluster to use
5662
--context string The name of the kubeconfig context to use
63+
--disable-compression If true, opt-out of response compression for all requests to the server
5764
-h, --help help for kubectl
5865
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
5966
--kubeconfig string Path to the kubeconfig file to use for CLI requests.

demo.gif

4.06 MB
Loading

explainer.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type explainer struct {
1717
}
1818

1919
// explain explains the field associated with the given path.
20-
func (e *explainer) explain(w io.Writer, path string) error {
20+
func (e explainer) explain(w io.Writer, path string) error {
2121
if path == "" {
2222
return fmt.Errorf("path is empty: gvk=%s", e.gvk)
2323
}
@@ -32,7 +32,7 @@ func (e *explainer) explain(w io.Writer, path string) error {
3232
// e.g. "pod.spec.containers.env" -> "pod.spec.containers"
3333
parent, ok := e.pathSchema[path[:strings.LastIndex(path, ".")]]
3434
if !ok {
35-
return fmt.Errorf("%q is not found", path)
35+
return fmt.Errorf("cannot explain %q: not found", path)
3636
}
3737

3838
// get the key from the path.

explorer_test.go explainer_test.go

-25
Original file line numberDiff line numberDiff line change
@@ -47,31 +47,6 @@ func fetchOpenAPIResources(version string) openapi.Resources {
4747
}
4848
return r
4949
}
50-
func Test_fullformInputFieldPath(t *testing.T) {
51-
tests := []struct {
52-
inputFieldPath string
53-
fullformedKind string
54-
want string
55-
}{
56-
{
57-
inputFieldPath: "sts.spec",
58-
fullformedKind: "statefulset",
59-
want: "statefulset.spec",
60-
},
61-
{
62-
inputFieldPath: "sts",
63-
fullformedKind: "statefulset",
64-
want: "statefulset",
65-
},
66-
}
67-
for _, tt := range tests {
68-
t.Run(fmt.Sprintf("make %s full-formed", tt.inputFieldPath), func(t *testing.T) {
69-
if got := fullformInputFieldPath(tt.inputFieldPath, tt.fullformedKind); got != tt.want {
70-
t.Errorf("fullformInputFieldPath() = %v, want %v", got, tt.want)
71-
}
72-
})
73-
}
74-
}
7550

7651
func Test_explain(t *testing.T) {
7752
tests := []struct {

explorer.go

-104
This file was deleted.

options.go

+88-20
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package main
22

33
import (
4+
"bytes"
45
"fmt"
56
"os"
7+
"regexp"
68
"sort"
79
"strings"
810

@@ -13,6 +15,7 @@ import (
1315
"k8s.io/cli-runtime/pkg/genericclioptions"
1416
"k8s.io/client-go/discovery"
1517
_ "k8s.io/client-go/plugin/pkg/client/auth"
18+
"k8s.io/kube-openapi/pkg/util/proto"
1619
cmdutil "k8s.io/kubectl/pkg/cmd/util"
1720
"k8s.io/kubectl/pkg/explain"
1821
"k8s.io/kubectl/pkg/util/openapi"
@@ -27,9 +30,9 @@ type Options struct {
2730
Discovery discovery.CachedDiscoveryInterface
2831
Schema openapi.Resources
2932

30-
inputFieldPath string
33+
inputFieldPath *regexp.Regexp
3134
resource string
32-
gvk schema.GroupVersionKind
35+
gvks []schema.GroupVersionKind
3336
}
3437

3538
func NewCmd() *cobra.Command {
@@ -48,17 +51,22 @@ Fields are identified via a simple JSONPath identifier:
4851
`,
4952
Short: "Fuzzy-find the explanation for a resource or its field.",
5053
Example: `
51-
# Fuzzy-find the field explanation from supported API resources.
54+
# Fuzzy-find the field to explain from all API resources.
5255
kubectl explore
5356
54-
# Fuzzy-find the field explanation from "pod"
57+
# Fuzzy-find the field to explain from fields matching the regex.
58+
kubectl explore pod.*node
59+
kubectl explore spec.*containers
60+
kubectl explore lifecycle
61+
62+
# Fuzzy-find the field to explain from all API resources in the selected cluster.
63+
kubectl explore --context=onecontext
64+
65+
# Fuzzy-find the field to explain from fields under "pod".
5566
kubectl explore pod
5667
57-
# Fuzzy-find the field explanation from "pod.spec.containers"
68+
# Fuzzy-find the field to explain from fields under "pod.spec.containers".
5869
kubectl explore pod.spec.containers
59-
60-
# Fuzzy-find the field explanation from supported API resources in the selected cluster.
61-
kubectl explore --context=onecontext
6270
`,
6371
}
6472
cmd.Flags().StringVar(&o.APIVersion, "api-version", o.APIVersion, "Get different explanations for particular API version (API group/version)")
@@ -83,13 +91,16 @@ func NewOptions(streams genericclioptions.IOStreams) *Options {
8391
}
8492

8593
func (o *Options) Complete(f cmdutil.Factory, args []string) error {
86-
if 0 < len(args) {
87-
o.inputFieldPath = args[0]
88-
}
89-
if len(args) == 1 {
94+
var err error
95+
if len(args) == 0 {
96+
o.inputFieldPath = regexp.MustCompile(".*")
97+
} else {
98+
o.inputFieldPath, err = regexp.Compile(args[0])
99+
if err != nil {
100+
return err
101+
}
90102
o.resource = args[0]
91103
}
92-
var err error
93104
o.Discovery, err = f.ToDiscoveryClient()
94105
if err != nil {
95106
return err
@@ -103,22 +114,79 @@ func (o *Options) Complete(f cmdutil.Factory, args []string) error {
103114
return err
104115
}
105116
if o.resource == "" {
106-
o.gvk, err = o.findGVK()
117+
gvk, err := o.findGVK()
118+
if err != nil {
119+
return err
120+
}
121+
o.gvks = []schema.GroupVersionKind{gvk}
107122
} else {
108-
o.gvk, err = o.getGVK(strings.Split(o.resource, ".")[0])
109-
}
110-
if err != nil {
111-
return err
123+
gvk, err := o.getGVK(strings.Split(o.resource, ".")[0])
124+
if err == nil {
125+
o.gvks = []schema.GroupVersionKind{gvk}
126+
} else {
127+
o.gvks, err = o.listGVKs()
128+
if err != nil {
129+
return err
130+
}
131+
}
112132
}
113133
return nil
114134
}
115135

116136
func (o *Options) Run() error {
117-
e, err := newExplorer(o)
137+
pathExplainers := make(map[string]explainer)
138+
var paths []string
139+
for _, gvk := range o.gvks {
140+
visitor := &schemaVisitor{
141+
pathSchema: make(map[string]proto.Schema),
142+
prevPath: strings.ToLower(gvk.Kind),
143+
err: nil,
144+
}
145+
s := o.Schema.LookupResource(gvk)
146+
if s == nil {
147+
return fmt.Errorf("no schema found for %s", gvk)
148+
}
149+
s.Accept(visitor)
150+
if visitor.err != nil {
151+
return visitor.err
152+
}
153+
filteredPaths := visitor.listPaths(func(s string) bool {
154+
return o.inputFieldPath.MatchString(s)
155+
})
156+
for _, p := range filteredPaths {
157+
pathExplainers[p] = explainer{
158+
schemaByGvk: s,
159+
gvk: gvk,
160+
pathSchema: visitor.pathSchema,
161+
}
162+
paths = append(paths, p)
163+
}
164+
}
165+
if len(paths) == 0 {
166+
return fmt.Errorf("no paths found for %q", o.inputFieldPath)
167+
}
168+
if len(paths) == 1 {
169+
return pathExplainers[paths[0]].explain(o.Out, paths[0])
170+
}
171+
sort.Strings(paths)
172+
idx, err := fuzzyfinder.Find(
173+
paths,
174+
func(i int) string { return paths[i] },
175+
fuzzyfinder.WithPreviewWindow(func(i, _, _ int) string {
176+
if i < 0 {
177+
return ""
178+
}
179+
var w bytes.Buffer
180+
if err := pathExplainers[paths[i]].explain(&w, paths[i]); err != nil {
181+
return fmt.Sprintf("preview is broken: %s", err)
182+
}
183+
return w.String()
184+
},
185+
))
118186
if err != nil {
119187
return err
120188
}
121-
return e.explore(o.Out)
189+
return pathExplainers[paths[idx]].explain(o.Out, paths[idx])
122190
}
123191

124192
func (o *Options) findGVK() (schema.GroupVersionKind, error) {

0 commit comments

Comments
 (0)