diff --git a/kubectl-plugin/pkg/cmd/get/get.go b/kubectl-plugin/pkg/cmd/get/get.go index 530f5f78bc1..c634d00410a 100644 --- a/kubectl-plugin/pkg/cmd/get/get.go +++ b/kubectl-plugin/pkg/cmd/get/get.go @@ -27,6 +27,7 @@ func NewGetCommand(cmdFactory cmdutil.Factory, streams genericclioptions.IOStrea cmd.AddCommand(NewGetClusterCommand(cmdFactory, streams)) cmd.AddCommand(NewGetWorkerGroupCommand(cmdFactory, streams)) cmd.AddCommand(NewGetNodesCommand(cmdFactory, streams)) + cmd.AddCommand(NewGetTokenCommand(cmdFactory, streams)) return cmd } diff --git a/kubectl-plugin/pkg/cmd/get/get_token.go b/kubectl-plugin/pkg/cmd/get/get_token.go new file mode 100644 index 00000000000..7c99a3ab4a9 --- /dev/null +++ b/kubectl-plugin/pkg/cmd/get/get_token.go @@ -0,0 +1,90 @@ +package get + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + "github.com/ray-project/kuberay/kubectl-plugin/pkg/util/client" + "github.com/ray-project/kuberay/kubectl-plugin/pkg/util/completion" + rayv1 "github.com/ray-project/kuberay/ray-operator/apis/ray/v1" + "github.com/ray-project/kuberay/ray-operator/controllers/ray/utils" +) + +type GetTokenOptions struct { + cmdFactory cmdutil.Factory + ioStreams *genericclioptions.IOStreams + namespace string + cluster string +} + +func NewGetTokenOptions(cmdFactory cmdutil.Factory, streams genericclioptions.IOStreams) *GetTokenOptions { + return &GetTokenOptions{ + cmdFactory: cmdFactory, + ioStreams: &streams, + } +} + +func NewGetTokenCommand(cmdFactory cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + options := NewGetTokenOptions(cmdFactory, streams) + + cmd := &cobra.Command{ + Use: "token [CLUSTER NAME]", + Aliases: []string{"token"}, + Short: "Get the auth token from the ray cluster.", + SilenceUsage: true, + ValidArgsFunction: completion.RayClusterCompletionFunc(cmdFactory), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := options.Complete(args, cmd); err != nil { + return err + } + // running cmd.Execute or cmd.ExecuteE sets the context, which will be done by root + k8sClient, err := client.NewClient(cmdFactory) + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + return options.Run(cmd.Context(), k8sClient) + }, + } + return cmd +} + +func (options *GetTokenOptions) Complete(args []string, cmd *cobra.Command) error { + namespace, err := cmd.Flags().GetString("namespace") + if err != nil { + return fmt.Errorf("failed to get namespace: %w", err) + } + options.namespace = namespace + if options.namespace == "" { + options.namespace = "default" + } + // guarded by cobra.ExactArgs(1) + options.cluster = args[0] + return nil +} + +func (options *GetTokenOptions) Run(ctx context.Context, k8sClient client.Client) error { + cluster, err := k8sClient.RayClient().RayV1().RayClusters(options.namespace).Get(ctx, options.cluster, v1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get RayCluster %s/%s: %w", options.namespace, options.cluster, err) + } + if cluster.Spec.AuthOptions == nil || cluster.Spec.AuthOptions.Mode != rayv1.AuthModeToken { + return fmt.Errorf("RayCluster %s/%s was not configured to use authentication tokens", options.namespace, options.cluster) + } + // TODO: support custom token secret? + secret, err := k8sClient.KubernetesClient().CoreV1().Secrets(options.namespace).Get(ctx, options.cluster, v1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get secret %s/%s: %w", options.namespace, options.cluster, err) + } + if token, ok := secret.Data[utils.RAY_AUTH_TOKEN_SECRET_KEY]; ok { + _, err = fmt.Fprint(options.ioStreams.Out, string(token)) + } else { + err = fmt.Errorf("secret %s/%s does not have an auth_token", options.namespace, options.cluster) + } + return err +} diff --git a/kubectl-plugin/pkg/cmd/get/get_token_test.go b/kubectl-plugin/pkg/cmd/get/get_token_test.go new file mode 100644 index 00000000000..e09d16942a0 --- /dev/null +++ b/kubectl-plugin/pkg/cmd/get/get_token_test.go @@ -0,0 +1,61 @@ +package get + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + kubefake "k8s.io/client-go/kubernetes/fake" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + "github.com/ray-project/kuberay/kubectl-plugin/pkg/util/client" + rayv1 "github.com/ray-project/kuberay/ray-operator/apis/ray/v1" + rayClientFake "github.com/ray-project/kuberay/ray-operator/pkg/client/clientset/versioned/fake" +) + +// Tests the Run() step of the command and ensure that the output is as expected. +func TestTokenGetRun(t *testing.T) { + cmdFactory := cmdutil.NewFactory(genericclioptions.NewConfigFlags(true)) + + testStreams, _, resBuf, _ := genericclioptions.NewTestIOStreams() + fakeTokenGetOptions := NewGetTokenOptions(cmdFactory, testStreams) + + rayCluster := &rayv1.RayCluster{ + ObjectMeta: v1.ObjectMeta{ + Name: "raycluster-kuberay", + Namespace: "test", + }, + Spec: rayv1.RayClusterSpec{ + AuthOptions: &rayv1.AuthOptions{ + Mode: rayv1.AuthModeToken, + }, + }, + } + + secret := &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: "raycluster-kuberay", + Namespace: "test", + }, + Data: map[string][]byte{ + "auth_token": []byte("token"), + }, + } + + kubeClientSet := kubefake.NewClientset(secret) + rayClient := rayClientFake.NewSimpleClientset(rayCluster) + k8sClients := client.NewClientForTesting(kubeClientSet, rayClient) + + cmd := &cobra.Command{} + cmd.Flags().StringVarP(&fakeTokenGetOptions.namespace, "namespace", "n", secret.Namespace, "") + err := fakeTokenGetOptions.Complete([]string{rayCluster.Name}, cmd) + require.NoError(t, err) + err = fakeTokenGetOptions.Run(t.Context(), k8sClients) + require.NoError(t, err) + + assert.Equal(t, secret.Data["auth_token"], resBuf.Bytes()) +}