-
Notifications
You must be signed in to change notification settings - Fork 426
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add kubectl ray delete rayservice/job/cluster (#2635)
- Loading branch information
Showing
3 changed files
with
283 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters