diff --git a/README.md b/README.md index 0f5d470e..8d9aab59 100644 --- a/README.md +++ b/README.md @@ -706,7 +706,17 @@ hyp create hyp-space \ #### List Spaces ```bash +# List spaces in default namespace hyp list hyp-space + +# List spaces in specific namespace +hyp list hyp-space --namespace my-namespace + +# List spaces across all namespaces +hyp list hyp-space --all-namespaces + +# List spaces with JSON output +hyp list hyp-space --output json ``` #### Describe a Space @@ -742,13 +752,27 @@ hyp get-logs hyp-space --name myspace hyp delete hyp-space --name myspace ``` +#### Port Forward to a Space + +Port forward to access a space from your local machine: + +```bash +# Port forward with default port (8888) +hyp portforward hyp-space --name myspace + +# Port forward with custom local port +hyp portforward hyp-space --name myspace --local-port 8080 +``` + +Access the space via `http://localhost:` after port forwarding is established. Press Ctrl+C to stop port forwarding. + #### Space Template Management Create reusable space templates: ```bash hyp create hyp-space-template --file template.yaml -hyp list hyp-space-template +hyp list hyp-space-template --all-namespaces hyp describe hyp-space-template --name hyp update hyp-space-template --name --file updated-template.yaml hyp delete hyp-space-template --name @@ -1283,6 +1307,23 @@ space = HPSpace.get(name="myspace") space.delete() ``` +#### Port Forward to a Space + +```python +from sagemaker.hyperpod.space.hyperpod_space import HPSpace + +# Get existing space +space = HPSpace.get(name="myspace") + +# Port forward with default remote port (8888) +space.portforward_space(local_port="8080") + +# Port forward with custom remote port +space.portforward_space(local_port="8080", remote_port="8888") +``` + +Access the space via `http://localhost:` after port forwarding is established. Press Ctrl+C to stop port forwarding. + #### Space Template Management ```python diff --git a/doc/cli/space/cli_space.md b/doc/cli/space/cli_space.md index c5b3b76d..5d050a4e 100644 --- a/doc/cli/space/cli_space.md +++ b/doc/cli/space/cli_space.md @@ -16,6 +16,7 @@ Complete reference for Amazon SageMaker Space management commands and configurat * [Start Space](#hyp-start-hyp-space) * [Stop Space](#hyp-stop-hyp-space) * [Get Logs](#hyp-get-logs-hyp-space) +* [Port Forward](#hyp-portforward-hyp-space) * [Create Space Access](#hyp-create-hyp-space-access) * [Create Space Template](#hyp-create-hyp-space-template) * [List Space Templates](#hyp-list-hyp-space-template) @@ -76,7 +77,7 @@ Commands for managing Amazon SageMaker Spaces. ### hyp list hyp-space -List all spaces in a namespace. +List all spaces in a namespace or across all namespaces. #### Syntax @@ -89,12 +90,23 @@ hyp list hyp-space [OPTIONS] | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `--namespace, -n` | TEXT | No | Kubernetes namespace (default: "default") | +| `--all-namespaces, -A` | FLAG | No | List spaces across all namespaces | | `--output, -o` | TEXT | No | Output format: table or json (default: "table") | -#### Example +#### Examples ```bash -hyp list hyp-space --namespace default --output table +# List spaces in default namespace +hyp list hyp-space + +# List spaces in specific namespace +hyp list hyp-space --namespace my-namespace + +# List spaces across all namespaces +hyp list hyp-space --all-namespaces + +# List spaces with JSON output +hyp list hyp-space --output json ``` ### hyp describe hyp-space @@ -261,6 +273,39 @@ hyp get-logs hyp-space [OPTIONS] hyp get-logs hyp-space --name my-space --namespace default --pod-name my-pod ``` +### hyp portforward hyp-space + +Port forward to a space resource for local development access. + +#### Syntax + +```bash +hyp portforward hyp-space [OPTIONS] +``` + +#### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `--name` | TEXT | Yes | Name of the space to port forward to | +| `--namespace, -n` | TEXT | No | Kubernetes namespace (default: "default") | +| `--local-port` | TEXT | No | Local port to forward from (default: "8888") | + +#### Examples + +```bash +# Port forward with default port (8888) +hyp portforward hyp-space --name my-space + +# Port forward with custom local port +hyp portforward hyp-space --name my-space --local-port 8080 + +# Port forward to space in specific namespace +hyp portforward hyp-space --name my-space --namespace my-namespace --local-port 8080 +``` + +Access the space via `http://localhost:` after port forwarding is established. Press Ctrl+C to stop port forwarding. + ## Space Access Commands Commands for managing space access resources. @@ -330,6 +375,7 @@ hyp list hyp-space-template [OPTIONS] | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `--namespace, -n` | TEXT | No | Kubernetes namespace | +| `--all-namespaces, -A` | FLAG | No | List spaces across all namespaces | | `--output, -o` | TEXT | No | Output format: table or json (default: "table") | #### Example diff --git a/doc/getting_started/space.md b/doc/getting_started/space.md new file mode 100644 index 00000000..23822256 --- /dev/null +++ b/doc/getting_started/space.md @@ -0,0 +1,337 @@ +--- +keywords: + - workspace + - kubernetes + - interactive + - development + - jupyter + - code editor +--- + +(space)= + +# Spaces with SageMaker HyperPod + +SageMaker HyperPod Spaces provide interactive development environments on EKS-orchestrated clusters. This guide covers how to create and manage spaces using both the HyperPod CLI and SDK. + +## Overview + +SageMaker HyperPod Spaces allow you to: + +- Create interactive development workspaces +- Specify custom Docker images for your environment +- Configure resource requirements (CPUs, GPUs, memory, etc.) +- Set up persistent storage with volumes +- Manage workspace lifecycle (create, start, stop, list, describe, update, delete) +- Access workspaces via port forwarding, web UI, or remote connections + + +## Creating Spaces + +You can create spaces using either the CLI or SDK approach: + +`````{tab-set} +````{tab-item} CLI +```bash +hyp create hyp-space \ + --name myspace \ + --display-name "My Space" +``` +```` +````{tab-item} SDK +```python +from sagemaker.hyperpod.space.hyperpod_space import HPSpace +from hyperpod_space_template.v1_0.model import SpaceConfig + +# Create space configuration +space_config = SpaceConfig( + name="myspace", + display_name="My Space", +) + +# Create and start the space +space = HPSpace(config=space_config) +space.create() +``` +```` +````` + +### Key Parameters + +When creating a space, you'll need to specify: + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| **name** | TEXT | Yes | Unique identifier for your space | +| **display-name** | TEXT | Yes | Human-readable name for the space | +| **namespace** | TEXT | No | Kubernetes namespace | +| **image** | TEXT | No | Docker image for the workspace environment | +| **cpu** | TEXT | No | CPU resource request | +| **cpu-limit** | TEXT | No | CPU resource limit | +| **memory** | TEXT | No | Memory resource request | +| **memory-limit** | TEXT | No | Memory resource limit | +| **gpu** | TEXT | No | GPU resource request | +| **gpu-limit** | TEXT | No | GPU resource limit | +| **accelerator-partition-type** | TEXT | No | Fractional GPU partition type (e.g., 'mig-3g.20gb') | +| **accelerator-partition-count** | TEXT | No | Fractional GPU partition count | +| **volume** | TEXT | No | Volume configuration (can be specified multiple times) | +| **debug** | FLAG | No | Enable debug mode | + + +## Managing Spaces + +### List Spaces + +`````{tab-set} +````{tab-item} CLI +```bash +# List spaces in default namespace +hyp list hyp-space + +# List spaces in specific namespace +hyp list hyp-space --namespace my-namespace + +# List spaces across all namespaces +hyp list hyp-space --all-namespaces +``` +```` +````{tab-item} SDK +```python +from sagemaker.hyperpod.space.hyperpod_space import HPSpace + +# List all spaces in default namespace +spaces = HPSpace.list() +for space in spaces: + print(f"Space: {space.config.name}, Status: {space.status}") + +# List spaces in specific namespace +spaces = HPSpace.list(namespace="your-namespace") +``` +```` +````` + +### Describe a Space + +`````{tab-set} +````{tab-item} CLI +```bash +hyp describe hyp-space --name myspace +``` +```` +````{tab-item} SDK +```python +from sagemaker.hyperpod.space.hyperpod_space import HPSpace + +# Get specific space +space = HPSpace.get(name="myspace", namespace="default") +print(f"Space name: {space.config.name}") +print(f"Display name: {space.config.display_name}") +``` +```` +````` + +### Update a Space + +`````{tab-set} +````{tab-item} CLI +```bash +hyp update hyp-space \ + --name myspace \ + --display-name "Updated Space Name" +``` +```` +````{tab-item} SDK +```python +from sagemaker.hyperpod.space.hyperpod_space import HPSpace + +# Get existing space +space = HPSpace.get(name="myspace") + +# Update space configuration +space.update( + display_name="Updated Space Name", +) +``` +```` +````` + +### Start/Stop a Space + +`````{tab-set} +````{tab-item} CLI +```bash +# Start a space +hyp start hyp-space --name myspace + +# Stop a space +hyp stop hyp-space --name myspace +``` +```` +````{tab-item} SDK +```python +from sagemaker.hyperpod.space.hyperpod_space import HPSpace + +# Get existing space +space = HPSpace.get(name="myspace") + +# Start the space +space.start() + +# Stop the space +space.stop() +``` +```` +````` + +### Get Logs from a Space + +`````{tab-set} +````{tab-item} CLI +```bash +hyp get-logs hyp-space --name myspace +``` +```` + +````{tab-item} SDK +```python +from sagemaker.hyperpod.space.hyperpod_space import HPSpace + +# Get space and retrieve logs +space = HPSpace.get(name="myspace") + +# Get logs from default pod and container +logs = space.get_logs() +print(logs) +``` +```` +````` + +### Port Forward to a Space + +`````{tab-set} +````{tab-item} CLI +```bash +# Port forward with default port (8888) +hyp portforward hyp-space --name myspace + +# Port forward with custom local port +hyp portforward hyp-space --name myspace --local-port 8080 +``` +```` +````{tab-item} SDK +```python +from sagemaker.hyperpod.space.hyperpod_space import HPSpace + +# Get existing space +space = HPSpace.get(name="myspace") + +# Port forward with default remote port (8888) +space.portforward_space(local_port="8080") + +# Port forward with custom remote port +space.portforward_space(local_port="8080", remote_port="8888") +``` +```` +````` + +Access the space via `http://localhost:` after port forwarding is established. Press Ctrl+C to stop port forwarding. + +### Create Space Access + +`````{tab-set} +````{tab-item} CLI +```bash +# Create VS Code remote access +hyp create hyp-space-access --name myspace --connection-type vscode-remote + +# Create web UI access +hyp create hyp-space-access --name myspace --connection-type web-ui +``` +```` +````{tab-item} SDK +```python +from sagemaker.hyperpod.space.hyperpod_space import HPSpace + +# Get existing space +space = HPSpace.get(name="myspace") + +# Create VS Code remote access +vscode_access = space.create_space_access(connection_type="vscode-remote") +print(f"VS Code URL: {vscode_access['SpaceConnectionUrl']}") + +# Create web UI access +web_access = space.create_space_access(connection_type="web-ui") +print(f"Web UI URL: {web_access['SpaceConnectionUrl']}") +``` +```` +````` + +### Delete a Space + +`````{tab-set} +````{tab-item} CLI +```bash +hyp delete hyp-space --name myspace +``` +```` +````{tab-item} SDK +```python +from sagemaker.hyperpod.space.hyperpod_space import HPSpace + +# Get existing space +space = HPSpace.get(name="myspace") + +# Delete the space +space.delete() +``` +```` +````` + +## Space Template Management + +Create reusable space templates for standardized workspace configurations: + +`````{tab-set} +````{tab-item} CLI +```bash +# Create a space template +hyp create hyp-space-template --file template.yaml + +# List all space templates +hyp list hyp-space-template --all-namespaces + +# Describe a specific template +hyp describe hyp-space-template --name + +# Update a space template +hyp update hyp-space-template --name --file updated-template.yaml + +# Delete a space template +hyp delete hyp-space-template --name +``` +```` +````{tab-item} SDK +```python +from sagemaker.hyperpod.space.hyperpod_space_template import HPSpaceTemplate + +# Create space template from YAML file +template = HPSpaceTemplate(file_path="template.yaml") +template.create() + +# List all space templates +templates = HPSpaceTemplate.list() +for template in templates: + print(f"Template: {template.name}") + +# Get specific space template +template = HPSpaceTemplate.get(name="my-template") +print(template.to_yaml()) + +# Update space template +template.update(file_path="updated-template.yaml") + +# Delete space template +template.delete() +``` +```` +````` diff --git a/src/sagemaker/hyperpod/cli/commands/space.py b/src/sagemaker/hyperpod/cli/commands/space.py index 6a2eb182..7ef29a0f 100644 --- a/src/sagemaker/hyperpod/cli/commands/space.py +++ b/src/sagemaker/hyperpod/cli/commands/space.py @@ -4,6 +4,7 @@ from tabulate import tabulate from sagemaker.hyperpod.space.hyperpod_space import HPSpace from sagemaker.hyperpod.cli.space_utils import generate_click_command +from sagemaker.hyperpod.cli.clients.kubernetes_client import KubernetesClient from hyperpod_space_template.registry import SCHEMA_REGISTRY from hyperpod_space_template.v1_0.model import SpaceConfig from sagemaker.hyperpod.common.telemetry.telemetry_logging import ( @@ -32,13 +33,28 @@ def space_create(version, debug, config): @click.command("hyp-space") @click.option("--namespace", "-n", required=False, default="default", help="Kubernetes namespace") +@click.option("--all-namespaces", "-A", is_flag=True, help="List spaces across all namespaces") @click.option("--output", "-o", type=click.Choice(["table", "json"]), default="table") @_hyperpod_telemetry_emitter(Feature.HYPERPOD_CLI, "list_spaces") @handle_cli_exceptions() -def space_list(namespace, output): +def space_list(namespace, all_namespaces, output): """List space resources.""" - spaces = HPSpace.list(namespace=namespace) - + spaces = [] + + if all_namespaces: + k8s_client = KubernetesClient() + namespaces = k8s_client.list_namespaces() + + for ns in namespaces: + try: + ns_spaces = HPSpace.list(namespace=ns) + spaces.extend(ns_spaces) + except Exception as e: + click.echo(f"Warning: Failed to list spaces in namespace '{ns}': {e}", err=True) + continue + else: + spaces = HPSpace.list(namespace=namespace) + if output == "json": spaces_data = [] for space in spaces: @@ -49,20 +65,21 @@ def space_list(namespace, output): if spaces: table_data = [] for space in spaces: - # Extract status conditions from raw resource available = "" progressing = "" degraded = "" - + if space.status and 'conditions' in space.status: conditions = {c['type']: c['status'] for c in space.status['conditions']} available = conditions.get('Available', '') progressing = conditions.get('Progressing', '') degraded = conditions.get('Degraded', '') - + + space_namespace = space.config.namespace + table_data.append([ space.config.name, - namespace, + space_namespace, available, progressing, degraded diff --git a/src/sagemaker/hyperpod/cli/commands/space_template.py b/src/sagemaker/hyperpod/cli/commands/space_template.py index 65e6f71b..a0c2990e 100644 --- a/src/sagemaker/hyperpod/cli/commands/space_template.py +++ b/src/sagemaker/hyperpod/cli/commands/space_template.py @@ -3,6 +3,7 @@ import yaml from tabulate import tabulate from sagemaker.hyperpod.space.hyperpod_space_template import HPSpaceTemplate +from sagemaker.hyperpod.cli.clients.kubernetes_client import KubernetesClient from sagemaker.hyperpod.common.cli_decorators import handle_cli_exceptions from sagemaker.hyperpod.common.telemetry.telemetry_logging import ( _hyperpod_telemetry_emitter, @@ -23,12 +24,27 @@ def space_template_create(file): @click.command("hyp-space-template") @click.option("--namespace", "-n", required=False, default=None, help="Kubernetes namespace") +@click.option("--all-namespaces", "-A", is_flag=True, help="List space templates across all namespaces") @click.option("--output", "-o", type=click.Choice(["table", "json"]), default="table") @_hyperpod_telemetry_emitter(Feature.HYPERPOD_CLI, "list_space_templates") @handle_cli_exceptions() -def space_template_list(namespace, output): +def space_template_list(namespace, all_namespaces, output): """List space-template resources.""" - templates = HPSpaceTemplate.list(namespace) + templates = [] + + if all_namespaces: + k8s_client = KubernetesClient() + namespaces = k8s_client.list_namespaces() + + for ns in namespaces: + try: + ns_templates = HPSpaceTemplate.list(ns) + templates.extend(ns_templates) + except Exception as e: + click.echo(f"Warning: Failed to list space templates in namespace '{ns}': {e}", err=True) + continue + else: + templates = HPSpaceTemplate.list(namespace) if output == "json": templates_data = [template.to_dict() for template in templates] diff --git a/test/unit_tests/cli/test_space.py b/test/unit_tests/cli/test_space.py index 6fea1202..042afbaf 100644 --- a/test/unit_tests/cli/test_space.py +++ b/test/unit_tests/cli/test_space.py @@ -142,6 +142,58 @@ def test_space_list_empty(self, mock_hp_space_class, mock_namespace_exists): assert result.exit_code == 0 assert "No spaces found" in result.output + @patch('sagemaker.hyperpod.cli.commands.space.HPSpace') + @patch('sagemaker.hyperpod.cli.commands.space.KubernetesClient') + def test_space_list_all_namespaces(self, mock_k8s_client_class, mock_hp_space_class, mock_namespace_exists): + """Test space list with --all-namespaces flag and table output""" + # Mock KubernetesClient + mock_k8s_client = Mock() + mock_k8s_client.list_namespaces.return_value = ['ns1', 'ns2', 'ns3'] + mock_k8s_client_class.return_value = mock_k8s_client + + # Mock spaces from different namespaces + mock_space1 = Mock() + mock_space1.config.name = "space1" + mock_space1.config.namespace = "ns1" + mock_space1.status = {"conditions": [ + {"type": "Available", "status": "True"}, + {"type": "Progressing", "status": "False"}, + {"type": "Degraded", "status": "False"} + ]} + + mock_space2 = Mock() + mock_space2.config.name = "space2" + mock_space2.config.namespace = "ns2" + mock_space2.status = {"conditions": [ + {"type": "Available", "status": "True"}, + {"type": "Progressing", "status": "False"}, + {"type": "Degraded", "status": "False"} + ]} + + # Mock HPSpace.list to return different spaces for different namespaces + def list_side_effect(namespace): + if namespace == 'ns1': + return [mock_space1] + elif namespace == 'ns2': + return [mock_space2] + else: + return [] + + mock_hp_space_class.list.side_effect = list_side_effect + + result = self.runner.invoke(space_list, [ + '--all-namespaces', + '--output', 'table' + ]) + + assert result.exit_code == 0 + assert "space1" in result.output + assert "space2" in result.output + assert "ns1" in result.output + assert "ns2" in result.output + mock_k8s_client.list_namespaces.assert_called_once() + assert mock_hp_space_class.list.call_count == 3 + @patch('sagemaker.hyperpod.cli.commands.space.HPSpace') def test_space_describe_yaml_output(self, mock_hp_space_class, mock_namespace_exists): """Test space describe with YAML output""" diff --git a/test/unit_tests/cli/test_space_template.py b/test/unit_tests/cli/test_space_template.py index f435408a..421a3144 100644 --- a/test/unit_tests/cli/test_space_template.py +++ b/test/unit_tests/cli/test_space_template.py @@ -177,3 +177,43 @@ def test_space_template_update_success(self, mock_hp_space_template): self.assertIn("Space template 'test-template' in namespace 'None' updated successfully", result.output) mock_hp_space_template.get.assert_called_once_with("test-template", None) mock_template_instance.update.assert_called_once_with("test.yaml") + + @patch("sagemaker.hyperpod.cli.commands.space_template.KubernetesClient") + @patch("sagemaker.hyperpod.cli.commands.space_template.HPSpaceTemplate") + def test_space_template_list_all_namespaces_table_output(self, mock_hp_space_template, mock_k8s_client): + """Test space template list with --all-namespaces flag and table output""" + mock_k8s_instance = Mock() + mock_k8s_instance.list_namespaces.return_value = ["default", "test-ns", "prod-ns"] + mock_k8s_client.return_value = mock_k8s_instance + + mock_template1 = Mock() + mock_template1.name = "template1" + mock_template1.namespace = "default" + mock_template1.config_data = {"spec": {"displayName": "Template 1", "defaultImage": "image1"}} + + mock_template2 = Mock() + mock_template2.name = "template2" + mock_template2.namespace = "test-ns" + mock_template2.config_data = {"spec": {"displayName": "Template 2", "defaultImage": "image2"}} + + mock_template3 = Mock() + mock_template3.name = "template3" + mock_template3.namespace = "prod-ns" + mock_template3.config_data = {"spec": {"displayName": "Template 3", "defaultImage": "image3"}} + + mock_hp_space_template.list.side_effect = [ + [mock_template1], + [mock_template2], + [mock_template3] + ] + + result = self.runner.invoke(space_template_list, ["--all-namespaces", "--output", "table"]) + + self.assertEqual(result.exit_code, 0) + self.assertIn("template1", result.output) + self.assertIn("template2", result.output) + self.assertIn("template3", result.output) + self.assertIn("default", result.output) + self.assertIn("test-ns", result.output) + self.assertIn("prod-ns", result.output) + self.assertEqual(mock_hp_space_template.list.call_count, 3)