Skip to content

Commit

Permalink
Add kubectl ray delete rayservice/job/cluster (#2635)
Browse files Browse the repository at this point in the history
  • Loading branch information
chiayi authored Dec 16, 2024
1 parent 4f9d0a6 commit 61a282f
Show file tree
Hide file tree
Showing 3 changed files with 283 additions and 0 deletions.
169 changes: 169 additions & 0 deletions kubectl-plugin/pkg/cmd/delete/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package kubectlraydelete

import (
"bufio"
"context"
"fmt"
"os"
"strings"

"github.com/ray-project/kuberay/kubectl-plugin/pkg/util"
"github.com/ray-project/kuberay/kubectl-plugin/pkg/util/client"
"github.com/ray-project/kuberay/kubectl-plugin/pkg/util/completion"
"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/templates"
)

type DeleteOptions struct {
configFlags *genericclioptions.ConfigFlags
ioStreams *genericiooptions.IOStreams
ResourceType util.ResourceType
ResourceName string
Namespace string
}

var deleteExample = templates.Examples(`
# Delete RayCluster
kubectl ray delete sample-raycluster
# Delete RayCluster with specificed ray resource
kubectl ray delete raycluster/sample-raycluster
# Delete RayJob
kubectl ray delete rayjob/sample-rayjob
# Delete RayService
kubectl ray delete rayservice/sample-rayservice
`)

func NewDeleteOptions(streams genericiooptions.IOStreams) *DeleteOptions {
configFlags := genericclioptions.NewConfigFlags(true)
return &DeleteOptions{
ioStreams: &streams,
configFlags: configFlags,
}
}

func NewDeleteCommand(streams genericclioptions.IOStreams) *cobra.Command {
options := NewDeleteOptions(streams)
factory := cmdutil.NewFactory(options.configFlags)

cmd := &cobra.Command{
Use: "delete (RAYCLUSTER | TYPE/NAME)",
Short: "Delete Ray resoruce.",
Example: deleteExample,
Long: `Deletes Ray custom resources such as RayCluster, RayService, or RayJob`,
ValidArgsFunction: completion.RayClusterResourceNameCompletionFunc(factory),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if err := options.Complete(cmd, args); err != nil {
return err
}
if err := options.Validate(); err != nil {
return err
}
return options.Run(cmd.Context(), factory)
},
}

options.configFlags.AddFlags(cmd.Flags())
return cmd
}

func (options *DeleteOptions) Complete(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return cmdutil.UsageErrorf(cmd, "%s", cmd.Use)
}

if *options.configFlags.Namespace == "" {
options.Namespace = "default"
} else {
options.Namespace = *options.configFlags.Namespace
}

typeAndName := strings.Split(args[0], "/")
if len(typeAndName) == 1 {
options.ResourceType = util.RayCluster
options.ResourceName = typeAndName[0]
} else {
if len(typeAndName) != 2 || typeAndName[1] == "" {
return cmdutil.UsageErrorf(cmd, "invalid resource type/name: %s", args[0])
}

switch strings.ToLower(typeAndName[0]) {
case string(util.RayCluster):
options.ResourceType = util.RayCluster
case string(util.RayJob):
options.ResourceType = util.RayJob
case string(util.RayService):
options.ResourceType = util.RayService
default:
return cmdutil.UsageErrorf(cmd, "unsupported resource type: %s", args[0])
}

options.ResourceName = typeAndName[1]
}

return nil
}

func (options *DeleteOptions) Validate() error {
// Overrides and binds the kube config then retrieves the merged result
config, err := options.configFlags.ToRawKubeConfigLoader().RawConfig()
if err != nil {
return fmt.Errorf("Error retrieving raw config: %w", err)
}
if len(config.CurrentContext) == 0 {
return fmt.Errorf("no context is currently set, use %q to select a new one", "kubectl config use-context <context>")
}
return nil
}

func (options *DeleteOptions) Run(ctx context.Context, factory cmdutil.Factory) error {
k8sClient, err := client.NewClient(factory)
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}

// Ask user for confirmation
reader := bufio.NewReader(os.Stdin)
fmt.Printf("Are you sure you want to delete %s %s? (y/yes/n/no) ", options.ResourceType, options.ResourceName)
confirmation, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("Failed to read user input: %w", err)
}

switch strings.ToLower(strings.TrimSpace(confirmation)) {
case "y", "yes":
case "n", "no":
fmt.Printf("Canceled deletion.\n")
return nil
default:
fmt.Printf("Unknown input %s\n", confirmation)
return nil
}

// Delete the Ray Resources
switch options.ResourceType {
case util.RayCluster:
err = k8sClient.RayClient().RayV1().RayClusters(options.Namespace).Delete(ctx, options.ResourceName, metav1.DeleteOptions{})
case util.RayJob:
err = k8sClient.RayClient().RayV1().RayJobs(options.Namespace).Delete(ctx, options.ResourceName, metav1.DeleteOptions{})
case util.RayService:
err = k8sClient.RayClient().RayV1().RayServices(options.Namespace).Delete(ctx, options.ResourceName, metav1.DeleteOptions{})
default:
err = fmt.Errorf("unknown/unsupported resource type: %s", options.ResourceType)
}

if err != nil {
return fmt.Errorf("Failed to delete %s/%s: %w", options.ResourceType, options.ResourceName, err)
}

fmt.Printf("Delete %s %s\n", options.ResourceType, options.ResourceName)
return nil
}
112 changes: 112 additions & 0 deletions kubectl-plugin/pkg/cmd/delete/delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package kubectlraydelete

import (
"testing"

"github.com/ray-project/kuberay/kubectl-plugin/pkg/util"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"k8s.io/cli-runtime/pkg/genericclioptions"
)

func TestComplete(t *testing.T) {
cmd := &cobra.Command{Use: "deleete"}

tests := []struct {
name string
namespace string
expectedResourceType util.ResourceType
expectedNamespace string
expectedName string
args []string
hasErr bool
}{
{
name: "valid raycluster without explicit resource and without namespace",
namespace: "",
expectedResourceType: util.RayCluster,
expectedNamespace: "default",
expectedName: "test-raycluster",
args: []string{"test-raycluster"},
hasErr: false,
},
{
name: "valid raycluster with explicit resource and with namespace",
namespace: "test-namespace",
expectedResourceType: util.RayCluster,
expectedNamespace: "test-namespace",
expectedName: "test-raycluster",
args: []string{"raycluster/test-raycluster"},
hasErr: false,
},
{
name: "valid raycluster without explicit resource and with namespace",
namespace: "test-namespace",
expectedResourceType: util.RayCluster,
expectedNamespace: "test-namespace",
expectedName: "test-raycluster",
args: []string{"test-raycluster"},
hasErr: false,
},
{
name: "valid rayjob with namespace",
namespace: "test-namespace",
expectedResourceType: util.RayJob,
expectedNamespace: "test-namespace",
expectedName: "test-rayjob",
args: []string{"rayjob/test-rayjob"},
hasErr: false,
},
{
name: "valid rayservice with namespace",
namespace: "test-namespace",
expectedResourceType: util.RayService,
expectedNamespace: "test-namespace",
expectedName: "test-rayservice",
args: []string{"rayservice/test-rayservice"},
hasErr: false,
},
{
name: "invalid service type",
namespace: "test-namespace",
args: []string{"rayserve/test-rayserve"},
hasErr: true,
},
{
name: "valid raycluster with namespace but weird ray type casing",
namespace: "test-namespace",
expectedResourceType: util.RayCluster,
expectedNamespace: "test-namespace",
expectedName: "test-raycluster",
args: []string{"rayCluStER/test-raycluster"},
hasErr: false,
},
{
name: "invalid args, too many args",
args: []string{"test", "raytype", "raytypename"},
hasErr: true,
},
{
name: "invalid args, non valid resource type",
args: []string{"test/test"},
hasErr: true,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
testStreams, _, _, _ := genericclioptions.NewTestIOStreams()
fakeDeleteOptions := NewDeleteOptions(testStreams)
fakeDeleteOptions.configFlags.Namespace = &tc.namespace
err := fakeDeleteOptions.Complete(cmd, tc.args)
if tc.hasErr {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
assert.Equal(t, tc.expectedName, fakeDeleteOptions.ResourceName)
assert.Equal(t, tc.expectedNamespace, fakeDeleteOptions.Namespace)
assert.Equal(t, tc.expectedResourceType, fakeDeleteOptions.ResourceType)
}
})
}
}
2 changes: 2 additions & 0 deletions kubectl-plugin/pkg/cmd/ray.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/spf13/cobra"

"github.com/ray-project/kuberay/kubectl-plugin/pkg/cmd/create"
kubectlraydelete "github.com/ray-project/kuberay/kubectl-plugin/pkg/cmd/delete"
"github.com/ray-project/kuberay/kubectl-plugin/pkg/cmd/get"
"github.com/ray-project/kuberay/kubectl-plugin/pkg/cmd/job"
"github.com/ray-project/kuberay/kubectl-plugin/pkg/cmd/log"
Expand Down Expand Up @@ -33,6 +34,7 @@ func NewRayCommand(streams genericiooptions.IOStreams) *cobra.Command {
cmd.AddCommand(job.NewJobCommand(streams))
cmd.AddCommand(version.NewVersionCommand(streams))
cmd.AddCommand(create.NewCreateCommand(streams))
cmd.AddCommand(kubectlraydelete.NewDeleteCommand(streams))

return cmd
}

0 comments on commit 61a282f

Please sign in to comment.