Skip to content

Commit 8fc7277

Browse files
aws-brianxiaMohamed Zeidan
authored andcommitted
Refactor Space CLI using the Space PySDK (#281)
* Implement CRUD operations for Space PySDK * Update Space PySDK per new schema * Refactor CLI to use the PySDK
1 parent 13f1c0c commit 8fc7277

6 files changed

Lines changed: 341 additions & 522 deletions

File tree

Lines changed: 55 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import click
22
import json
3+
import yaml
34
from tabulate import tabulate
4-
from sagemaker.hyperpod.cli.clients.kubernetes_client import KubernetesClient
5+
from sagemaker.hyperpod.space.hyperpod_space import HPSpace
56
from sagemaker.hyperpod.cli.space_utils import generate_click_command
67
from hyperpod_space_template.registry import SCHEMA_REGISTRY
8+
from hyperpod_space_template.v1_0.model import SpaceConfig
79
from sagemaker.hyperpod.common.telemetry.telemetry_logging import (
810
_hyperpod_telemetry_emitter,
911
)
@@ -17,16 +19,12 @@
1719
)
1820
def space_create(version, config):
1921
"""Create a space resource."""
20-
2122
try:
22-
name = config.get("name")
23-
namespace = config.get("namespace")
24-
space_spec = config.get("space_spec")
25-
26-
k8s_client = KubernetesClient()
27-
k8s_client.create_space(namespace, space_spec)
23+
space_config = SpaceConfig(**config)
24+
space = HPSpace(config=space_config)
25+
space.create()
2826

29-
click.echo(f"Space '{name}' created successfully in namespace '{namespace}'")
27+
click.echo(f"Space '{space_config.name}' created successfully in namespace '{space_config.namespace}'")
3028
except Exception as e:
3129
click.echo(f"Error creating space: {e}", err=True)
3230

@@ -36,24 +34,38 @@ def space_create(version, config):
3634
@click.option("--output", "-o", type=click.Choice(["table", "json"]), default="table")
3735
def space_list(namespace, output):
3836
"""List space resources."""
39-
k8s_client = KubernetesClient()
40-
4137
try:
42-
resources = k8s_client.list_spaces(namespace)
38+
spaces = HPSpace.list(namespace=namespace)
4339

4440
if output == "json":
45-
click.echo(json.dumps(resources, indent=2))
41+
spaces_data = []
42+
for space in spaces:
43+
space_dict = space.config.model_dump()
44+
spaces_data.append(space_dict)
45+
click.echo(json.dumps(spaces_data, indent=2))
4646
else:
47-
items = resources.get("items", [])
48-
if items:
47+
if spaces:
4948
table_data = []
50-
for item in items:
49+
for space in spaces:
50+
# Extract status conditions from raw resource
51+
available = ""
52+
progressing = ""
53+
degraded = ""
54+
55+
if space.status and 'conditions' in space.status:
56+
conditions = {c['type']: c['status'] for c in space.status['conditions']}
57+
available = conditions.get('Available', '')
58+
progressing = conditions.get('Progressing', '')
59+
degraded = conditions.get('Degraded', '')
60+
5161
table_data.append([
52-
item["metadata"]["name"],
53-
item["metadata"]["namespace"],
54-
item.get("status", {}).get("phase", "Unknown")
62+
space.config.name,
63+
namespace,
64+
available,
65+
progressing,
66+
degraded
5567
])
56-
click.echo(tabulate(table_data, headers=["NAME", "NAMESPACE", "STATUS"]))
68+
click.echo(tabulate(table_data, headers=["NAME", "NAMESPACE", "AVAILABLE", "PROGRESSING", "DEGRADED"]))
5769
else:
5870
click.echo("No spaces found")
5971
except Exception as e:
@@ -66,17 +78,16 @@ def space_list(namespace, output):
6678
@click.option("--output", "-o", type=click.Choice(["yaml", "json"]), default="yaml")
6779
def space_describe(name, namespace, output):
6880
"""Describe a space resource."""
69-
k8s_client = KubernetesClient()
70-
7181
try:
72-
resource = k8s_client.get_space(namespace, name)
73-
resource["metadata"].pop('managedFields', None)
82+
current_space = HPSpace.get(name=name, namespace=namespace)
83+
84+
# Combine config and raw resource data
85+
current_space.raw_resource.get('metadata', {}).pop('managedFields', None)
7486

7587
if output == "json":
76-
click.echo(json.dumps(resource, indent=2))
88+
click.echo(json.dumps(current_space.raw_resource, indent=2))
7789
else:
78-
import yaml
79-
click.echo(yaml.dump(resource, default_flow_style=False))
90+
click.echo(yaml.dump(current_space.raw_resource, default_flow_style=False))
8091
except Exception as e:
8192
click.echo(f"Error describing space '{name}': {e}", err=True)
8293

@@ -86,10 +97,9 @@ def space_describe(name, namespace, output):
8697
@click.option("--namespace", "-n", required=False, default="default", help="Kubernetes namespace")
8798
def space_delete(name, namespace):
8899
"""Delete a space resource."""
89-
k8s_client = KubernetesClient()
90-
91100
try:
92-
k8s_client.delete_space(namespace, name)
101+
current_space = HPSpace.get(name=name, namespace=namespace)
102+
current_space.delete()
93103

94104
click.echo(f"Space '{name}' deleted successfully")
95105
except Exception as e:
@@ -104,39 +114,26 @@ def space_delete(name, namespace):
104114
)
105115
def space_update(version, config):
106116
"""Update a space resource."""
107-
k8s_client = KubernetesClient()
108-
109117
try:
110-
name = config["name"]
111-
namespace = config["namespace"]
112-
space_spec = config.get("space_spec", {})
118+
current_space = HPSpace.get(name=config['name'], namespace=config['namespace'])
119+
if not config.get("display_name"):
120+
config["display_name"] = current_space.config.display_name
113121

114-
k8s_client.patch_space(
115-
namespace=namespace,
116-
name=name,
117-
body=space_spec
118-
)
122+
current_space.update(**config)
119123

120-
click.echo(f"Space '{name}' updated successfully")
124+
click.echo(f"Space '{current_space.config.name}' updated successfully")
121125
except Exception as e:
122-
click.echo(f"Error updating space '{name}': {e}", err=True)
126+
click.echo(f"Error updating space: {e}", err=True)
123127

124128

125129
@click.command("hyp-space")
126130
@click.option("--name", required=True, help="Name of the space")
127131
@click.option("--namespace", "-n", required=False, default="default", help="Kubernetes namespace")
128132
def space_start(name, namespace):
129133
"""Start a space resource."""
130-
k8s_client = KubernetesClient()
131-
132134
try:
133-
# Patch the resource to set desired status to "Running"
134-
patch_body = {"spec": {"desiredStatus": "Running"}}
135-
k8s_client.patch_space(
136-
namespace=namespace,
137-
name=name,
138-
body=patch_body
139-
)
135+
current_space = HPSpace.get(name=name, namespace=namespace)
136+
current_space.start()
140137

141138
click.echo(f"Space '{name}' start requested")
142139
except Exception as e:
@@ -148,16 +145,9 @@ def space_start(name, namespace):
148145
@click.option("--namespace", "-n", required=False, default="default", help="Kubernetes namespace")
149146
def space_stop(name, namespace):
150147
"""Stop a space resource."""
151-
k8s_client = KubernetesClient()
152-
153148
try:
154-
# Patch the resource to set desired status to "Stopped"
155-
patch_body = {"spec": {"desiredStatus": "Stopped"}}
156-
k8s_client.patch_space(
157-
namespace=namespace,
158-
name=name,
159-
body=patch_body
160-
)
149+
current_space = HPSpace.get(name=name, namespace=namespace)
150+
current_space.stop()
161151

162152
click.echo(f"Space '{name}' stop requested")
163153
except Exception as e:
@@ -167,31 +157,13 @@ def space_stop(name, namespace):
167157
@click.command("hyp-space")
168158
@click.option("--name", required=True, help="Name of the space")
169159
@click.option("--namespace", "-n", required=False, default="default", help="Kubernetes namespace")
170-
def space_get_logs(name, namespace):
160+
@click.option("--pod-name", required=False, help="Name of the pod to get logs from")
161+
@click.option("--container", required=False, help="Name of the container to get logs from")
162+
def space_get_logs(name, namespace, pod_name, container):
171163
"""Get logs for a space resource."""
172-
k8s_client = KubernetesClient()
173-
174164
try:
175-
# Get pods associated with the 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 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-
165+
current_space = HPSpace.get(name=name, namespace=namespace)
166+
logs = current_space.get_logs(pod_name=pod_name, container=container)
192167
click.echo(logs)
193168
except Exception as e:
194169
click.echo(f"Error getting logs for space '{name}': {e}", err=True)
195-
196-
197-

src/sagemaker/hyperpod/cli/space_utils.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,17 @@ def wrapped_func(*args, **kwargs):
181181

182182
filtered_kwargs[key] = value
183183

184+
# For update operations, add temporary display_name if not provided to pass validation
185+
is_update_and_display_name_not_exist = False
186+
if is_update and 'display_name' not in filtered_kwargs:
187+
filtered_kwargs['display_name'] = 'dummy'
188+
is_update_and_display_name_not_exist = True
189+
184190
try:
185191
flat = Model(**filtered_kwargs)
186-
domain_config = flat.to_domain()
192+
config_dict = flat.model_dump(exclude_none=True, by_alias=True)
193+
if is_update_and_display_name_not_exist:
194+
config_dict['display_name'] = None
187195
except ValidationError as e:
188196
error_messages = []
189197
for err in e.errors():
@@ -195,7 +203,7 @@ def wrapped_func(*args, **kwargs):
195203
f"Configuration validation errors:\n" + "\n".join(error_messages)
196204
)
197205

198-
return func(version, domain_config)
206+
return func(version, config_dict)
199207

200208
# 2) inject click options from JSON Schema
201209
wrapped_func = click.option(

0 commit comments

Comments
 (0)