From 47ed5f0c14b0a1727d62d4985b6ea88663865720 Mon Sep 17 00:00:00 2001 From: Eilia Araghbidikashani Date: Wed, 29 Apr 2026 12:26:47 -0700 Subject: [PATCH] feat: support generic {ide}-remote connection types for space access --- README.md | 8 ++- doc/cli/space/cli_space.md | 3 +- doc/getting_started/space.md | 7 +++ .../04-spaces/00-create-space.md | 6 ++ .../hyperpod/cli/commands/space_access.py | 2 +- .../hyperpod/space/hyperpod_space.py | 23 ++++++-- test/unit_tests/cli/test_space_access.py | 57 +++++++++++++++++++ test/unit_tests/test_hyperpod_space.py | 38 +++++++++++++ 8 files changed, 136 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 57cbf39a..5079b86a 100644 --- a/README.md +++ b/README.md @@ -879,10 +879,12 @@ hyp delete hyp-space-template --name #### Space Access -Create remote access to spaces: +Create remote access to spaces. The `--connection-type` accepts `web-ui` or any `{ide}-remote` pattern (e.g. `vscode-remote`, `kiro-remote`, `cursor-remote`): ```bash hyp create hyp-space-access --name myspace --connection-type vscode-remote +hyp create hyp-space-access --name myspace --connection-type kiro-remote +hyp create hyp-space-access --name myspace --connection-type cursor-remote hyp create hyp-space-access --name myspace --connection-type web-ui ``` @@ -1389,6 +1391,10 @@ space = HPSpace.get(name="myspace") vscode_access = space.create_space_access(connection_type="vscode-remote") print(f"VS Code URL: {vscode_access['SpaceConnectionUrl']}") +# Create Kiro remote access +kiro_access = space.create_space_access(connection_type="kiro-remote") +print(f"Kiro URL: {kiro_access['SpaceConnectionUrl']}") + # Create web UI access web_access = space.create_space_access(connection_type="web-ui") print(f"Web UI URL: {web_access['SpaceConnectionUrl']}") diff --git a/doc/cli/space/cli_space.md b/doc/cli/space/cli_space.md index 5d050a4e..895cf0de 100644 --- a/doc/cli/space/cli_space.md +++ b/doc/cli/space/cli_space.md @@ -326,12 +326,13 @@ hyp create hyp-space-access [OPTIONS] |-----------|------|----------|-------------| | `--name` | TEXT | Yes | Name of the space to create access for | | `--namespace, -n` | TEXT | No | Kubernetes namespace (default: "default") | -| `--connection-type, -t` | TEXT | No | Remote access type: vscode-remote or web-ui (default: "vscode-remote") | +| `--connection-type, -t` | TEXT | No | Connection type: 'web-ui' or any '{ide}-remote' pattern (e.g. vscode-remote, kiro-remote, cursor-remote). Default: "vscode-remote" | #### Example ```bash hyp create hyp-space-access --name my-space --namespace default --connection-type vscode-remote +hyp create hyp-space-access --name my-space --connection-type kiro-remote ``` ## Space Template Commands diff --git a/doc/getting_started/space.md b/doc/getting_started/space.md index 23822256..ec001fa2 100644 --- a/doc/getting_started/space.md +++ b/doc/getting_started/space.md @@ -244,6 +244,9 @@ Access the space via `http://localhost:` after port forwarding is es # Create VS Code remote access hyp create hyp-space-access --name myspace --connection-type vscode-remote +# Create Kiro remote access +hyp create hyp-space-access --name myspace --connection-type kiro-remote + # Create web UI access hyp create hyp-space-access --name myspace --connection-type web-ui ``` @@ -259,6 +262,10 @@ space = HPSpace.get(name="myspace") vscode_access = space.create_space_access(connection_type="vscode-remote") print(f"VS Code URL: {vscode_access['SpaceConnectionUrl']}") +# Create Kiro remote access +kiro_access = space.create_space_access(connection_type="kiro-remote") +print(f"Kiro URL: {kiro_access['SpaceConnectionUrl']}") + # Create web UI access web_access = space.create_space_access(connection_type="web-ui") print(f"Web UI URL: {web_access['SpaceConnectionUrl']}") diff --git a/examples/end_to_end_walkthrough/04-spaces/00-create-space.md b/examples/end_to_end_walkthrough/04-spaces/00-create-space.md index 432d5603..9e022dee 100644 --- a/examples/end_to_end_walkthrough/04-spaces/00-create-space.md +++ b/examples/end_to_end_walkthrough/04-spaces/00-create-space.md @@ -142,6 +142,12 @@ This will print a `JSON`-formatted output to the console that contains a `SpaceC ![VSCode connected](../images/vscode-connected.png) +You can also connect using other IDEs that support the `{ide}-remote` pattern, such as Kiro or Cursor: +```bash +hyp create hyp-space-access --name $SPACE_NAME --connection-type kiro-remote +hyp create hyp-space-access --name $SPACE_NAME --connection-type cursor-remote +``` + Lastly let's generate a JupyterLab web UI URL (if the web ui has been enabled in your environment): ```bash hyp create hyp-space-access --name $SPACE_NAME --connection-type web-ui diff --git a/src/sagemaker/hyperpod/cli/commands/space_access.py b/src/sagemaker/hyperpod/cli/commands/space_access.py index adcb92e3..de710586 100644 --- a/src/sagemaker/hyperpod/cli/commands/space_access.py +++ b/src/sagemaker/hyperpod/cli/commands/space_access.py @@ -13,7 +13,7 @@ @click.option("--connection-type", "-t", required=False, default="vscode-remote", - help="Remote access type supported values: [vscode-remote, web-ui] [default: vscode-remote]" + help="Connection type: 'web-ui' or '{ide}-remote' pattern (e.g. vscode-remote, kiro-remote, cursor-remote) [default: vscode-remote]" ) @_hyperpod_telemetry_emitter(Feature.HYPERPOD_CLI, "create_space_access") @handle_cli_exceptions() diff --git a/src/sagemaker/hyperpod/space/hyperpod_space.py b/src/sagemaker/hyperpod/space/hyperpod_space.py index cba16243..c19ccb4f 100644 --- a/src/sagemaker/hyperpod/space/hyperpod_space.py +++ b/src/sagemaker/hyperpod/space/hyperpod_space.py @@ -1,4 +1,5 @@ import logging +import re import yaml import boto3 from sagemaker.hyperpod.common.utils import create_boto3_client @@ -819,12 +820,15 @@ def get_logs(self, pod_name: Optional[str] = None, container: Optional[str] = No except Exception as e: handle_exception(e, pod_name, self.config.namespace) + # Validates the {ide}-remote pattern: alphanumeric segments separated by single hyphens. + _remote_connection_type_regex = re.compile(r"^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*-remote$") + @_hyperpod_telemetry_emitter(Feature.HYPERPOD, "create_space_access") def create_space_access(self, connection_type: str = "vscode-remote") -> Dict[str, str]: """Create a space access for this space. Creates a space access resource that provides remote connection capabilities - to the space. Supports VS Code remote development and web UI access types. + to the space. Supports IDE remote development and web UI access types. **Parameters:** @@ -837,7 +841,9 @@ def create_space_access(self, connection_type: str = "vscode-remote") -> Dict[st - Description * - connection_type - str, optional - - The IDE type for remote access. Must be "vscode-remote" or "web-ui" (default: "vscode-remote") + - The connection type for remote access. Must be "web-ui" or follow the + '{ide}-remote' pattern (e.g. "vscode-remote", "kiro-remote", "cursor-remote"). + Default: "vscode-remote" **Returns:** @@ -845,7 +851,7 @@ def create_space_access(self, connection_type: str = "vscode-remote") -> Dict[st **Raises:** - ValueError: If connection_type is not "vscode-remote" or "web-ui" + ValueError: If connection_type is not "web-ui" or a valid '{ide}-remote' pattern Exception: If the space access creation fails or Kubernetes API call fails .. dropdown:: Usage Examples @@ -858,6 +864,10 @@ def create_space_access(self, connection_type: str = "vscode-remote") -> Dict[st >>> access = space.create_space_access("vscode-remote") >>> print(f"Connection URL: {access['SpaceConnectionUrl']}") + >>> # Create Kiro remote access + >>> access = space.create_space_access("kiro-remote") + >>> print(f"Connection URL: {access['SpaceConnectionUrl']}") + >>> # Create web UI access >>> access = space.create_space_access("web-ui") >>> print(f"Web UI URL: {access['SpaceConnectionUrl']}") @@ -865,8 +875,11 @@ def create_space_access(self, connection_type: str = "vscode-remote") -> Dict[st self.verify_kube_config() logger = self.get_logger() - if connection_type not in {"vscode-remote", "web-ui"}: - raise ValueError("--connection-type must be 'vscode-remote' or 'web-ui'.") + if connection_type != "web-ui" and not self._remote_connection_type_regex.match(connection_type): + raise ValueError( + f"--connection-type must be 'web-ui' or follow the '{{ide}}-remote' pattern " + f"(e.g. 'vscode-remote', 'kiro-remote', 'cursor-remote')." + ) config = { "metadata": { diff --git a/test/unit_tests/cli/test_space_access.py b/test/unit_tests/cli/test_space_access.py index 71bb0326..a96512b7 100644 --- a/test/unit_tests/cli/test_space_access.py +++ b/test/unit_tests/cli/test_space_access.py @@ -52,3 +52,60 @@ def test_space_access_create_default_values(self, mock_hp_space_class): mock_hp_space_class.get.assert_called_once_with(name='test-space', namespace='default') mock_space_instance.create_space_access.assert_called_once_with(connection_type='vscode-remote') + @patch('sagemaker.hyperpod.cli.commands.space_access.HPSpace') + def test_space_access_create_kiro_remote(self, mock_hp_space_class): + """Test space access creation with kiro-remote connection type""" + mock_space_instance = Mock() + mock_space_instance.create_space_access.return_value = { + "SpaceConnectionType": "kiro-remote", + "SpaceConnectionUrl": "https://kiro-url.com" + } + mock_hp_space_class.get.return_value = mock_space_instance + + result = self.runner.invoke(space_access_create, [ + '--name', 'test-space', + '--connection-type', 'kiro-remote' + ]) + + assert result.exit_code == 0 + assert "https://kiro-url.com" in result.output + mock_space_instance.create_space_access.assert_called_once_with(connection_type='kiro-remote') + + @patch('sagemaker.hyperpod.cli.commands.space_access.HPSpace') + def test_space_access_create_cursor_remote(self, mock_hp_space_class): + """Test space access creation with cursor-remote connection type""" + mock_space_instance = Mock() + mock_space_instance.create_space_access.return_value = { + "SpaceConnectionType": "cursor-remote", + "SpaceConnectionUrl": "https://cursor-url.com" + } + mock_hp_space_class.get.return_value = mock_space_instance + + result = self.runner.invoke(space_access_create, [ + '--name', 'test-space', + '--connection-type', 'cursor-remote' + ]) + + assert result.exit_code == 0 + assert "https://cursor-url.com" in result.output + mock_space_instance.create_space_access.assert_called_once_with(connection_type='cursor-remote') + + + @pytest.mark.parametrize("invalid_type", [ + "invalid-type", "-remote", "remote", "my--vscode-remote", "vscode_remote", "", + ]) + @patch('sagemaker.hyperpod.cli.commands.space_access.HPSpace') + def test_space_access_create_invalid_connection_type(self, mock_hp_space_class, invalid_type): + """Test space access creation rejects invalid connection type patterns""" + mock_space_instance = Mock() + mock_space_instance.create_space_access.side_effect = ValueError( + "--connection-type must be 'web-ui' or follow the '{ide}-remote' pattern" + ) + mock_hp_space_class.get.return_value = mock_space_instance + + result = self.runner.invoke(space_access_create, [ + '--name', 'test-space', + '--connection-type', invalid_type + ] if invalid_type else ['--name', 'test-space', '--connection-type', '']) + + assert result.exit_code != 0 \ No newline at end of file diff --git a/test/unit_tests/test_hyperpod_space.py b/test/unit_tests/test_hyperpod_space.py index a9d51a77..e128f648 100644 --- a/test/unit_tests/test_hyperpod_space.py +++ b/test/unit_tests/test_hyperpod_space.py @@ -707,6 +707,44 @@ def test_create_space_access_custom_ide(self, mock_verify_config, mock_custom_ap ) self.assertEqual(result, {"SpaceConnectionType": "web-ui", "SpaceConnectionUrl": "https://example.com/webui-access"}) + @patch('sagemaker.hyperpod.space.hyperpod_space.client.CustomObjectsApi') + @patch.object(HPSpace, 'verify_kube_config') + def test_create_space_access_kiro_remote(self, mock_verify_config, mock_custom_api_class): + """Test space access creation with kiro-remote connection type""" + mock_custom_api = Mock() + mock_custom_api_class.return_value = mock_custom_api + mock_custom_api.create_namespaced_custom_object.return_value = { + "status": {"workspaceConnectionUrl": "https://example.com/kiro-access"} + } + + result = self.hp_space.create_space_access(connection_type="kiro-remote") + + self.assertEqual(result["SpaceConnectionType"], "kiro-remote") + self.assertEqual(result["SpaceConnectionUrl"], "https://example.com/kiro-access") + + @patch('sagemaker.hyperpod.space.hyperpod_space.client.CustomObjectsApi') + @patch.object(HPSpace, 'verify_kube_config') + def test_create_space_access_cursor_remote(self, mock_verify_config, mock_custom_api_class): + """Test space access creation with cursor-remote connection type""" + mock_custom_api = Mock() + mock_custom_api_class.return_value = mock_custom_api + mock_custom_api.create_namespaced_custom_object.return_value = { + "status": {"workspaceConnectionUrl": "https://example.com/cursor-access"} + } + + result = self.hp_space.create_space_access(connection_type="cursor-remote") + + self.assertEqual(result["SpaceConnectionType"], "cursor-remote") + self.assertEqual(result["SpaceConnectionUrl"], "https://example.com/cursor-access") + + @patch.object(HPSpace, 'verify_kube_config') + def test_create_space_access_invalid_pattern(self, mock_verify_config): + """Test space access creation rejects invalid connection type patterns""" + invalid_types = ["invalid-type", "-remote", "remote", "", "my--vscode-remote", "vscode_remote"] + for invalid_type in invalid_types: + with self.assertRaises(ValueError): + self.hp_space.create_space_access(connection_type=invalid_type) + @patch('sagemaker.hyperpod.space.hyperpod_space.client.CustomObjectsApi') @patch.object(HPSpace, 'verify_kube_config') @patch('sagemaker.hyperpod.space.hyperpod_space.handle_exception')