Skip to content

Commit 2983c8f

Browse files
author
Takashi Kusumi
authored
Add dynamic completion for a resource query (#209)
1 parent 7bc45f0 commit 2983c8f

File tree

5 files changed

+269
-2
lines changed

5 files changed

+269
-2
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,8 @@ stern -p
234234
Stern supports command-line auto completion for bash, zsh or fish. `stern
235235
--completion=(bash|zsh|fish)` outputs the shell completion code which work by being
236236
evaluated in `.bashrc`, etc for the specified shell. In addition, Stern
237-
supports dynamic completion for `--namespace` and `--context`. In order to use
238-
that, kubectl must be installed on your environment.
237+
supports dynamic completion for `--namespace`, `--context` and a resource query
238+
in the form `<resource>/<name>`.
239239

240240
If you use bash, stern bash completion code depends on the
241241
[bash-completion](https://github.com/scop/bash-completion). On the macOS, you

cmd/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ func NewSternCmd(stream genericclioptions.IOStreams) (*cobra.Command, error) {
389389

390390
return o.Run(cmd)
391391
},
392+
ValidArgsFunction: queryCompletionFunc(o),
392393
}
393394

394395
o.AddFlags(cmd.Flags())

cmd/flag_completion.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ import (
2121
"io"
2222
"strings"
2323

24+
"github.com/pkg/errors"
2425
"github.com/spf13/cobra"
2526
"github.com/stern/stern/kubernetes"
27+
"github.com/stern/stern/stern"
2628
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
clientset "k8s.io/client-go/kubernetes"
2730
)
2831

2932
func runCompletion(shell string, cmd *cobra.Command, out io.Writer) error {
@@ -119,7 +122,137 @@ func contextCompletionFunc(o *options) func(cmd *cobra.Command, args []string, t
119122
}
120123
}
121124

125+
// queryCompletionFunc is a completion function that completes a resource
126+
// that match the toComplete prefix.
127+
func queryCompletionFunc(o *options) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
128+
return func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
129+
var comps []string
130+
parts := strings.Split(toComplete, "/")
131+
if len(parts) != 2 {
132+
// list available resources in the form "<resource>/"
133+
for _, matcher := range stern.ResourceMatchers {
134+
if strings.HasPrefix(matcher.Name(), toComplete) {
135+
comps = append(comps, matcher.Name()+"/")
136+
}
137+
}
138+
return comps, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
139+
}
140+
141+
// list available names in the resources in the form "<resource>/<name>"
142+
uniqueNamespaces := makeUnique(o.namespaces)
143+
if o.allNamespaces || len(uniqueNamespaces) > 1 {
144+
// do not support multiple namespaces for simplicity
145+
return compError(errors.New("multiple namespaces are not supported"))
146+
}
147+
148+
clientConfig := kubernetes.NewClientConfig(o.kubeConfig, o.context)
149+
clientset, err := kubernetes.NewClientSet(clientConfig)
150+
if err != nil {
151+
return compError(err)
152+
}
153+
var namespace string
154+
if len(uniqueNamespaces) == 1 {
155+
namespace = uniqueNamespaces[0]
156+
} else {
157+
n, _, err := clientConfig.Namespace()
158+
if err != nil {
159+
return compError(err)
160+
}
161+
namespace = n
162+
}
163+
164+
kind, name := parts[0], parts[1]
165+
names, err := retrieveNamesFromResource(context.TODO(), clientset, namespace, kind)
166+
if err != nil {
167+
return compError(err)
168+
}
169+
for _, n := range names {
170+
if strings.HasPrefix(n, name) {
171+
comps = append(comps, kind+"/"+n)
172+
}
173+
}
174+
return comps, cobra.ShellCompDirectiveNoFileComp
175+
}
176+
}
177+
122178
func compError(err error) ([]string, cobra.ShellCompDirective) {
123179
cobra.CompError(err.Error())
124180
return nil, cobra.ShellCompDirectiveError
125181
}
182+
183+
func retrieveNamesFromResource(ctx context.Context, client clientset.Interface, namespace, kind string) ([]string, error) {
184+
opt := metav1.ListOptions{}
185+
var names []string
186+
switch {
187+
// core
188+
case stern.PodMatcher.Matches(kind):
189+
l, err := client.CoreV1().Pods(namespace).List(ctx, opt)
190+
if err != nil {
191+
return nil, err
192+
}
193+
for _, item := range l.Items {
194+
names = append(names, item.GetName())
195+
}
196+
case stern.ReplicationControllerMatcher.Matches(kind):
197+
l, err := client.CoreV1().ReplicationControllers(namespace).List(ctx, opt)
198+
if err != nil {
199+
return nil, err
200+
}
201+
for _, item := range l.Items {
202+
names = append(names, item.GetName())
203+
}
204+
case stern.ServiceMatcher.Matches(kind):
205+
l, err := client.CoreV1().Services(namespace).List(ctx, opt)
206+
if err != nil {
207+
return nil, err
208+
}
209+
for _, item := range l.Items {
210+
names = append(names, item.GetName())
211+
}
212+
// apps
213+
case stern.DeploymentMatcher.Matches(kind):
214+
l, err := client.AppsV1().Deployments(namespace).List(ctx, opt)
215+
if err != nil {
216+
return nil, err
217+
}
218+
for _, item := range l.Items {
219+
names = append(names, item.GetName())
220+
}
221+
case stern.DaemonSetMatcher.Matches(kind):
222+
l, err := client.AppsV1().DaemonSets(namespace).List(ctx, opt)
223+
if err != nil {
224+
return nil, err
225+
}
226+
for _, item := range l.Items {
227+
names = append(names, item.GetName())
228+
}
229+
case stern.ReplicaSetMatcher.Matches(kind):
230+
l, err := client.AppsV1().ReplicaSets(namespace).List(ctx, opt)
231+
if err != nil {
232+
return nil, err
233+
}
234+
for _, item := range l.Items {
235+
names = append(names, item.GetName())
236+
}
237+
case stern.StatefulSetMatcher.Matches(kind):
238+
l, err := client.AppsV1().StatefulSets(namespace).List(ctx, opt)
239+
if err != nil {
240+
return nil, err
241+
}
242+
for _, item := range l.Items {
243+
names = append(names, item.GetName())
244+
}
245+
// batch
246+
case stern.JobMatcher.Matches(kind):
247+
l, err := client.BatchV1().Jobs(namespace).List(ctx, opt)
248+
if err != nil {
249+
return nil, err
250+
}
251+
for _, item := range l.Items {
252+
names = append(names, item.GetName())
253+
}
254+
default:
255+
return nil, fmt.Errorf("resource type %s is not supported", kind)
256+
}
257+
return names, nil
258+
}

cmd/flag_completion_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"reflect"
6+
"testing"
7+
8+
appsv1 "k8s.io/api/apps/v1"
9+
batchv1 "k8s.io/api/batch/v1"
10+
corev1 "k8s.io/api/core/v1"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/apimachinery/pkg/runtime"
13+
"k8s.io/client-go/kubernetes/fake"
14+
)
15+
16+
func TestRetrieveNamesFromResource(t *testing.T) {
17+
genMeta := func(name string) metav1.ObjectMeta {
18+
return metav1.ObjectMeta{
19+
Name: name,
20+
Namespace: "ns1",
21+
}
22+
}
23+
objs := []runtime.Object{
24+
&corev1.Pod{ObjectMeta: genMeta("pod1")},
25+
&corev1.Pod{ObjectMeta: genMeta("pod2")},
26+
&corev1.Pod{ObjectMeta: genMeta("pod3")},
27+
&corev1.ReplicationController{ObjectMeta: genMeta("rc1")},
28+
&corev1.Service{ObjectMeta: genMeta("svc1")},
29+
&appsv1.Deployment{ObjectMeta: genMeta("deploy1")},
30+
&appsv1.Deployment{ObjectMeta: genMeta("deploy2")},
31+
&appsv1.DaemonSet{ObjectMeta: genMeta("ds1")},
32+
&appsv1.DaemonSet{ObjectMeta: genMeta("ds2")},
33+
&appsv1.ReplicaSet{ObjectMeta: genMeta("rs1")},
34+
&appsv1.ReplicaSet{ObjectMeta: genMeta("rs2")},
35+
&appsv1.StatefulSet{ObjectMeta: genMeta("sts1")},
36+
&appsv1.StatefulSet{ObjectMeta: genMeta("sts2")},
37+
&batchv1.Job{ObjectMeta: genMeta("job1")},
38+
&batchv1.Job{ObjectMeta: genMeta("job2")},
39+
}
40+
client := fake.NewSimpleClientset(objs...)
41+
tests := []struct {
42+
desc string
43+
kinds []string
44+
expected []string
45+
wantError bool
46+
}{
47+
// core
48+
{
49+
desc: "pods",
50+
kinds: []string{"po", "pods", "pod"},
51+
expected: []string{"pod1", "pod2", "pod3"},
52+
},
53+
{
54+
desc: "replicationcontrollers",
55+
kinds: []string{"rc", "replicationcontrollers", "replicationcontroller"},
56+
expected: []string{"rc1"},
57+
},
58+
// apps
59+
{
60+
desc: "deployments",
61+
kinds: []string{"deploy", "deployments", "deployment"},
62+
expected: []string{"deploy1", "deploy2"},
63+
},
64+
{
65+
desc: "daemonsets",
66+
kinds: []string{"ds", "daemonsets", "daemonset"},
67+
expected: []string{"ds1", "ds2"},
68+
},
69+
{
70+
desc: "replicasets",
71+
kinds: []string{"rs", "replicasets", "replicaset"},
72+
expected: []string{"rs1", "rs2"},
73+
},
74+
{
75+
desc: "statefulsets",
76+
kinds: []string{"sts", "statefulsets", "statefulset"},
77+
expected: []string{"sts1", "sts2"},
78+
},
79+
// batch
80+
{
81+
desc: "jobs",
82+
kinds: []string{"job", "jobs"},
83+
expected: []string{"job1", "job2"},
84+
},
85+
// invalid
86+
{
87+
desc: "invalid",
88+
kinds: []string{"", "unknown"},
89+
wantError: true,
90+
},
91+
}
92+
93+
for _, tt := range tests {
94+
t.Run(tt.desc, func(t *testing.T) {
95+
for _, kind := range tt.kinds {
96+
names, err := retrieveNamesFromResource(context.Background(), client, "ns1", kind)
97+
if tt.wantError {
98+
if err == nil {
99+
t.Errorf("expected error, but got no error")
100+
}
101+
return
102+
}
103+
if err != nil {
104+
t.Errorf("unexpected error: %v", err)
105+
return
106+
}
107+
if !reflect.DeepEqual(tt.expected, names) {
108+
t.Errorf("expected %v, but actual %v", tt.expected, names)
109+
}
110+
// expect empty slice with no error when no objects are found in the valid resource
111+
names, err = retrieveNamesFromResource(context.Background(), client, "not-matched", kind)
112+
if err != nil {
113+
t.Errorf("unexpected error: %v", err)
114+
return
115+
}
116+
if len(names) != 0 {
117+
t.Errorf("expected empty slice, but got %v", names)
118+
return
119+
}
120+
}
121+
})
122+
}
123+
}

stern/resource_matcher.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,14 @@ var (
4949
ReplicaSetMatcher = ResourceMatcher{name: "replicaset", aliases: []string{"rs", "replicasets"}}
5050
StatefulSetMatcher = ResourceMatcher{name: "statefulset", aliases: []string{"sts", "statefulsets"}}
5151
JobMatcher = ResourceMatcher{name: "job", aliases: []string{"jobs"}} // job does not have a short name
52+
ResourceMatchers = []ResourceMatcher{
53+
PodMatcher,
54+
ReplicationControllerMatcher,
55+
ServiceMatcher,
56+
DeploymentMatcher,
57+
DaemonSetMatcher,
58+
ReplicaSetMatcher,
59+
StatefulSetMatcher,
60+
JobMatcher,
61+
}
5262
)

0 commit comments

Comments
 (0)