Source code for hyperpod_space_template.v1_1.model

  1from pydantic import BaseModel, ConfigDict, Field, field_validator
  2from typing import Optional, List, Dict, Literal, Any
  3from enum import Enum
  4
  5# Minimum amazon-sagemaker-spaces addon version required for this template version
  6MIN_ADDON_VERSION = "0.1.6"
  7
  8
[docs] 9class OwnershipType(str, Enum): 10 PUBLIC = "Public" 11 OWNER_ONLY = "OwnerOnly"
12 13
[docs] 14class DesiredStatus(str, Enum): 15 RUNNING = "Running" 16 STOPPED = "Stopped"
17 18
[docs] 19class VolumeSpec(BaseModel): 20 """VolumeSpec defines a volume to mount from an existing PVC""" 21 name: str = Field( 22 description="Name is a unique identifier for this volume within the pod (maps to pod.spec.volumes[].name)", 23 min_length=1 24 ) 25 mount_path: str = Field( 26 alias="mountPath", 27 description="MountPath is the path where the volume should be mounted (Unix-style path, e.g. /data)", 28 min_length=1 29 ) 30 persistent_volume_claim_name: str = Field( 31 alias="persistentVolumeClaimName", 32 description="PersistentVolumeClaimName is the name of the existing PVC to mount", 33 min_length=1 34 )
35 36
[docs] 37class ContainerConfig(BaseModel): 38 """ContainerConfig defines container command and args configuration""" 39 command: Optional[List[str]] = Field( 40 default=None, 41 description="Command specifies the container command" 42 ) 43 args: Optional[List[str]] = Field( 44 default=None, 45 description="Args specifies the container arguments" 46 )
47 48
[docs] 49class AccessStrategyRef(BaseModel): 50 """AccessStrategyRef references a WorkspaceAccessStrategy""" 51 name: str = Field( 52 description="Name of the WorkspaceAccessStrategy" 53 ) 54 namespace: Optional[str] = Field( 55 default=None, 56 description="Namespace where the WorkspaceAccessStrategy is located" 57 )
58 59
[docs] 60class TemplateRef(BaseModel): 61 """TemplateRef defines a reference to a WorkspaceTemplate""" 62 name: str = Field( 63 description="Name of the WorkspaceTemplate" 64 ) 65 namespace: Optional[str] = Field( 66 default=None, 67 description="Namespace where the WorkspaceTemplate is located" 68 )
69 70
[docs] 71class IdleDetectionSpec(BaseModel): 72 """IdleDetectionSpec defines idle detection methods""" 73 http_get: Optional[Dict[str, Any]] = Field( 74 default=None, 75 alias="httpGet", 76 description="HTTPGet specifies the HTTP request to perform for idle detection" 77 )
78 79
[docs] 80class IdleShutdownSpec(BaseModel): 81 """IdleShutdownSpec defines idle shutdown configuration""" 82 enabled: bool = Field( 83 description="Enabled indicates if idle shutdown is enabled" 84 ) 85 idle_timeout_in_minutes: int = Field( 86 alias="idleTimeoutInMinutes", 87 description="IdleTimeoutInMinutes specifies idle timeout in minutes", 88 ge=1 89 ) 90 detection: IdleDetectionSpec = Field( 91 description="Detection specifies how to detect idle state" 92 )
93 94
[docs] 95class StorageSpec(BaseModel): 96 """StorageSpec defines the storage configuration for Workspace""" 97 storage_class_name: Optional[str] = Field( 98 default=None, 99 alias="storageClassName", 100 description="StorageClassName specifies the storage class to use for persistent storage" 101 ) 102 size: Optional[str] = Field( 103 default="10Gi", 104 description="Size specifies the size of the persistent volume. Supports standard Kubernetes resource quantities (e.g., '10Gi', '500Mi', '1Ti'). Integer values without units are interpreted as bytes" 105 ) 106 mount_path: Optional[str] = Field( 107 default="/home", 108 alias="mountPath", 109 description="MountPath specifies where to mount the persistent volume in the container. Default is /home/jovyan (jovyan is the standard user in Jupyter images)" 110 )
111 112
[docs] 113class ResourceRequirements(BaseModel): 114 """ResourceRequirements describes the compute resource requirements""" 115 requests: Optional[Dict[str, Optional[str]]] = Field( 116 default=None, 117 description="Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits." 118 ) 119 limits: Optional[Dict[str, Optional[str]]] = Field( 120 default=None, 121 description="Limits describes the maximum amount of compute resources allowed." 122 )
123 124
[docs] 125class SpaceConfig(BaseModel): 126 """SpaceConfig defines the desired state of a Space""" 127 model_config = ConfigDict(extra="forbid") 128 129 name: str = Field( 130 description="Space name", 131 min_length=1, 132 max_length=63, 133 pattern=r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' 134 ) 135 display_name: str = Field( 136 alias="display_name", 137 description="Display Name of the space", 138 min_length=1 139 ) 140 namespace: str = Field( 141 default="default", 142 description="Kubernetes namespace", 143 min_length=1 144 ) 145 image: Optional[str] = Field( 146 default=None, 147 description="Image specifies the container image to use" 148 ) 149 desired_status: Optional[DesiredStatus] = Field( 150 default=None, 151 alias="desired_status", 152 description="DesiredStatus specifies the desired operational status" 153 ) 154 ownership_type: Optional[OwnershipType] = Field( 155 default=None, 156 alias="ownership_type", 157 description="OwnershipType specifies who can modify the space. 'Public' means anyone with RBAC permissions can update/delete the space. 'OwnerOnly' means only the creator can update/delete the space." 158 ) 159 resources: Optional[ResourceRequirements] = Field( 160 default=None, 161 description="Resources specifies the resource requirements" 162 ) 163 storage: Optional[StorageSpec] = Field( 164 default=None, 165 description="Storage specifies the storage configuration" 166 ) 167 volumes: Optional[List[VolumeSpec]] = Field( 168 default=None, 169 description="Volumes specifies additional volumes to mount from existing PersistentVolumeClaims" 170 ) 171 container_config: Optional[ContainerConfig] = Field( 172 default=None, 173 alias="container_config", 174 description="ContainerConfig specifies container command and args configuration" 175 ) 176 node_selector: Optional[Dict[str, str]] = Field( 177 default=None, 178 alias="node_selector", 179 description="NodeSelector specifies node selection constraints for the space pod (JSON string)" 180 ) 181 affinity: Optional[Dict[str, Any]] = Field( 182 default=None, 183 description="Affinity specifies node affinity and anti-affinity rules for the space pod (JSON string)" 184 ) 185 tolerations: Optional[List[Dict[str, Any]]] = Field( 186 default=None, 187 description="Tolerations specifies tolerations for the space pod to schedule on nodes with matching taints (JSON string)" 188 ) 189 lifecycle: Optional[Dict[str, Any]] = Field( 190 default=None, 191 description="Lifecycle specifies actions that the management system should take in response to container lifecycle events (JSON string)" 192 ) 193 template_ref: Optional[TemplateRef] = Field( 194 default=None, 195 alias="template_ref", 196 description="TemplateRef references a WorkspaceTemplate to use as base configuration. When set, template provides defaults and workspace spec fields act as overrides" 197 ) 198 idle_shutdown: Optional[IdleShutdownSpec] = Field( 199 default=None, 200 alias="idle_shutdown", 201 description="IdleShutdown specifies idle shutdown configuration" 202 ) 203 app_type: Optional[str] = Field( 204 default=None, 205 alias="app_type", 206 description="AppType specifies the application type for this workspace" 207 ) 208 service_account_name: Optional[str] = Field( 209 default=None, 210 alias="service_account_name", 211 description="ServiceAccountName specifies the name of the ServiceAccount to use for the workspace pod" 212 ) 213 access_type: Optional[OwnershipType] = Field( 214 default=None, 215 alias="access_type", 216 description="AccessType specifies who can connect to the workspace. 'Public' means anyone with RBAC permissions can connect. 'OwnerOnly' means only the creator can connect." 217 ) 218 env: Optional[List[Dict[str, Any]]] = Field( 219 default=None, 220 description="Environment variables for the workspace container (list of {name, value} objects)" 221 ) 222 access_strategy: Optional[AccessStrategyRef] = Field( 223 default=None, 224 alias="access_strategy", 225 description="AccessStrategy references a WorkspaceAccessStrategy to use" 226 ) 227 pod_security_context: Optional[Dict[str, Any]] = Field( 228 default=None, 229 alias="pod_security_context", 230 description="Pod-level security context. Overrides template defaults when specified (JSON string)" 231 ) 232 container_security_context: Optional[Dict[str, Any]] = Field( 233 default=None, 234 alias="container_security_context", 235 description="Container-level security context for the main workspace container. Overrides template defaults (JSON string)" 236 ) 237 init_containers: Optional[List[Dict[str, Any]]] = Field( 238 default=None, 239 alias="init_containers", 240 description="Init containers to run before the workspace container starts (JSON string, max 10)" 241 ) 242 queue_name: Optional[str] = Field( 243 default=None, 244 alias="queue_name", 245 description="Queue name for space scheduling", 246 min_length=1, 247 max_length=63, 248 pattern=r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' 249 ) 250 priority: Optional[str] = Field( 251 default=None, 252 description="Priority class for space scheduling", 253 min_length=1 254 ) 255
[docs] 256 @field_validator('volumes') 257 def validate_no_duplicate_volumes(cls, v): 258 """Validate no duplicate volume names or mount paths.""" 259 if not v: 260 return v 261 262 # Check for duplicate volume names 263 names = [vol.name for vol in v] 264 if len(names) != len(set(names)): 265 raise ValueError("Duplicate volume names found") 266 267 # Check for duplicate mount paths 268 mount_paths = [vol.mount_path for vol in v] 269 if len(mount_paths) != len(set(mount_paths)): 270 raise ValueError("Duplicate mount paths found") 271 272 return v
273
[docs] 274 def to_domain(self) -> Dict: 275 """ 276 Convert flat config to domain model for space creation 277 """ 278 # Create the space spec 279 spec = { 280 "displayName": self.display_name 281 } 282 283 # Add optional spec fields 284 if self.image is not None: 285 spec["image"] = self.image 286 if self.desired_status is not None: 287 spec["desiredStatus"] = self.desired_status.value 288 if self.ownership_type is not None: 289 spec["ownershipType"] = self.ownership_type.value 290 if self.resources is not None: 291 spec["resources"] = self.resources.model_dump(exclude_none=True) 292 if self.storage is not None: 293 spec["storage"] = self.storage.model_dump(exclude_none=True, by_alias=True) 294 if self.volumes is not None: 295 spec["volumes"] = [vol.model_dump(exclude_none=True, by_alias=True) for vol in self.volumes] 296 if self.container_config is not None: 297 spec["containerConfig"] = self.container_config.model_dump(exclude_none=True) 298 if self.node_selector is not None: 299 spec["nodeSelector"] = self.node_selector 300 if self.affinity is not None: 301 spec["affinity"] = self.affinity 302 if self.tolerations is not None: 303 spec["tolerations"] = self.tolerations 304 if self.lifecycle is not None: 305 spec["lifecycle"] = self.lifecycle 306 if self.template_ref is not None: 307 spec["templateRef"] = self.template_ref.model_dump(exclude_none=True, by_alias=True) 308 if self.idle_shutdown is not None: 309 spec["idleShutdown"] = self.idle_shutdown.model_dump(exclude_none=True, by_alias=True) 310 if self.app_type is not None: 311 spec["appType"] = self.app_type 312 if self.service_account_name is not None: 313 spec["serviceAccountName"] = self.service_account_name 314 if self.access_type is not None: 315 spec["accessType"] = self.access_type.value 316 if self.env is not None: 317 spec["env"] = self.env 318 if self.access_strategy is not None: 319 spec["accessStrategy"] = self.access_strategy.model_dump(exclude_none=True) 320 if self.pod_security_context is not None: 321 spec["podSecurityContext"] = self.pod_security_context 322 if self.container_security_context is not None: 323 spec["containerSecurityContext"] = self.container_security_context 324 if self.init_containers is not None: 325 spec["initContainers"] = self.init_containers 326 327 # Create metadata 328 metadata = {"name": self.name} 329 if self.namespace is not None: 330 metadata["namespace"] = self.namespace 331 332 # Add kueue labels for task governance 333 labels = {} 334 if self.queue_name is not None: 335 labels["kueue.x-k8s.io/queue-name"] = self.queue_name 336 if self.priority is not None: 337 labels["kueue.x-k8s.io/priority-class"] = self.priority 338 if labels: 339 metadata["labels"] = labels 340 341 # Create the complete space configuration 342 space_config = { 343 "apiVersion": "workspace.jupyter.org/v1alpha1", 344 "kind": "Workspace", 345 "metadata": metadata, 346 "spec": spec 347 } 348 349 return { 350 "name": self.name, 351 "namespace": self.namespace, 352 "space_spec": space_config 353 }