Skip to content

Commit 22c2b45

Browse files
[Feature][kubectl-plugin] Implement kubectl ray session (#2298)
Signed-off-by: Chi-Sheng Liu <[email protected]>
1 parent 6cbb5df commit 22c2b45

File tree

5 files changed

+284
-25
lines changed

5 files changed

+284
-25
lines changed

kubectl-plugin/go.mod

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@ go 1.22.0
55
toolchain go1.22.5
66

77
require (
8-
github.com/gosuri/uitable v0.0.4
98
github.com/onsi/ginkgo/v2 v2.15.0
109
github.com/onsi/gomega v1.31.0
1110
github.com/spf13/cobra v1.8.1
1211
github.com/spf13/pflag v1.0.5
1312
github.com/stretchr/testify v1.8.4
14-
k8s.io/api v0.30.2
1513
k8s.io/apimachinery v0.30.2
1614
k8s.io/cli-runtime v0.30.2
1715
k8s.io/client-go v0.30.2
@@ -26,7 +24,8 @@ require (
2624
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
2725
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
2826
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
29-
github.com/fatih/color v1.17.0 // indirect
27+
github.com/fatih/camelcase v1.0.0 // indirect
28+
github.com/fvbommel/sortorder v1.1.0 // indirect
3029
github.com/go-errors/errors v1.4.2 // indirect
3130
github.com/go-logr/logr v1.4.1 // indirect
3231
github.com/go-openapi/jsonpointer v0.19.6 // indirect
@@ -50,9 +49,6 @@ require (
5049
github.com/json-iterator/go v1.1.12 // indirect
5150
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
5251
github.com/mailru/easyjson v0.7.7 // indirect
53-
github.com/mattn/go-colorable v0.1.13 // indirect
54-
github.com/mattn/go-isatty v0.0.20 // indirect
55-
github.com/mattn/go-runewidth v0.0.15 // indirect
5652
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
5753
github.com/moby/spdystream v0.2.0 // indirect
5854
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
@@ -64,7 +60,6 @@ require (
6460
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
6561
github.com/pkg/errors v0.9.1 // indirect
6662
github.com/pmezard/go-difflib v1.0.0 // indirect
67-
github.com/rivo/uniseg v0.2.0 // indirect
6863
github.com/russross/blackfriday/v2 v2.1.0 // indirect
6964
github.com/xlab/treeprint v1.2.0 // indirect
7065
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
@@ -81,6 +76,7 @@ require (
8176
gopkg.in/inf.v0 v0.9.1 // indirect
8277
gopkg.in/yaml.v2 v2.4.0 // indirect
8378
gopkg.in/yaml.v3 v3.0.1 // indirect
79+
k8s.io/api v0.30.2 // indirect
8480
k8s.io/component-base v0.30.2 // indirect
8581
k8s.io/klog/v2 v2.120.1 // indirect
8682
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect

kubectl-plugin/go.sum

Lines changed: 4 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

kubectl-plugin/pkg/cmd/ray.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package cmd
22

33
import (
4-
cluster "github.com/ray-project/kuberay/kubectl-plugin/pkg/cmd/cluster"
5-
"k8s.io/cli-runtime/pkg/genericclioptions"
4+
"k8s.io/cli-runtime/pkg/genericiooptions"
65

76
"github.com/spf13/cobra"
7+
8+
"github.com/ray-project/kuberay/kubectl-plugin/pkg/cmd/cluster"
9+
"github.com/ray-project/kuberay/kubectl-plugin/pkg/cmd/session"
810
)
911

10-
func NewRayCommand(streams genericclioptions.IOStreams) *cobra.Command {
12+
func NewRayCommand(streams genericiooptions.IOStreams) *cobra.Command {
1113
cmd := &cobra.Command{
1214
Use: "ray",
1315
Short: "ray kubectl plugin",
@@ -19,5 +21,6 @@ func NewRayCommand(streams genericclioptions.IOStreams) *cobra.Command {
1921
}
2022

2123
cmd.AddCommand(cluster.NewClusterCommand(streams))
24+
cmd.AddCommand(session.NewSessionCommand(streams))
2225
return cmd
2326
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package session
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
"k8s.io/cli-runtime/pkg/genericclioptions"
9+
"k8s.io/cli-runtime/pkg/genericiooptions"
10+
"k8s.io/client-go/kubernetes"
11+
"k8s.io/kubectl/pkg/cmd/portforward"
12+
cmdutil "k8s.io/kubectl/pkg/cmd/util"
13+
14+
"github.com/spf13/cobra"
15+
)
16+
17+
const (
18+
DASHBOARD_PORT = 8265
19+
CLIENT_PORT = 10001
20+
)
21+
22+
type SessionOptions struct {
23+
ioStreams *genericiooptions.IOStreams
24+
configFlags *genericclioptions.ConfigFlags
25+
ResourceName string
26+
Namespace string
27+
}
28+
29+
func NewSessionOptions(streams genericiooptions.IOStreams) *SessionOptions {
30+
return &SessionOptions{
31+
ioStreams: &streams,
32+
configFlags: genericclioptions.NewConfigFlags(true),
33+
}
34+
}
35+
36+
func NewSessionCommand(streams genericiooptions.IOStreams) *cobra.Command {
37+
options := NewSessionOptions(streams)
38+
factory := cmdutil.NewFactory(options.configFlags)
39+
40+
cmd := &cobra.Command{
41+
Use: "session NAME",
42+
Short: "Forward local ports to the Ray resources. Currently only supports RayCluster.",
43+
RunE: func(cmd *cobra.Command, args []string) error {
44+
if err := options.Complete(cmd, args); err != nil {
45+
return err
46+
}
47+
if err := options.Validate(); err != nil {
48+
return err
49+
}
50+
return options.Run(cmd.Context(), factory)
51+
},
52+
}
53+
options.configFlags.AddFlags(cmd.Flags())
54+
return cmd
55+
}
56+
57+
func (options *SessionOptions) Complete(cmd *cobra.Command, args []string) error {
58+
if len(args) != 1 {
59+
return cmdutil.UsageErrorf(cmd, "%s", cmd.Use)
60+
}
61+
options.ResourceName = args[0]
62+
63+
if *options.configFlags.Namespace == "" {
64+
options.Namespace = "default"
65+
} else {
66+
options.Namespace = *options.configFlags.Namespace
67+
}
68+
69+
return nil
70+
}
71+
72+
func (options *SessionOptions) Validate() error {
73+
// Overrides and binds the kube config then retrieves the merged result
74+
config, err := options.configFlags.ToRawKubeConfigLoader().RawConfig()
75+
if err != nil {
76+
return fmt.Errorf("Error retrieving raw config: %w", err)
77+
}
78+
if len(config.CurrentContext) == 0 {
79+
return fmt.Errorf("no context is currently set, use %q to select a new one", "kubectl config use-context <context>")
80+
}
81+
return nil
82+
}
83+
84+
func (options *SessionOptions) Run(ctx context.Context, factory cmdutil.Factory) error {
85+
kubeClientSet, err := factory.KubernetesClientSet()
86+
if err != nil {
87+
return fmt.Errorf("failed to initialize clientset: %w", err)
88+
}
89+
90+
svcName, err := findServiceName(ctx, kubeClientSet, options.Namespace, options.ResourceName)
91+
if err != nil {
92+
return err
93+
}
94+
95+
portForwardCmd := portforward.NewCmdPortForward(factory, *options.ioStreams)
96+
portForwardCmd.SetArgs([]string{svcName, fmt.Sprintf("%d:%d", DASHBOARD_PORT, DASHBOARD_PORT), fmt.Sprintf("%d:%d", CLIENT_PORT, CLIENT_PORT)})
97+
98+
fmt.Printf("Ray Dashboard: http://localhost:%d\nRay Interactive Client: http://localhost:%d\n\n", DASHBOARD_PORT, CLIENT_PORT)
99+
100+
if err := portForwardCmd.ExecuteContext(ctx); err != nil {
101+
return fmt.Errorf("failed to port-forward: %w", err)
102+
}
103+
104+
return nil
105+
}
106+
107+
func findServiceName(ctx context.Context, kubeClientSet kubernetes.Interface, namespace, resourceName string) (string, error) {
108+
listopts := metav1.ListOptions{
109+
LabelSelector: fmt.Sprintf("ray.io/cluster=%s, ray.io/node-type=head", resourceName),
110+
}
111+
112+
rayHeadSvcs, err := kubeClientSet.CoreV1().Services(namespace).List(ctx, listopts)
113+
if err != nil {
114+
return "", fmt.Errorf("unable to retrieve ray head services: %w", err)
115+
}
116+
117+
if len(rayHeadSvcs.Items) == 0 {
118+
return "", fmt.Errorf("no ray head services found")
119+
}
120+
if len(rayHeadSvcs.Items) > 1 {
121+
return "", fmt.Errorf("more than one ray head service found")
122+
}
123+
124+
rayHeadSrc := rayHeadSvcs.Items[0]
125+
return "service/" + rayHeadSrc.Name, nil
126+
}

0 commit comments

Comments
 (0)