Skip to content

Commit 68dff40

Browse files
Merge pull request #258 from carsonmh/view-rayclusters-cli
Status, Details, and List rayclusters CLI Functions
2 parents cefb6c4 + a2168b5 commit 68dff40

File tree

7 files changed

+275
-7
lines changed

7 files changed

+275
-7
lines changed

src/codeflare_sdk/cli/commands/define.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def cli():
1313

1414
@cli.command()
1515
@click.option("--name", type=str, required=True)
16-
@click.option("--namespace", "-n", type=str)
16+
@click.option("--namespace", "-n", type=str, required=True)
1717
@click.option("--head_info", cls=PythonLiteralOption, type=list)
1818
@click.option("--machine_types", cls=PythonLiteralOption, type=list)
1919
@click.option("--min_cpus", type=int)

src/codeflare_sdk/cli/commands/delete.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@ def cli():
1313

1414
@cli.command()
1515
@click.argument("name", type=str)
16-
@click.option("--namespace", type=str, default="default")
16+
@click.option("--namespace", type=str, required=True)
1717
def raycluster(name, namespace):
1818
"""
1919
Delete a specified RayCluster from the Kubernetes cluster
2020
"""
21-
cluster = get_cluster(name, namespace)
21+
try:
22+
cluster = get_cluster(name, namespace)
23+
except FileNotFoundError:
24+
click.echo(f"Cluster {name} not found in {namespace} namespace")
25+
return
2226
cluster.down()
2327
click.echo(f"Cluster deleted successfully")
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import click
2+
3+
from codeflare_sdk.cluster.cluster import get_cluster
4+
5+
6+
@click.group()
7+
def cli():
8+
"""Get the details of a specified resource"""
9+
pass
10+
11+
12+
@cli.command()
13+
@click.argument("name", type=str)
14+
@click.option("--namespace", type=str, required=True)
15+
@click.pass_context
16+
def raycluster(ctx, name, namespace):
17+
"""Get the details of a specified RayCluster"""
18+
try:
19+
cluster = get_cluster(name, namespace)
20+
except FileNotFoundError:
21+
click.echo(f"Cluster {name} not found in {namespace} namespace")
22+
return
23+
cluster.details()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import click
2+
from kubernetes import client, config
3+
4+
from codeflare_sdk.cluster.cluster import (
5+
list_clusters_all_namespaces,
6+
list_all_clusters,
7+
get_current_namespace,
8+
)
9+
from codeflare_sdk.cli.cli_utils import load_auth
10+
11+
12+
@click.group()
13+
def cli():
14+
"""List a specified resource"""
15+
pass
16+
17+
18+
@cli.command()
19+
@click.option("--namespace", type=str)
20+
@click.option("--all", is_flag=True)
21+
@click.pass_context
22+
def rayclusters(ctx, namespace, all):
23+
"""List all rayclusters in a specified namespace"""
24+
if all and namespace:
25+
click.echo("--all and --namespace are mutually exclusive")
26+
return
27+
if not all and not namespace:
28+
click.echo("You must specify either --namespace or --all")
29+
return
30+
if not all:
31+
list_all_clusters(namespace)
32+
return
33+
list_clusters_all_namespaces()
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import click
2+
3+
from codeflare_sdk.cluster.cluster import get_cluster
4+
5+
6+
@click.group()
7+
def cli():
8+
"""Get the status of a specified resource"""
9+
pass
10+
11+
12+
@cli.command()
13+
@click.argument("name", type=str)
14+
@click.option("--namespace", type=str, required=True)
15+
@click.pass_context
16+
def raycluster(ctx, name, namespace):
17+
"""Get the status of a specified RayCluster"""
18+
try:
19+
cluster = get_cluster(name, namespace)
20+
except FileNotFoundError:
21+
click.echo(f"Cluster {name} not found in {namespace} namespace")
22+
return
23+
cluster.status()

src/codeflare_sdk/cluster/cluster.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,17 @@ def list_all_clusters(namespace: str, print_to_console: bool = True):
412412
"""
413413
Returns (and prints by default) a list of all clusters in a given namespace.
414414
"""
415-
clusters = _get_ray_clusters(namespace)
415+
clusters = _get_ray_clusters_in_namespace(namespace)
416+
if print_to_console:
417+
pretty_print.print_clusters(clusters)
418+
return clusters
419+
420+
421+
def list_clusters_all_namespaces(print_to_console: bool = True):
422+
"""
423+
Returns (and prints by default) a list of all clusters in the Kubernetes cluster.
424+
"""
425+
clusters = _get_all_ray_clusters()
416426
if print_to_console:
417427
pretty_print.print_clusters(clusters)
418428
return clusters
@@ -529,7 +539,7 @@ def _ray_cluster_status(name, namespace="default") -> Optional[RayCluster]:
529539
return None
530540

531541

532-
def _get_ray_clusters(namespace="default") -> List[RayCluster]:
542+
def _get_ray_clusters_in_namespace(namespace="default") -> List[RayCluster]:
533543
list_of_clusters = []
534544
try:
535545
config_check()
@@ -548,6 +558,23 @@ def _get_ray_clusters(namespace="default") -> List[RayCluster]:
548558
return list_of_clusters
549559

550560

561+
def _get_all_ray_clusters() -> List[RayCluster]:
562+
list_of_clusters = []
563+
try:
564+
config_check()
565+
api_instance = client.CustomObjectsApi(api_config_handler())
566+
rcs = api_instance.list_cluster_custom_object(
567+
group="ray.io",
568+
version="v1alpha1",
569+
plural="rayclusters",
570+
)
571+
except Exception as e:
572+
return _kube_api_error_handling(e)
573+
for rc in rcs["items"]:
574+
list_of_clusters.append(_map_to_ray_cluster(rc))
575+
return list_of_clusters
576+
577+
551578
def _get_app_wrappers(
552579
namespace="default", filter=List[AppWrapperStatus]
553580
) -> List[AppWrapper]:

tests/unit_test.py

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
get_cluster,
3535
_app_wrapper_status,
3636
_ray_cluster_status,
37+
list_clusters_all_namespaces,
3738
)
3839
from codeflare_sdk.cluster.auth import (
3940
TokenAuthentication,
@@ -200,14 +201,168 @@ def test_cluster_deletion_cli(mocker):
200201
runner = CliRunner()
201202
delete_cluster_command = """
202203
delete raycluster
203-
quicktest
204+
quicktest --namespace=default
204205
"""
205206
result = runner.invoke(cli, delete_cluster_command)
206207

207208
assert result.exit_code == 0
208209
assert "Cluster deleted successfully" in result.output
209210

210211

212+
def test_list_clusters_all_namespaces(mocker, capsys):
213+
mocker.patch(
214+
"kubernetes.client.CustomObjectsApi.list_cluster_custom_object",
215+
side_effect=get_ray_obj_no_namespace,
216+
)
217+
list_clusters_all_namespaces()
218+
captured = capsys.readouterr()
219+
assert captured.out == (
220+
" 🚀 CodeFlare Cluster Details 🚀 \n"
221+
" \n"
222+
" ╭──────────────────────────────────────────────────────────────╮ \n"
223+
" │ Name │ \n"
224+
" │ quicktest Active ✅ │ \n"
225+
" │ │ \n"
226+
" │ URI: ray://quicktest-head-svc.ns.svc:10001 │ \n"
227+
" │ │ \n"
228+
" │ Dashboard🔗 │ \n"
229+
" │ │ \n"
230+
" │ Cluster Resources │ \n"
231+
" │ ╭─ Workers ──╮ ╭───────── Worker specs(each) ─────────╮ │ \n"
232+
" │ │ Min Max │ │ Memory CPU GPU │ │ \n"
233+
" │ │ │ │ │ │ \n"
234+
" │ │ 1 1 │ │ 2G~2G 1 0 │ │ \n"
235+
" │ │ │ │ │ │ \n"
236+
" │ ╰────────────╯ ╰──────────────────────────────────────╯ │ \n"
237+
" ╰──────────────────────────────────────────────────────────────╯ \n"
238+
)
239+
240+
241+
def test_raycluster_details_cli(mocker):
242+
runner = CliRunner()
243+
mocker.patch(
244+
"kubernetes.client.CustomObjectsApi.list_namespaced_custom_object",
245+
side_effect=get_ray_obj,
246+
)
247+
mocker.patch(
248+
"codeflare_sdk.cluster.cluster.Cluster.status",
249+
return_value=(False, CodeFlareClusterStatus.UNKNOWN),
250+
)
251+
mocker.patch(
252+
"codeflare_sdk.cluster.cluster.Cluster.cluster_dashboard_uri",
253+
return_value="",
254+
)
255+
mocker.patch.object(client, "ApiClient")
256+
raycluster_details_command = """
257+
details raycluster quicktest --namespace=default
258+
"""
259+
result = runner.invoke(cli, raycluster_details_command)
260+
quicktest_details = (
261+
" ╭──────────────────────────────────────────────────────────────╮ \n"
262+
+ " │ Name │ \n"
263+
+ " │ quicktest Inactive ❌ │ \n"
264+
+ " │ │ \n"
265+
+ " │ URI: ray://quicktest-head-svc.ns.svc:10001 │ \n"
266+
+ " │ │ \n"
267+
+ " │ Dashboard🔗 │ \n"
268+
+ " │ │ \n"
269+
+ " │ Cluster Resources │ \n"
270+
+ " │ ╭─ Workers ──╮ ╭───────── Worker specs(each) ─────────╮ │ \n"
271+
+ " │ │ Min Max │ │ Memory CPU GPU │ │ \n"
272+
+ " │ │ │ │ │ │ \n"
273+
+ " │ │ 1 1 │ │ 2~2 1 0 │ │ \n"
274+
+ " │ │ │ │ │ │ \n"
275+
+ " │ ╰────────────╯ ╰──────────────────────────────────────╯ │ \n"
276+
+ " ╰──────────────────────────────────────────────────────────────╯ "
277+
)
278+
assert quicktest_details in result.output
279+
280+
281+
def test_raycluster_status_cli(mocker):
282+
runner = CliRunner()
283+
mocker.patch(
284+
"kubernetes.client.CustomObjectsApi.list_namespaced_custom_object",
285+
side_effect=get_ray_obj,
286+
)
287+
mocker.patch(
288+
"codeflare_sdk.cluster.cluster.get_current_namespace",
289+
return_value="ns",
290+
)
291+
mocker.patch(
292+
"codeflare_sdk.cluster.cluster.Cluster.cluster_dashboard_uri",
293+
return_value="",
294+
)
295+
mocker.patch.object(client, "ApiClient")
296+
test_raycluster = RayCluster(
297+
"quicktest",
298+
RayClusterStatus.READY,
299+
1,
300+
1,
301+
"1",
302+
"1",
303+
1,
304+
1,
305+
"default",
306+
"dashboard-url",
307+
)
308+
mocker.patch(
309+
"codeflare_sdk.cluster.cluster._app_wrapper_status",
310+
return_value=test_raycluster,
311+
)
312+
mocker.patch(
313+
"codeflare_sdk.cluster.cluster._ray_cluster_status",
314+
return_value=test_raycluster,
315+
)
316+
raycluster_status_command = """
317+
status raycluster quicktest --namespace=default
318+
"""
319+
result = runner.invoke(cli, raycluster_status_command)
320+
assert "Active" in result.output
321+
322+
323+
def test_raycluster_list_cli(mocker):
324+
runner = CliRunner()
325+
mocker.patch(
326+
"kubernetes.client.CustomObjectsApi.list_namespaced_custom_object",
327+
side_effect=get_ray_obj,
328+
)
329+
mocker.patch(
330+
"codeflare_sdk.cluster.cluster.get_current_namespace",
331+
return_value="ns",
332+
)
333+
mocker.patch(
334+
"codeflare_sdk.cluster.cluster.Cluster.status",
335+
return_value=(False, CodeFlareClusterStatus.UNKNOWN),
336+
)
337+
mocker.patch(
338+
"codeflare_sdk.cluster.cluster.Cluster.cluster_dashboard_uri",
339+
return_value="",
340+
)
341+
mocker.patch.object(client, "ApiClient")
342+
list_rayclusters_command = """
343+
list rayclusters --namespace=ns
344+
"""
345+
result = runner.invoke(cli, list_rayclusters_command)
346+
assert (
347+
" ╭──────────────────────────────────────────────────────────────╮ \n"
348+
+ " │ Name │ \n"
349+
+ " │ quicktest Active ✅ │ \n"
350+
+ " │ │ \n"
351+
+ " │ URI: ray://quicktest-head-svc.ns.svc:10001 │ \n"
352+
+ " │ │ \n"
353+
+ " │ Dashboard🔗 │ \n"
354+
+ " │ │ \n"
355+
+ " │ Cluster Resources │ \n"
356+
+ " │ ╭─ Workers ──╮ ╭───────── Worker specs(each) ─────────╮ │ \n"
357+
+ " │ │ Min Max │ │ Memory CPU GPU │ │ \n"
358+
+ " │ │ │ │ │ │ \n"
359+
+ " │ │ 1 1 │ │ 2G~2G 1 0 │ │ \n"
360+
+ " │ │ │ │ │ │ \n"
361+
+ " │ ╰────────────╯ ╰──────────────────────────────────────╯ │ \n"
362+
+ " ╰──────────────────────────────────────────────────────────────╯ "
363+
) in result.output
364+
365+
211366
# For mocking openshift client results
212367
fake_res = openshift.Result("fake")
213368

@@ -992,6 +1147,10 @@ def get_ray_obj(group, version, namespace, plural, cls=None):
9921147
return api_obj
9931148

9941149

1150+
def get_ray_obj_no_namespace(group, version, plural, cls=None):
1151+
return get_ray_obj(group, version, "ns", plural, cls)
1152+
1153+
9951154
def get_aw_obj(group, version, namespace, plural):
9961155
api_obj1 = {
9971156
"items": [
@@ -2360,4 +2519,3 @@ def test_cleanup():
23602519
os.remove("tls-cluster-namespace/tls.key")
23612520
os.rmdir("tls-cluster-namespace")
23622521
os.remove("cli-test-cluster.yaml")
2363-
os.removedirs(os.path.expanduser("~/.codeflare"))

0 commit comments

Comments
 (0)