Skip to content

Commit f69885b

Browse files
[Feat][kubectl-plugin] Add dynamic shell completion for kubectl ray session (#2390)
Signed-off-by: Chi-Sheng Liu <[email protected]>
1 parent 73e6c5d commit f69885b

File tree

3 files changed

+152
-16
lines changed

3 files changed

+152
-16
lines changed

kubectl-plugin/pkg/cmd/session/session.go

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/ray-project/kuberay/kubectl-plugin/pkg/util"
99
"github.com/ray-project/kuberay/kubectl-plugin/pkg/util/client"
10+
"github.com/ray-project/kuberay/kubectl-plugin/pkg/util/completion"
1011
"github.com/spf13/cobra"
1112
"k8s.io/cli-runtime/pkg/genericclioptions"
1213
"k8s.io/cli-runtime/pkg/genericiooptions"
@@ -23,7 +24,6 @@ type appPort struct {
2324
type SessionOptions struct {
2425
configFlags *genericclioptions.ConfigFlags
2526
ioStreams *genericiooptions.IOStreams
26-
client client.Client
2727
ResourceType util.ResourceType
2828
ResourceName string
2929
Namespace string
@@ -76,20 +76,22 @@ func NewSessionOptions(streams genericiooptions.IOStreams) *SessionOptions {
7676

7777
func NewSessionCommand(streams genericiooptions.IOStreams) *cobra.Command {
7878
options := NewSessionOptions(streams)
79+
factory := cmdutil.NewFactory(options.configFlags)
7980

8081
cmd := &cobra.Command{
81-
Use: "session (RAYCLUSTER | TYPE/NAME)",
82-
Short: "Forward local ports to the Ray resources.",
83-
Long: sessionLong,
84-
Example: sessionExample,
82+
Use: "session (RAYCLUSTER | TYPE/NAME)",
83+
Short: "Forward local ports to the Ray resources.",
84+
Long: sessionLong,
85+
Example: sessionExample,
86+
ValidArgsFunction: completion.RayClusterResourceNameCompletionFunc(factory),
8587
RunE: func(cmd *cobra.Command, args []string) error {
8688
if err := options.Complete(cmd, args); err != nil {
8789
return err
8890
}
8991
if err := options.Validate(); err != nil {
9092
return err
9193
}
92-
return options.Run(cmd.Context())
94+
return options.Run(cmd.Context(), factory)
9395
},
9496
}
9597
options.configFlags.AddFlags(cmd.Flags())
@@ -130,13 +132,6 @@ func (options *SessionOptions) Complete(cmd *cobra.Command, args []string) error
130132
options.Namespace = *options.configFlags.Namespace
131133
}
132134

133-
factory := cmdutil.NewFactory(options.configFlags)
134-
k8sClient, err := client.NewClient(factory)
135-
if err != nil {
136-
return fmt.Errorf("failed to create client: %w", err)
137-
}
138-
options.client = k8sClient
139-
140135
return nil
141136
}
142137

@@ -152,10 +147,13 @@ func (options *SessionOptions) Validate() error {
152147
return nil
153148
}
154149

155-
func (options *SessionOptions) Run(ctx context.Context) error {
156-
factory := cmdutil.NewFactory(options.configFlags)
150+
func (options *SessionOptions) Run(ctx context.Context, factory cmdutil.Factory) error {
151+
k8sClient, err := client.NewClient(factory)
152+
if err != nil {
153+
return fmt.Errorf("failed to create client: %w", err)
154+
}
157155

158-
svcName, err := options.client.GetRayHeadSvcName(ctx, options.Namespace, options.ResourceType, options.ResourceName)
156+
svcName, err := k8sClient.GetRayHeadSvcName(ctx, options.Namespace, options.ResourceType, options.ResourceName)
159157
if err != nil {
160158
return err
161159
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package completion
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/spf13/cobra"
8+
9+
cmdutil "k8s.io/kubectl/pkg/cmd/util"
10+
"k8s.io/kubectl/pkg/util/completion"
11+
12+
"github.com/ray-project/kuberay/kubectl-plugin/pkg/util"
13+
)
14+
15+
// RayResourceTypeCompletionFunc Returns a completion function that completes the Ray resource type.
16+
// That is, raycluster, rayjob, or rayservice.
17+
func RayResourceTypeCompletionFunc() func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
18+
return func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
19+
var comps []string
20+
directive := cobra.ShellCompDirectiveNoFileComp
21+
resourceTypes := getAllRayResourceType()
22+
for _, resourceType := range resourceTypes {
23+
if strings.HasPrefix(resourceType, toComplete) {
24+
comps = append(comps, resourceType)
25+
}
26+
}
27+
return comps, directive
28+
}
29+
}
30+
31+
// RayClusterCompletionFunc Returns a completion function that completes RayCluster resource names.
32+
func RayClusterCompletionFunc(f cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
33+
return completion.ResourceNameCompletionFunc(f, string(util.RayCluster))
34+
}
35+
36+
// RayJobCompletionFunc Returns a completion function that completes RayJob resource names.
37+
func RayJobCompletionFunc(f cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
38+
return completion.ResourceNameCompletionFunc(f, string(util.RayJob))
39+
}
40+
41+
// RayServiceCompletionFunc Returns a completion function that completes RayService resource names.
42+
func RayServiceCompletionFunc(f cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
43+
return completion.ResourceNameCompletionFunc(f, string(util.RayService))
44+
}
45+
46+
// RayClusterResourceNameCompletionFunc Returns completions of:
47+
// 1- RayCluster names that match the toComplete prefix
48+
// 2- Ray resource types which match the toComplete prefix
49+
func RayClusterResourceNameCompletionFunc(f cmdutil.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
50+
return func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
51+
var comps []string
52+
directive := cobra.ShellCompDirectiveNoFileComp
53+
if len(args) == 0 {
54+
comps, directive = doRayClusterCompletion(f, toComplete)
55+
}
56+
return comps, directive
57+
}
58+
}
59+
60+
func getAllRayResourceType() []string {
61+
return []string{
62+
string(util.RayCluster),
63+
string(util.RayJob),
64+
string(util.RayService),
65+
}
66+
}
67+
68+
// doRayClusterCompletion Returns completions of:
69+
// 1- RayCluster names that match the toComplete prefix
70+
// 2- Ray resource types which match the toComplete prefix
71+
// Ref: https://github.com/kubernetes/kubectl/blob/262825a8a665c7cae467dfaa42b63be5a5b8e5a2/pkg/util/completion/completion.go#L434
72+
func doRayClusterCompletion(f cmdutil.Factory, toComplete string) ([]string, cobra.ShellCompDirective) {
73+
var comps []string
74+
directive := cobra.ShellCompDirectiveNoFileComp
75+
slashIdx := strings.Index(toComplete, "/")
76+
if slashIdx == -1 {
77+
// Standard case, complete RayCluster names
78+
comps = completion.CompGetResource(f, string(util.RayCluster), toComplete)
79+
80+
// Also include resource choices for the <type>/<name> form
81+
resourceTypes := getAllRayResourceType()
82+
83+
if len(comps) == 0 {
84+
// If there are no RayCluster to complete, we will only be completing
85+
// <type>/. We should disable adding a space after the /.
86+
directive |= cobra.ShellCompDirectiveNoSpace
87+
}
88+
89+
for _, resource := range resourceTypes {
90+
if strings.HasPrefix(resource, toComplete) {
91+
comps = append(comps, fmt.Sprintf("%s/", resource))
92+
}
93+
}
94+
} else {
95+
// Dealing with the <type>/<name> form, use the specified resource type
96+
resourceType := toComplete[:slashIdx]
97+
toComplete = toComplete[slashIdx+1:]
98+
nameComps := completion.CompGetResource(f, resourceType, toComplete)
99+
for _, c := range nameComps {
100+
comps = append(comps, fmt.Sprintf("%s/%s", resourceType, c))
101+
}
102+
}
103+
return comps, directive
104+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package completion
2+
3+
import (
4+
"sort"
5+
"testing"
6+
7+
"github.com/spf13/cobra"
8+
)
9+
10+
func TestRayResourceTypeCompletionFunc(t *testing.T) {
11+
compFunc := RayResourceTypeCompletionFunc()
12+
comps, directive := compFunc(nil, []string{}, "")
13+
checkCompletion(t, comps, []string{"raycluster", "rayjob", "rayservice"}, directive, cobra.ShellCompDirectiveNoFileComp)
14+
}
15+
16+
func checkCompletion(t *testing.T, comps, expectedComps []string, directive, expectedDirective cobra.ShellCompDirective) {
17+
if e, d := expectedDirective, directive; e != d {
18+
t.Errorf("expected directive\n%v\nbut got\n%v", e, d)
19+
}
20+
21+
sort.Strings(comps)
22+
sort.Strings(expectedComps)
23+
24+
if len(expectedComps) != len(comps) {
25+
t.Fatalf("expected completions\n%v\nbut got\n%v", expectedComps, comps)
26+
}
27+
28+
for i := range comps {
29+
if expectedComps[i] != comps[i] {
30+
t.Errorf("expected completions\n%v\nbut got\n%v", expectedComps, comps)
31+
break
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)