diff --git a/src/codeflare_sdk/cli/commands/define.py b/src/codeflare_sdk/cli/commands/define.py index 16b6fa480..09cfd1f0e 100644 --- a/src/codeflare_sdk/cli/commands/define.py +++ b/src/codeflare_sdk/cli/commands/define.py @@ -13,7 +13,7 @@ def cli(): @cli.command() @click.option("--name", type=str, required=True) -@click.option("--namespace", "-n", type=str) +@click.option("--namespace", "-n", type=str, required=True) @click.option("--head_info", cls=PythonLiteralOption, type=list) @click.option("--machine_types", cls=PythonLiteralOption, type=list) @click.option("--min_cpus", type=int) diff --git a/src/codeflare_sdk/cli/commands/delete.py b/src/codeflare_sdk/cli/commands/delete.py index c1ec12451..7ce9744bd 100644 --- a/src/codeflare_sdk/cli/commands/delete.py +++ b/src/codeflare_sdk/cli/commands/delete.py @@ -13,11 +13,15 @@ def cli(): @cli.command() @click.argument("name", type=str) -@click.option("--namespace", type=str, default="default") +@click.option("--namespace", type=str, required=True) def raycluster(name, namespace): """ Delete a specified RayCluster from the Kubernetes cluster """ - cluster = get_cluster(name, namespace) + try: + cluster = get_cluster(name, namespace) + except FileNotFoundError: + click.echo(f"Cluster {name} not found in {namespace} namespace") + return cluster.down() click.echo(f"Cluster deleted successfully") diff --git a/src/codeflare_sdk/cli/commands/details.py b/src/codeflare_sdk/cli/commands/details.py new file mode 100644 index 000000000..b865caa47 --- /dev/null +++ b/src/codeflare_sdk/cli/commands/details.py @@ -0,0 +1,23 @@ +import click + +from codeflare_sdk.cluster.cluster import get_cluster + + +@click.group() +def cli(): + """Get the details of a specified resource""" + pass + + +@cli.command() +@click.argument("name", type=str) +@click.option("--namespace", type=str, required=True) +@click.pass_context +def raycluster(ctx, name, namespace): + """Get the details of a specified RayCluster""" + try: + cluster = get_cluster(name, namespace) + except FileNotFoundError: + click.echo(f"Cluster {name} not found in {namespace} namespace") + return + cluster.details() diff --git a/src/codeflare_sdk/cli/commands/list.py b/src/codeflare_sdk/cli/commands/list.py new file mode 100644 index 000000000..dd3ad4e22 --- /dev/null +++ b/src/codeflare_sdk/cli/commands/list.py @@ -0,0 +1,33 @@ +import click +from kubernetes import client, config + +from codeflare_sdk.cluster.cluster import ( + list_clusters_all_namespaces, + list_all_clusters, + get_current_namespace, +) +from codeflare_sdk.cli.cli_utils import load_auth + + +@click.group() +def cli(): + """List a specified resource""" + pass + + +@cli.command() +@click.option("--namespace", type=str) +@click.option("--all", is_flag=True) +@click.pass_context +def rayclusters(ctx, namespace, all): + """List all rayclusters in a specified namespace""" + if all and namespace: + click.echo("--all and --namespace are mutually exclusive") + return + if not all and not namespace: + click.echo("You must specify either --namespace or --all") + return + if not all: + list_all_clusters(namespace) + return + list_clusters_all_namespaces() diff --git a/src/codeflare_sdk/cli/commands/status.py b/src/codeflare_sdk/cli/commands/status.py new file mode 100644 index 000000000..fc76ffc1d --- /dev/null +++ b/src/codeflare_sdk/cli/commands/status.py @@ -0,0 +1,23 @@ +import click + +from codeflare_sdk.cluster.cluster import get_cluster + + +@click.group() +def cli(): + """Get the status of a specified resource""" + pass + + +@cli.command() +@click.argument("name", type=str) +@click.option("--namespace", type=str, required=True) +@click.pass_context +def raycluster(ctx, name, namespace): + """Get the status of a specified RayCluster""" + try: + cluster = get_cluster(name, namespace) + except FileNotFoundError: + click.echo(f"Cluster {name} not found in {namespace} namespace") + return + cluster.status() diff --git a/src/codeflare_sdk/cluster/cluster.py b/src/codeflare_sdk/cluster/cluster.py index b0075dfc3..a25dd1b9d 100644 --- a/src/codeflare_sdk/cluster/cluster.py +++ b/src/codeflare_sdk/cluster/cluster.py @@ -412,7 +412,17 @@ def list_all_clusters(namespace: str, print_to_console: bool = True): """ Returns (and prints by default) a list of all clusters in a given namespace. """ - clusters = _get_ray_clusters(namespace) + clusters = _get_ray_clusters_in_namespace(namespace) + if print_to_console: + pretty_print.print_clusters(clusters) + return clusters + + +def list_clusters_all_namespaces(print_to_console: bool = True): + """ + Returns (and prints by default) a list of all clusters in the Kubernetes cluster. + """ + clusters = _get_all_ray_clusters() if print_to_console: pretty_print.print_clusters(clusters) return clusters @@ -529,7 +539,7 @@ def _ray_cluster_status(name, namespace="default") -> Optional[RayCluster]: return None -def _get_ray_clusters(namespace="default") -> List[RayCluster]: +def _get_ray_clusters_in_namespace(namespace="default") -> List[RayCluster]: list_of_clusters = [] try: config_check() @@ -548,6 +558,23 @@ def _get_ray_clusters(namespace="default") -> List[RayCluster]: return list_of_clusters +def _get_all_ray_clusters() -> List[RayCluster]: + list_of_clusters = [] + try: + config_check() + api_instance = client.CustomObjectsApi(api_config_handler()) + rcs = api_instance.list_cluster_custom_object( + group="ray.io", + version="v1alpha1", + plural="rayclusters", + ) + except Exception as e: + return _kube_api_error_handling(e) + for rc in rcs["items"]: + list_of_clusters.append(_map_to_ray_cluster(rc)) + return list_of_clusters + + def _get_app_wrappers( namespace="default", filter=List[AppWrapperStatus] ) -> List[AppWrapper]: diff --git a/tests/unit_test.py b/tests/unit_test.py index 783ec928f..45b70382d 100644 --- a/tests/unit_test.py +++ b/tests/unit_test.py @@ -34,6 +34,7 @@ get_cluster, _app_wrapper_status, _ray_cluster_status, + list_clusters_all_namespaces, ) from codeflare_sdk.cluster.auth import ( TokenAuthentication, @@ -200,7 +201,7 @@ def test_cluster_deletion_cli(mocker): runner = CliRunner() delete_cluster_command = """ delete raycluster - quicktest + quicktest --namespace=default """ result = runner.invoke(cli, delete_cluster_command) @@ -208,6 +209,160 @@ def test_cluster_deletion_cli(mocker): assert "Cluster deleted successfully" in result.output +def test_list_clusters_all_namespaces(mocker, capsys): + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_cluster_custom_object", + side_effect=get_ray_obj_no_namespace, + ) + list_clusters_all_namespaces() + captured = capsys.readouterr() + assert captured.out == ( + " ๐Ÿš€ CodeFlare Cluster Details ๐Ÿš€ \n" + " \n" + " โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ \n" + " โ”‚ Name โ”‚ \n" + " โ”‚ quicktest Active โœ… โ”‚ \n" + " โ”‚ โ”‚ \n" + " โ”‚ URI: ray://quicktest-head-svc.ns.svc:10001 โ”‚ \n" + " โ”‚ โ”‚ \n" + " โ”‚ Dashboard๐Ÿ”— โ”‚ \n" + " โ”‚ โ”‚ \n" + " โ”‚ Cluster Resources โ”‚ \n" + " โ”‚ โ•ญโ”€ Workers โ”€โ”€โ•ฎ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Worker specs(each) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ \n" + " โ”‚ โ”‚ Min Max โ”‚ โ”‚ Memory CPU GPU โ”‚ โ”‚ \n" + " โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ \n" + " โ”‚ โ”‚ 1 1 โ”‚ โ”‚ 2G~2G 1 0 โ”‚ โ”‚ \n" + " โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ \n" + " โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚ \n" + " โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ \n" + ) + + +def test_raycluster_details_cli(mocker): + runner = CliRunner() + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + side_effect=get_ray_obj, + ) + mocker.patch( + "codeflare_sdk.cluster.cluster.Cluster.status", + return_value=(False, CodeFlareClusterStatus.UNKNOWN), + ) + mocker.patch( + "codeflare_sdk.cluster.cluster.Cluster.cluster_dashboard_uri", + return_value="", + ) + mocker.patch.object(client, "ApiClient") + raycluster_details_command = """ + details raycluster quicktest --namespace=default + """ + result = runner.invoke(cli, raycluster_details_command) + quicktest_details = ( + " โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ \n" + + " โ”‚ Name โ”‚ \n" + + " โ”‚ quicktest Inactive โŒ โ”‚ \n" + + " โ”‚ โ”‚ \n" + + " โ”‚ URI: ray://quicktest-head-svc.ns.svc:10001 โ”‚ \n" + + " โ”‚ โ”‚ \n" + + " โ”‚ Dashboard๐Ÿ”— โ”‚ \n" + + " โ”‚ โ”‚ \n" + + " โ”‚ Cluster Resources โ”‚ \n" + + " โ”‚ โ•ญโ”€ Workers โ”€โ”€โ•ฎ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Worker specs(each) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ \n" + + " โ”‚ โ”‚ Min Max โ”‚ โ”‚ Memory CPU GPU โ”‚ โ”‚ \n" + + " โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ \n" + + " โ”‚ โ”‚ 1 1 โ”‚ โ”‚ 2~2 1 0 โ”‚ โ”‚ \n" + + " โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ \n" + + " โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚ \n" + + " โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ " + ) + assert quicktest_details in result.output + + +def test_raycluster_status_cli(mocker): + runner = CliRunner() + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + side_effect=get_ray_obj, + ) + mocker.patch( + "codeflare_sdk.cluster.cluster.get_current_namespace", + return_value="ns", + ) + mocker.patch( + "codeflare_sdk.cluster.cluster.Cluster.cluster_dashboard_uri", + return_value="", + ) + mocker.patch.object(client, "ApiClient") + test_raycluster = RayCluster( + "quicktest", + RayClusterStatus.READY, + 1, + 1, + "1", + "1", + 1, + 1, + "default", + "dashboard-url", + ) + mocker.patch( + "codeflare_sdk.cluster.cluster._app_wrapper_status", + return_value=test_raycluster, + ) + mocker.patch( + "codeflare_sdk.cluster.cluster._ray_cluster_status", + return_value=test_raycluster, + ) + raycluster_status_command = """ + status raycluster quicktest --namespace=default + """ + result = runner.invoke(cli, raycluster_status_command) + assert "Active" in result.output + + +def test_raycluster_list_cli(mocker): + runner = CliRunner() + mocker.patch( + "kubernetes.client.CustomObjectsApi.list_namespaced_custom_object", + side_effect=get_ray_obj, + ) + mocker.patch( + "codeflare_sdk.cluster.cluster.get_current_namespace", + return_value="ns", + ) + mocker.patch( + "codeflare_sdk.cluster.cluster.Cluster.status", + return_value=(False, CodeFlareClusterStatus.UNKNOWN), + ) + mocker.patch( + "codeflare_sdk.cluster.cluster.Cluster.cluster_dashboard_uri", + return_value="", + ) + mocker.patch.object(client, "ApiClient") + list_rayclusters_command = """ + list rayclusters --namespace=ns + """ + result = runner.invoke(cli, list_rayclusters_command) + assert ( + " โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ \n" + + " โ”‚ Name โ”‚ \n" + + " โ”‚ quicktest Active โœ… โ”‚ \n" + + " โ”‚ โ”‚ \n" + + " โ”‚ URI: ray://quicktest-head-svc.ns.svc:10001 โ”‚ \n" + + " โ”‚ โ”‚ \n" + + " โ”‚ Dashboard๐Ÿ”— โ”‚ \n" + + " โ”‚ โ”‚ \n" + + " โ”‚ Cluster Resources โ”‚ \n" + + " โ”‚ โ•ญโ”€ Workers โ”€โ”€โ•ฎ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Worker specs(each) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ \n" + + " โ”‚ โ”‚ Min Max โ”‚ โ”‚ Memory CPU GPU โ”‚ โ”‚ \n" + + " โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ \n" + + " โ”‚ โ”‚ 1 1 โ”‚ โ”‚ 2G~2G 1 0 โ”‚ โ”‚ \n" + + " โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ \n" + + " โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚ \n" + + " โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ " + ) in result.output + + # For mocking openshift client results fake_res = openshift.Result("fake") @@ -992,6 +1147,10 @@ def get_ray_obj(group, version, namespace, plural, cls=None): return api_obj +def get_ray_obj_no_namespace(group, version, plural, cls=None): + return get_ray_obj(group, version, "ns", plural, cls) + + def get_aw_obj(group, version, namespace, plural): api_obj1 = { "items": [ @@ -2360,4 +2519,3 @@ def test_cleanup(): os.remove("tls-cluster-namespace/tls.key") os.rmdir("tls-cluster-namespace") os.remove("cli-test-cluster.yaml") - os.removedirs(os.path.expanduser("~/.codeflare"))