Skip to content

Commit b8f7333

Browse files
brianFruitaws-brianxia
authored andcommitted
Add dev_space.py CLI command (#263)
* Add dev_space.py CLI command * Add dev space unit tests --------- Co-authored-by: Brian Xia <[email protected]>
1 parent d2b76fa commit b8f7333

File tree

2 files changed

+800
-0
lines changed

2 files changed

+800
-0
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import click
2+
import json
3+
from tabulate import tabulate
4+
from sagemaker.hyperpod.cli.clients.kubernetes_client import KubernetesClient
5+
from sagemaker.hyperpod.cli.dev_space_utils import generate_click_command
6+
from hyperpod_dev_space_template.registry import SCHEMA_REGISTRY
7+
from sagemaker.hyperpod.common.telemetry.telemetry_logging import (
8+
_hyperpod_telemetry_emitter,
9+
)
10+
from sagemaker.hyperpod.common.telemetry.constants import Feature
11+
12+
13+
@click.command("hyp-dev-space")
14+
@generate_click_command(
15+
schema_pkg="hyperpod_dev_space_template",
16+
registry=SCHEMA_REGISTRY,
17+
)
18+
def dev_space_create(version, config):
19+
"""Create a dev-space resource."""
20+
21+
try:
22+
name = config.get("name")
23+
namespace = config.get("namespace")
24+
dev_space_spec = config.get("dev_space_spec")
25+
26+
k8s_client = KubernetesClient()
27+
k8s_client.create_dev_space(namespace, dev_space_spec)
28+
29+
click.echo(f"Dev space '{name}' created successfully in namespace '{namespace}'")
30+
except Exception as e:
31+
click.echo(f"Error creating dev space: {e}", err=True)
32+
33+
34+
@click.command("hyp-dev-space")
35+
@click.option("--namespace", "-n", required=False, default="default", help="Kubernetes namespace")
36+
@click.option("--output", "-o", type=click.Choice(["table", "json"]), default="table")
37+
def dev_space_list(namespace, output):
38+
"""List dev-space resources."""
39+
k8s_client = KubernetesClient()
40+
41+
try:
42+
resources = k8s_client.list_dev_spaces(namespace)
43+
44+
if output == "json":
45+
click.echo(json.dumps(resources, indent=2))
46+
else:
47+
items = resources.get("items", [])
48+
if items:
49+
table_data = []
50+
for item in items:
51+
table_data.append([
52+
item["metadata"]["name"],
53+
item["metadata"]["namespace"],
54+
item.get("status", {}).get("phase", "Unknown")
55+
])
56+
click.echo(tabulate(table_data, headers=["NAME", "NAMESPACE", "STATUS"]))
57+
else:
58+
click.echo("No dev spaces found")
59+
except Exception as e:
60+
click.echo(f"Error listing dev spaces: {e}", err=True)
61+
62+
63+
@click.command("hyp-dev-space")
64+
@click.option("--name", required=True, help="Name of the dev space")
65+
@click.option("--namespace", "-n", required=False, default="default", help="Kubernetes namespace")
66+
@click.option("--output", "-o", type=click.Choice(["yaml", "json"]), default="yaml")
67+
def dev_space_describe(name, namespace, output):
68+
"""Describe a dev-space resource."""
69+
k8s_client = KubernetesClient()
70+
71+
try:
72+
resource = k8s_client.get_dev_space(namespace, name)
73+
resource["metadata"].pop('managedFields', None)
74+
75+
if output == "json":
76+
click.echo(json.dumps(resource, indent=2))
77+
else:
78+
import yaml
79+
click.echo(yaml.dump(resource, default_flow_style=False))
80+
except Exception as e:
81+
click.echo(f"Error describing dev space '{name}': {e}", err=True)
82+
83+
84+
@click.command("hyp-dev-space")
85+
@click.option("--name", required=True, help="Name of the dev space")
86+
@click.option("--namespace", "-n", required=False, default="default", help="Kubernetes namespace")
87+
def dev_space_delete(name, namespace):
88+
"""Delete a dev-space resource."""
89+
k8s_client = KubernetesClient()
90+
91+
try:
92+
k8s_client.delete_dev_space(namespace, name)
93+
94+
click.echo(f"Dev space '{name}' deleted successfully")
95+
except Exception as e:
96+
click.echo(f"Error deleting dev space '{name}': {e}", err=True)
97+
98+
99+
@click.command("hyp-dev-space")
100+
@generate_click_command(
101+
schema_pkg="hyperpod_dev_space_template",
102+
registry=SCHEMA_REGISTRY,
103+
is_update=True,
104+
)
105+
def dev_space_update(version, config):
106+
"""Update a dev-space resource."""
107+
k8s_client = KubernetesClient()
108+
109+
try:
110+
name = config["name"]
111+
namespace = config["namespace"]
112+
dev_space_spec = config.get("dev_space_spec", {})
113+
114+
k8s_client.patch_dev_space(
115+
namespace=namespace,
116+
name=name,
117+
body=dev_space_spec
118+
)
119+
120+
click.echo(f"Dev space '{name}' updated successfully")
121+
except Exception as e:
122+
click.echo(f"Error updating dev space '{name}': {e}", err=True)
123+
124+
125+
@click.command("hyp-dev-space")
126+
@click.option("--name", required=True, help="Name of the dev space")
127+
@click.option("--namespace", "-n", required=False, default="default", help="Kubernetes namespace")
128+
def dev_space_start(name, namespace):
129+
"""Start a dev-space resource."""
130+
k8s_client = KubernetesClient()
131+
132+
try:
133+
# Patch the resource to set desired status to "Running"
134+
patch_body = {"spec": {"desiredStatus": "Running"}}
135+
k8s_client.patch_dev_space(
136+
namespace=namespace,
137+
name=name,
138+
body=patch_body
139+
)
140+
141+
click.echo(f"Dev space '{name}' start requested")
142+
except Exception as e:
143+
click.echo(f"Error starting dev space '{name}': {e}", err=True)
144+
145+
146+
@click.command("hyp-dev-space")
147+
@click.option("--name", required=True, help="Name of the dev space")
148+
@click.option("--namespace", "-n", required=False, default="default", help="Kubernetes namespace")
149+
def dev_space_stop(name, namespace):
150+
"""Stop a dev-space resource."""
151+
k8s_client = KubernetesClient()
152+
153+
try:
154+
# Patch the resource to set desired status to "Stopped"
155+
patch_body = {"spec": {"desiredStatus": "Stopped"}}
156+
k8s_client.patch_dev_space(
157+
namespace=namespace,
158+
name=name,
159+
body=patch_body
160+
)
161+
162+
click.echo(f"Dev space '{name}' stop requested")
163+
except Exception as e:
164+
click.echo(f"Error stopping dev space '{name}': {e}", err=True)
165+
166+
167+
@click.command("hyp-dev-space")
168+
@click.option("--name", required=True, help="Name of the dev space")
169+
@click.option("--namespace", "-n", required=False, default="default", help="Kubernetes namespace")
170+
def dev_space_get_logs(name, namespace):
171+
"""Get logs for a dev-space resource."""
172+
k8s_client = KubernetesClient()
173+
174+
try:
175+
# Get pods associated with the dev space
176+
pods = k8s_client.list_pods_with_labels(
177+
namespace=namespace,
178+
label_selector=f"sagemaker.aws.com/space-name={name}"
179+
)
180+
181+
if not pods.items:
182+
click.echo(f"No pods found for dev space '{name}'")
183+
return
184+
185+
# Get logs from the first pod
186+
pod_name = pods.items[0].metadata.name
187+
logs = k8s_client.get_logs_for_pod(
188+
pod_name=pod_name,
189+
namespace=namespace,
190+
)
191+
192+
click.echo(logs)
193+
except Exception as e:
194+
click.echo(f"Error getting logs for dev space '{name}': {e}", err=True)
195+
196+
197+
@click.command("hyp-dev-space")
198+
@click.option("--name", required=True, help="Name of the dev space")
199+
@click.option("--namespace", "-n", required=False, default="default", help="Kubernetes namespace")
200+
@click.option("--port", required=True, help="Mapping localhost port to pod")
201+
def dev_space_port_forward(name, namespace, port):
202+
"""Forward a local port to a dev-space pod."""
203+
k8s_client = KubernetesClient()
204+
205+
try:
206+
# Get pods associated with the dev space
207+
pods = k8s_client.list_pods_with_labels(
208+
namespace=namespace,
209+
label_selector=f"sagemaker.aws.com/space-name={name}"
210+
)
211+
212+
if not pods.items:
213+
click.echo(f"No pods found for dev space '{name}'")
214+
return
215+
216+
# Get the first running pod
217+
pod_name = pods.items[0].metadata.name
218+
219+
k8s_client.port_forward_dev_space(
220+
namespace=namespace,
221+
pod_name=pod_name,
222+
local_port=port,
223+
)
224+
225+
except Exception as e:
226+
click.echo(f"Error forwarding port for dev space '{name}': {e}", err=True)

0 commit comments

Comments
 (0)