Source code for sagemaker.hyperpod.space.hyperpod_space

  1import logging
  2import yaml
  3import boto3
  4from sagemaker.hyperpod.common.utils import create_boto3_client
  5from typing import List, Optional, ClassVar, Dict, Set, Any
  6from pydantic import BaseModel, Field, ConfigDict, model_validator
  7from kubernetes import client, config
  8from kubernetes.client.rest import ApiException
  9from kr8s.objects import Pod
 10
 11from sagemaker.hyperpod.common.config.metadata import Metadata
 12from hyperpod_space_template.v1_0.model import ResourceRequirements
 13from sagemaker.hyperpod.common.utils import (
 14    handle_exception,
 15    get_default_namespace,
 16    setup_logging,
 17    verify_kubernetes_version_compatibility,
 18)
 19from sagemaker.hyperpod.space.utils import (
 20    map_kubernetes_response_to_model,
 21    validate_space_mig_resources,
 22    validate_mig_profile_in_cluster,
 23)
 24from sagemaker.hyperpod.common.telemetry.telemetry_logging import (
 25    _hyperpod_telemetry_emitter,
 26)
 27from sagemaker.hyperpod.common.telemetry.constants import Feature
 28from sagemaker.hyperpod.cli.constants.space_constants import (
 29    SPACE_GROUP,
 30    SPACE_VERSION,
 31    SPACE_PLURAL,
 32    DEFAULT_SPACE_PORT,
 33)
 34from sagemaker.hyperpod.cli.constants.space_access_constants import (
 35    SPACE_ACCESS_GROUP,
 36    SPACE_ACCESS_VERSION,
 37    SPACE_ACCESS_PLURAL,
 38)
 39from hyperpod_space_template.v1_0.model import SpaceConfig, ResourceRequirements
 40
 41
[docs] 42class HPSpace(BaseModel): 43 """HyperPod Space on Amazon SageMaker HyperPod clusters. 44 45 This class provides methods to create, manage, and monitor spaces 46 on SageMaker HyperPod clusters orchestrated by Amazon EKS. Spaces are 47 interactive workspaces that provide development environments with 48 configurable resources, storage, and access controls. 49 50 **Attributes:** 51 52 .. list-table:: 53 :header-rows: 1 54 :widths: 20 20 60 55 56 * - Attribute 57 - Type 58 - Description 59 * - config 60 - SpaceConfig 61 - The space configuration using the space parameter model 62 * - raw_resource 63 - Dict[str, Any], optional 64 - The complete Kubernetes resource data including apiVersion, kind, metadata, and status 65 66 .. dropdown:: Usage Examples 67 :open: 68 69 .. code-block:: python 70 71 >>> # Create a new space 72 >>> from hyperpod_space_template.v1_0.model import SpaceConfig 73 >>> config = SpaceConfig(name="my-space", display_name="My Space") 74 >>> space = HPSpace(config=config) 75 >>> space.create() 76 77 >>> # List all spaces 78 >>> spaces = HPSpace.list() 79 >>> for space in spaces: 80 ... print(f"Space: {space.config.name}") 81 """ 82 83 is_kubeconfig_loaded: ClassVar[bool] = False 84 model_config = ConfigDict(extra="forbid") 85 86 config: SpaceConfig = Field( 87 description="The space configuration using the space parameter model" 88 ) 89 90 raw_resource: Optional[Dict[str, Any]] = Field( 91 default=None, 92 description="The complete Kubernetes resource data including apiVersion, kind, metadata, and status" 93 ) 94 95 @classmethod 96 def get_logger(cls): 97 """Get logger for the HPSpace class. 98 99 **Returns:** 100 101 logging.Logger: Logger instance configured for the HPSpace class 102 103 .. dropdown:: Usage Examples 104 :open: 105 106 .. code-block:: python 107 108 >>> logger = HPSpace.get_logger() 109 >>> logger.info("Space operation completed") 110 """ 111 return logging.getLogger(__name__) 112 113 @property 114 def api_version(self) -> Optional[str]: 115 """Get the apiVersion from the Kubernetes resource. 116 117 **Returns:** 118 119 str or None: The API version of the Kubernetes resource, or None if raw_resource is not available 120 121 .. dropdown:: Usage Examples 122 :open: 123 124 .. code-block:: python 125 126 >>> space = HPSpace.get("my-space") 127 >>> print(f"API Version: {space.api_version}") 128 """ 129 return self.raw_resource.get("apiVersion") if self.raw_resource else None 130 131 @property 132 def kind(self) -> Optional[str]: 133 """Get the kind from the Kubernetes resource. 134 135 **Returns:** 136 137 str or None: The kind of the Kubernetes resource, or None if raw_resource is not available 138 139 .. dropdown:: Usage Examples 140 :open: 141 142 .. code-block:: python 143 144 >>> space = HPSpace.get("my-space") 145 >>> print(f"Resource Kind: {space.kind}") 146 """ 147 return self.raw_resource.get("kind") if self.raw_resource else None 148 149 @property 150 def metadata(self) -> Optional[Dict[str, Any]]: 151 """Get the metadata from the Kubernetes resource. 152 153 **Returns:** 154 155 Dict[str, Any] or None: The metadata section of the Kubernetes resource, or None if raw_resource is not available 156 157 .. dropdown:: Usage Examples 158 :open: 159 160 .. code-block:: python 161 162 >>> space = HPSpace.get("my-space") 163 >>> print(f"Creation Time: {space.metadata['creationTimestamp']}") 164 """ 165 return self.raw_resource.get("metadata") if self.raw_resource else None 166 167 @property 168 def status(self) -> Optional[Dict[str, Any]]: 169 """Get the status from the Kubernetes resource. 170 171 **Returns:** 172 173 Dict[str, Any] or None: The status section of the Kubernetes resource, or None if raw_resource is not available 174 175 .. dropdown:: Usage Examples 176 :open: 177 178 .. code-block:: python 179 180 >>> space = HPSpace.get("my-space") 181 >>> conditions = space.status.get('conditions', []) 182 >>> for condition in conditions: 183 ... print(f"{condition['type']}: {condition['status']}") 184 """ 185 return self.raw_resource.get("status") if self.raw_resource else None 186 187 @classmethod 188 def verify_kube_config(cls): 189 """Verify and load Kubernetes configuration. 190 191 Loads the Kubernetes configuration from the default kubeconfig location 192 and verifies compatibility with the cluster. This method is called 193 automatically by other methods that interact with the Kubernetes API. 194 195 **Raises:** 196 197 RuntimeError: If the kubeconfig cannot be loaded or is invalid 198 199 .. dropdown:: Usage Examples 200 :open: 201 202 .. code-block:: python 203 204 >>> # Verify kubeconfig before operations 205 >>> HPSpace.verify_kube_config() 206 """ 207 if not cls.is_kubeconfig_loaded: 208 try: 209 config.load_kube_config() 210 cls.is_kubeconfig_loaded = True 211 verify_kubernetes_version_compatibility(cls.get_logger()) 212 except Exception as e: 213 raise RuntimeError(f"Failed to load kubeconfig: {e}") 214 215 @staticmethod 216 def _extract_mig_profiles(resources: Optional[ResourceRequirements]) -> Set[str]: 217 """Extract MIG profile resource keys from resources without validation. 218 219 **Parameters:** 220 221 .. list-table:: 222 :header-rows: 1 223 :widths: 20 20 60 224 225 * - Parameter 226 - Type 227 - Description 228 * - resources 229 - ResourceRequirements or None 230 - The resource requirements to extract MIG profiles from 231 232 **Returns:** 233 234 set: Set of MIG profile resource keys found in the resources 235 """ 236 if not resources: 237 return set() 238 239 mig_profiles = set() 240 241 if resources.requests: 242 mig_profiles.update([ 243 key for key in resources.requests.keys() 244 if key.startswith("nvidia.com/mig-") 245 ]) 246 247 if resources.limits: 248 mig_profiles.update([ 249 key for key in resources.limits.keys() 250 if key.startswith("nvidia.com/mig-") 251 ]) 252 253 return mig_profiles 254 255 def _validate_and_extract_mig_profiles(self, resources: Optional[ResourceRequirements]) -> Set[str]: 256 """Validate MIG resources and extract MIG profiles. 257 258 **Parameters:** 259 260 .. list-table:: 261 :header-rows: 1 262 :widths: 20 20 60 263 264 * - Parameter 265 - Type 266 - Description 267 * - resources 268 - ResourceRequirements or None 269 - The resource requirements to validate 270 271 **Returns:** 272 273 set: Set of MIG profile resource keys found in the resources 274 275 **Raises:** 276 277 RuntimeError: If MIG validation fails or profiles are invalid 278 """ 279 if not resources: 280 return set() 281 282 # Validate requests 283 if resources.requests: 284 valid, err = validate_space_mig_resources(resources.requests) 285 if not valid: 286 raise RuntimeError(err) 287 288 # Validate limits 289 if resources.limits: 290 valid, err = validate_space_mig_resources(resources.limits) 291 if not valid: 292 raise RuntimeError(err) 293 294 # Extract MIG profiles 295 mig_profiles = self._extract_mig_profiles(resources) 296 297 # Validate that requests and limits use the same MIG profile 298 if len(mig_profiles) > 1: 299 raise RuntimeError( 300 "MIG profile mismatch: requests and limits must use the same MIG profile. " 301 f"Found: {', '.join(mig_profiles)}" 302 ) 303 304 # Validate MIG profile exists in cluster 305 if mig_profiles: 306 mig_profile = list(mig_profiles)[0] 307 valid, err = validate_mig_profile_in_cluster(mig_profile) 308 if not valid: 309 raise RuntimeError(err) 310 311 return mig_profiles 312
[docs] 313 @_hyperpod_telemetry_emitter(Feature.HYPERPOD, "create_space") 314 def create(self, debug: bool = False): 315 """Create and submit the HyperPod Space to the Kubernetes cluster. 316 317 Creates a new space resource in the Kubernetes cluster based on the 318 configuration provided in the space config. Validates MIG profiles 319 if enabled and converts the configuration to the appropriate domain model. 320 321 **Parameters:** 322 323 .. list-table:: 324 :header-rows: 1 325 :widths: 20 20 60 326 327 * - Parameter 328 - Type 329 - Description 330 * - debug 331 - bool, optional 332 - Enable debug logging (default: False) 333 334 **Raises:** 335 336 RuntimeError: If MIG profile validation fails or unsupported profiles are used 337 Exception: If the space creation fails or Kubernetes API call fails 338 339 .. dropdown:: Usage Examples 340 :open: 341 342 .. code-block:: python 343 344 >>> # Create a space with debug logging 345 >>> space = HPSpace(config=space_config) 346 >>> space.create(debug=True) 347 348 >>> # Create a space with default settings 349 >>> space.create() 350 """ 351 self.verify_kube_config() 352 353 logger = self.get_logger() 354 logger = setup_logging(logger, debug) 355 356 # Validate and extract MIG profiles 357 self._validate_and_extract_mig_profiles(self.config.resources) 358 359 # Convert config to domain model 360 domain_config = self.config.to_domain() 361 config_body = domain_config["space_spec"] 362 363 logger.debug( 364 "Creating HyperPod Space with config:\n%s", 365 yaml.dump(config_body), 366 ) 367 368 custom_api = client.CustomObjectsApi() 369 370 try: 371 custom_api.create_namespaced_custom_object( 372 group=SPACE_GROUP, 373 version=SPACE_VERSION, 374 namespace=self.config.namespace, 375 plural=SPACE_PLURAL, 376 body=config_body, 377 ) 378 logger.debug(f"Successfully created HyperPod Space '{self.config.name}'!") 379 except Exception as e: 380 logger.error(f"Failed to create HyperPod Space {self.config.name}!") 381 handle_exception(e, self.config.name, self.config.namespace, debug=debug)
382
[docs] 383 @classmethod 384 @_hyperpod_telemetry_emitter(Feature.HYPERPOD, "list_spaces") 385 def list(cls, namespace: Optional[str] = None) -> List["HPSpace"]: 386 """List all HyperPod Spaces in the specified namespace created by the caller. 387 388 Retrieves all spaces that were either created by the current caller (based on 389 AWS STS identity) or are marked as 'Public' ownership type. Uses pagination 390 to handle large numbers of spaces efficiently. 391 392 **Parameters:** 393 394 .. list-table:: 395 :header-rows: 1 396 :widths: 20 20 60 397 398 * - Parameter 399 - Type 400 - Description 401 * - namespace 402 - str, optional 403 - The Kubernetes namespace to list spaces from. If None, uses the default namespace from current context 404 405 **Returns:** 406 407 List[HPSpace]: List of HPSpace instances created by the caller or marked as public 408 409 **Raises:** 410 411 Exception: If the Kubernetes API call fails or spaces cannot be retrieved 412 413 .. dropdown:: Usage Examples 414 :open: 415 416 .. code-block:: python 417 418 >>> # List spaces in default namespace 419 >>> spaces = HPSpace.list() 420 >>> print(f"Found {len(spaces)} spaces") 421 422 >>> # List spaces in specific namespace 423 >>> spaces = HPSpace.list(namespace="my-namespace") 424 >>> for space in spaces: 425 ... print(f"Space: {space.config.name}") 426 """ 427 cls.verify_kube_config() 428 429 if not namespace: 430 namespace = get_default_namespace() 431 432 # Get caller identity 433 sts_client = create_boto3_client('sts') 434 caller_identity = sts_client.get_caller_identity() 435 caller_arn = caller_identity['Arn'] 436 437 custom_api = client.CustomObjectsApi() 438 spaces = [] 439 continue_token = None 440 441 try: 442 while True: 443 response = custom_api.list_namespaced_custom_object( 444 group=SPACE_GROUP, 445 version=SPACE_VERSION, 446 namespace=namespace, 447 plural=SPACE_PLURAL, 448 _continue=continue_token 449 ) 450 451 for item in response.get("items", []): 452 # Check if space was created by the caller or it's set as 'Public' 453 created_by = item.get('metadata', {}).get('annotations', {}).get('workspace.jupyter.org/created-by') 454 ownership_type = item.get('spec', {}).get('ownershipType', '') 455 if created_by == caller_arn or ownership_type == "Public": 456 config_data = map_kubernetes_response_to_model(item, SpaceConfig) 457 space_config = SpaceConfig(**config_data) 458 459 space = cls( 460 config=space_config, 461 raw_resource=item 462 ) 463 spaces.append(space) 464 465 # Check if there are more pages 466 continue_token = response.get('metadata', {}).get('continue') 467 if not continue_token: 468 break 469 470 return spaces 471 except Exception as e: 472 handle_exception(e, "list", namespace)
473
[docs] 474 @classmethod 475 @_hyperpod_telemetry_emitter(Feature.HYPERPOD, "get_space") 476 def get(cls, name: str, namespace: str = None) -> "HPSpace": 477 """Get a specific HyperPod Space by name. 478 479 Retrieves a single space resource from the Kubernetes cluster and maps 480 the response to the SpaceConfig model for easy access to configuration 481 and status information. 482 483 **Parameters:** 484 485 .. list-table:: 486 :header-rows: 1 487 :widths: 20 20 60 488 489 * - Parameter 490 - Type 491 - Description 492 * - name 493 - str 494 - The name of the space to retrieve 495 * - namespace 496 - str, optional 497 - The Kubernetes namespace. If None, uses the default namespace from current context 498 499 **Returns:** 500 501 HPSpace: The space instance with configuration and raw Kubernetes resource data 502 503 **Raises:** 504 505 Exception: If the space is not found or Kubernetes API call fails 506 507 .. dropdown:: Usage Examples 508 :open: 509 510 .. code-block:: python 511 512 >>> # Get space from default namespace 513 >>> space = HPSpace.get("my-space") 514 >>> print(f"Space status: {space.status}") 515 516 >>> # Get space from specific namespace 517 >>> space = HPSpace.get("my-space", namespace="production") 518 >>> print(f"Display name: {space.config.display_name}") 519 """ 520 cls.verify_kube_config() 521 522 if not namespace: 523 namespace = get_default_namespace() 524 525 custom_api = client.CustomObjectsApi() 526 527 try: 528 response = custom_api.get_namespaced_custom_object( 529 group=SPACE_GROUP, 530 version=SPACE_VERSION, 531 namespace=namespace, 532 plural=SPACE_PLURAL, 533 name=name 534 ) 535 536 # Use dynamic mapping based on SpaceConfig model 537 config_data = map_kubernetes_response_to_model(response, SpaceConfig) 538 539 space_config = SpaceConfig(**config_data) 540 541 return cls( 542 config=space_config, 543 raw_resource=response 544 ) 545 except Exception as e: 546 handle_exception(e, name, namespace)
547
[docs] 548 @_hyperpod_telemetry_emitter(Feature.HYPERPOD, "delete_space") 549 def delete(self): 550 """Delete the HyperPod Space from the Kubernetes cluster. 551 552 Permanently removes the space resource from the Kubernetes cluster. 553 This operation cannot be undone and will terminate any running 554 workloads associated with the space. 555 556 **Raises:** 557 558 Exception: If the deletion fails or Kubernetes API call fails 559 560 .. dropdown:: Usage Examples 561 :open: 562 563 .. code-block:: python 564 565 >>> # Delete a space 566 >>> space = HPSpace.get("my-space") 567 >>> space.delete() 568 """ 569 self.verify_kube_config() 570 logger = self.get_logger() 571 572 custom_api = client.CustomObjectsApi() 573 574 try: 575 custom_api.delete_namespaced_custom_object( 576 group=SPACE_GROUP, 577 version=SPACE_VERSION, 578 namespace=self.config.namespace, 579 plural=SPACE_PLURAL, 580 name=self.config.name 581 ) 582 logger.debug(f"Successfully deleted HyperPod Space '{self.config.name}'!") 583 except Exception as e: 584 logger.error(f"Failed to delete HyperPod Space {self.config.name}!") 585 handle_exception(e, self.config.name, self.config.namespace)
586
[docs] 587 @_hyperpod_telemetry_emitter(Feature.HYPERPOD, "update_space") 588 def update(self, **kwargs): 589 """Update the HyperPod Space configuration. 590 591 Updates the space configuration with the provided parameters. Validates 592 MIG profiles if resource updates are requested and ensures compatibility 593 with the current node instance type. 594 595 **Parameters:** 596 597 .. list-table:: 598 :header-rows: 1 599 :widths: 20 20 60 600 601 * - Parameter 602 - Type 603 - Description 604 * - **kwargs 605 - Any 606 - Configuration fields to update (e.g., desired_status="Stopped", display_name="New Name") 607 608 **Raises:** 609 610 RuntimeError: If MIG profile validation fails or unsupported profiles are used 611 Exception: If the update fails or Kubernetes API call fails 612 613 .. dropdown:: Usage Examples 614 :open: 615 616 .. code-block:: python 617 618 >>> # Update space status 619 >>> space = HPSpace.get("my-space") 620 >>> space.update(desired_status="Stopped") 621 622 >>> # Update display name and resources 623 >>> space.update( 624 ... display_name="Updated Space", 625 ... resources={"requests": {"cpu": "2", "memory": "4Gi"}} 626 ... ) 627 """ 628 self.verify_kube_config() 629 logger = self.get_logger() 630 631 # Validate MIG profile configuration 632 if "resources" in kwargs: 633 resources = kwargs["resources"] 634 635 if isinstance(resources, dict): 636 resources = ResourceRequirements(**resources) 637 638 # Validate and extract MIG profiles 639 mig_profiles = self._validate_and_extract_mig_profiles(resources) 640 641 # Remove existing MIG profiles if changing to a different one 642 if mig_profiles: 643 mig_profile = list(mig_profiles)[0] 644 645 existing_config = HPSpace.get(self.config.name, self.config.namespace).config 646 existing_mig_profiles = self._extract_mig_profiles(existing_config.resources) 647 648 if existing_mig_profiles and mig_profile not in existing_mig_profiles: 649 # Remove existing MIG profiles by setting to None 650 for existing_profile in existing_mig_profiles: 651 if existing_profile != mig_profile: 652 kwargs["resources"].setdefault("requests", {})[existing_profile] = None 653 kwargs["resources"].setdefault("limits", {})[existing_profile] = None 654 655 custom_api = client.CustomObjectsApi() 656 657 # Update space config with the input config 658 current_config = self.config.model_dump(by_alias=True) 659 current_config.update(kwargs) 660 self.config = SpaceConfig(**current_config) 661 662 # Convert to domain model and extract spec 663 domain_config = self.config.to_domain() 664 spec_updates = domain_config["space_spec"]["spec"] 665 666 try: 667 custom_api.patch_namespaced_custom_object( 668 group=SPACE_GROUP, 669 version=SPACE_VERSION, 670 namespace=self.config.namespace, 671 plural=SPACE_PLURAL, 672 name=self.config.name, 673 body={"spec": spec_updates} 674 ) 675 logger.debug(f"Successfully updated HyperPod Space '{self.config.name}'!") 676 except Exception as e: 677 logger.error(f"Failed to update HyperPod Space {self.config.name}!") 678 handle_exception(e, self.config.name, self.config.namespace)
679
[docs] 680 @_hyperpod_telemetry_emitter(Feature.HYPERPOD, "start_space") 681 def start(self): 682 """Start the HyperPod Space by setting desired status to Running. 683 684 Convenience method that updates the space's desired status to "Running", 685 which will cause the Kubernetes operator to start the space workloads. 686 687 .. dropdown:: Usage Examples 688 :open: 689 690 .. code-block:: python 691 692 >>> # Start a space 693 >>> space = HPSpace.get("my-space") 694 >>> space.start() 695 """ 696 self.update(desired_status="Running")
697
[docs] 698 @_hyperpod_telemetry_emitter(Feature.HYPERPOD, "stop_space") 699 def stop(self): 700 """Stop the HyperPod Space by setting desired status to Stopped. 701 702 Convenience method that updates the space's desired status to "Stopped", 703 which will cause the Kubernetes operator to stop the space workloads. 704 705 .. dropdown:: Usage Examples 706 :open: 707 708 .. code-block:: python 709 710 >>> # Stop a space 711 >>> space = HPSpace.get("my-space") 712 >>> space.stop() 713 """ 714 self.update(desired_status="Stopped")
715
[docs] 716 def list_pods(self) -> List[str]: 717 """List all pods associated with this space. 718 719 Retrieves all Kubernetes pods that are labeled as belonging to this 720 space using the workspace-name label selector. 721 722 **Returns:** 723 724 List[str]: List of pod names associated with the space 725 726 **Raises:** 727 728 Exception: If the Kubernetes API call fails 729 730 .. dropdown:: Usage Examples 731 :open: 732 733 .. code-block:: python 734 735 >>> # List pods for a space 736 >>> space = HPSpace.get("my-space") 737 >>> pods = space.list_pods() 738 >>> print(f"Found {len(pods)} pods: {pods}") 739 """ 740 self.verify_kube_config() 741 logger = self.get_logger() 742 743 v1 = client.CoreV1Api() 744 745 try: 746 pods = v1.list_namespaced_pod( 747 namespace=self.config.namespace, 748 label_selector=f"{SPACE_GROUP}/workspace-name={self.config.name}" 749 ) 750 return [pod.metadata.name for pod in pods.items] 751 except Exception as e: 752 handle_exception(e, self.config.name, self.config.namespace)
753
[docs] 754 def get_logs(self, pod_name: Optional[str] = None, container: Optional[str] = None) -> str: 755 """Get logs from a pod associated with this space. 756 757 Retrieves logs from a specific pod and container. If no pod is specified, 758 uses the first available pod. If no container is specified, defaults to 759 the "workspace" container. 760 761 **Parameters:** 762 763 .. list-table:: 764 :header-rows: 1 765 :widths: 20 20 60 766 767 * - Parameter 768 - Type 769 - Description 770 * - pod_name 771 - str, optional 772 - Name of the pod to get logs from. If None, gets logs from the first available pod 773 * - container 774 - str, optional 775 - Name of the container to get logs from. Defaults to "workspace" 776 777 **Returns:** 778 779 str: The pod logs as a string 780 781 **Raises:** 782 783 RuntimeError: If no pods are found for the space 784 Exception: If the Kubernetes API call fails 785 786 .. dropdown:: Usage Examples 787 :open: 788 789 .. code-block:: python 790 791 >>> # Get logs from default pod and container 792 >>> space = HPSpace.get("my-space") 793 >>> logs = space.get_logs() 794 >>> print(logs) 795 796 >>> # Get logs from specific pod and container 797 >>> logs = space.get_logs(pod_name="my-pod", container="sidecar") 798 """ 799 self.verify_kube_config() 800 logger = self.get_logger() 801 802 if not pod_name: 803 pods = self.list_pods() 804 if not pods: 805 raise RuntimeError(f"No pods found for space '{self.config.name}'") 806 pod_name = pods[0] 807 808 if not container: 809 container = "workspace" 810 811 v1 = client.CoreV1Api() 812 813 try: 814 return v1.read_namespaced_pod_log( 815 name=pod_name, 816 namespace=self.config.namespace, 817 container=container 818 ) 819 except Exception as e: 820 handle_exception(e, pod_name, self.config.namespace)
821
[docs] 822 @_hyperpod_telemetry_emitter(Feature.HYPERPOD, "create_space_access") 823 def create_space_access(self, connection_type: str = "vscode-remote") -> Dict[str, str]: 824 """Create a space access for this space. 825 826 Creates a space access resource that provides remote connection capabilities 827 to the space. Supports VS Code remote development and web UI access types. 828 829 **Parameters:** 830 831 .. list-table:: 832 :header-rows: 1 833 :widths: 20 20 60 834 835 * - Parameter 836 - Type 837 - Description 838 * - connection_type 839 - str, optional 840 - The IDE type for remote access. Must be "vscode-remote" or "web-ui" (default: "vscode-remote") 841 842 **Returns:** 843 844 Dict[str, str]: Dictionary containing 'SpaceConnectionType' and 'SpaceConnectionUrl' keys 845 846 **Raises:** 847 848 ValueError: If connection_type is not "vscode-remote" or "web-ui" 849 Exception: If the space access creation fails or Kubernetes API call fails 850 851 .. dropdown:: Usage Examples 852 :open: 853 854 .. code-block:: python 855 856 >>> # Create VS Code remote access 857 >>> space = HPSpace.get("my-space") 858 >>> access = space.create_space_access("vscode-remote") 859 >>> print(f"Connection URL: {access['SpaceConnectionUrl']}") 860 861 >>> # Create web UI access 862 >>> access = space.create_space_access("web-ui") 863 >>> print(f"Web UI URL: {access['SpaceConnectionUrl']}") 864 """ 865 self.verify_kube_config() 866 logger = self.get_logger() 867 868 if connection_type not in {"vscode-remote", "web-ui"}: 869 raise ValueError("--connection-type must be 'vscode-remote' or 'web-ui'.") 870 871 config = { 872 "metadata": { 873 "namespace": self.config.namespace, 874 }, 875 "spec": { 876 "workspaceName": self.config.name, 877 "workspaceConnectionType": connection_type, 878 } 879 } 880 881 custom_api = client.CustomObjectsApi() 882 883 try: 884 response = custom_api.create_namespaced_custom_object( 885 group=SPACE_ACCESS_GROUP, 886 version=SPACE_ACCESS_VERSION, 887 namespace=self.config.namespace, 888 plural=SPACE_ACCESS_PLURAL, 889 body=config 890 ) 891 logger.debug(f"Successfully created space access for '{self.config.name}'!") 892 return { 893 "SpaceConnectionType": connection_type, 894 "SpaceConnectionUrl": response["status"]["workspaceConnectionUrl"] 895 } 896 except Exception as e: 897 logger.error(f"Failed to create space access for {self.config.name}!") 898 handle_exception(e, self.config.name, self.config.namespace)
899
[docs] 900 @_hyperpod_telemetry_emitter(Feature.HYPERPOD, "portforward_space") 901 def portforward_space(self, local_port: str, remote_port: str = DEFAULT_SPACE_PORT): 902 """Forward local port to the space pod for development access. 903 904 Creates a port forwarding connection from a local port to a remote port 905 on the space pod, enabling direct access to services running inside the 906 space. 907 908 **Parameters:** 909 910 .. list-table:: 911 :header-rows: 1 912 :widths: 20 20 60 913 914 * - Parameter 915 - Type 916 - Description 917 * - local_port 918 - str 919 - The local port to forward from 920 * - remote_port 921 - str, optional 922 - The remote port on the space pod to forward to (default: DEFAULT_SPACE_PORT) 923 924 **Raises:** 925 926 RuntimeError: If no pods are found for the space or if the space is not in Available status 927 KeyboardInterrupt: When the user stops the port forwarding with Ctrl+C 928 Exception: If the port forwarding setup fails or Kubernetes API call fails 929 930 .. dropdown:: Usage Examples 931 :open: 932 933 .. code-block:: python 934 935 >>> # Forward local port 8080 to default remote port 936 >>> space = HPSpace.get("myspace") 937 >>> space.portforward_space("8080") 938 939 >>> # Forward local port 3000 to remote port 8888 940 >>> space.portforward_space("3000", "8888") 941 942 >>> # Access forwarded service (in another terminal) 943 >>> # curl http://localhost:8080 944 """ 945 946 self.verify_kube_config() 947 logger = self.get_logger() 948 949 # Check if space is in Available status 950 if self.status and self.status.get("conditions"): 951 is_available = False 952 for condition in self.status["conditions"]: 953 if condition.get("type") == "Available" and condition.get("status") == "True": 954 is_available = True 955 break 956 957 if not is_available: 958 raise RuntimeError(f"Space '{self.config.name}' is not in Available status. Port forwarding is only allowed for available spaces.") 959 960 pods = self.list_pods() 961 if not pods: 962 raise RuntimeError(f"No pods found for space '{self.config.name}'") 963 964 pod_name = pods[0] 965 pod = Pod.get(name=pod_name, namespace=self.config.namespace) 966 pf = pod.portforward(remote_port=int(remote_port), local_port=int(local_port)) 967 968 logger.debug(f"Forwarding from local port {local_port} to space pod: {pod_name}.") 969 970 try: 971 pf.run_forever() 972 except KeyboardInterrupt: 973 logger.debug("Stopping space port forward...") 974 finally: 975 pf.stop()