Skip to content

Status, Details, and List rayclusters CLI Functions #258

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 3, 2023
2 changes: 1 addition & 1 deletion src/codeflare_sdk/cli/commands/define.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 6 additions & 2 deletions src/codeflare_sdk/cli/commands/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its a little awkward that delete command returns a "Written to: xyz.yaml` message. Anyway we can avoid this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

except FileNotFoundError:
click.echo(f"Cluster {name} not found in {namespace} namespace")
return
cluster.down()
click.echo(f"Cluster deleted successfully")
23 changes: 23 additions & 0 deletions src/codeflare_sdk/cli/commands/details.py
Original file line number Diff line number Diff line change
@@ -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()
33 changes: 33 additions & 0 deletions src/codeflare_sdk/cli/commands/list.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be raycluster? Its a little confusing when sometimes we want to use raycluster and othertimes we use rayclusters

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll create an alias so the user can do either

"""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()
23 changes: 23 additions & 0 deletions src/codeflare_sdk/cli/commands/status.py
Original file line number Diff line number Diff line change
@@ -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()
31 changes: 29 additions & 2 deletions src/codeflare_sdk/cluster/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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
Comment on lines +561 to +575
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add this functionality into the original _get_ray_clusters() function?

the idea is that by default, _get_ray_clusters() get from all namespaces. If you provide a namespace, then it will select only from that namespace.

@Maxusmusti WDYT?



def _get_app_wrappers(
namespace="default", filter=List[AppWrapperStatus]
) -> List[AppWrapper]:
Expand Down
162 changes: 160 additions & 2 deletions tests/unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
get_cluster,
_app_wrapper_status,
_ray_cluster_status,
list_clusters_all_namespaces,
)
from codeflare_sdk.cluster.auth import (
TokenAuthentication,
Expand Down Expand Up @@ -200,14 +201,168 @@ def test_cluster_deletion_cli(mocker):
runner = CliRunner()
delete_cluster_command = """
delete raycluster
quicktest
quicktest --namespace=default
"""
result = runner.invoke(cli, delete_cluster_command)

assert result.exit_code == 0
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")

Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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"))