Skip to content

Commit 5d3bceb

Browse files
[Feat][Kubectl-Plugin] Implement kubectl session for RayJob and RayService (#2379)
1 parent 800ac16 commit 5d3bceb

File tree

6 files changed

+615
-147
lines changed

6 files changed

+615
-147
lines changed

kubectl-plugin/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/spf13/cobra v1.8.1
1111
github.com/spf13/pflag v1.0.5
1212
github.com/stretchr/testify v1.8.4
13+
k8s.io/api v0.30.2
1314
k8s.io/apimachinery v0.30.2
1415
k8s.io/cli-runtime v0.30.2
1516
k8s.io/client-go v0.30.2
@@ -76,7 +77,6 @@ require (
7677
gopkg.in/inf.v0 v0.9.1 // indirect
7778
gopkg.in/yaml.v2 v2.4.0 // indirect
7879
gopkg.in/yaml.v3 v3.0.1 // indirect
79-
k8s.io/api v0.30.2 // indirect
8080
k8s.io/component-base v0.30.2 // indirect
8181
k8s.io/klog/v2 v2.120.1 // indirect
8282
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect

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

Lines changed: 111 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,93 @@ package session
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

7-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
"github.com/ray-project/kuberay/kubectl-plugin/pkg/util"
9+
"github.com/ray-project/kuberay/kubectl-plugin/pkg/util/client"
10+
"github.com/spf13/cobra"
811
"k8s.io/cli-runtime/pkg/genericclioptions"
912
"k8s.io/cli-runtime/pkg/genericiooptions"
10-
"k8s.io/client-go/kubernetes"
1113
"k8s.io/kubectl/pkg/cmd/portforward"
1214
cmdutil "k8s.io/kubectl/pkg/cmd/util"
13-
14-
"github.com/spf13/cobra"
15+
"k8s.io/kubectl/pkg/util/templates"
1516
)
1617

17-
const (
18-
DASHBOARD_PORT = 8265
19-
CLIENT_PORT = 10001
20-
)
18+
type appPort struct {
19+
name string
20+
port int
21+
}
2122

2223
type SessionOptions struct {
23-
ioStreams *genericiooptions.IOStreams
2424
configFlags *genericclioptions.ConfigFlags
25+
ioStreams *genericiooptions.IOStreams
26+
client client.Client
27+
ResourceType util.ResourceType
2528
ResourceName string
2629
Namespace string
2730
}
2831

32+
var (
33+
dashboardPort = appPort{
34+
name: "Ray Dashboard",
35+
port: 8265,
36+
}
37+
clientPort = appPort{
38+
name: "Ray Interactive Client",
39+
port: 10001,
40+
}
41+
servePort = appPort{
42+
name: "Ray Serve",
43+
port: 8000,
44+
}
45+
)
46+
47+
var (
48+
sessionLong = templates.LongDesc(`
49+
Forward local ports to the Ray resources.
50+
51+
Forward different local ports depending on the resource type: RayCluster, RayJob, or RayService.
52+
`)
53+
54+
sessionExample = templates.Examples(`
55+
# Without specifying the resource type, forward local ports to the RayCluster resource
56+
kubectl ray session my-raycluster
57+
58+
# Forward local ports to the RayCluster resource
59+
kubectl ray session raycluster/my-raycluster
60+
61+
# Forward local ports to the RayCluster used for the RayJob resource
62+
kubectl ray session rayjob/my-rayjob
63+
64+
# Forward local ports to the RayCluster used for the RayService resource
65+
kubectl ray session rayservice/my-rayservice
66+
`)
67+
)
68+
2969
func NewSessionOptions(streams genericiooptions.IOStreams) *SessionOptions {
70+
configFlags := genericclioptions.NewConfigFlags(true)
3071
return &SessionOptions{
3172
ioStreams: &streams,
32-
configFlags: genericclioptions.NewConfigFlags(true),
73+
configFlags: configFlags,
3374
}
3475
}
3576

3677
func NewSessionCommand(streams genericiooptions.IOStreams) *cobra.Command {
3778
options := NewSessionOptions(streams)
38-
factory := cmdutil.NewFactory(options.configFlags)
3979

4080
cmd := &cobra.Command{
41-
Use: "session NAME",
42-
Short: "Forward local ports to the Ray resources. Currently only supports RayCluster.",
81+
Use: "session (RAYCLUSTER | TYPE/NAME)",
82+
Short: "Forward local ports to the Ray resources.",
83+
Long: sessionLong,
84+
Example: sessionExample,
4385
RunE: func(cmd *cobra.Command, args []string) error {
4486
if err := options.Complete(cmd, args); err != nil {
4587
return err
4688
}
4789
if err := options.Validate(); err != nil {
4890
return err
4991
}
50-
return options.Run(cmd.Context(), factory)
92+
return options.Run(cmd.Context())
5193
},
5294
}
5395
options.configFlags.AddFlags(cmd.Flags())
@@ -58,14 +100,43 @@ func (options *SessionOptions) Complete(cmd *cobra.Command, args []string) error
58100
if len(args) != 1 {
59101
return cmdutil.UsageErrorf(cmd, "%s", cmd.Use)
60102
}
61-
options.ResourceName = args[0]
103+
104+
typeAndName := strings.Split(args[0], "/")
105+
if len(typeAndName) == 1 {
106+
options.ResourceType = util.RayCluster
107+
options.ResourceName = typeAndName[0]
108+
} else {
109+
if len(typeAndName) != 2 || typeAndName[1] == "" {
110+
return cmdutil.UsageErrorf(cmd, "invalid resource type/name: %s", args[0])
111+
}
112+
113+
switch typeAndName[0] {
114+
case string(util.RayCluster):
115+
options.ResourceType = util.RayCluster
116+
case string(util.RayJob):
117+
options.ResourceType = util.RayJob
118+
case string(util.RayService):
119+
options.ResourceType = util.RayService
120+
default:
121+
return cmdutil.UsageErrorf(cmd, "unsupported resource type: %s", typeAndName[0])
122+
}
123+
124+
options.ResourceName = typeAndName[1]
125+
}
62126

63127
if *options.configFlags.Namespace == "" {
64128
options.Namespace = "default"
65129
} else {
66130
options.Namespace = *options.configFlags.Namespace
67131
}
68132

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+
69140
return nil
70141
}
71142

@@ -81,46 +152,42 @@ func (options *SessionOptions) Validate() error {
81152
return nil
82153
}
83154

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-
}
155+
func (options *SessionOptions) Run(ctx context.Context) error {
156+
factory := cmdutil.NewFactory(options.configFlags)
89157

90-
svcName, err := findServiceName(ctx, kubeClientSet, options.Namespace, options.ResourceName)
158+
svcName, err := options.client.GetRayHeadSvcName(ctx, options.Namespace, options.ResourceType, options.ResourceName)
91159
if err != nil {
92160
return err
93161
}
162+
fmt.Printf("Forwarding ports to service %s\n", svcName)
163+
164+
var appPorts []appPort
165+
switch options.ResourceType {
166+
case util.RayCluster:
167+
appPorts = []appPort{dashboardPort, clientPort}
168+
case util.RayJob:
169+
appPorts = []appPort{dashboardPort}
170+
case util.RayService:
171+
appPorts = []appPort{dashboardPort, servePort}
172+
default:
173+
return fmt.Errorf("unsupported resource type: %s", options.ResourceType)
174+
}
94175

95176
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)})
177+
args := []string{"service/" + svcName}
178+
for _, appPort := range appPorts {
179+
args = append(args, fmt.Sprintf("%d:%d", appPort.port, appPort.port))
180+
}
181+
portForwardCmd.SetArgs(args)
97182

98-
fmt.Printf("Ray Dashboard: http://localhost:%d\nRay Interactive Client: http://localhost:%d\n\n", DASHBOARD_PORT, CLIENT_PORT)
183+
for _, appPort := range appPorts {
184+
fmt.Printf("%s: http://localhost:%d\n", appPort.name, appPort.port)
185+
}
186+
fmt.Println()
99187

100188
if err := portForwardCmd.ExecuteContext(ctx); err != nil {
101189
return fmt.Errorf("failed to port-forward: %w", err)
102190
}
103191

104192
return nil
105193
}
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)