diff --git a/CHANGELOG.md b/CHANGELOG.md index f51d02f0..a847f6c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## v3.9.0 (2026-06-11) + +### Features +- Support generic `{ide}-remote` connection types for space access (#414) +- Add v1.1 Space parameters (`queue_name`, `priority` for task governance) and addon version validation warning; bump space template to v1.1.0 (#421) +- Add v1.2 schema support for custom and jumpstart inference endpoints (#417) + +### Inference Operator +- Update inference helm chart to v2.1.1 with latest CRDs (init container support, custom service accounts, templated manager config) (#416, #418) +- DPD CRD changes with chart version bump to v3.2; pin operator to amd64 nodes via nodeAffinity; bump inference-operator subchart to v2.2.1 (#427) + +### GPU Operator +- Upgrade GPU Operator v25.3.4 → v26.3.1 to remediate CVEs in base images (#415, #419) +- Add ap-south-2 (HYD) ECR regional-values for GPU operator (#424) + +### Health Monitoring Agent +- Release Health Monitoring Agent 1.0.1892.0_1.0.424.0 with bug fixes (add 2 min sleep before nvml component checks to prevent missing GPU false positives) (#425) + +### Bug Fixes +- Exclude `kubernetes==36.0.0` which breaks EKS auth and relax `pyyaml` pin (#426) +- Rename `lr_warmup_ratio` to `lr_warmup_steps_ratio` to match Hub recipe schema (#428) + ## v3.8.0 (2026-04-16) ### Features diff --git a/README.md b/README.md index fa2457a0..8d596ed7 100644 --- a/README.md +++ b/README.md @@ -852,6 +852,14 @@ hyp create hyp-space \ | `--lifecycle` | TEXT | No | Lifecycle specifies actions that the management system should take in response to container lifecycle events (JSON string) | | `--app-type` | TEXT | No | AppType specifies the application type for this workspace | | `--service-account-name` | TEXT | No | ServiceAccountName specifies the name of the ServiceAccount to use for the workspace pod | +| `--queue-name` | TEXT | No | Queue name for space scheduling (1-63 characters, alphanumeric with hyphens). Required when task governance is enabled on HyperPod EKS clusters. | +| `--priority` | TEXT | No | Priority class for space scheduling. Sets the `kueue.x-k8s.io/priority-class` label. | +| `--access-type` | TEXT | No | AccessType specifies who can connect to the workspace ('Public' or 'OwnerOnly') | +| `--env` | TEXT | No | Environment variables for the workspace container (JSON string, list of {name, value} objects) | +| `--access-strategy` | TEXT | No | References a WorkspaceAccessStrategy. Format: --access-strategy name=,namespace= | +| `--pod-security-context` | TEXT | No | Pod-level security context. Overrides template defaults when specified (JSON string) | +| `--container-security-context` | TEXT | No | Container-level security context for the main workspace container. Overrides template defaults (JSON string) | +| `--init-containers` | TEXT | No | Init containers to run before the workspace container starts (JSON string, max 10) | | `--idle-shutdown` | TEXT | No | Idle shutdown configuration. Format: --idle-shutdown enabled=,idleTimeoutInMinutes=,detection= | | `--template-ref` | TEXT | No | TemplateRef references a WorkspaceTemplate to use as base configuration. Format: --template-ref name=,namespace= | | `--container-config` | TEXT | No | Container configuration. Format: --container-config command=,args= | @@ -1350,7 +1358,7 @@ monitor_config = get_monitoring_config() ```python from sagemaker.hyperpod.space.hyperpod_space import HPSpace -from hyperpod_space_template.v1_0.model import SpaceConfig +from hyperpod_space_template.v1_1.model import SpaceConfig # Create space configuration space_config = SpaceConfig( diff --git a/doc/cli/space/cli_space.md b/doc/cli/space/cli_space.md index 895cf0de..c6039bfc 100644 --- a/doc/cli/space/cli_space.md +++ b/doc/cli/space/cli_space.md @@ -51,6 +51,14 @@ hyp create hyp-space [OPTIONS] | `--lifecycle` | TEXT | No | Lifecycle specifies actions that the management system should take in response to container lifecycle events (JSON string) | | `--app-type` | TEXT | No | AppType specifies the application type for this workspace | | `--service-account-name` | TEXT | No | ServiceAccountName specifies the name of the ServiceAccount to use for the workspace pod | +| `--queue-name` | TEXT | No | Queue name for space scheduling (1-63 characters, alphanumeric with hyphens). Required when task governance is enabled on HyperPod EKS clusters. Sets the `kueue.x-k8s.io/queue-name` label. | +| `--priority` | TEXT | No | Priority class for space scheduling. Sets the `kueue.x-k8s.io/priority-class` label. | +| `--access-type` | TEXT | No | AccessType specifies who can connect to the workspace ('Public' or 'OwnerOnly') | +| `--env` | TEXT | No | Environment variables for the workspace container (JSON string, list of {name, value} objects) | +| `--access-strategy` | TEXT | No | References a WorkspaceAccessStrategy. Format: name=,namespace= | +| `--pod-security-context` | TEXT | No | Pod-level security context. Overrides template defaults when specified (JSON string) | +| `--container-security-context` | TEXT | No | Container-level security context for the main workspace container. Overrides template defaults (JSON string) | +| `--init-containers` | TEXT | No | Init containers to run before the workspace container starts (JSON string, max 10) | | `--idle-shutdown` | TEXT | No | Idle shutdown configuration. Format: enabled=,idleTimeoutInMinutes=,detection= | | `--template-ref` | TEXT | No | TemplateRef references a WorkspaceTemplate to use as base configuration. Format: name=,namespace= | | `--container-config` | TEXT | No | Container configuration. Format: command=,args= | @@ -160,6 +168,14 @@ hyp update hyp-space [OPTIONS] | `--lifecycle` | TEXT | No | Lifecycle specifies actions that the management system should take in response to container lifecycle events (JSON string) | | `--app-type` | TEXT | No | AppType specifies the application type for this workspace | | `--service-account-name` | TEXT | No | ServiceAccountName specifies the name of the ServiceAccount to use for the workspace pod | +| `--queue-name` | TEXT | No | Queue name for space scheduling (1-63 characters, alphanumeric with hyphens). Required when task governance is enabled on HyperPod EKS clusters. Sets the `kueue.x-k8s.io/queue-name` label. | +| `--priority` | TEXT | No | Priority class for space scheduling. Sets the `kueue.x-k8s.io/priority-class` label. | +| `--access-type` | TEXT | No | AccessType specifies who can connect to the workspace ('Public' or 'OwnerOnly') | +| `--env` | TEXT | No | Environment variables for the workspace container (JSON string, list of {name, value} objects) | +| `--access-strategy` | TEXT | No | References a WorkspaceAccessStrategy. Format: name=,namespace= | +| `--pod-security-context` | TEXT | No | Pod-level security context. Overrides template defaults when specified (JSON string) | +| `--container-security-context` | TEXT | No | Container-level security context for the main workspace container. Overrides template defaults (JSON string) | +| `--init-containers` | TEXT | No | Init containers to run before the workspace container starts (JSON string, max 10) | | `--idle-shutdown` | TEXT | No | Idle shutdown configuration. Format: enabled=,idleTimeoutInMinutes=,detection= | | `--template-ref` | TEXT | No | TemplateRef references a WorkspaceTemplate to use as base configuration. Format: name=,namespace= | | `--container-config` | TEXT | No | Container configuration. Format: command=,args= | diff --git a/doc/getting_started.md b/doc/getting_started.md index 718ab168..190bf5ee 100644 --- a/doc/getting_started.md +++ b/doc/getting_started.md @@ -9,6 +9,7 @@ Cluster Management Training Inference +Space ``` diff --git a/doc/getting_started/space.md b/doc/getting_started/space.md index ec001fa2..3f8933b1 100644 --- a/doc/getting_started/space.md +++ b/doc/getting_started/space.md @@ -41,7 +41,7 @@ hyp create hyp-space \ ````{tab-item} SDK ```python from sagemaker.hyperpod.space.hyperpod_space import HPSpace -from hyperpod_space_template.v1_0.model import SpaceConfig +from hyperpod_space_template.v1_1.model import SpaceConfig # Create space configuration space_config = SpaceConfig( @@ -66,16 +66,14 @@ When creating a space, you'll need to specify: | **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 | +| **cpu** | TEXT | No | CPU resource request (e.g., '500m') | +| **memory** | TEXT | No | Memory resource request (e.g., '2Gi') | +| **gpu** | TEXT | No | GPU resource request (e.g., '1') | +| **volume** | TEXT | No | Volume configuration. Format: `name=,mountPath=,persistentVolumeClaimName=` | +| **queue-name** | TEXT | No | Queue name for space scheduling. Required when task governance is enabled. | +| **template-ref** | TEXT | No | Reference to a WorkspaceTemplate. Format: `name=,namespace=` | + +For the full list of parameters, see the [CLI reference](../cli/space/cli_space.md) or run `hyp create hyp-space --help`. ## Managing Spaces diff --git a/doc/sdk/space/hyperpod_space.rst b/doc/sdk/space/hyperpod_space.rst index 73357ac4..b3f0de8e 100644 --- a/doc/sdk/space/hyperpod_space.rst +++ b/doc/sdk/space/hyperpod_space.rst @@ -25,6 +25,6 @@ HPSpaceTemplate Space Configs ------------- -.. automodule:: hyperpod_space_template.v1_0.model +.. automodule:: hyperpod_space_template.v1_1.model :members: SpaceConfig :show-inheritance: diff --git a/helm_chart/HyperPodHelmChart/Chart.yaml b/helm_chart/HyperPodHelmChart/Chart.yaml index dc5876cd..7242153a 100644 --- a/helm_chart/HyperPodHelmChart/Chart.yaml +++ b/helm_chart/HyperPodHelmChart/Chart.yaml @@ -81,7 +81,7 @@ dependencies: repository: "file://charts/team-role-and-bindings" condition: team-role-and-bindings.enabled - name: hyperpod-inference-operator - version: "2.1.1" + version: "2.3.0" repository: "file://charts/inference-operator" condition: inferenceOperators.enabled - name: hyperpod-patching diff --git a/helm_chart/HyperPodHelmChart/charts/gpu-operator/regional-values/values-ap-south-2.yaml b/helm_chart/HyperPodHelmChart/charts/gpu-operator/regional-values/values-ap-south-2.yaml new file mode 100644 index 00000000..46b0e28f --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/gpu-operator/regional-values/values-ap-south-2.yaml @@ -0,0 +1,13 @@ +gpu-operator: + operator: + repository: "580982410692.dkr.ecr.ap-south-2.amazonaws.com" + toolkit: + repository: "580982410692.dkr.ecr.ap-south-2.amazonaws.com/mirror-k8s" + devicePlugin: + repository: "580982410692.dkr.ecr.ap-south-2.amazonaws.com" + gfd: + repository: "580982410692.dkr.ecr.ap-south-2.amazonaws.com" + migManager: + repository: "580982410692.dkr.ecr.ap-south-2.amazonaws.com/mirror-cloud-native" + validator: + repository: "580982410692.dkr.ecr.ap-south-2.amazonaws.com/mirror-cloud-native" diff --git a/helm_chart/HyperPodHelmChart/charts/health-monitoring-agent/templates/_helpers.tpl b/helm_chart/HyperPodHelmChart/charts/health-monitoring-agent/templates/_helpers.tpl index 1ca2ce24..ab4e1007 100644 --- a/helm_chart/HyperPodHelmChart/charts/health-monitoring-agent/templates/_helpers.tpl +++ b/helm_chart/HyperPodHelmChart/charts/health-monitoring-agent/templates/_helpers.tpl @@ -55,7 +55,7 @@ Generate the health monitoring agent image URI based on AWS region */}} {{- define "health-monitoring-agent.imageUri" -}} {{- $region := "" -}} -{{- $imageTag := .Values.imageTag | default "1.0.1481.0_1.0.392.0" -}} +{{- $imageTag := .Values.imageTag | default "1.0.2297.0_1.0.474.0" -}} {{/* Debug: Show image tag selection if debug is enabled */}} {{- if .Values.debug -}} diff --git a/helm_chart/HyperPodHelmChart/charts/health-monitoring-agent/templates/health-monitoring-agent.yaml b/helm_chart/HyperPodHelmChart/charts/health-monitoring-agent/templates/health-monitoring-agent.yaml index 8c407699..301548a0 100644 --- a/helm_chart/HyperPodHelmChart/charts/health-monitoring-agent/templates/health-monitoring-agent.yaml +++ b/helm_chart/HyperPodHelmChart/charts/health-monitoring-agent/templates/health-monitoring-agent.yaml @@ -78,6 +78,12 @@ spec: - ml.p5.4xlarge - ml.p4d.24xlarge - ml.p4de.24xlarge + - ml.g4dn.xlarge + - ml.g4dn.2xlarge + - ml.g4dn.4xlarge + - ml.g4dn.8xlarge + - ml.g4dn.12xlarge + - ml.g4dn.16xlarge - ml.g5.xlarge - ml.g5.2xlarge - ml.g5.4xlarge @@ -104,6 +110,12 @@ spec: - ml.g6e.12xlarge - ml.g6e.24xlarge - ml.g6e.48xlarge + - ml.g7.2xlarge + - ml.g7.4xlarge + - ml.g7.8xlarge + - ml.g7.12xlarge + - ml.g7.24xlarge + - ml.g7.48xlarge - ml.g7e.2xlarge - ml.g7e.4xlarge - ml.g7e.8xlarge diff --git a/helm_chart/HyperPodHelmChart/charts/health-monitoring-agent/values.yaml b/helm_chart/HyperPodHelmChart/charts/health-monitoring-agent/values.yaml index ada57790..5ead265e 100644 --- a/helm_chart/HyperPodHelmChart/charts/health-monitoring-agent/values.yaml +++ b/helm_chart/HyperPodHelmChart/charts/health-monitoring-agent/values.yaml @@ -25,7 +25,7 @@ imageTag: "" # Override the health monitoring agent image URI # If specified, this will override the automatic region-based URI selection -# Example: "905418368575.dkr.ecr.us-west-2.amazonaws.com/hyperpod-health-monitoring-agent:1.0.1481.0_1.0.392.0" +# Example: "905418368575.dkr.ecr.us-west-2.amazonaws.com/hyperpod-health-monitoring-agent:1.0.2297.0_1.0.474.0" hmaimage: "" # Enable debug output for region selection process diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/Chart.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/Chart.yaml new file mode 100644 index 00000000..c7975fdd --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: hyperpod-ray-endpoint-operator +description: A Helm chart to distribute hyperpod-ray-endpoint-operator +type: application + +version: 0.1.0 +appVersion: "0.1.0" + +keywords: + - kubernetes + - operator + +annotations: + kubebuilder.io/generated-by: kubebuilder diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/crds/hyperpodrayendpointaccessstrategies.access.sagemaker.amazonaws.com.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/crds/hyperpodrayendpointaccessstrategies.access.sagemaker.amazonaws.com.yaml new file mode 100644 index 00000000..8cf49155 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/crds/hyperpodrayendpointaccessstrategies.access.sagemaker.amazonaws.com.yaml @@ -0,0 +1,99 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + "helm.sh/resource-policy": keep + controller-gen.kubebuilder.io/version: v0.20.1 + name: hyperpodrayendpointaccessstrategies.access.sagemaker.amazonaws.com +spec: + group: access.sagemaker.amazonaws.com + names: + kind: HyperpodRayEndpointAccessStrategy + listKind: HyperpodRayEndpointAccessStrategyList + plural: hyperpodrayendpointaccessstrategies + singular: hyperpodrayendpointaccessstrategy + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: HyperpodRayEndpointAccessStrategy defines how dashboard access + is configured for RayClusters. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: HyperpodRayEndpointAccessStrategySpec defines the desired + state of HyperpodRayEndpointAccessStrategy. + properties: + accessResourceTemplates: + description: AccessResourceTemplates defines Kubernetes resources + to create per RayCluster. + items: + description: AccessResourceTemplate defines a templated resource + the controller creates per RayCluster. + properties: + apiVersion: + description: APIVersion of the resource. + type: string + kind: + description: Kind of the resource (e.g., IngressRoute). + type: string + namePrefix: + description: NamePrefix is prepended to the RayCluster name + to form the resource name. + type: string + template: + description: Template is a Go template that renders the resource + spec. + type: string + required: + - apiVersion + - kind + - namePrefix + - template + type: object + type: array + accessType: + description: 'AccessType controls ownership checks: "ownerOnly" or + "public".' + enum: + - ownerOnly + - public + type: string + bearerAuthURLTemplate: + description: |- + BearerAuthURLTemplate is a Go template for generating the dashboard URL. + Available variables: .RayCluster.Name, .RayCluster.Namespace, .RayCluster.EncodedNamespace, .Domain + type: string + displayName: + description: DisplayName is a human-readable name for this strategy. + type: string + required: + - accessType + - bearerAuthURLTemplate + type: object + status: + description: HyperpodRayEndpointAccessStrategyStatus defines the observed + state. + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/NOTES.txt b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/NOTES.txt new file mode 100644 index 00000000..1cec53d1 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/NOTES.txt @@ -0,0 +1,15 @@ +Thank you for installing {{ .Chart.Name }}. + +Your release is named {{ .Release.Name }}. + +The controller and CRDs have been installed in namespace {{ .Release.Namespace }}. + +To verify the installation: + + kubectl get pods -n {{ .Release.Namespace }} + kubectl get customresourcedefinitions + +To learn more about the release, try: + + $ helm status {{ .Release.Name }} -n {{ .Release.Namespace }} + $ helm get all {{ .Release.Name }} -n {{ .Release.Namespace }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/_helpers.tpl b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/_helpers.tpl new file mode 100644 index 00000000..5cebbc31 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "hyperpod-ray-endpoint-operator.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "hyperpod-ray-endpoint-operator.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Namespace for generated references. +Always uses the Helm release namespace. +*/}} +{{- define "hyperpod-ray-endpoint-operator.namespaceName" -}} +{{- .Release.Namespace }} +{{- end }} + +{{/* +Resource name with proper truncation for Kubernetes 63-character limit. +Takes a dict with: + - .suffix: Resource name suffix (e.g., "metrics", "webhook") + - .context: Template context (root context with .Values, .Release, etc.) +Dynamically calculates safe truncation to ensure total name length <= 63 chars. +*/}} +{{- define "hyperpod-ray-endpoint-operator.resourceName" -}} +{{- $fullname := include "hyperpod-ray-endpoint-operator.fullname" .context }} +{{- $suffix := .suffix }} +{{- $maxLen := sub 62 (len $suffix) | int }} +{{- if gt (len $fullname) $maxLen }} +{{- printf "%s-%s" (trunc $maxLen $fullname | trimSuffix "-") $suffix | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" $fullname $suffix | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + +{{/* +ServiceAccount name to use. +If serviceAccount.enable is false and serviceAccount.name is set, use that name. +Otherwise, use the standard resourceName helper with "controller-manager" suffix. +*/}} +{{- define "hyperpod-ray-endpoint-operator.serviceAccountName" -}} +{{- if and (hasKey .Values.serviceAccount "enable") (not .Values.serviceAccount.enable) .Values.serviceAccount.name }} +{{- .Values.serviceAccount.name }} +{{- else }} +{{- include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "controller-manager" "context" .) }} +{{- end }} +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/cert-manager/extension-api-cert.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/cert-manager/extension-api-cert.yaml new file mode 100644 index 00000000..12b80905 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/cert-manager/extension-api-cert.yaml @@ -0,0 +1,23 @@ +{{- if .Values.certManager.enable }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "extension-api-cert" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + commonName: extension-api + dnsNames: + - {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "extension-api" "context" $) }}.{{ .Release.Namespace }}.svc + - {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "extension-api" "context" $) }}.{{ .Release.Namespace }}.svc.cluster.local + duration: 2160h + issuerRef: + kind: Issuer + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }} + privateKey: + rotationPolicy: Always + renewBefore: 360h + secretName: extension-api-cert + subject: + organizations: + - hyperpod-ray-endpoint-operator +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/cert-manager/selfsigned-issuer.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/cert-manager/selfsigned-issuer.yaml new file mode 100644 index 00000000..f7f5f9c0 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/cert-manager/selfsigned-issuer.yaml @@ -0,0 +1,14 @@ +{{- if .Values.certManager.enable }} +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + selfSigned: {} +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/cert-manager/serving-cert.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/cert-manager/serving-cert.yaml new file mode 100644 index 00000000..dd9c2fec --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/cert-manager/serving-cert.yaml @@ -0,0 +1,20 @@ +{{- if .Values.certManager.enable }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "serving-cert" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + dnsNames: + - {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "webhook-service" "context" $) }}.{{ .Release.Namespace }}.svc + - {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "webhook-service" "context" $) }}.{{ .Release.Namespace }}.svc.cluster.local + issuerRef: + kind: Issuer + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }} + secretName: webhook-server-cert +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/auth-middleware.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/auth-middleware.yaml new file mode 100644 index 00000000..4533e9db --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/auth-middleware.yaml @@ -0,0 +1,133 @@ +{{- if or (not (hasKey .Values.authMiddleware "enabled")) (.Values.authMiddleware.enabled) }} +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: auth-middleware + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-middleware" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + replicas: {{ .Values.authMiddleware.replicas }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + app.kubernetes.io/component: auth-middleware + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/component: auth-middleware + spec: + {{- with .Values.authMiddleware.tolerations }} + tolerations: {{ toYaml . | nindent 10 }} + {{- end }} + containers: + - command: + - /authmiddleware + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: PORT + value: {{ .Values.authMiddleware.env.port | quote }} + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: JWT_SECRET_NAME + value: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "extensionapi-jwt-secret" "context" $) | quote }} + - name: JWT_ISSUER + value: {{ .Values.authMiddleware.env.jwtIssuer | quote }} + - name: JWT_AUDIENCE + value: {{ .Values.authMiddleware.env.jwtAudience | quote }} + - name: SESSION_TTL + value: {{ .Values.authMiddleware.env.sessionTTL | quote }} + - name: COOKIE_NAME + value: {{ .Values.authMiddleware.env.cookieName | quote }} + - name: COOKIE_SECURE + value: {{ .Values.authMiddleware.env.cookieSecure | quote }} + - name: COOKIE_SAMESITE + value: {{ .Values.authMiddleware.env.cookieSameSite | quote }} + - name: LOG_FILE_PATH + value: {{ .Values.authMiddleware.env.logFilePath | quote }} + image: "{{ .Values.authMiddleware.image.repository }}{{- if not (contains "@" .Values.authMiddleware.image.repository) }}:{{ .Values.authMiddleware.image.tag | default .Chart.AppVersion }}{{- end }}" + {{- with .Values.authMiddleware.image.pullPolicy }} + imagePullPolicy: {{ . }} + {{- end }} + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + name: authmiddleware + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + {{- toYaml .Values.authMiddleware.resources | nindent 10 }} + securityContext: + {{- toYaml .Values.authMiddleware.securityContext | nindent 10 }} + volumeMounts: + - mountPath: /tmp + name: tmp + - mountPath: /var/log/aws/clusters + name: log + {{- with .Values.authMiddleware.nodeSelector }} + nodeSelector: {{ toYaml . | nindent 10 }} + {{- end }} + securityContext: + {{- toYaml .Values.authMiddleware.podSecurityContext | nindent 8 }} + serviceAccountName: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-middleware" "context" $) }} + {{- if and (hasKey .Values.authMiddleware "terminationGracePeriodSeconds") (ne .Values.authMiddleware.terminationGracePeriodSeconds nil) }} + terminationGracePeriodSeconds: {{ .Values.authMiddleware.terminationGracePeriodSeconds }} + {{- end }} + volumes: + - emptyDir: {} + name: tmp + - hostPath: + path: /var/log/aws/clusters/ + name: log +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: auth-middleware + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-middleware" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + ports: + - name: http + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + app.kubernetes.io/component: auth-middleware + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + type: ClusterIP +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/extension-api.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/extension-api.yaml new file mode 100644 index 00000000..5d9196a3 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/extension-api.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "extension-api" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + ports: + - name: https + port: 443 + protocol: TCP + targetPort: 7443 + selector: + control-plane: controller-manager + type: ClusterIP diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/extensionapi-jwt-secret.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/extensionapi-jwt-secret.yaml new file mode 100644 index 00000000..a5d5f04a --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/extensionapi-jwt-secret.yaml @@ -0,0 +1,12 @@ +{{- $secretName := include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "extensionapi-jwt-secret" "context" $) -}} +apiVersion: v1 +kind: Secret +metadata: + labels: + app: extensionapi-jwt + component: security + name: {{ $secretName }} + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/resource-policy": keep +type: Opaque diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/jwt-rotator.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/jwt-rotator.yaml new file mode 100644 index 00000000..c50d858a --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/jwt-rotator.yaml @@ -0,0 +1,125 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + labels: + app.kubernetes.io/component: jwt-rotator + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "jwt-rotator" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + concurrencyPolicy: Forbid + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 3 + template: + metadata: + labels: + app.kubernetes.io/component: jwt-rotator + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + spec: + containers: + - command: + - /rotator + - --secret-name={{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "extensionapi-jwt-secret" "context" $) }} + - --secret-namespace={{ .Release.Namespace }} + - --number-of-keys=13 + {{- if .Values.kmsKeyArn }} + - --kms-key-arn={{ .Values.kmsKeyArn }} + {{- end }} + image: "{{ .Values.manager.image.repository }}{{- if not (contains "@" .Values.manager.image.repository) }}:{{ .Values.manager.image.tag | default .Chart.AppVersion }}{{- end }}" + name: rotator + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + # Never (not OnFailure) so failed pods are preserved for log inspection. + # With OnFailure, pods are deleted after backoffLimit and logs are lost. + restartPolicy: Never + securityContext: + fsGroup: 65532 + runAsGroup: 65532 + runAsNonRoot: true + runAsUser: 65532 + seccompProfile: + type: RuntimeDefault + serviceAccountName: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "jwt-rotator" "context" $) }} + schedule: '*/30 * * * *' + successfulJobsHistoryLimit: 3 +--- +apiVersion: batch/v1 +kind: Job +metadata: + labels: + app.kubernetes.io/component: jwt-rotator + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "jwt-rotator-init" "context" $) }} + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation +spec: + backoffLimit: 3 + template: + metadata: + labels: + app.kubernetes.io/component: jwt-rotator + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + spec: + containers: + - command: + - /rotator + - --secret-name={{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "extensionapi-jwt-secret" "context" $) }} + - --secret-namespace={{ .Release.Namespace }} + - --number-of-keys=13 + {{- if .Values.kmsKeyArn }} + - --kms-key-arn={{ .Values.kmsKeyArn }} + {{- end }} + image: "{{ .Values.manager.image.repository }}{{- if not (contains "@" .Values.manager.image.repository) }}:{{ .Values.manager.image.tag | default .Chart.AppVersion }}{{- end }}" + imagePullPolicy: {{ .Values.manager.image.pullPolicy }} + name: rotator + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + # Never (not OnFailure) so failed pods are preserved for log inspection. + # With OnFailure, pods are deleted after backoffLimit and logs are lost. + restartPolicy: Never + securityContext: + fsGroup: 65532 + runAsGroup: 65532 + runAsNonRoot: true + runAsUser: 65532 + seccompProfile: + type: RuntimeDefault + serviceAccountName: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "jwt-rotator" "context" $) }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/runtime-config.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/runtime-config.yaml new file mode 100644 index 00000000..ace094cd --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/runtime-config.yaml @@ -0,0 +1,187 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: strip-bearer-auth-suffix + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + app.kubernetes.io/component: traefik-middleware + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + replacePathRegex: + regex: "^/bearer-auth(.*)$" + replacement: "/$1" +--- +apiVersion: access.sagemaker.amazonaws.com/v1alpha1 +kind: HyperpodRayEndpointAccessStrategy +metadata: + name: ray-access-strategy-private + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + app.kubernetes.io/component: access-strategy + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + accessType: ownerOnly + bearerAuthURLTemplate: "https://{{`{{ .RayCluster.Name }}-{{ .RayCluster.EncodedNamespace }}.{{ .Domain }}`}}/bearer-auth" + accessResourceTemplates: + - kind: Middleware + apiVersion: traefik.io/v1alpha1 + namePrefix: cluster-forward-auth + template: | + spec: + forwardAuth: + address: "http://{{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-middleware" "context" $) }}.{{ .Release.Namespace }}.svc.cluster.local:8080/verify?cluster-uid={{`{{ .RayCluster.UID }}`}}" + trustForwardHeader: false + addAuthCookiesToResponse: + - ray_session + authRequestHeaders: + - Host + - X-Forwarded-Host + - X-Forwarded-Uri + - Cookie + - Authorization + - kind: Middleware + apiVersion: traefik.io/v1alpha1 + namePrefix: cluster-bearer-auth + template: | + spec: + forwardAuth: + address: "http://{{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-middleware" "context" $) }}.{{ .Release.Namespace }}.svc.cluster.local:8080/bearer-auth?cluster-uid={{`{{ .RayCluster.UID }}`}}" + trustForwardHeader: false + addAuthCookiesToResponse: + - ray_session + authRequestHeaders: + - Host + - X-Forwarded-Host + - X-Forwarded-Uri + - Cookie + - Authorization + - kind: IngressRoute + apiVersion: traefik.io/v1alpha1 + namePrefix: authorized-route + template: | + spec: + entryPoints: + - web + routes: + - match: "Host(`{{`{{ .RayCluster.Name }}-{{ .RayCluster.EncodedNamespace }}.{{ .Domain }}`}}`)" + kind: Rule + priority: 100 + middlewares: + - name: cluster-forward-auth-{{`{{ .RayCluster.Name }}`}} + namespace: "{{`{{ .RayCluster.Namespace }}`}}" + services: + - name: "{{`{{ .RayCluster.Name }}`}}-head-svc" + namespace: "{{`{{ .RayCluster.Namespace }}`}}" + port: {{`{{ .RayCluster.DashboardPort }}`}} + - kind: IngressRoute + apiVersion: traefik.io/v1alpha1 + namePrefix: bearer-auth-route + template: | + spec: + entryPoints: + - web + routes: + - match: "Host(`{{`{{ .RayCluster.Name }}-{{ .RayCluster.EncodedNamespace }}.{{ .Domain }}`}}`) && PathPrefix(`/bearer-auth`)" + kind: Rule + priority: 110 + middlewares: + - name: cluster-bearer-auth-{{`{{ .RayCluster.Name }}`}} + namespace: "{{`{{ .RayCluster.Namespace }}`}}" + - name: strip-bearer-auth-suffix + namespace: {{ .Release.Namespace }} + services: + - name: "{{`{{ .RayCluster.Name }}`}}-head-svc" + namespace: "{{`{{ .RayCluster.Namespace }}`}}" + port: {{`{{ .RayCluster.DashboardPort }}`}} +--- +apiVersion: access.sagemaker.amazonaws.com/v1alpha1 +kind: HyperpodRayEndpointAccessStrategy +metadata: + name: ray-access-strategy-public + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + app.kubernetes.io/component: access-strategy + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + accessType: public + bearerAuthURLTemplate: "https://{{`{{ .RayCluster.Name }}-{{ .RayCluster.EncodedNamespace }}.{{ .Domain }}`}}/bearer-auth" + accessResourceTemplates: + - kind: Middleware + apiVersion: traefik.io/v1alpha1 + namePrefix: cluster-forward-auth + template: | + spec: + forwardAuth: + address: "http://{{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-middleware" "context" $) }}.{{ .Release.Namespace }}.svc.cluster.local:8080/verify?cluster-uid={{`{{ .RayCluster.UID }}`}}" + trustForwardHeader: false + addAuthCookiesToResponse: + - ray_session + authRequestHeaders: + - Host + - X-Forwarded-Host + - X-Forwarded-Uri + - Cookie + - Authorization + - kind: Middleware + apiVersion: traefik.io/v1alpha1 + namePrefix: cluster-bearer-auth + template: | + spec: + forwardAuth: + address: "http://{{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-middleware" "context" $) }}.{{ .Release.Namespace }}.svc.cluster.local:8080/bearer-auth?cluster-uid={{`{{ .RayCluster.UID }}`}}" + trustForwardHeader: false + addAuthCookiesToResponse: + - ray_session + authRequestHeaders: + - Host + - X-Forwarded-Host + - X-Forwarded-Uri + - Cookie + - Authorization + - kind: IngressRoute + apiVersion: traefik.io/v1alpha1 + namePrefix: authorized-route + template: | + spec: + entryPoints: + - web + routes: + - match: "Host(`{{`{{ .RayCluster.Name }}-{{ .RayCluster.EncodedNamespace }}.{{ .Domain }}`}}`)" + kind: Rule + priority: 100 + middlewares: + - name: cluster-forward-auth-{{`{{ .RayCluster.Name }}`}} + namespace: "{{`{{ .RayCluster.Namespace }}`}}" + services: + - name: "{{`{{ .RayCluster.Name }}`}}-head-svc" + namespace: "{{`{{ .RayCluster.Namespace }}`}}" + port: {{`{{ .RayCluster.DashboardPort }}`}} + - kind: IngressRoute + apiVersion: traefik.io/v1alpha1 + namePrefix: bearer-auth-route + template: | + spec: + entryPoints: + - web + routes: + - match: "Host(`{{`{{ .RayCluster.Name }}-{{ .RayCluster.EncodedNamespace }}.{{ .Domain }}`}}`) && PathPrefix(`/bearer-auth`)" + kind: Rule + priority: 110 + middlewares: + - name: cluster-bearer-auth-{{`{{ .RayCluster.Name }}`}} + namespace: "{{`{{ .RayCluster.Namespace }}`}}" + - name: strip-bearer-auth-suffix + namespace: {{ .Release.Namespace }} + services: + - name: "{{`{{ .RayCluster.Name }}`}}-head-svc" + namespace: "{{`{{ .RayCluster.Namespace }}`}}" + port: {{`{{ .RayCluster.DashboardPort }}`}} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/v1alpha1.connection.access.sagemaker.amazonaws.com.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/v1alpha1.connection.access.sagemaker.amazonaws.com.yaml new file mode 100644 index 00000000..b49deeea --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/extras/v1alpha1.connection.access.sagemaker.amazonaws.com.yaml @@ -0,0 +1,15 @@ +apiVersion: apiregistration.k8s.io/v1 +kind: APIService +metadata: + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "extension-api-cert" "context" $) }} + name: v1alpha1.connection.access.sagemaker.amazonaws.com +spec: + group: connection.access.sagemaker.amazonaws.com + groupPriorityMinimum: 100 + insecureSkipTLSVerify: false + service: + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "extension-api" "context" $) }} + namespace: {{ .Release.Namespace }} + version: v1alpha1 + versionPriority: 100 diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/manager/manager.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/manager/manager.yaml new file mode 100644 index 00000000..1d117c47 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/manager/manager.yaml @@ -0,0 +1,191 @@ +{{- if or (not (hasKey .Values.manager "enabled")) (.Values.manager.enabled) }} +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + control-plane: controller-manager + {{- with .Values.manager.labels }} + {{- with omit . "app.kubernetes.io/managed-by" "app.kubernetes.io/name" "helm.sh/chart" "app.kubernetes.io/instance" "control-plane" }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- end }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "controller-manager" "context" $) }} + namespace: {{ .Release.Namespace }} + {{- if .Values.manager.annotations }} + annotations: + {{- toYaml .Values.manager.annotations | nindent 4 }} + {{- end }} +spec: + {{- with .Values.manager.strategy }} + strategy: {{ toYaml . | nindent 6 }} + {{- end }} + replicas: {{ .Values.manager.replicas }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + control-plane: controller-manager + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: manager + {{- with .Values.manager.pod }} + {{- with .annotations }} + {{- with omit . "kubectl.kubernetes.io/default-container" }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + {{- end }} + labels: + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + control-plane: controller-manager + {{- with .Values.manager.pod }} + {{- with .labels }} + {{- with omit . "app.kubernetes.io/name" "helm.sh/chart" "app.kubernetes.io/instance" "app.kubernetes.io/managed-by" "control-plane" }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + {{- end }} + spec: + {{- with .Values.manager.topologySpreadConstraints }} + topologySpreadConstraints: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.manager.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with .Values.manager.tolerations }} + tolerations: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.manager.affinity }} + affinity: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.manager.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - args: + {{- if .Values.metrics.enable }} + - --metrics-bind-address=:{{ .Values.metrics.port }} + {{- if not .Values.metrics.secure }} + - --metrics-secure=false + {{- end }} + {{- else }} + # Bind to :0 to disable the controller-runtime managed metrics server + - --metrics-bind-address=0 + {{- end }} + - --health-probe-bind-address=:8081 + {{- range .Values.manager.args }} + - {{ . }} + {{- end }} + {{- if .Values.extensionApiDomain }} + - --extension-api-domain={{ .Values.extensionApiDomain }} + {{- end }} + - --extension-api-jwt-secret={{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "extensionapi-jwt-secret" "context" $) }} + {{- if .Values.adminGroup }} + - --admin-group={{ .Values.adminGroup }} + {{- end }} + {{- if .Values.certManager.enable }} + - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs + {{- end }} + command: + - /manager + env: +{{- if or .Values.manager.env (and (kindIs "map" .Values.manager.envOverrides) (not (empty .Values.manager.envOverrides))) }} + {{- if .Values.manager.env }} + {{- toYaml .Values.manager.env | nindent 10 }} + {{- end }} + {{- if kindIs "map" .Values.manager.envOverrides }} + {{- range $k, $v := .Values.manager.envOverrides }} + - name: {{ $k }} + value: {{ $v | quote }} + {{ end }} + {{- end }} + {{- else }} + [] + {{- end }} + image: "{{ .Values.manager.image.repository }}{{- if not (contains "@" .Values.manager.image.repository) }}:{{ .Values.manager.image.tag | default .Chart.AppVersion }}{{- end }}" + {{- with .Values.manager.image.pullPolicy }} + imagePullPolicy: {{ . }} + {{- end }} + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + ports: + - containerPort: 8081 + name: health + protocol: TCP + - containerPort: {{ .Values.webhook.port }} + name: webhook-server + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + {{- if .Values.manager.resources }} + {{- toYaml .Values.manager.resources | nindent 10 }} + {{- else }} + {} + {{- end }} + securityContext: + {{- if .Values.manager.securityContext }} + {{- toYaml .Values.manager.securityContext | nindent 10 }} + {{- else }} + {} + {{- end }} + volumeMounts: + {{- if .Values.manager.extraVolumeMounts }} + {{- toYaml .Values.manager.extraVolumeMounts | nindent 10 }} + {{- end }} + - mountPath: /var/log/aws/clusters + name: log + - mountPath: /tmp/extension-api/serving-certs + name: extension-api-cert + readOnly: true + {{- if .Values.certManager.enable }} + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: webhook-certs + readOnly: true + {{- end }} + {{- with .Values.manager.nodeSelector }} + nodeSelector: {{ toYaml . | nindent 10 }} + {{- end }} + securityContext: + {{- if .Values.manager.podSecurityContext }} + {{- toYaml .Values.manager.podSecurityContext | nindent 8 }} + {{- else }} + {} + {{- end }} + serviceAccountName: {{ include "hyperpod-ray-endpoint-operator.serviceAccountName" . }} + {{- if and (hasKey .Values.manager "terminationGracePeriodSeconds") (ne .Values.manager.terminationGracePeriodSeconds nil) }} + terminationGracePeriodSeconds: {{ .Values.manager.terminationGracePeriodSeconds }} + {{- end }} + volumes: + {{- if .Values.manager.extraVolumes }} + {{- toYaml .Values.manager.extraVolumes | nindent 8 }} + {{- end }} + - hostPath: + path: /var/log/aws/clusters/ + name: log + - name: extension-api-cert + secret: + secretName: extension-api-cert + {{- if .Values.certManager.enable }} + - name: webhook-certs + secret: + secretName: webhook-server-cert + {{- end }} +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/metrics/metrics-service.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/metrics/metrics-service.yaml new file mode 100644 index 00000000..2c9fb3aa --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/metrics/metrics-service.yaml @@ -0,0 +1,22 @@ +{{- if .Values.metrics.enable }} +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + control-plane: controller-manager + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "metrics-service" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + ports: + - name: {{ if .Values.metrics.secure }}https{{ else }}http{{ end }} + port: {{ .Values.metrics.port }} + protocol: TCP + targetPort: {{ .Values.metrics.port }} + selector: + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + control-plane: controller-manager +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/prometheus/controller-manager-metrics-monitor.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/prometheus/controller-manager-metrics-monitor.yaml new file mode 100644 index 00000000..80783f15 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/prometheus/controller-manager-metrics-monitor.yaml @@ -0,0 +1,44 @@ +{{- if .Values.prometheus.enable }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + control-plane: controller-manager + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "controller-manager-metrics-monitor" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + endpoints: + - {{- if .Values.metrics.secure }} + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + {{- end }} + path: /metrics + port: {{ if .Values.metrics.secure }}https{{ else }}http{{ end }} + scheme: {{ if .Values.metrics.secure }}https{{ else }}http{{ end }} + {{- if .Values.metrics.secure }} + tlsConfig: + serverName: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "metrics-service" "context" $) }}.{{ .Release.Namespace }}.svc + {{- if .Values.certManager.enable }} + ca: + secret: + name: metrics-server-cert + key: ca.crt + cert: + secret: + name: metrics-server-cert + key: tls.crt + keySecret: + name: metrics-server-cert + key: tls.key + {{- else }} + insecureSkipVerify: true + {{- end }} + {{- end }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + control-plane: controller-manager +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/access-strategy-admin.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/access-strategy-admin.yaml new file mode 100644 index 00000000..98ee85e3 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/access-strategy-admin.yaml @@ -0,0 +1,29 @@ +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "access-strategy-admin" "context" $) }} +rules: +- apiGroups: + - access.sagemaker.amazonaws.com + resources: + - hyperpodrayendpointaccessstrategies + verbs: + - '*' +- apiGroups: + - access.sagemaker.amazonaws.com + resources: + - hyperpodrayendpointaccessstrategies/status + verbs: + - get diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/access-strategy-editor.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/access-strategy-editor.yaml new file mode 100644 index 00000000..3e8aeb2b --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/access-strategy-editor.yaml @@ -0,0 +1,35 @@ +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "access-strategy-editor" "context" $) }} +rules: +- apiGroups: + - access.sagemaker.amazonaws.com + resources: + - hyperpodrayendpointaccessstrategies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - access.sagemaker.amazonaws.com + resources: + - hyperpodrayendpointaccessstrategies/status + verbs: + - get diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/access-strategy-viewer.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/access-strategy-viewer.yaml new file mode 100644 index 00000000..c7ecc0f1 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/access-strategy-viewer.yaml @@ -0,0 +1,31 @@ +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "access-strategy-viewer" "context" $) }} +rules: +- apiGroups: + - access.sagemaker.amazonaws.com + resources: + - hyperpodrayendpointaccessstrategies + verbs: + - get + - list + - watch +- apiGroups: + - access.sagemaker.amazonaws.com + resources: + - hyperpodrayendpointaccessstrategies/status + verbs: + - get diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-mw-node-reader-binding.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-mw-node-reader-binding.yaml new file mode 100644 index 00000000..961567ec --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-mw-node-reader-binding.yaml @@ -0,0 +1,18 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: auth-middleware + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-mw-node-reader-binding" "context" $) }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-mw-node-reader" "context" $) }} +subjects: +- kind: ServiceAccount + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-middleware" "context" $) }} + namespace: {{ .Release.Namespace }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-mw-node-reader.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-mw-node-reader.yaml new file mode 100644 index 00000000..be8632f1 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-mw-node-reader.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/component: auth-middleware + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-mw-node-reader" "context" $) }} +rules: +- apiGroups: + - "" + resources: + - nodes + verbs: + - get diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-mw-secrets-reader-binding.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-mw-secrets-reader-binding.yaml new file mode 100644 index 00000000..dbab8bfe --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-mw-secrets-reader-binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: auth-middleware + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-mw-secrets-reader-binding" "context" $) }} + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-mw-secrets-reader" "context" $) }} +subjects: +- kind: ServiceAccount + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-middleware" "context" $) }} + namespace: {{ .Release.Namespace }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-mw-secrets-reader.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-mw-secrets-reader.yaml new file mode 100644 index 00000000..fcfc72a9 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-mw-secrets-reader.yaml @@ -0,0 +1,27 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/component: auth-middleware + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-mw-secrets-reader" "context" $) }} + namespace: {{ .Release.Namespace }} +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - list + - watch +- apiGroups: + - "" + resourceNames: + - {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "extensionapi-jwt-secret" "context" $) }} + resources: + - secrets + verbs: + - get diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-reader-kube-system.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-reader-kube-system.yaml new file mode 100644 index 00000000..29b44220 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/auth-reader-kube-system.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + annotations: + internal.operator/target-namespace: kube-system + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-reader" "context" $) }} + namespace: {{ index .Values.rbac.roleNamespaces "auth-reader" | default "kube-system" }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: extension-apiserver-authentication-reader +subjects: +- kind: ServiceAccount + name: {{ include "hyperpod-ray-endpoint-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/connection-creator-binding.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/connection-creator-binding.yaml new file mode 100644 index 00000000..5975da09 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/connection-creator-binding.yaml @@ -0,0 +1,28 @@ +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: RoleBinding +{{- else }} +kind: ClusterRoleBinding +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "connection-creator-binding" "context" $) }} +roleRef: + apiGroup: rbac.authorization.k8s.io + {{- if .Values.rbac.namespaced }} + kind: Role + {{- else }} + kind: ClusterRole + {{- end }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "connection-creator" "context" $) }} +subjects: +- apiGroup: rbac.authorization.k8s.io + kind: Group + name: system:authenticated diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/connection-creator.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/connection-creator.yaml new file mode 100644 index 00000000..fa3138a9 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/connection-creator.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "connection-creator" "context" $) }} +rules: +- apiGroups: + - connection.access.sagemaker.amazonaws.com + resources: + - raydashboardconnections + verbs: + - create diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/jwt-rotator.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/jwt-rotator.yaml new file mode 100644 index 00000000..fba0e8d2 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/jwt-rotator.yaml @@ -0,0 +1,36 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/component: jwt-rotator + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "jwt-rotator" "context" $) }} + namespace: {{ .Release.Namespace }} +rules: +- apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "update"] + resourceNames: ["{{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "extensionapi-jwt-secret" "context" $) }}"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/component: jwt-rotator + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "jwt-rotator" "context" $) }} + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "jwt-rotator" "context" $) }} +subjects: +- kind: ServiceAccount + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "jwt-rotator" "context" $) }} + namespace: {{ .Release.Namespace }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/jwt-secrets-reader-binding.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/jwt-secrets-reader-binding.yaml new file mode 100644 index 00000000..c842a058 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/jwt-secrets-reader-binding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + component: security + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "jwt-secrets-reader-binding" "context" $) }} + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "jwt-secrets-reader" "context" $) }} +subjects: +- kind: ServiceAccount + name: {{ include "hyperpod-ray-endpoint-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/jwt-secrets-reader.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/jwt-secrets-reader.yaml new file mode 100644 index 00000000..74154534 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/jwt-secrets-reader.yaml @@ -0,0 +1,27 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + component: security + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "jwt-secrets-reader" "context" $) }} + namespace: {{ .Release.Namespace }} +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - list + - watch +- apiGroups: + - "" + resourceNames: + - {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "extensionapi-jwt-secret" "context" $) }} + resources: + - secrets + verbs: + - get diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/leader-election-role.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/leader-election-role.yaml new file mode 100644 index 00000000..c0f61af5 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/leader-election-role.yaml @@ -0,0 +1,42 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "leader-election-role" "context" $) }} + namespace: {{ .Release.Namespace }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/leader-election-rolebinding.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/leader-election-rolebinding.yaml new file mode 100644 index 00000000..0acd15b4 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/leader-election-rolebinding.yaml @@ -0,0 +1,18 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "leader-election-rolebinding" "context" $) }} + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "leader-election-role" "context" $) }} +subjects: +- kind: ServiceAccount + name: {{ include "hyperpod-ray-endpoint-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/manager-role.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/manager-role.yaml new file mode 100644 index 00000000..416a9b26 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/manager-role.yaml @@ -0,0 +1,110 @@ +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: Role +{{- else }} +kind: ClusterRole +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "manager-role" "context" $) }} +rules: +- apiGroups: + - "" + resources: + - nodes + verbs: + - get +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch +- apiGroups: + - ray.io + resources: + - rayclusters + verbs: + - get + - list + - watch +- apiGroups: + - ray.io + resources: + - rayclusters/finalizers + verbs: + - update +- apiGroups: + - ray.io + resources: + - rayjobs + verbs: + - get + - list + - watch +- apiGroups: + - ray.io + resources: + - rayservices + verbs: + - get + - list + - watch +- apiGroups: + - ray.io + resources: + - raycronjobs + verbs: + - get + - list + - watch +- apiGroups: + - access.sagemaker.amazonaws.com + resources: + - hyperpodrayendpointaccessstrategies + verbs: + - get + - list + - watch +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +- apiGroups: + - traefik.io + resources: + - ingressroutes + - middlewares + verbs: + - get + - list + - watch + - create + - update + - delete +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/manager-rolebinding.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/manager-rolebinding.yaml new file mode 100644 index 00000000..82904afe --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/manager-rolebinding.yaml @@ -0,0 +1,28 @@ +apiVersion: rbac.authorization.k8s.io/v1 +{{- if .Values.rbac.namespaced }} +kind: RoleBinding +{{- else }} +kind: ClusterRoleBinding +{{- end }} +metadata: +{{- if .Values.rbac.namespaced }} + namespace: {{ .Release.Namespace }} +{{- end }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "manager-rolebinding" "context" $) }} +roleRef: + apiGroup: rbac.authorization.k8s.io + {{- if .Values.rbac.namespaced }} + kind: Role + {{- else }} + kind: ClusterRole + {{- end }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "manager-role" "context" $) }} +subjects: +- kind: ServiceAccount + name: {{ include "hyperpod-ray-endpoint-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/metrics-auth-role.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/metrics-auth-role.yaml new file mode 100644 index 00000000..5402e253 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/metrics-auth-role.yaml @@ -0,0 +1,19 @@ +{{- if and .Values.metrics.enable .Values.metrics.secure }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "metrics-auth-role" "context" $) }} +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/metrics-auth-rolebinding.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/metrics-auth-rolebinding.yaml new file mode 100644 index 00000000..b27b8aa6 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/metrics-auth-rolebinding.yaml @@ -0,0 +1,14 @@ +{{- if and .Values.metrics.enable .Values.metrics.secure }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "metrics-auth-rolebinding" "context" $) }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "metrics-auth-role" "context" $) }} +subjects: +- kind: ServiceAccount + name: {{ include "hyperpod-ray-endpoint-operator.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/metrics-reader.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/metrics-reader.yaml new file mode 100644 index 00000000..22ba6d96 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/metrics-reader.yaml @@ -0,0 +1,11 @@ +{{- if and .Values.metrics.enable .Values.metrics.secure }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "metrics-reader" "context" $) }} +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/serviceaccounts.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/serviceaccounts.yaml new file mode 100644 index 00000000..40654578 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/rbac/serviceaccounts.yaml @@ -0,0 +1,48 @@ +{{- if or (not (hasKey .Values.serviceAccount "enable")) .Values.serviceAccount.enable }} +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "controller-manager" "context" $) }} + namespace: {{ .Release.Namespace }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: auth-middleware + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "auth-middleware" "context" $) }} + namespace: {{ .Release.Namespace }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/component: jwt-rotator + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "jwt-rotator" "context" $) }} + namespace: {{ .Release.Namespace }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/webhook/mutating-webhook-configuration.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/webhook/mutating-webhook-configuration.yaml new file mode 100644 index 00000000..c529c375 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/webhook/mutating-webhook-configuration.yaml @@ -0,0 +1,87 @@ +{{- if .Values.webhook.enable }} +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + annotations: + {{- if .Values.certManager.enable }} + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "serving-cert" "context" $) }} + {{- end }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "mutating-webhook-configuration" "context" $) }} +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "webhook-service" "context" $) }} + namespace: {{ .Release.Namespace }} + path: /mutate-ray-io-v1-raycluster + failurePolicy: Ignore + name: mraycluster-v1.kb.io + rules: + - apiGroups: + - ray.io + apiVersions: + - v1 + operations: + - CREATE + resources: + - rayclusters + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "webhook-service" "context" $) }} + namespace: {{ .Release.Namespace }} + path: /mutate-ray-io-v1-rayjob + failurePolicy: Ignore + name: mrayjob-v1.kb.io + rules: + - apiGroups: + - ray.io + apiVersions: + - v1 + operations: + - CREATE + resources: + - rayjobs + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "webhook-service" "context" $) }} + namespace: {{ .Release.Namespace }} + path: /mutate-ray-io-v1-rayservice + failurePolicy: Ignore + name: mrayservice-v1.kb.io + rules: + - apiGroups: + - ray.io + apiVersions: + - v1 + operations: + - CREATE + resources: + - rayservices + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "webhook-service" "context" $) }} + namespace: {{ .Release.Namespace }} + path: /mutate-ray-io-v1-raycronjob + failurePolicy: Ignore + name: mraycronjob-v1.kb.io + rules: + - apiGroups: + - ray.io + apiVersions: + - v1 + operations: + - CREATE + resources: + - raycronjobs + sideEffects: None +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/webhook/validating-webhook-configuration.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/webhook/validating-webhook-configuration.yaml new file mode 100644 index 00000000..4266b1e2 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/webhook/validating-webhook-configuration.yaml @@ -0,0 +1,91 @@ +{{- if .Values.webhook.enable }} +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + annotations: + {{- if .Values.certManager.enable }} + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "serving-cert" "context" $) }} + {{- end }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "validating-webhook-configuration" "context" $) }} +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "webhook-service" "context" $) }} + namespace: {{ .Release.Namespace }} + path: /validate-ray-io-v1-raycluster + failurePolicy: Fail + name: vraycluster-v1.kb.io + rules: + - apiGroups: + - ray.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - rayclusters + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "webhook-service" "context" $) }} + namespace: {{ .Release.Namespace }} + path: /validate-ray-io-v1-rayjob + failurePolicy: Fail + name: vrayjob-v1.kb.io + rules: + - apiGroups: + - ray.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - rayjobs + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "webhook-service" "context" $) }} + namespace: {{ .Release.Namespace }} + path: /validate-ray-io-v1-rayservice + failurePolicy: Fail + name: vrayservice-v1.kb.io + rules: + - apiGroups: + - ray.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - rayservices + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "webhook-service" "context" $) }} + namespace: {{ .Release.Namespace }} + path: /validate-ray-io-v1-raycronjob + failurePolicy: Fail + name: vraycronjob-v1.kb.io + rules: + - apiGroups: + - ray.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - raycronjobs + sideEffects: None +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/webhook/webhook-service.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/webhook/webhook-service.yaml new file mode 100644 index 00000000..f252d5a0 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/templates/webhook/webhook-service.yaml @@ -0,0 +1,20 @@ +{{- if .Values.webhook.enable }} +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + app.kubernetes.io/instance: {{ .Release.Name }} + name: {{ include "hyperpod-ray-endpoint-operator.resourceName" (dict "suffix" "webhook-service" "context" $) }} + namespace: {{ .Release.Namespace }} +spec: + ports: + - port: 443 + protocol: TCP + targetPort: {{ .Values.webhook.port }} + selector: + app.kubernetes.io/name: {{ include "hyperpod-ray-endpoint-operator.name" . }} + control-plane: controller-manager +{{- end }} diff --git a/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/values.yaml b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/values.yaml new file mode 100644 index 00000000..be00e80e --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/hyperpod-ray-endpoint-operator/values.yaml @@ -0,0 +1,296 @@ +## String to partially override chart.fullname template (will maintain the release name) +## +# nameOverride: "" + +## String to fully override chart.fullname template +## +# fullnameOverride: "" + +## KMS configuration for JWT signing +## When set, both the manager and auth-middleware use AWS KMS HMAC instead of K8s secrets. +## +kmsKeyArn: "" + +## Extension API domain for Ray dashboard URLs (e.g., spaces.example.com) +## Required for the extension API and controller to function. +## +extensionApiDomain: "" + +## Admin group for webhook annotation protection bypass +## +adminGroup: "system:masters" + +## Configure the controller manager deployment +## +manager: + ## Set to false to skip manager installation + ## + enabled: true + + replicas: 1 + + image: + repository: controller + ## Image tag (defaults to Chart.appVersion if not set) + ## + # tag: "" + pullPolicy: IfNotPresent + + ## Arguments + ## + args: + - --leader-elect + - --log-file-path=/var/log/aws/clusters/ray-endpoint-operator/hyperpod-ray-endpoint-operator.log + - --enable-extension-api + + ## Environment variables + ## + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + + ## Env overrides (--set manager.envOverrides.VAR=value) + ## Same name in env above: this value takes precedence. + ## + envOverrides: {} + + ## Image pull secrets + ## + # imagePullSecrets: + # - name: myregistrykey + + ## Pod-level security settings + ## + podSecurityContext: + fsGroup: 65532 + runAsGroup: 65532 + runAsNonRoot: true + runAsUser: 65532 + seccompProfile: + type: RuntimeDefault + + ## Container-level security settings + ## + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + + ## Resource limits and requests + ## + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + + ## Manager pod's affinity + ## + affinity: {} + + ## Manager pod's node selector + ## + nodeSelector: + kubernetes.io/arch: amd64 + sagemaker.amazonaws.com/compute-type: hyperpod + + ## Manager pod's tolerations + ## + tolerations: [] + + ## Deployment strategy + ## + # strategy: + # type: RollingUpdate + # rollingUpdate: + # maxSurge: 25% + # maxUnavailable: 25% + + ## Priority class name + ## + # priorityClassName: "" + + ## Topology spread constraints + ## + # topologySpreadConstraints: [] + + ## Termination grace period seconds + ## + terminationGracePeriodSeconds: 10 + + ## Custom Deployment labels + ## + # labels: {} + + ## Custom Deployment annotations + ## + # annotations: {} + + ## Custom Pod labels and annotations + ## + # pod: + # labels: {} + # annotations: {} + + ## Extra volumes and volume mounts + ## + extraVolumes: [] + + extraVolumeMounts: [] + +## RBAC configuration +## +rbac: + ## RBAC resource scope + ## - false (default): ClusterRole/ClusterRoleBinding (all namespaces) + ## - true: Role/RoleBinding (release namespace only) + ## + namespaced: false + + ## Multi-namespace RBAC role mappings (advanced use) + ## Maps role suffixes to target namespaces for multi-namespace deployments + ## + roleNamespaces: + "auth-reader": "kube-system" + + ## Helper roles for CRD management (admin/editor/viewer) + ## + helpers: + ## Install convenience admin/editor/viewer roles for CRDs + ## + enable: false + +## ServiceAccount configuration +## +serviceAccount: + # Install default ServiceAccount provided + enable: true + + ## Existing ServiceAccount name (only when enable=false) + ## Note: When enable=true, respects nameOverride/fullnameOverride + ## + # name: "" + + ## Custom ServiceAccount annotations + ## + # annotations: {} + + ## Custom ServiceAccount labels + ## + # labels: {} + +## Custom Resource Definitions +## +crd: + # Install CRDs with the chart + enable: true + # Keep CRDs when uninstalling + keep: true + +## Controller metrics endpoint. +## Enable to expose /metrics endpoint +## +metrics: + enable: true + # Metrics server port + port: 8443 + # Enable secure metrics: HTTPS with certs/auth (true) or HTTP (false). + # Note: Metrics authn/authz needs ClusterRole access. + secure: true + +## Cert-manager integration for TLS certificates. +## Required for webhook certificates and metrics endpoint certificates. +## +certManager: + enable: true + +## Webhook server configuration +## +webhook: + enable: true + # Webhook server port + port: 9443 + +## Prometheus ServiceMonitor for metrics scraping. +## Requires prometheus-operator to be installed in the cluster. +## +prometheus: + enable: false + +## Auth-middleware deployment configuration +## +authMiddleware: + ## Set to false to skip auth-middleware installation + ## + enabled: true + + replicas: 2 + + image: + repository: authmiddleware + ## Image tag (defaults to Chart.appVersion if not set) + ## + # tag: "" + pullPolicy: IfNotPresent + + ## Environment variables + ## + env: + port: "8080" + jwtIssuer: "hyperpod-ray-endpoint-operator" + jwtAudience: "hyperpod-ray-endpoint-operator" + sessionTTL: "6h" + cookieName: "ray_session" + cookieSecure: "true" + cookieSameSite: "lax" + logFilePath: "/var/log/aws/clusters/ray-endpoint-operator/hyperpod-ray-endpoint-operator-auth-middleware.log" + + ## Resource limits and requests + ## + resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 128Mi + + ## Pod-level security settings + ## + podSecurityContext: + fsGroup: 65532 + runAsGroup: 65532 + runAsNonRoot: true + runAsUser: 65532 + seccompProfile: + type: RuntimeDefault + + ## Container-level security settings + ## + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + + ## Node selector + ## + nodeSelector: + kubernetes.io/arch: amd64 + sagemaker.amazonaws.com/compute-type: hyperpod + + ## Tolerations + ## + tolerations: [] + + ## Termination grace period seconds + ## + terminationGracePeriodSeconds: 30 + diff --git a/helm_chart/HyperPodHelmChart/charts/inference-operator/Chart.yaml b/helm_chart/HyperPodHelmChart/charts/inference-operator/Chart.yaml index f4a92b05..e6160f00 100644 --- a/helm_chart/HyperPodHelmChart/charts/inference-operator/Chart.yaml +++ b/helm_chart/HyperPodHelmChart/charts/inference-operator/Chart.yaml @@ -15,11 +15,11 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 2.1.1 +version: 2.3.0 # This is the version number of the application being deployed. Keep this aligned # with operator image MAJOR.MINOR version. -appVersion: "3.1" +appVersion: "3.3" dependencies: - name: aws-mountpoint-s3-csi-driver diff --git a/helm_chart/HyperPodHelmChart/charts/inference-operator/config/crd/inference.sagemaker.aws.amazon.com_inferenceendpointconfigs.yaml b/helm_chart/HyperPodHelmChart/charts/inference-operator/config/crd/inference.sagemaker.aws.amazon.com_inferenceendpointconfigs.yaml index c5c6bd38..c25a22b1 100644 --- a/helm_chart/HyperPodHelmChart/charts/inference-operator/config/crd/inference.sagemaker.aws.amazon.com_inferenceendpointconfigs.yaml +++ b/helm_chart/HyperPodHelmChart/charts/inference-operator/config/crd/inference.sagemaker.aws.amazon.com_inferenceendpointconfigs.yaml @@ -53,8 +53,9 @@ spec: properties: InitialReplicaCount: description: |- - Number of desired pods. This is a pointer to distinguish between explicit - zero and not specified. Defaults to 1. + Deprecated: This field has no effect and will be removed in a future release. + Use spec.replicas for initial pod count and spec.autoScalingSpec.minReplicaCount + for the steady-state floor under autoscaling. format: int32 type: integer autoScalingSpec: @@ -4393,6 +4394,13 @@ spec: to /ping if not specified. pattern: ^/.* type: string + idleTimeoutSeconds: + description: Idle timeout in seconds for the ALB connection. If + not specified, defaults to 60 seconds. + format: int32 + maximum: 4000 + minimum: 1 + type: integer routingAlgorithm: default: least_outstanding_requests description: Routing algorithm for the ALB target group (least_oustanding_requests @@ -4441,6 +4449,39 @@ spec: type: integer type: object type: object + modelCacheConfig: + description: Configuration for model caching (image and data). + properties: + imageCache: + description: |- + Configuration for image caching. When enabled, the operator pre-pulls the + inference server container image onto target nodes via a DaemonSet. + properties: + enabled: + description: |- + Enable or disable image caching for this deployment. + When enabled, the operator creates a ModelImageCache resource that pins + the inference server image on target nodes. + type: boolean + required: + - enabled + type: object + weightsCache: + description: Configuration for pre-caching model weights on local + NVMe storage. + properties: + enabled: + default: false + description: Whether model data caching is enabled. + type: boolean + hostPath: + default: /opt/dlami/nvme + description: Host path where cached model weights are stored. + maxLength: 255 + minLength: 1 + type: string + type: object + type: object modelName: description: Name of model that will be created on Sagemaker maxLength: 63 @@ -4741,6 +4782,817 @@ spec: type: object x-kubernetes-map-type: atomic type: object + pdSpec: + description: |- + Configuration for disaggregated prefill and decode (DPD). + Presence of pdSpec enables DPD mode, creating separate prefill and decode Deployments. + properties: + autoScalingSpec: + description: Autoscaling configuration for prefill and decode + roles independently. + properties: + decodingAutoScaling: + description: Autoscaling configuration for decode pods. + properties: + cloudWatchTrigger: + description: CloudWatch metric trigger to use for autoscaling + properties: + activationTargetValue: + default: 0 + description: Activation Value for CloudWatch metric + to scale from 0 to 1. Only applicable if minReplicaCount + = 0 + type: number + dimensions: + description: Dimensions for Cloudwatch metrics + items: + properties: + name: + description: CloudWatch Metric dimension name + type: string + value: + description: CloudWatch Metric dimension value + type: string + required: + - name + - value + type: object + type: array + metricCollectionPeriod: + default: 300 + description: Defines the Period for CloudWatch query + format: int32 + type: integer + metricCollectionStartTime: + default: 300 + description: Defines the StartTime for CloudWatch + query + format: int32 + type: integer + metricName: + description: Metric name to query for Cloudwatch trigger + type: string + metricStat: + default: Average + description: Statistics metric to be used by Trigger. + Used to define Stat for CloudWatch query. Default + is Average. + type: string + metricType: + default: Average + description: 'The type of metric to be used by HPA. + Enum: AverageValue - Uses average value of metric + per pod, Value - Uses absolute metric value' + enum: + - Value + - Average + type: string + minValue: + default: 0 + description: Minimum metric value used in case of + empty response from CloudWatch. Default is 0. + type: number + name: + description: Name for the CloudWatch trigger + type: string + namespace: + description: AWS CloudWatch namespace for metric + type: string + targetValue: + description: TargetValue for CloudWatch metric + type: number + useCachedMetrics: + default: true + description: Enable caching of metric values during + polling interval. Default is true + type: boolean + type: object + cloudWatchTriggerList: + description: Multiple CloudWatch metric triggers to use + for autoscaling. Takes priority over CloudWatchTrigger + if both are provided. + items: + properties: + activationTargetValue: + default: 0 + description: Activation Value for CloudWatch metric + to scale from 0 to 1. Only applicable if minReplicaCount + = 0 + type: number + dimensions: + description: Dimensions for Cloudwatch metrics + items: + properties: + name: + description: CloudWatch Metric dimension name + type: string + value: + description: CloudWatch Metric dimension value + type: string + required: + - name + - value + type: object + type: array + metricCollectionPeriod: + default: 300 + description: Defines the Period for CloudWatch query + format: int32 + type: integer + metricCollectionStartTime: + default: 300 + description: Defines the StartTime for CloudWatch + query + format: int32 + type: integer + metricName: + description: Metric name to query for Cloudwatch + trigger + type: string + metricStat: + default: Average + description: Statistics metric to be used by Trigger. + Used to define Stat for CloudWatch query. Default + is Average. + type: string + metricType: + default: Average + description: 'The type of metric to be used by HPA. + Enum: AverageValue - Uses average value of metric + per pod, Value - Uses absolute metric value' + enum: + - Value + - Average + type: string + minValue: + default: 0 + description: Minimum metric value used in case of + empty response from CloudWatch. Default is 0. + type: number + name: + description: Name for the CloudWatch trigger + type: string + namespace: + description: AWS CloudWatch namespace for metric + type: string + targetValue: + description: TargetValue for CloudWatch metric + type: number + useCachedMetrics: + default: true + description: Enable caching of metric values during + polling interval. Default is true + type: boolean + type: object + maxItems: 100 + type: array + cooldownPeriod: + default: 300 + description: The period to wait after the last trigger + reported active before scaling the resource back to + 0. Default 300 seconds. + format: int32 + minimum: 0 + type: integer + initialCooldownPeriod: + default: 300 + description: The delay before the cooldownPeriod starts + after the initial creation of the ScaledObject. Default + 300 seconds. + format: int32 + minimum: 0 + type: integer + maxReplicaCount: + default: 5 + description: The maximum number of model pods to scale + to. Default 5. + format: int32 + minimum: 0 + type: integer + minReplicaCount: + default: 1 + description: The minimum number of model pods to scale + down to. Default 1. + format: int32 + minimum: 0 + type: integer + pollingInterval: + default: 30 + description: This is the interval to check each trigger + on. Default 30 seconds. + format: int32 + minimum: 0 + type: integer + prometheusTrigger: + description: Prometheus metric trigger to use for autoscaling + properties: + activationTargetValue: + default: 0 + description: Activation Value for Prometheus metric + to scale from 0 to 1. Only applicable if minReplicaCount + = 0 + type: number + customHeaders: + description: Custom headers to include while querying + the prometheus endpoint. + type: string + metricType: + default: Average + description: 'The type of metric to be used by HPA. + Enum: AverageValue - Uses average value of metric + per pod, Value - Uses absolute metric value' + enum: + - Value + - Average + type: string + name: + description: Name for the Prometheus trigger + type: string + namespace: + description: Namespace for namespaced queries + type: string + query: + description: PromQLQuery for the metric. + type: string + serverAddress: + description: Server address for AMP workspace + pattern: ^https:\/\/aps-workspaces\.[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\.amazonaws\.com\/workspaces\/ws-[a-zA-Z0-9-]+$|^$ + type: string + targetValue: + description: Target metric value for scaling + type: number + useCachedMetrics: + default: true + description: Enable caching of metric values during + polling interval. Default is true + type: boolean + type: object + prometheusTriggerList: + description: Multiple Prometheus metric triggers to use + for autoscaling. Takes priority over PrometheusTrigger + if both are provided. + items: + properties: + activationTargetValue: + default: 0 + description: Activation Value for Prometheus metric + to scale from 0 to 1. Only applicable if minReplicaCount + = 0 + type: number + customHeaders: + description: Custom headers to include while querying + the prometheus endpoint. + type: string + metricType: + default: Average + description: 'The type of metric to be used by HPA. + Enum: AverageValue - Uses average value of metric + per pod, Value - Uses absolute metric value' + enum: + - Value + - Average + type: string + name: + description: Name for the Prometheus trigger + type: string + namespace: + description: Namespace for namespaced queries + type: string + query: + description: PromQLQuery for the metric. + type: string + serverAddress: + description: Server address for AMP workspace + pattern: ^https:\/\/aps-workspaces\.[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\.amazonaws\.com\/workspaces\/ws-[a-zA-Z0-9-]+$|^$ + type: string + targetValue: + description: Target metric value for scaling + type: number + useCachedMetrics: + default: true + description: Enable caching of metric values during + polling interval. Default is true + type: boolean + type: object + maxItems: 100 + type: array + scaleDownStabilizationTime: + default: 300 + description: The time window to stabilize for HPA before + scaling down. Default 300 seconds. + format: int32 + minimum: 0 + type: integer + scaleUpStabilizationTime: + default: 0 + description: The time window to stabilize for HPA before + scaling up. Default 0 seconds. + format: int32 + minimum: 0 + type: integer + type: object + prefillAutoScaling: + description: Autoscaling configuration for prefill pods. + properties: + cloudWatchTrigger: + description: CloudWatch metric trigger to use for autoscaling + properties: + activationTargetValue: + default: 0 + description: Activation Value for CloudWatch metric + to scale from 0 to 1. Only applicable if minReplicaCount + = 0 + type: number + dimensions: + description: Dimensions for Cloudwatch metrics + items: + properties: + name: + description: CloudWatch Metric dimension name + type: string + value: + description: CloudWatch Metric dimension value + type: string + required: + - name + - value + type: object + type: array + metricCollectionPeriod: + default: 300 + description: Defines the Period for CloudWatch query + format: int32 + type: integer + metricCollectionStartTime: + default: 300 + description: Defines the StartTime for CloudWatch + query + format: int32 + type: integer + metricName: + description: Metric name to query for Cloudwatch trigger + type: string + metricStat: + default: Average + description: Statistics metric to be used by Trigger. + Used to define Stat for CloudWatch query. Default + is Average. + type: string + metricType: + default: Average + description: 'The type of metric to be used by HPA. + Enum: AverageValue - Uses average value of metric + per pod, Value - Uses absolute metric value' + enum: + - Value + - Average + type: string + minValue: + default: 0 + description: Minimum metric value used in case of + empty response from CloudWatch. Default is 0. + type: number + name: + description: Name for the CloudWatch trigger + type: string + namespace: + description: AWS CloudWatch namespace for metric + type: string + targetValue: + description: TargetValue for CloudWatch metric + type: number + useCachedMetrics: + default: true + description: Enable caching of metric values during + polling interval. Default is true + type: boolean + type: object + cloudWatchTriggerList: + description: Multiple CloudWatch metric triggers to use + for autoscaling. Takes priority over CloudWatchTrigger + if both are provided. + items: + properties: + activationTargetValue: + default: 0 + description: Activation Value for CloudWatch metric + to scale from 0 to 1. Only applicable if minReplicaCount + = 0 + type: number + dimensions: + description: Dimensions for Cloudwatch metrics + items: + properties: + name: + description: CloudWatch Metric dimension name + type: string + value: + description: CloudWatch Metric dimension value + type: string + required: + - name + - value + type: object + type: array + metricCollectionPeriod: + default: 300 + description: Defines the Period for CloudWatch query + format: int32 + type: integer + metricCollectionStartTime: + default: 300 + description: Defines the StartTime for CloudWatch + query + format: int32 + type: integer + metricName: + description: Metric name to query for Cloudwatch + trigger + type: string + metricStat: + default: Average + description: Statistics metric to be used by Trigger. + Used to define Stat for CloudWatch query. Default + is Average. + type: string + metricType: + default: Average + description: 'The type of metric to be used by HPA. + Enum: AverageValue - Uses average value of metric + per pod, Value - Uses absolute metric value' + enum: + - Value + - Average + type: string + minValue: + default: 0 + description: Minimum metric value used in case of + empty response from CloudWatch. Default is 0. + type: number + name: + description: Name for the CloudWatch trigger + type: string + namespace: + description: AWS CloudWatch namespace for metric + type: string + targetValue: + description: TargetValue for CloudWatch metric + type: number + useCachedMetrics: + default: true + description: Enable caching of metric values during + polling interval. Default is true + type: boolean + type: object + maxItems: 100 + type: array + cooldownPeriod: + default: 300 + description: The period to wait after the last trigger + reported active before scaling the resource back to + 0. Default 300 seconds. + format: int32 + minimum: 0 + type: integer + initialCooldownPeriod: + default: 300 + description: The delay before the cooldownPeriod starts + after the initial creation of the ScaledObject. Default + 300 seconds. + format: int32 + minimum: 0 + type: integer + maxReplicaCount: + default: 5 + description: The maximum number of model pods to scale + to. Default 5. + format: int32 + minimum: 0 + type: integer + minReplicaCount: + default: 1 + description: The minimum number of model pods to scale + down to. Default 1. + format: int32 + minimum: 0 + type: integer + pollingInterval: + default: 30 + description: This is the interval to check each trigger + on. Default 30 seconds. + format: int32 + minimum: 0 + type: integer + prometheusTrigger: + description: Prometheus metric trigger to use for autoscaling + properties: + activationTargetValue: + default: 0 + description: Activation Value for Prometheus metric + to scale from 0 to 1. Only applicable if minReplicaCount + = 0 + type: number + customHeaders: + description: Custom headers to include while querying + the prometheus endpoint. + type: string + metricType: + default: Average + description: 'The type of metric to be used by HPA. + Enum: AverageValue - Uses average value of metric + per pod, Value - Uses absolute metric value' + enum: + - Value + - Average + type: string + name: + description: Name for the Prometheus trigger + type: string + namespace: + description: Namespace for namespaced queries + type: string + query: + description: PromQLQuery for the metric. + type: string + serverAddress: + description: Server address for AMP workspace + pattern: ^https:\/\/aps-workspaces\.[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\.amazonaws\.com\/workspaces\/ws-[a-zA-Z0-9-]+$|^$ + type: string + targetValue: + description: Target metric value for scaling + type: number + useCachedMetrics: + default: true + description: Enable caching of metric values during + polling interval. Default is true + type: boolean + type: object + prometheusTriggerList: + description: Multiple Prometheus metric triggers to use + for autoscaling. Takes priority over PrometheusTrigger + if both are provided. + items: + properties: + activationTargetValue: + default: 0 + description: Activation Value for Prometheus metric + to scale from 0 to 1. Only applicable if minReplicaCount + = 0 + type: number + customHeaders: + description: Custom headers to include while querying + the prometheus endpoint. + type: string + metricType: + default: Average + description: 'The type of metric to be used by HPA. + Enum: AverageValue - Uses average value of metric + per pod, Value - Uses absolute metric value' + enum: + - Value + - Average + type: string + name: + description: Name for the Prometheus trigger + type: string + namespace: + description: Namespace for namespaced queries + type: string + query: + description: PromQLQuery for the metric. + type: string + serverAddress: + description: Server address for AMP workspace + pattern: ^https:\/\/aps-workspaces\.[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\.amazonaws\.com\/workspaces\/ws-[a-zA-Z0-9-]+$|^$ + type: string + targetValue: + description: Target metric value for scaling + type: number + useCachedMetrics: + default: true + description: Enable caching of metric values during + polling interval. Default is true + type: boolean + type: object + maxItems: 100 + type: array + scaleDownStabilizationTime: + default: 300 + description: The time window to stabilize for HPA before + scaling down. Default 300 seconds. + format: int32 + minimum: 0 + type: integer + scaleUpStabilizationTime: + default: 0 + description: The time window to stabilize for HPA before + scaling up. Default 0 seconds. + format: int32 + minimum: 0 + type: integer + type: object + type: object + decodingSpec: + description: Configuration for decode pods. + properties: + args: + description: |- + Additional vLLM args for this role (e.g., --tensor-parallel-size, --gpu-memory-utilization, --max-num-seqs). + These are appended after the shared workerConfig.args and override any matching flags. + items: + type: string + type: array + nodeSelector: + additionalProperties: + type: string + description: Node selector for scheduling pods of this role + onto specific nodes. + type: object + replicas: + default: 1 + description: Number of replicas for this role. + format: int32 + minimum: 1 + type: integer + resources: + description: |- + Resource requests and limits for pods of this role. + Must include GPU resource requests for DPD to function. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + 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. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + prefillSpec: + description: Configuration for prefill pods. + properties: + args: + description: |- + Additional vLLM args for this role (e.g., --tensor-parallel-size, --gpu-memory-utilization, --max-num-seqs). + These are appended after the shared workerConfig.args and override any matching flags. + items: + type: string + type: array + nodeSelector: + additionalProperties: + type: string + description: Node selector for scheduling pods of this role + onto specific nodes. + type: object + replicas: + default: 1 + description: Number of replicas for this role. + format: int32 + minimum: 1 + type: integer + resources: + description: |- + Resource requests and limits for pods of this role. + Must include GPU resource requests for DPD to function. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + 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. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + type: object + routingThreshold: + default: 4096 + description: |- + Token count threshold for conditional routing. + Requests with estimated input tokens >= threshold go through DPD path (remote prefill). + Requests below threshold go directly to decoder for local chunked prefill. + Default 4096. Set to 0 to always disaggregate. + format: int32 + minimum: 0 + type: integer + topologySpec: + description: Topology constraints for prefill and decode pod scheduling. + properties: + availabilityZone: + description: |- + Force pods into a specific availability zone. If empty, any AZ is allowed + (subject to sameAZ constraint). + type: string + placementGroup: + description: Placement group name for lowest network latency + between prefill and decode pods. + type: string + sameAZ: + default: true + description: |- + Enforce same availability zone placement for prefill and decode pods. + Required for optimal EFA latency. Default: true. + type: boolean + type: object + required: + - decodingSpec + - prefillSpec + type: object replicas: default: 1 description: The desired number of inference server replicas. Default @@ -5844,6 +6696,30 @@ spec: - state type: object type: object + imageCacheStatus: + description: Status of model server image caching + properties: + message: + description: Human-readable message. + type: string + readyNodes: + description: Number of nodes where the image is cached. + format: int32 + type: integer + state: + description: Current state of the image cache. + enum: + - Pending + - Downloading + - Ready + - Failed + - PartiallyReady + type: string + targetNodes: + description: Total number of target nodes. + format: int32 + type: integer + type: object metricsStatus: description: Status of metrics collection properties: @@ -5983,8 +6859,9 @@ spec: properties: InitialReplicaCount: description: |- - Number of desired pods. This is a pointer to distinguish between explicit - zero and not specified. Defaults to 1. + Deprecated: This field has no effect and will be removed in a future release. + Use spec.replicas for initial pod count and spec.autoScalingSpec.minReplicaCount + for the steady-state floor under autoscaling. format: int32 type: integer autoScalingSpec: diff --git a/helm_chart/HyperPodHelmChart/charts/inference-operator/config/crd/inference.sagemaker.aws.amazon.com_jumpstartmodels.yaml b/helm_chart/HyperPodHelmChart/charts/inference-operator/config/crd/inference.sagemaker.aws.amazon.com_jumpstartmodels.yaml index a5e37ffd..90a9b6a7 100644 --- a/helm_chart/HyperPodHelmChart/charts/inference-operator/config/crd/inference.sagemaker.aws.amazon.com_jumpstartmodels.yaml +++ b/helm_chart/HyperPodHelmChart/charts/inference-operator/config/crd/inference.sagemaker.aws.amazon.com_jumpstartmodels.yaml @@ -907,6 +907,13 @@ spec: to /ping if not specified. pattern: ^/.* type: string + idleTimeoutSeconds: + description: Idle timeout in seconds for the ALB connection. If + not specified, defaults to 60 seconds. + format: int32 + maximum: 4000 + minimum: 1 + type: integer routingAlgorithm: default: least_outstanding_requests description: Routing algorithm for the ALB target group (least_oustanding_requests @@ -1007,6 +1014,39 @@ spec: - acceptEula - modelId type: object + modelCacheConfig: + description: Configuration for model caching (image and data). + properties: + imageCache: + description: |- + Configuration for image caching. When enabled, the operator pre-pulls the + inference server container image onto target nodes via a DaemonSet. + properties: + enabled: + description: |- + Enable or disable image caching for this deployment. + When enabled, the operator creates a ModelImageCache resource that pins + the inference server image on target nodes. + type: boolean + required: + - enabled + type: object + weightsCache: + description: Configuration for pre-caching model weights on local + NVMe storage. + properties: + enabled: + default: false + description: Whether model data caching is enabled. + type: boolean + hostPath: + default: /opt/dlami/nvme + description: Host path where cached model weights are stored. + maxLength: 255 + minLength: 1 + type: string + type: object + type: object replicas: default: 1 description: The desired number of inference server replicas. Default @@ -1349,6 +1389,30 @@ spec: - state type: object type: object + imageCacheStatus: + description: Status of model server image caching + properties: + message: + description: Human-readable message. + type: string + readyNodes: + description: Number of nodes where the image is cached. + format: int32 + type: integer + state: + description: Current state of the image cache. + enum: + - Pending + - Downloading + - Ready + - Failed + - PartiallyReady + type: string + targetNodes: + description: Total number of target nodes. + format: int32 + type: integer + type: object metricsStatus: description: Status of metrics collection properties: diff --git a/helm_chart/HyperPodHelmChart/charts/inference-operator/config/crd/inference.sagemaker.aws.amazon.com_modelimagecaches.yaml b/helm_chart/HyperPodHelmChart/charts/inference-operator/config/crd/inference.sagemaker.aws.amazon.com_modelimagecaches.yaml new file mode 100644 index 00000000..e9ccc635 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/inference-operator/config/crd/inference.sagemaker.aws.amazon.com_modelimagecaches.yaml @@ -0,0 +1,338 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: modelimagecaches.inference.sagemaker.aws.amazon.com +spec: + group: inference.sagemaker.aws.amazon.com + names: + kind: ModelImageCache + listKind: ModelImageCacheList + plural: modelimagecaches + shortNames: + - mic + singular: modelimagecache + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.image + name: Image + type: string + - jsonPath: .status.state + name: State + type: string + - jsonPath: .status.readyNodes + name: Ready + type: integer + - jsonPath: .status.targetNodes + name: Target + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + ModelImageCache is the Schema for the modelimagecaches API. + It represents a cached container image on a set of nodes, managed via a backing DaemonSet. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ModelImageCacheSpec defines the desired state of ModelImageCache. + properties: + image: + description: The full container image URI to cache on target nodes. + type: string + nodeAffinity: + description: Node affinity for more complex node scheduling constraints. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with the corresponding + weight. + properties: + matchExpressions: + description: A list of node selector requirements by + node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements by + node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. The + terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements by + node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements by + node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + nodeSelector: + additionalProperties: + type: string + description: Node selector to restrict which nodes receive the cached + image. + type: object + parentReferences: + description: |- + List of parent resources that reference this image cache. + Used to track ownership across namespaces. + items: + description: ParentReference identifies a resource that uses this + image cache. + properties: + kind: + description: Kind of the parent resource (e.g., InferenceEndpointConfig, + JumpStartModel). + type: string + name: + description: Name of the parent resource. + type: string + namespace: + description: Namespace of the parent resource. + type: string + uid: + description: UID of the parent resource. + type: string + required: + - kind + - name + - namespace + - uid + type: object + type: array + required: + - image + type: object + status: + description: ModelImageCacheStatus defines the observed state of ModelImageCache. + properties: + lastTransitionTime: + description: Time of the last state transition. + format: date-time + type: string + message: + description: Human-readable message describing the current state. + type: string + readyNodes: + description: Number of nodes where the image has been successfully + cached. + format: int32 + type: integer + resolvedDigest: + description: |- + The resolved image digest as reported by the container runtime (e.g., sha256:abc123...). + Populated once a cache pod successfully pulls and runs the image. + type: string + state: + description: Current state of the image cache. + enum: + - Pending + - Downloading + - Ready + - Failed + - PartiallyReady + - Deleting + type: string + targetNodes: + description: Total number of nodes targeted by this image cache. + format: int32 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm_chart/HyperPodHelmChart/charts/inference-operator/config/crd/inference.sagemaker.aws.amazon.com_modelweightscacheconfigs.yaml b/helm_chart/HyperPodHelmChart/charts/inference-operator/config/crd/inference.sagemaker.aws.amazon.com_modelweightscacheconfigs.yaml new file mode 100644 index 00000000..7f4a6202 --- /dev/null +++ b/helm_chart/HyperPodHelmChart/charts/inference-operator/config/crd/inference.sagemaker.aws.amazon.com_modelweightscacheconfigs.yaml @@ -0,0 +1,507 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: modelweightscacheconfigs.inference.sagemaker.aws.amazon.com +spec: + group: inference.sagemaker.aws.amazon.com + names: + kind: ModelWeightsCacheConfig + listKind: ModelWeightsCacheConfigList + plural: modelweightscacheconfigs + singular: modelweightscacheconfig + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.state + name: State + type: string + - jsonPath: .status.readyNodes + name: Ready + type: integer + - jsonPath: .status.targetNodes + name: Target + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: ModelWeightsCacheConfig is the Schema for the modelweightscacheconfigs + API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ModelWeightsCacheConfigSpec defines the desired state of + ModelWeightsCacheConfig. + properties: + enabled: + default: true + description: Whether data caching is enabled. When false, the controller + takes no action. + type: boolean + hostPath: + default: /opt/dlami/nvme + description: Host path where cached model weights are stored. + maxLength: 255 + minLength: 1 + type: string + jumpStartModel: + description: |- + JumpStart model configuration. When present, the controller uses JumpStart model resolution + regardless of modelSourceType. Reuses the existing ModelSpec from JumpStartModel CRD. + properties: + acceptEula: + default: false + description: For models that require a Model Access Config, specify + True or False to indicate whether model terms of use have been + accepted. + type: boolean + additionalConfigs: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + maxItems: 10 + type: array + gatedModelDownloadRole: + description: The Amazon Resource Name (ARN) of an IAM role that + will be used to download gated model + maxLength: 2048 + minLength: 20 + pattern: ^arn:aws[a-z\-]*:iam::\d{12}:role/?[a-zA-Z_0-9+=,.@\-_/]+$ + type: string + modelHubName: + default: SageMakerPublicHub + description: The name of the model hub content. Can be an ARN + or a simple name. + maxLength: 63 + pattern: ^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,62}$ + type: string + modelId: + description: The unique identifier of the model within the specified + hub (hubContentArn). + maxLength: 63 + pattern: ^[a-zA-Z0-9](-*[a-zA-Z0-9]){0,62}$ + type: string + modelVersion: + description: The version of the model to deploy, in semantic versioning + format (e.g., 1.0.0). + maxLength: 14 + minLength: 5 + pattern: ^\d{1,4}.\d{1,4}.\d{1,4}$ + type: string + required: + - acceptEula + - modelId + type: object + modelSourceConfig: + description: Reference to the model source to cache. Reuses the existing + ModelSourceConfig structure. + properties: + fsxStorage: + properties: + dnsName: + description: FSX File System DNS Name + type: string + fileSystemId: + description: FSX File System ID + type: string + mountName: + description: FSX File System Mount Name + type: string + required: + - fileSystemId + type: object + huggingFaceModel: + description: HuggingFace model configuration. Required when modelSourceType + is "huggingface". + properties: + commitSHA: + description: |- + Git commit SHA for the model revision. Must be a full 40-character lowercase hex SHA. + If not provided, the operator defaults to "main" branch. + pattern: ^[0-9a-f]{40}$ + type: string + modelId: + description: HuggingFace Hub model identifier in org/model + format (e.g. "meta-llama/Llama-3.1-8B-Instruct"). + pattern: ^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$ + type: string + tokenSecretRef: + description: |- + Reference to a Kubernetes Secret containing the HuggingFace API token. + The token is injected as the HF_TOKEN environment variable into the InitContainer only. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + required: + - modelId + type: object + modelLocation: + description: Specific location where the model data exists + type: string + modelSourceType: + enum: + - fsx + - s3 + - huggingface + - kubernetesVolume + type: string + prefetchEnabled: + default: false + description: In case the model seems to fit within the instance's + memory (VRAM), this option can be used to pre-fetch the model + to RAM and then the inference server will load to the GPU/CPU + device thereafter. + type: boolean + s3Storage: + properties: + bucketName: + description: S3 bucket location + type: string + region: + description: S3 bucket region + type: string + required: + - bucketName + - region + type: object + required: + - modelSourceType + type: object + nodeAffinity: + description: Node affinity for cache population scheduling. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with the corresponding + weight. + properties: + matchExpressions: + description: A list of node selector requirements by + node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements by + node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. The + terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements by + node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements by + node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector applies + to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + nodeSelector: + additionalProperties: + type: string + description: Node selector constraints for cache population. Cache + pods run only on matching nodes. + type: object + required: + - modelSourceConfig + type: object + status: + description: ModelWeightsCacheConfigStatus defines the observed state + of ModelWeightsCacheConfig. + properties: + conditions: + description: Conditions for detailed status reporting + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + message: + description: Human-readable message about current state + type: string + readyNodes: + description: Number of nodes where cache is ready + format: int32 + type: integer + state: + description: Current state of the cache (Pending, Downloading, Ready, + Failed, Deleting) + type: string + targetNodes: + description: Total number of eligible nodes targeted by the DaemonSet + format: int32 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm_chart/HyperPodHelmChart/charts/inference-operator/config/manager/manager.yaml b/helm_chart/HyperPodHelmChart/charts/inference-operator/config/manager/manager.yaml index 407f067b..f13bc4b4 100644 --- a/helm_chart/HyperPodHelmChart/charts/inference-operator/config/manager/manager.yaml +++ b/helm_chart/HyperPodHelmChart/charts/inference-operator/config/manager/manager.yaml @@ -19,26 +19,18 @@ spec: labels: control-plane: {{ .Values.namePrefix }}-controller-manager spec: - # TODO(user): Uncomment the following code to configure the nodeAffinity expression - # according to the platforms which are supported by your solution. - # It is considered best practice to support multiple architectures. You can - # build your manager image using the makefile target docker-buildx. - # affinity: - # nodeAffinity: - # requiredDuringSchedulingIgnoredDuringExecution: - # nodeSelectorTerms: - # - matchExpressions: - # - key: kubernetes.io/arch - # operator: In - # values: - # - amd64 - # - arm64 - # - ppc64le - # - s390x - # - key: kubernetes.io/os - # operator: In - # values: - # - linux + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} securityContext: runAsNonRoot: true # TODO(user): For common cases that do not require escalating privileges diff --git a/helm_chart/HyperPodHelmChart/charts/inference-operator/config/rbac/role.yaml b/helm_chart/HyperPodHelmChart/charts/inference-operator/config/rbac/role.yaml index eb929cbd..fe7cba26 100644 --- a/helm_chart/HyperPodHelmChart/charts/inference-operator/config/rbac/role.yaml +++ b/helm_chart/HyperPodHelmChart/charts/inference-operator/config/rbac/role.yaml @@ -13,6 +13,8 @@ rules: - jumpstartmodels - inferenceendpointconfigs - sagemakerendpointregistrations + - modelweightscacheconfigs + - modelimagecaches verbs: - create - delete @@ -27,6 +29,8 @@ rules: - jumpstartmodels/finalizers - inferenceendpointconfigs/finalizers - sagemakerendpointregistrations/finalizers + - modelweightscacheconfigs/finalizers + - modelimagecaches/finalizers verbs: - update - apiGroups: @@ -35,6 +39,8 @@ rules: - jumpstartmodels/status - inferenceendpointconfigs/status - sagemakerendpointregistrations/status + - modelweightscacheconfigs/status + - modelimagecaches/status verbs: - get - patch @@ -118,4 +124,21 @@ rules: verbs: - get - list - - watch \ No newline at end of file + - watch +- apiGroups: + - scheduling.k8s.io + resources: + - priorityclasses + verbs: + - create + - list + - watch +- apiGroups: + - scheduling.k8s.io + resources: + - priorityclasses + resourceNames: + - hyperpod-image-cache + verbs: + - get + - delete diff --git a/helm_chart/HyperPodHelmChart/charts/inference-operator/values.yaml b/helm_chart/HyperPodHelmChart/charts/inference-operator/values.yaml index 075a6df0..97356c6f 100644 --- a/helm_chart/HyperPodHelmChart/charts/inference-operator/values.yaml +++ b/helm_chart/HyperPodHelmChart/charts/inference-operator/values.yaml @@ -23,8 +23,9 @@ image: ap-southeast-1: 474668384327.dkr.ecr.ap-southeast-1.amazonaws.com ap-southeast-4: 311141544681.dkr.ecr.ap-southeast-4.amazonaws.com ap-southeast-3: 158128612970.dkr.ecr.ap-southeast-3.amazonaws.com + ap-south-2: 680458885894.dkr.ecr.ap-south-2.amazonaws.com eu-south-2: 025050981094.dkr.ecr.eu-south-2.amazonaws.com - tag: v3.1 + tag: v3.3 pullPolicy: Always repository: initContainer: @@ -38,6 +39,27 @@ tlsCertificateS3Bucket: enableWebhooks: true enableCustomServiceAccounts: false +# Architecture-aware scheduling for the operator deployment. +# Pins to amd64 Linux nodes since operator images are amd64-only. +# Override via EKS AddOn configurationValues if needed. +affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + - key: kubernetes.io/os + operator: In + values: + - linux + +nodeSelector: {} + +tolerations: [] + s3: enabled: true # IAM role ARN used for S3 CSI driver k8s service account diff --git a/helm_chart/HyperPodHelmChart/charts/mlflow/values.yaml b/helm_chart/HyperPodHelmChart/charts/mlflow/values.yaml index 19c63fd4..c979de22 100644 --- a/helm_chart/HyperPodHelmChart/charts/mlflow/values.yaml +++ b/helm_chart/HyperPodHelmChart/charts/mlflow/values.yaml @@ -1,5 +1,5 @@ mlflow: serviceAccount: name: "mlflow-service-account1" - namespace: "kubeflow" + namespace: "default" roleARN: "arn:aws:iam::555555555555:role/hyperpod-mlflow-role" diff --git a/helm_chart/HyperPodHelmChart/charts/namespaced-role-and-bindings/values.yaml b/helm_chart/HyperPodHelmChart/charts/namespaced-role-and-bindings/values.yaml index 44ad2e29..2bb23ee3 100644 --- a/helm_chart/HyperPodHelmChart/charts/namespaced-role-and-bindings/values.yaml +++ b/helm_chart/HyperPodHelmChart/charts/namespaced-role-and-bindings/values.yaml @@ -1,2 +1,2 @@ -namespace: "kubeflow" +namespace: "default" roleName: "hyperpod-scientist-user-namespace-level-role" \ No newline at end of file diff --git a/helm_chart/readme.md b/helm_chart/readme.md index 6851c8d6..153e17d5 100644 --- a/helm_chart/readme.md +++ b/helm_chart/readme.md @@ -234,19 +234,19 @@ helm upgrade dependencies helm_chart/HyperPodHelmChart --namespace kube-system - **Supported Regions and their ECR URIs**: ``` - us-east-1 (US East (N. Virginia)): 767398015722.dkr.ecr.us-east-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.1481.0_1.0.392.0 - us-west-2 (US West (Oregon)): 905418368575.dkr.ecr.us-west-2.amazonaws.com/hyperpod-health-monitoring-agent:1.0.1481.0_1.0.392.0 - us-east-2 (US East (Ohio)): 851725546812.dkr.ecr.us-east-2.amazonaws.com/hyperpod-health-monitoring-agent:1.0.1481.0_1.0.392.0 - us-west-1 (US West (N. California)): 011528288828.dkr.ecr.us-west-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.1481.0_1.0.392.0 - eu-central-1 (Europe (Frankfurt)): 211125453373.dkr.ecr.eu-central-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.1481.0_1.0.392.0 - eu-north-1 (Europe (Stockholm)): 654654141839.dkr.ecr.eu-north-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.1481.0_1.0.392.0 - eu-west-1 (Europe (Ireland)): 533267293120.dkr.ecr.eu-west-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.1481.0_1.0.392.0 - eu-west-2 (Europe (London)): 011528288831.dkr.ecr.eu-west-2.amazonaws.com/hyperpod-health-monitoring-agent:1.0.1481.0_1.0.392.0 - ap-northeast-1 (Asia Pacific (Tokyo)): 533267052152.dkr.ecr.ap-northeast-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.1481.0_1.0.392.0 - ap-south-1 (Asia Pacific (Mumbai)): 011528288864.dkr.ecr.ap-south-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.1481.0_1.0.392.0 - ap-southeast-1 (Asia Pacific (Singapore)): 905418428165.dkr.ecr.ap-southeast-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.1481.0_1.0.392.0 - ap-southeast-2 (Asia Pacific (Sydney)): 851725636348.dkr.ecr.ap-southeast-2.amazonaws.com/hyperpod-health-monitoring-agent:1.0.1481.0_1.0.392.0 - sa-east-1 (South America (São Paulo)): 025066253954.dkr.ecr.sa-east-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.1481.0_1.0.392.0 + us-east-1 (US East (N. Virginia)): 767398015722.dkr.ecr.us-east-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.2297.0_1.0.474.0 + us-west-2 (US West (Oregon)): 905418368575.dkr.ecr.us-west-2.amazonaws.com/hyperpod-health-monitoring-agent:1.0.2297.0_1.0.474.0 + us-east-2 (US East (Ohio)): 851725546812.dkr.ecr.us-east-2.amazonaws.com/hyperpod-health-monitoring-agent:1.0.2297.0_1.0.474.0 + us-west-1 (US West (N. California)): 011528288828.dkr.ecr.us-west-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.2297.0_1.0.474.0 + eu-central-1 (Europe (Frankfurt)): 211125453373.dkr.ecr.eu-central-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.2297.0_1.0.474.0 + eu-north-1 (Europe (Stockholm)): 654654141839.dkr.ecr.eu-north-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.2297.0_1.0.474.0 + eu-west-1 (Europe (Ireland)): 533267293120.dkr.ecr.eu-west-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.2297.0_1.0.474.0 + eu-west-2 (Europe (London)): 011528288831.dkr.ecr.eu-west-2.amazonaws.com/hyperpod-health-monitoring-agent:1.0.2297.0_1.0.474.0 + ap-northeast-1 (Asia Pacific (Tokyo)): 533267052152.dkr.ecr.ap-northeast-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.2297.0_1.0.474.0 + ap-south-1 (Asia Pacific (Mumbai)): 011528288864.dkr.ecr.ap-south-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.2297.0_1.0.474.0 + ap-southeast-1 (Asia Pacific (Singapore)): 905418428165.dkr.ecr.ap-southeast-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.2297.0_1.0.474.0 + ap-southeast-2 (Asia Pacific (Sydney)): 851725636348.dkr.ecr.ap-southeast-2.amazonaws.com/hyperpod-health-monitoring-agent:1.0.2297.0_1.0.474.0 + sa-east-1 (South America (São Paulo)): 025066253954.dkr.ecr.sa-east-1.amazonaws.com/hyperpod-health-monitoring-agent:1.0.2297.0_1.0.474.0 ``` ## 7. Troubleshooting diff --git a/hyperpod-custom-inference-template/CHANGELOG.md b/hyperpod-custom-inference-template/CHANGELOG.md index 15bd45cf..6a80600a 100644 --- a/hyperpod-custom-inference-template/CHANGELOG.md +++ b/hyperpod-custom-inference-template/CHANGELOG.md @@ -1,3 +1,9 @@ +## v1.2.1 (2026-06-11) + +### Features + +* Add v1.2 schema and template support for custom inference endpoints (#417) + ## v1.2.0 (2025-11-21) ### Features diff --git a/hyperpod-custom-inference-template/pyproject.toml b/hyperpod-custom-inference-template/pyproject.toml index 88a983d3..1af4297b 100644 --- a/hyperpod-custom-inference-template/pyproject.toml +++ b/hyperpod-custom-inference-template/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hyperpod-custom-inference-template" -version = "1.2.0" +version = "1.2.1" readme = "README.md" authors = [{name = "Amazon Web Services"}] license = {text = "Apache-2.0"} diff --git a/hyperpod-jumpstart-inference-template/CHANGELOG.md b/hyperpod-jumpstart-inference-template/CHANGELOG.md index a0d4cfe9..5e51cd00 100644 --- a/hyperpod-jumpstart-inference-template/CHANGELOG.md +++ b/hyperpod-jumpstart-inference-template/CHANGELOG.md @@ -1,3 +1,9 @@ +## v1.2.0 (2026-06-11) + +### Features + +* Add v1.2 schema and template support for JumpStart inference endpoints (#417) + ## v1.1.1 (2025-11-25) ### Features diff --git a/hyperpod-jumpstart-inference-template/pyproject.toml b/hyperpod-jumpstart-inference-template/pyproject.toml index 37f1ae55..b13eabb4 100644 --- a/hyperpod-jumpstart-inference-template/pyproject.toml +++ b/hyperpod-jumpstart-inference-template/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hyperpod-jumpstart-inference-template" -version = "1.1.1" +version = "1.2.0" readme = "README.md" authors = [{name = "Amazon Web Services"}] license = {text = "Apache-2.0"} diff --git a/hyperpod-space-template/CHANGELOG.md b/hyperpod-space-template/CHANGELOG.md index 5c47e7f5..76f013d1 100644 --- a/hyperpod-space-template/CHANGELOG.md +++ b/hyperpod-space-template/CHANGELOG.md @@ -1,6 +1,21 @@ +## v1.1.0 (2026-04-20) + +### Features + +* Added `access_type` parameter to control who can connect to the workspace (`Public` or `OwnerOnly`) +* Added `env` parameter for specifying environment variables for the workspace container +* Added `access_strategy` parameter to reference a WorkspaceAccessStrategy +* Added `pod_security_context` parameter for pod-level security context configuration +* Added `container_security_context` parameter for container-level security context configuration +* Added `init_containers` parameter to run init containers before the workspace container starts +* Added `queue_name` and `priority` parameters for task governance support (Kueue integration) + +### Changes + +* Bumped minimum addon version from `0.1.1` to `0.1.6` + ## v1.0.0 (2025-11-20) ### Features * HyperPod Dev Spaces template for data scientists to create, manage, and access interactive ML development environments with configurable resource allocation and namespace isolation - diff --git a/hyperpod-space-template/hyperpod_space_template/registry.py b/hyperpod-space-template/hyperpod_space_template/registry.py index 9d120531..a8763234 100644 --- a/hyperpod-space-template/hyperpod_space_template/registry.py +++ b/hyperpod-space-template/hyperpod_space_template/registry.py @@ -11,10 +11,12 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. from .v1_0.model import SpaceConfig +from .v1_1 import model as v1_1_model from typing import Dict, Type from pydantic import BaseModel # Direct version-to-model mapping SCHEMA_REGISTRY: Dict[str, Type[BaseModel]] = { "1.0": SpaceConfig, + "1.1": v1_1_model.SpaceConfig, } diff --git a/hyperpod-space-template/hyperpod_space_template/v1_0/model.py b/hyperpod-space-template/hyperpod_space_template/v1_0/model.py index 5bf4d56e..6c448cc8 100644 --- a/hyperpod-space-template/hyperpod_space_template/v1_0/model.py +++ b/hyperpod-space-template/hyperpod_space_template/v1_0/model.py @@ -2,6 +2,9 @@ from typing import Optional, List, Dict, Literal, Any from enum import Enum +# Minimum amazon-sagemaker-spaces addon version required for this template version +MIN_ADDON_VERSION = "0.1.1" + class OwnershipType(str, Enum): PUBLIC = "Public" diff --git a/hyperpod-space-template/hyperpod_space_template/v1_1/__init__.py b/hyperpod-space-template/hyperpod_space_template/v1_1/__init__.py new file mode 100644 index 00000000..65490521 --- /dev/null +++ b/hyperpod-space-template/hyperpod_space_template/v1_1/__init__.py @@ -0,0 +1,12 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. diff --git a/hyperpod-space-template/hyperpod_space_template/v1_1/model.py b/hyperpod-space-template/hyperpod_space_template/v1_1/model.py new file mode 100644 index 00000000..7956fe93 --- /dev/null +++ b/hyperpod-space-template/hyperpod_space_template/v1_1/model.py @@ -0,0 +1,353 @@ +from pydantic import BaseModel, ConfigDict, Field, field_validator +from typing import Optional, List, Dict, Literal, Any +from enum import Enum + +# Minimum amazon-sagemaker-spaces addon version required for this template version +MIN_ADDON_VERSION = "0.1.6" + + +class OwnershipType(str, Enum): + PUBLIC = "Public" + OWNER_ONLY = "OwnerOnly" + + +class DesiredStatus(str, Enum): + RUNNING = "Running" + STOPPED = "Stopped" + + +class VolumeSpec(BaseModel): + """VolumeSpec defines a volume to mount from an existing PVC""" + name: str = Field( + description="Name is a unique identifier for this volume within the pod (maps to pod.spec.volumes[].name)", + min_length=1 + ) + mount_path: str = Field( + alias="mountPath", + description="MountPath is the path where the volume should be mounted (Unix-style path, e.g. /data)", + min_length=1 + ) + persistent_volume_claim_name: str = Field( + alias="persistentVolumeClaimName", + description="PersistentVolumeClaimName is the name of the existing PVC to mount", + min_length=1 + ) + + +class ContainerConfig(BaseModel): + """ContainerConfig defines container command and args configuration""" + command: Optional[List[str]] = Field( + default=None, + description="Command specifies the container command" + ) + args: Optional[List[str]] = Field( + default=None, + description="Args specifies the container arguments" + ) + + +class AccessStrategyRef(BaseModel): + """AccessStrategyRef references a WorkspaceAccessStrategy""" + name: str = Field( + description="Name of the WorkspaceAccessStrategy" + ) + namespace: Optional[str] = Field( + default=None, + description="Namespace where the WorkspaceAccessStrategy is located" + ) + + +class TemplateRef(BaseModel): + """TemplateRef defines a reference to a WorkspaceTemplate""" + name: str = Field( + description="Name of the WorkspaceTemplate" + ) + namespace: Optional[str] = Field( + default=None, + description="Namespace where the WorkspaceTemplate is located" + ) + + +class IdleDetectionSpec(BaseModel): + """IdleDetectionSpec defines idle detection methods""" + http_get: Optional[Dict[str, Any]] = Field( + default=None, + alias="httpGet", + description="HTTPGet specifies the HTTP request to perform for idle detection" + ) + + +class IdleShutdownSpec(BaseModel): + """IdleShutdownSpec defines idle shutdown configuration""" + enabled: bool = Field( + description="Enabled indicates if idle shutdown is enabled" + ) + idle_timeout_in_minutes: int = Field( + alias="idleTimeoutInMinutes", + description="IdleTimeoutInMinutes specifies idle timeout in minutes", + ge=1 + ) + detection: IdleDetectionSpec = Field( + description="Detection specifies how to detect idle state" + ) + + +class StorageSpec(BaseModel): + """StorageSpec defines the storage configuration for Workspace""" + storage_class_name: Optional[str] = Field( + default=None, + alias="storageClassName", + description="StorageClassName specifies the storage class to use for persistent storage" + ) + size: Optional[str] = Field( + default="10Gi", + 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" + ) + mount_path: Optional[str] = Field( + default="/home", + alias="mountPath", + description="MountPath specifies where to mount the persistent volume in the container. Default is /home/jovyan (jovyan is the standard user in Jupyter images)" + ) + + +class ResourceRequirements(BaseModel): + """ResourceRequirements describes the compute resource requirements""" + requests: Optional[Dict[str, Optional[str]]] = Field( + default=None, + 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." + ) + limits: Optional[Dict[str, Optional[str]]] = Field( + default=None, + description="Limits describes the maximum amount of compute resources allowed." + ) + + +class SpaceConfig(BaseModel): + """SpaceConfig defines the desired state of a Space""" + model_config = ConfigDict(extra="forbid") + + name: str = Field( + description="Space name", + min_length=1, + max_length=63, + pattern=r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + ) + display_name: str = Field( + alias="display_name", + description="Display Name of the space", + min_length=1 + ) + namespace: str = Field( + default="default", + description="Kubernetes namespace", + min_length=1 + ) + image: Optional[str] = Field( + default=None, + description="Image specifies the container image to use" + ) + desired_status: Optional[DesiredStatus] = Field( + default=None, + alias="desired_status", + description="DesiredStatus specifies the desired operational status" + ) + ownership_type: Optional[OwnershipType] = Field( + default=None, + alias="ownership_type", + 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." + ) + resources: Optional[ResourceRequirements] = Field( + default=None, + description="Resources specifies the resource requirements" + ) + storage: Optional[StorageSpec] = Field( + default=None, + description="Storage specifies the storage configuration" + ) + volumes: Optional[List[VolumeSpec]] = Field( + default=None, + description="Volumes specifies additional volumes to mount from existing PersistentVolumeClaims" + ) + container_config: Optional[ContainerConfig] = Field( + default=None, + alias="container_config", + description="ContainerConfig specifies container command and args configuration" + ) + node_selector: Optional[Dict[str, str]] = Field( + default=None, + alias="node_selector", + description="NodeSelector specifies node selection constraints for the space pod (JSON string)" + ) + affinity: Optional[Dict[str, Any]] = Field( + default=None, + description="Affinity specifies node affinity and anti-affinity rules for the space pod (JSON string)" + ) + tolerations: Optional[List[Dict[str, Any]]] = Field( + default=None, + description="Tolerations specifies tolerations for the space pod to schedule on nodes with matching taints (JSON string)" + ) + lifecycle: Optional[Dict[str, Any]] = Field( + default=None, + description="Lifecycle specifies actions that the management system should take in response to container lifecycle events (JSON string)" + ) + template_ref: Optional[TemplateRef] = Field( + default=None, + alias="template_ref", + description="TemplateRef references a WorkspaceTemplate to use as base configuration. When set, template provides defaults and workspace spec fields act as overrides" + ) + idle_shutdown: Optional[IdleShutdownSpec] = Field( + default=None, + alias="idle_shutdown", + description="IdleShutdown specifies idle shutdown configuration" + ) + app_type: Optional[str] = Field( + default=None, + alias="app_type", + description="AppType specifies the application type for this workspace" + ) + service_account_name: Optional[str] = Field( + default=None, + alias="service_account_name", + description="ServiceAccountName specifies the name of the ServiceAccount to use for the workspace pod" + ) + access_type: Optional[OwnershipType] = Field( + default=None, + alias="access_type", + description="AccessType specifies who can connect to the workspace. 'Public' means anyone with RBAC permissions can connect. 'OwnerOnly' means only the creator can connect." + ) + env: Optional[List[Dict[str, Any]]] = Field( + default=None, + description="Environment variables for the workspace container (list of {name, value} objects)" + ) + access_strategy: Optional[AccessStrategyRef] = Field( + default=None, + alias="access_strategy", + description="AccessStrategy references a WorkspaceAccessStrategy to use" + ) + pod_security_context: Optional[Dict[str, Any]] = Field( + default=None, + alias="pod_security_context", + description="Pod-level security context. Overrides template defaults when specified (JSON string)" + ) + container_security_context: Optional[Dict[str, Any]] = Field( + default=None, + alias="container_security_context", + description="Container-level security context for the main workspace container. Overrides template defaults (JSON string)" + ) + init_containers: Optional[List[Dict[str, Any]]] = Field( + default=None, + alias="init_containers", + description="Init containers to run before the workspace container starts (JSON string, max 10)" + ) + queue_name: Optional[str] = Field( + default=None, + alias="queue_name", + description="Queue name for space scheduling", + min_length=1, + max_length=63, + pattern=r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + ) + priority: Optional[str] = Field( + default=None, + description="Priority class for space scheduling", + min_length=1 + ) + + @field_validator('volumes') + def validate_no_duplicate_volumes(cls, v): + """Validate no duplicate volume names or mount paths.""" + if not v: + return v + + # Check for duplicate volume names + names = [vol.name for vol in v] + if len(names) != len(set(names)): + raise ValueError("Duplicate volume names found") + + # Check for duplicate mount paths + mount_paths = [vol.mount_path for vol in v] + if len(mount_paths) != len(set(mount_paths)): + raise ValueError("Duplicate mount paths found") + + return v + + def to_domain(self) -> Dict: + """ + Convert flat config to domain model for space creation + """ + # Create the space spec + spec = { + "displayName": self.display_name + } + + # Add optional spec fields + if self.image is not None: + spec["image"] = self.image + if self.desired_status is not None: + spec["desiredStatus"] = self.desired_status.value + if self.ownership_type is not None: + spec["ownershipType"] = self.ownership_type.value + if self.resources is not None: + spec["resources"] = self.resources.model_dump(exclude_none=True) + if self.storage is not None: + spec["storage"] = self.storage.model_dump(exclude_none=True, by_alias=True) + if self.volumes is not None: + spec["volumes"] = [vol.model_dump(exclude_none=True, by_alias=True) for vol in self.volumes] + if self.container_config is not None: + spec["containerConfig"] = self.container_config.model_dump(exclude_none=True) + if self.node_selector is not None: + spec["nodeSelector"] = self.node_selector + if self.affinity is not None: + spec["affinity"] = self.affinity + if self.tolerations is not None: + spec["tolerations"] = self.tolerations + if self.lifecycle is not None: + spec["lifecycle"] = self.lifecycle + if self.template_ref is not None: + spec["templateRef"] = self.template_ref.model_dump(exclude_none=True, by_alias=True) + if self.idle_shutdown is not None: + spec["idleShutdown"] = self.idle_shutdown.model_dump(exclude_none=True, by_alias=True) + if self.app_type is not None: + spec["appType"] = self.app_type + if self.service_account_name is not None: + spec["serviceAccountName"] = self.service_account_name + if self.access_type is not None: + spec["accessType"] = self.access_type.value + if self.env is not None: + spec["env"] = self.env + if self.access_strategy is not None: + spec["accessStrategy"] = self.access_strategy.model_dump(exclude_none=True) + if self.pod_security_context is not None: + spec["podSecurityContext"] = self.pod_security_context + if self.container_security_context is not None: + spec["containerSecurityContext"] = self.container_security_context + if self.init_containers is not None: + spec["initContainers"] = self.init_containers + + # Create metadata + metadata = {"name": self.name} + if self.namespace is not None: + metadata["namespace"] = self.namespace + + # Add kueue labels for task governance + labels = {} + if self.queue_name is not None: + labels["kueue.x-k8s.io/queue-name"] = self.queue_name + if self.priority is not None: + labels["kueue.x-k8s.io/priority-class"] = self.priority + if labels: + metadata["labels"] = labels + + # Create the complete space configuration + space_config = { + "apiVersion": "workspace.jupyter.org/v1alpha1", + "kind": "Workspace", + "metadata": metadata, + "spec": spec + } + + return { + "name": self.name, + "namespace": self.namespace, + "space_spec": space_config + } diff --git a/hyperpod-space-template/hyperpod_space_template/v1_1/schema.json b/hyperpod-space-template/hyperpod_space_template/v1_1/schema.json new file mode 100644 index 00000000..479053c4 --- /dev/null +++ b/hyperpod-space-template/hyperpod_space_template/v1_1/schema.json @@ -0,0 +1,639 @@ +{ + "$defs": { + "AccessStrategyRef": { + "description": "AccessStrategyRef references a WorkspaceAccessStrategy", + "properties": { + "name": { + "description": "Name of the WorkspaceAccessStrategy", + "title": "Name", + "type": "string" + }, + "namespace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Namespace where the WorkspaceAccessStrategy is located", + "title": "Namespace" + } + }, + "required": [ + "name" + ], + "title": "AccessStrategyRef", + "type": "object" + }, + "ContainerConfig": { + "description": "ContainerConfig defines container command and args configuration", + "properties": { + "command": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Command specifies the container command", + "title": "Command" + }, + "args": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Args specifies the container arguments", + "title": "Args" + } + }, + "title": "ContainerConfig", + "type": "object" + }, + "DesiredStatus": { + "enum": [ + "Running", + "Stopped" + ], + "title": "DesiredStatus", + "type": "string" + }, + "IdleDetectionSpec": { + "description": "IdleDetectionSpec defines idle detection methods", + "properties": { + "httpGet": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "HTTPGet specifies the HTTP request to perform for idle detection", + "title": "Httpget" + } + }, + "title": "IdleDetectionSpec", + "type": "object" + }, + "IdleShutdownSpec": { + "description": "IdleShutdownSpec defines idle shutdown configuration", + "properties": { + "enabled": { + "description": "Enabled indicates if idle shutdown is enabled", + "title": "Enabled", + "type": "boolean" + }, + "idleTimeoutInMinutes": { + "description": "IdleTimeoutInMinutes specifies idle timeout in minutes", + "minimum": 1, + "title": "Idletimeoutinminutes", + "type": "integer" + }, + "detection": { + "$ref": "#/$defs/IdleDetectionSpec", + "description": "Detection specifies how to detect idle state" + } + }, + "required": [ + "enabled", + "idleTimeoutInMinutes", + "detection" + ], + "title": "IdleShutdownSpec", + "type": "object" + }, + "OwnershipType": { + "enum": [ + "Public", + "OwnerOnly" + ], + "title": "OwnershipType", + "type": "string" + }, + "ResourceRequirements": { + "description": "ResourceRequirements describes the compute resource requirements", + "properties": { + "requests": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "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.", + "title": "Requests" + }, + "limits": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Limits describes the maximum amount of compute resources allowed.", + "title": "Limits" + } + }, + "title": "ResourceRequirements", + "type": "object" + }, + "StorageSpec": { + "description": "StorageSpec defines the storage configuration for Workspace", + "properties": { + "storageClassName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "StorageClassName specifies the storage class to use for persistent storage", + "title": "Storageclassname" + }, + "size": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "10Gi", + "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", + "title": "Size" + }, + "mountPath": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "/home", + "description": "MountPath specifies where to mount the persistent volume in the container. Default is /home/jovyan (jovyan is the standard user in Jupyter images)", + "title": "Mountpath" + } + }, + "title": "StorageSpec", + "type": "object" + }, + "TemplateRef": { + "description": "TemplateRef defines a reference to a WorkspaceTemplate", + "properties": { + "name": { + "description": "Name of the WorkspaceTemplate", + "title": "Name", + "type": "string" + }, + "namespace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Namespace where the WorkspaceTemplate is located", + "title": "Namespace" + } + }, + "required": [ + "name" + ], + "title": "TemplateRef", + "type": "object" + }, + "VolumeSpec": { + "description": "VolumeSpec defines a volume to mount from an existing PVC", + "properties": { + "name": { + "description": "Name is a unique identifier for this volume within the pod (maps to pod.spec.volumes[].name)", + "minLength": 1, + "title": "Name", + "type": "string" + }, + "mountPath": { + "description": "MountPath is the path where the volume should be mounted (Unix-style path, e.g. /data)", + "minLength": 1, + "title": "Mountpath", + "type": "string" + }, + "persistentVolumeClaimName": { + "description": "PersistentVolumeClaimName is the name of the existing PVC to mount", + "minLength": 1, + "title": "Persistentvolumeclaimname", + "type": "string" + } + }, + "required": [ + "name", + "mountPath", + "persistentVolumeClaimName" + ], + "title": "VolumeSpec", + "type": "object" + } + }, + "additionalProperties": false, + "description": "SpaceConfig defines the desired state of a Space", + "properties": { + "name": { + "description": "Space name", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "title": "Name", + "type": "string" + }, + "display_name": { + "description": "Display Name of the space", + "minLength": 1, + "title": "Display Name", + "type": "string" + }, + "namespace": { + "default": "default", + "description": "Kubernetes namespace", + "minLength": 1, + "title": "Namespace", + "type": "string" + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Image specifies the container image to use", + "title": "Image" + }, + "desired_status": { + "anyOf": [ + { + "$ref": "#/$defs/DesiredStatus" + }, + { + "type": "null" + } + ], + "default": null, + "description": "DesiredStatus specifies the desired operational status" + }, + "ownership_type": { + "anyOf": [ + { + "$ref": "#/$defs/OwnershipType" + }, + { + "type": "null" + } + ], + "default": null, + "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." + }, + "resources": { + "anyOf": [ + { + "$ref": "#/$defs/ResourceRequirements" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Resources specifies the resource requirements" + }, + "storage": { + "anyOf": [ + { + "$ref": "#/$defs/StorageSpec" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Storage specifies the storage configuration" + }, + "volumes": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/VolumeSpec" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Volumes specifies additional volumes to mount from existing PersistentVolumeClaims", + "title": "Volumes" + }, + "container_config": { + "anyOf": [ + { + "$ref": "#/$defs/ContainerConfig" + }, + { + "type": "null" + } + ], + "default": null, + "description": "ContainerConfig specifies container command and args configuration" + }, + "node_selector": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "NodeSelector specifies node selection constraints for the space pod (JSON string)", + "title": "Node Selector" + }, + "affinity": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Affinity specifies node affinity and anti-affinity rules for the space pod (JSON string)", + "title": "Affinity" + }, + "tolerations": { + "anyOf": [ + { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Tolerations specifies tolerations for the space pod to schedule on nodes with matching taints (JSON string)", + "title": "Tolerations" + }, + "lifecycle": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Lifecycle specifies actions that the management system should take in response to container lifecycle events (JSON string)", + "title": "Lifecycle" + }, + "template_ref": { + "anyOf": [ + { + "$ref": "#/$defs/TemplateRef" + }, + { + "type": "null" + } + ], + "default": null, + "description": "TemplateRef references a WorkspaceTemplate to use as base configuration. When set, template provides defaults and workspace spec fields act as overrides" + }, + "idle_shutdown": { + "anyOf": [ + { + "$ref": "#/$defs/IdleShutdownSpec" + }, + { + "type": "null" + } + ], + "default": null, + "description": "IdleShutdown specifies idle shutdown configuration" + }, + "app_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AppType specifies the application type for this workspace", + "title": "App Type" + }, + "service_account_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "ServiceAccountName specifies the name of the ServiceAccount to use for the workspace pod", + "title": "Service Account Name" + }, + "access_type": { + "anyOf": [ + { + "$ref": "#/$defs/OwnershipType" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AccessType specifies who can connect to the workspace. 'Public' means anyone with RBAC permissions can connect. 'OwnerOnly' means only the creator can connect." + }, + "env": { + "anyOf": [ + { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Environment variables for the workspace container (list of {name, value} objects)", + "title": "Env" + }, + "access_strategy": { + "anyOf": [ + { + "$ref": "#/$defs/AccessStrategyRef" + }, + { + "type": "null" + } + ], + "default": null, + "description": "AccessStrategy references a WorkspaceAccessStrategy to use" + }, + "pod_security_context": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Pod-level security context. Overrides template defaults when specified (JSON string)", + "title": "Pod Security Context" + }, + "container_security_context": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Container-level security context for the main workspace container. Overrides template defaults (JSON string)", + "title": "Container Security Context" + }, + "init_containers": { + "anyOf": [ + { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Init containers to run before the workspace container starts (JSON string, max 10)", + "title": "Init Containers" + }, + "queue_name": { + "anyOf": [ + { + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Queue name for space scheduling", + "title": "Queue Name" + }, + "priority": { + "anyOf": [ + { + "minLength": 1, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Priority class for space scheduling", + "title": "Priority" + } + }, + "required": [ + "name", + "display_name" + ], + "title": "SpaceConfig", + "type": "object" +} \ No newline at end of file diff --git a/hyperpod-space-template/pyproject.toml b/hyperpod-space-template/pyproject.toml index adaab3a8..345c7ccc 100644 --- a/hyperpod-space-template/pyproject.toml +++ b/hyperpod-space-template/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hyperpod-space-template" -version = "1.0.0" +version = "1.1.0" description = "Template for HyperPod Space configuration" authors = [ {name = "Amazon Web Services"}, @@ -24,3 +24,4 @@ include = ["hyperpod_space_template*"] [tool.setuptools.package-data] "hyperpod_space_template.v1_0" = ["schema.json"] +"hyperpod_space_template.v1_1" = ["schema.json"] diff --git a/hyperpod-space-template/update_schema.py b/hyperpod-space-template/update_schema.py index 85a789db..a8b49d38 100644 --- a/hyperpod-space-template/update_schema.py +++ b/hyperpod-space-template/update_schema.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 import json -from hyperpod_space_template.v1_0.model import SpaceConfig +from hyperpod_space_template.v1_1.model import SpaceConfig schema = SpaceConfig.model_json_schema() -with open('hyperpod_space_template/v1_0/schema.json', 'w') as f: +with open('hyperpod_space_template/v1_1/schema.json', 'w') as f: json.dump(schema, f, indent=2) print('✅ Schema updated!') diff --git a/pyproject.toml b/pyproject.toml index 4724d3e9..67ed147a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] dynamic = ["dependencies"] name = "sagemaker-hyperpod" -version = "3.8.0" +version = "3.9.0" description = "Amazon SageMaker HyperPod SDK and CLI" readme = "README.md" requires-python = ">=3.8" diff --git a/setup.py b/setup.py index 88a98508..4f452095 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ setup( data_files=sagemaker_hyperpod_recipes, name="sagemaker-hyperpod", - version="3.8.0", + version="3.9.0", description="Amazon SageMaker HyperPod SDK and CLI", long_description=open("README.md").read(), long_description_content_type="text/markdown", @@ -62,9 +62,9 @@ "awscli-cwlogs>=1.4.6", "boto3>=1.35.3,<2.0", "botocore>=1.35.6 ", - "kubernetes>=33.1.0", + "kubernetes>=33.1.0,!=36.0.0", "kr8s>=0.20.0", - "pyyaml==6.0.2", + "pyyaml>=6.0.2", "ratelimit==2.2.1", "tabulate==0.9.0", "itables>=2.2.2", diff --git a/src/sagemaker/hyperpod/cli/commands/ray_dashboard_connection.py b/src/sagemaker/hyperpod/cli/commands/ray_dashboard_connection.py new file mode 100644 index 00000000..6795dc8c --- /dev/null +++ b/src/sagemaker/hyperpod/cli/commands/ray_dashboard_connection.py @@ -0,0 +1,89 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import click +from kubernetes import client, config +from kubernetes.client.rest import ApiException + +from sagemaker.hyperpod.cli.constants.ray_dashboard_connection_constants import ( + RAY_DASHBOARD_CONNECTION_GROUP, + RAY_DASHBOARD_CONNECTION_VERSION, + RAY_DASHBOARD_CONNECTION_PLURAL, +) +from sagemaker.hyperpod.common.telemetry.telemetry_logging import ( + _hyperpod_telemetry_emitter, +) +from sagemaker.hyperpod.common.telemetry.constants import Feature +from sagemaker.hyperpod.common.cli_decorators import handle_cli_exceptions + + +def _load_kube_config(): + """Load kubeconfig so the default ApiClient is authenticated. + + Uses the library's default client (as HPSpace does) rather than copying + the token out of Configuration.api_key. The api_key entry is named + differently across kubernetes-client releases ("authorization" in 36.0.0, + "BearerToken" in 36.0.3+), so reading it by name is version-fragile. + """ + config.load_kube_config() + + +@click.command("ray-dashboard-connection") +@click.option("--cluster-name", required=True, help="Name of the RayCluster") +@click.option("--namespace", "-n", required=False, default="default", help="Namespace of the RayCluster") +@_hyperpod_telemetry_emitter(Feature.HYPERPOD_CLI, "create_ray_dashboard_connection") +@handle_cli_exceptions() +def create_ray_dashboard_connection(cluster_name, namespace): + """Create a RayDashboardConnection to get a dashboard URL for a RayCluster.""" + _load_kube_config() + + body = { + "apiVersion": f"{RAY_DASHBOARD_CONNECTION_GROUP}/{RAY_DASHBOARD_CONNECTION_VERSION}", + "kind": "RayDashboardConnection", + "metadata": { + "namespace": namespace, + }, + "spec": { + "clusterName": cluster_name, + }, + } + + api = client.CustomObjectsApi() + + try: + result = api.create_namespaced_custom_object( + group=RAY_DASHBOARD_CONNECTION_GROUP, + version=RAY_DASHBOARD_CONNECTION_VERSION, + namespace=namespace, + plural=RAY_DASHBOARD_CONNECTION_PLURAL, + body=body, + ) + except ApiException as e: + if e.status == 404: + body_str = e.body or "" + if "raydashboardconnections" in body_str.lower() or RAY_DASHBOARD_CONNECTION_GROUP in body_str: + raise click.ClickException( + "The RayDashboardConnection API is not available on this cluster.\n" + "Please install the hyperpod-ray-endpoint-operator Helm chart.\n" + ) + raise click.ClickException(f"Not found: {body_str}") + raise + + connection_url = result.get("status", {}).get("connectionUrl", "") + if connection_url: + click.echo(connection_url) + else: + raise click.ClickException( + f"Failed to get dashboard URL for RayCluster '{cluster_name}' in namespace '{namespace}'.\n" + "Please contact your cluster administrator." + ) diff --git a/src/sagemaker/hyperpod/cli/commands/space.py b/src/sagemaker/hyperpod/cli/commands/space.py index 7ef29a0f..7c9a1fc8 100644 --- a/src/sagemaker/hyperpod/cli/commands/space.py +++ b/src/sagemaker/hyperpod/cli/commands/space.py @@ -6,7 +6,6 @@ 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 ( _hyperpod_telemetry_emitter, ) @@ -25,6 +24,7 @@ @handle_cli_exceptions() def space_create(version, debug, config): """Create a space resource.""" + SpaceConfig = SCHEMA_REGISTRY[version] space_config = SpaceConfig(**config) space = HPSpace(config=space_config) space.create(debug=debug) @@ -125,6 +125,7 @@ def space_delete(name, namespace): schema_pkg="hyperpod_space_template", registry=SCHEMA_REGISTRY, is_update=True, + version_key="1.1", ) @_hyperpod_telemetry_emitter(Feature.HYPERPOD_CLI, "update_space") @handle_cli_exceptions() diff --git a/src/sagemaker/hyperpod/cli/constants/ray_dashboard_connection_constants.py b/src/sagemaker/hyperpod/cli/constants/ray_dashboard_connection_constants.py new file mode 100644 index 00000000..2524785b --- /dev/null +++ b/src/sagemaker/hyperpod/cli/constants/ray_dashboard_connection_constants.py @@ -0,0 +1,16 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +RAY_DASHBOARD_CONNECTION_GROUP = "connection.access.sagemaker.amazonaws.com" +RAY_DASHBOARD_CONNECTION_VERSION = "v1alpha1" +RAY_DASHBOARD_CONNECTION_PLURAL = "raydashboardconnections" diff --git a/src/sagemaker/hyperpod/cli/hyp_cli.py b/src/sagemaker/hyperpod/cli/hyp_cli.py index 4b2107c0..8de323cf 100644 --- a/src/sagemaker/hyperpod/cli/hyp_cli.py +++ b/src/sagemaker/hyperpod/cli/hyp_cli.py @@ -54,6 +54,7 @@ space_template_update, ) from sagemaker.hyperpod.cli.commands.space_access import space_access_create +from sagemaker.hyperpod.cli.commands.ray_dashboard_connection import create_ray_dashboard_connection from sagemaker.hyperpod.cli.commands.init import ( init, @@ -216,6 +217,7 @@ def exec(): create.add_command(space_create) create.add_command(space_template_create) create.add_command(space_access_create) +create.add_command(create_ray_dashboard_connection) list.add_command(list_jobs) recipe_list_cmd = copy.copy(list_jobs) diff --git a/src/sagemaker/hyperpod/cli/recipe_param_order.py b/src/sagemaker/hyperpod/cli/recipe_param_order.py index 0eec314a..aed682ed 100644 --- a/src/sagemaker/hyperpod/cli/recipe_param_order.py +++ b/src/sagemaker/hyperpod/cli/recipe_param_order.py @@ -49,7 +49,7 @@ ("max_steps", "Core Hyperparameters"), # Advanced Hyperparameters (includes technique-specific params) - ("lr_warmup_ratio", "Advanced Hyperparameters"), + ("lr_warmup_steps_ratio", "Advanced Hyperparameters"), ("max_context_length", "Advanced Hyperparameters"), ("max_prompt_length", "Advanced Hyperparameters"), ("max_length", "Advanced Hyperparameters"), diff --git a/src/sagemaker/hyperpod/cli/recipe_utils.py b/src/sagemaker/hyperpod/cli/recipe_utils.py index 8306d34f..63aa10c8 100644 --- a/src/sagemaker/hyperpod/cli/recipe_utils.py +++ b/src/sagemaker/hyperpod/cli/recipe_utils.py @@ -6,7 +6,7 @@ import click import boto3 import sys -from jinja2 import Template +from jinja2.sandbox import SandboxedEnvironment from kubernetes import client, config from pathlib import Path from typing import Dict, Any, Tuple, Optional @@ -320,7 +320,8 @@ def _submit_k8s_resources(custom_api, rendered_yaml: str) -> None: def _render_k8s_template(template_content: str, config_data: Dict[str, Any]) -> str: """Render Kubernetes template with configuration data.""" - template = Template(template_content) + env = SandboxedEnvironment() + template = env.from_string(template_content) return template.render(**config_data) diff --git a/src/sagemaker/hyperpod/cli/space_utils.py b/src/sagemaker/hyperpod/cli/space_utils.py index cec5249e..66d67282 100644 --- a/src/sagemaker/hyperpod/cli/space_utils.py +++ b/src/sagemaker/hyperpod/cli/space_utils.py @@ -38,7 +38,7 @@ def generate_click_command( raise ValueError("You must pass a registry mapping version→Model") # get schema defaults for manually handled options - schema = load_schema_for_version(version_key or "1.0", schema_pkg) + schema = load_schema_for_version(version_key or "1.1", schema_pkg) props = schema.get("properties", {}) def decorator(func: Callable) -> Callable: @@ -169,6 +169,22 @@ def _parse_template_ref(ctx, param, value): except Exception as e: raise click.UsageError(f"Error parsing template ref: {str(e)}") + def _parse_access_strategy(ctx, param, value): + """Parse access strategy from command line format to dictionary format.""" + if not value: + return None + + try: + parts = {} + for item in value.split(','): + if '=' not in item: + raise click.UsageError(f"Invalid access-strategy format: '{item}' should be key=value") + key, val = item.split('=', 1) + parts[key.strip()] = val.strip() + return parts + except Exception as e: + raise click.UsageError(f"Error parsing access-strategy: {str(e)}") + def _parse_idle_shutdown_param(ctx, param, value): """Parse idle shutdown parameters from command line format to dictionary format.""" if not value: @@ -199,7 +215,7 @@ def _parse_idle_shutdown_param(ctx, param, value): # 1) the wrapper click will call def wrapped_func(*args, **kwargs): - version = version_key or kwargs.pop("version", "1.0") + version = version_key or kwargs.pop("version", "1.1") debug = kwargs.pop("debug", False) Model = registry.get(version) @@ -235,6 +251,10 @@ def wrapped_func(*args, **kwargs): if template_ref is not None: kwargs["template_ref"] = template_ref + access_strategy = kwargs.pop("access_strategy", None) + if access_strategy is not None: + kwargs["access_strategy"] = access_strategy + idle_shutdown = kwargs.pop("idle_shutdown", None) if idle_shutdown is not None: kwargs["idle_shutdown"] = idle_shutdown @@ -382,6 +402,12 @@ def wrapped_func(*args, **kwargs): help="TemplateRef references a WorkspaceTemplate to use as base configuration. Format: --template-ref name=,namespace=", )(wrapped_func) + wrapped_func = click.option( + "--access-strategy", + callback=_parse_access_strategy, + help="AccessStrategy references a WorkspaceAccessStrategy. Format: --access-strategy name=,namespace=", + )(wrapped_func) + wrapped_func = click.option( "--idle-shutdown", callback=_parse_idle_shutdown_param, @@ -397,6 +423,7 @@ def wrapped_func(*args, **kwargs): "storage", "container_config", "template_ref", + "access_strategy", "idle_shutdown", "debug", # Exclude debug from validation ] @@ -442,7 +469,7 @@ def wrapped_func(*args, **kwargs): if version_key is None: wrapped_func = click.option( "--version", - default="1.0", + default="1.1", help="Schema version to use", )(wrapped_func) diff --git a/src/sagemaker/hyperpod/space/__init__.py b/src/sagemaker/hyperpod/space/__init__.py index b1c18285..d5cf8918 100644 --- a/src/sagemaker/hyperpod/space/__init__.py +++ b/src/sagemaker/hyperpod/space/__init__.py @@ -13,7 +13,7 @@ from sagemaker.hyperpod.space.hyperpod_space import HPSpace from sagemaker.hyperpod.space.hyperpod_space_template import HPSpaceTemplate -from hyperpod_space_template.v1_0.model import SpaceConfig +from hyperpod_space_template.v1_1.model import SpaceConfig __all__ = [ "HPSpace", diff --git a/src/sagemaker/hyperpod/space/hyperpod_space.py b/src/sagemaker/hyperpod/space/hyperpod_space.py index c19ccb4f..dde963e0 100644 --- a/src/sagemaker/hyperpod/space/hyperpod_space.py +++ b/src/sagemaker/hyperpod/space/hyperpod_space.py @@ -3,14 +3,17 @@ import yaml import boto3 from sagemaker.hyperpod.common.utils import create_boto3_client -from typing import List, Optional, ClassVar, Dict, Set, Any +from typing import List, Optional, ClassVar, Dict, Set, Any, Union from pydantic import BaseModel, Field, ConfigDict, model_validator from kubernetes import client, config from kubernetes.client.rest import ApiException from kr8s.objects import Pod from sagemaker.hyperpod.common.config.metadata import Metadata -from hyperpod_space_template.v1_0.model import ResourceRequirements +from hyperpod_space_template.v1_0.model import SpaceConfig as SpaceConfigV1_0 +from hyperpod_space_template.v1_1.model import SpaceConfig as SpaceConfigV1_1, ResourceRequirements + +SpaceConfig = Union[SpaceConfigV1_0, SpaceConfigV1_1] from sagemaker.hyperpod.common.utils import ( handle_exception, get_default_namespace, @@ -21,6 +24,7 @@ map_kubernetes_response_to_model, validate_space_mig_resources, validate_mig_profile_in_cluster, + warn_if_addon_version_incompatible, ) from sagemaker.hyperpod.common.telemetry.telemetry_logging import ( _hyperpod_telemetry_emitter, @@ -37,7 +41,6 @@ SPACE_ACCESS_VERSION, SPACE_ACCESS_PLURAL, ) -from hyperpod_space_template.v1_0.model import SpaceConfig, ResourceRequirements class HPSpace(BaseModel): @@ -312,6 +315,7 @@ def _validate_and_extract_mig_profiles(self, resources: Optional[ResourceRequire return mig_profiles @_hyperpod_telemetry_emitter(Feature.HYPERPOD, "create_space") + @warn_if_addon_version_incompatible def create(self, debug: bool = False): """Create and submit the HyperPod Space to the Kubernetes cluster. @@ -454,8 +458,8 @@ def list(cls, namespace: Optional[str] = None) -> List["HPSpace"]: created_by = item.get('metadata', {}).get('annotations', {}).get('workspace.jupyter.org/created-by') ownership_type = item.get('spec', {}).get('ownershipType', '') if created_by == caller_arn or ownership_type == "Public": - config_data = map_kubernetes_response_to_model(item, SpaceConfig) - space_config = SpaceConfig(**config_data) + config_data = map_kubernetes_response_to_model(item, SpaceConfigV1_1) + space_config = SpaceConfigV1_1(**config_data) space = cls( config=space_config, @@ -535,9 +539,9 @@ def get(cls, name: str, namespace: str = None) -> "HPSpace": ) # Use dynamic mapping based on SpaceConfig model - config_data = map_kubernetes_response_to_model(response, SpaceConfig) + config_data = map_kubernetes_response_to_model(response, SpaceConfigV1_1) - space_config = SpaceConfig(**config_data) + space_config = SpaceConfigV1_1(**config_data) return cls( config=space_config, @@ -586,12 +590,15 @@ def delete(self): handle_exception(e, self.config.name, self.config.namespace) @_hyperpod_telemetry_emitter(Feature.HYPERPOD, "update_space") + @warn_if_addon_version_incompatible def update(self, **kwargs): """Update the HyperPod Space configuration. Updates the space configuration with the provided parameters. Validates MIG profiles if resource updates are requested and ensures compatibility - with the current node instance type. + with the current node instance type. The configuration is always + reconstructed using the latest schema version (v1.1), preserving all + existing fields on the space. **Parameters:** @@ -657,8 +664,12 @@ def update(self, **kwargs): # Update space config with the input config current_config = self.config.model_dump(by_alias=True) + # Convert any Pydantic model instances in kwargs to dicts for compatibility + for key, value in kwargs.items(): + if isinstance(value, BaseModel): + kwargs[key] = value.model_dump(exclude_none=True) current_config.update(kwargs) - self.config = SpaceConfig(**current_config) + self.config = SpaceConfigV1_1(**current_config) # Convert to domain model and extract spec domain_config = self.config.to_domain() diff --git a/src/sagemaker/hyperpod/space/utils.py b/src/sagemaker/hyperpod/space/utils.py index 5c5ec8d3..0eec965b 100644 --- a/src/sagemaker/hyperpod/space/utils.py +++ b/src/sagemaker/hyperpod/space/utils.py @@ -1,11 +1,66 @@ """Utility functions for space operations.""" +import logging import os import re +import sys +import warnings +from functools import wraps from typing import Dict, Any, Set, List, Tuple, Optional from pydantic import BaseModel from kubernetes import client from sagemaker.hyperpod.training.constants import VALIDATE_PROFILE_IN_CLUSTER +from sagemaker.hyperpod.cli.utils import get_eks_cluster_name, get_hyperpod_cluster_region +from sagemaker.hyperpod.common.utils import create_boto3_client + +logger = logging.getLogger(__name__) + +SPACES_ADDON_NAME = "amazon-sagemaker-spaces" + + +def _parse_version(version_str: str) -> Tuple[int, ...]: + """Parse a version string like '0.1.6' into a comparable tuple.""" + return tuple(int(x) for x in version_str.split(".")) + + +def get_spaces_addon_version(eks_cluster_name: str) -> Optional[str]: + """Get the installed version of the amazon-sagemaker-spaces addon. + + Returns the version string (e.g., "0.1.1") or None if it cannot be determined. + """ + try: + region = get_hyperpod_cluster_region() + response = create_boto3_client("eks", region_name=region).describe_addon( + clusterName=eks_cluster_name, addonName=SPACES_ADDON_NAME, + ) + raw_version = response["addon"]["addonVersion"] + match = re.match(r"v?(\d+\.\d+\.\d+)", raw_version) + return match.group(1) if match else None + except Exception as e: + logger.debug(f"Could not determine spaces addon version: {e}") + return None + + +def warn_if_addon_version_incompatible(func): + """Decorator that warns if the space template requires a newer addon version than installed.""" + @wraps(func) + def wrapper(self, *args, **kwargs): + try: + config_module = sys.modules[type(self.config).__module__] + min_version = getattr(config_module, 'MIN_ADDON_VERSION') + + eks_cluster_name = get_eks_cluster_name() + addon_version = get_spaces_addon_version(eks_cluster_name) + if addon_version and _parse_version(addon_version) < _parse_version(min_version): + warnings.warn( + f"The installed '{SPACES_ADDON_NAME}' addon version is {addon_version}, " + f"but this operation requires version >= {min_version}. " + f"Some space parameters may be ignored or rejected by the addon.", + ) + except Exception as e: + logger.debug(f"Addon version check skipped: {e}") + return func(self, *args, **kwargs) + return wrapper def camel_to_snake(name: str) -> str: diff --git a/test/integration_tests/init/test_recipe_job_creation.py b/test/integration_tests/init/test_recipe_job_creation.py index 5e6743be..a0f89b27 100644 --- a/test/integration_tests/init/test_recipe_job_creation.py +++ b/test/integration_tests/init/test_recipe_job_creation.py @@ -97,7 +97,7 @@ def test_configure_recipe_job(runner, job_name, test_directory): "--data-path", "/data/recipes-data/sft/zc_train_256.jsonl", "--global-batch-size", "8", "--learning-rate", "0.0001", - "--lr-warmup-ratio", "0.1", + "--lr-warmup-steps-ratio", "0.1", "--max-epochs", "20", "--output-path", "/data/output/qwen3-sft", "--results-directory", "/data/results/qwen3-sft", diff --git a/test/integration_tests/space/cli/test_cli_space.py b/test/integration_tests/space/cli/test_cli_space.py index e912668f..41080061 100644 --- a/test/integration_tests/space/cli/test_cli_space.py +++ b/test/integration_tests/space/cli/test_cli_space.py @@ -71,6 +71,7 @@ def _test_http_endpoint(self, port, timeout=30): def test_space_create(self, runner, space_name): """Test creating a space via CLI.""" result = runner.invoke(space_create, [ + "--version", VERSION, "--name", space_name, "--display-name", DISPLAY_NAME, "--namespace", NAMESPACE, diff --git a/test/integration_tests/space/cli/test_cli_space_v1_1.py b/test/integration_tests/space/cli/test_cli_space_v1_1.py new file mode 100644 index 00000000..62d0aa4d --- /dev/null +++ b/test/integration_tests/space/cli/test_cli_space_v1_1.py @@ -0,0 +1,101 @@ +import json +import time +import pytest +from click.testing import CliRunner +from sagemaker.hyperpod.cli.commands.space import ( + space_create, space_describe, space_delete, space_update, +) +from sagemaker.hyperpod.space.hyperpod_space import HPSpace +from test.integration_tests.utils import get_time_str + +# --------- Test Configuration --------- +NAMESPACE = "default" +SPACE_NAME = "space-cli-v1-1-integ-" + get_time_str() +DISPLAY_NAME = f"Space CLI V1.1 Integ Test {get_time_str()}" + + +@pytest.fixture(scope="module") +def runner(): + return CliRunner() + + +@pytest.fixture(scope="module") +def space_name(): + return SPACE_NAME + + +class TestSpaceCLIv1_1: + """Integration tests for HyperPod Space CLI v1.1 options.""" + + @pytest.mark.dependency(name="create_v1_1_cli") + def test_space_create_with_v1_1_options(self, runner, space_name): + """Test creating a space with v1.1 CLI options.""" + result = runner.invoke(space_create, [ + "--name", space_name, + "--display-name", DISPLAY_NAME, + "--namespace", NAMESPACE, + "--queue-name", "default-queue", + "--priority", "high-priority", + "--access-type", "Public", + "--env", json.dumps([{"name": "MY_VAR", "value": "my_value"}]), + "--pod-security-context", json.dumps({"runAsUser": 1000}), + "--container-security-context", json.dumps({"allowPrivilegeEscalation": False}), + ]) + assert result.exit_code == 0, f"Failed: {result.output}" + assert f"Space '{space_name}' created successfully" in result.output + + @pytest.mark.dependency(depends=["create_v1_1_cli"]) + def test_describe_shows_v1_1_fields(self, runner, space_name): + """Test that describe output contains v1.1 fields.""" + result = runner.invoke(space_describe, [ + "--name", space_name, + "--namespace", NAMESPACE, + "--output", "json", + ]) + assert result.exit_code == 0, result.output + data = json.loads(result.output) + + # Verify kueue labels + labels = data.get("metadata", {}).get("labels", {}) + assert labels.get("kueue.x-k8s.io/queue-name") == "default-queue" + assert labels.get("kueue.x-k8s.io/priority-class") == "high-priority" + + # Verify spec fields + spec = data.get("spec", {}) + assert spec.get("accessType") == "Public" + assert spec.get("env") == [{"name": "MY_VAR", "value": "my_value"}] + assert spec.get("podSecurityContext") == {"runAsUser": 1000} + assert spec.get("containerSecurityContext") == {"allowPrivilegeEscalation": False} + + @pytest.mark.dependency(depends=["create_v1_1_cli"]) + def test_update_env_via_cli(self, runner, space_name): + """Test updating env via CLI on a v1.1 space.""" + new_env = json.dumps([{"name": "MY_VAR", "value": "updated"}, {"name": "EXTRA", "value": "val"}]) + result = runner.invoke(space_update, [ + "--name", space_name, + "--namespace", NAMESPACE, + "--env", new_env, + ]) + assert result.exit_code == 0, f"Failed: {result.output}" + assert f"Space '{space_name}' updated successfully" in result.output + + # Verify update persisted + result = runner.invoke(space_describe, [ + "--name", space_name, + "--namespace", NAMESPACE, + "--output", "json", + ]) + data = json.loads(result.output) + env = data.get("spec", {}).get("env", []) + assert {"name": "MY_VAR", "value": "updated"} in env + assert {"name": "EXTRA", "value": "val"} in env + + @pytest.mark.dependency(depends=["create_v1_1_cli"]) + def test_delete_v1_1_space(self, runner, space_name): + """Test deleting the v1.1 space.""" + result = runner.invoke(space_delete, [ + "--name", space_name, + "--namespace", NAMESPACE, + ]) + assert result.exit_code == 0, result.output + assert f"Requested deletion for Space '{space_name}'" in result.output diff --git a/test/integration_tests/space/sdk/test_sdk_space_v1_1.py b/test/integration_tests/space/sdk/test_sdk_space_v1_1.py new file mode 100644 index 00000000..9f96d05f --- /dev/null +++ b/test/integration_tests/space/sdk/test_sdk_space_v1_1.py @@ -0,0 +1,94 @@ +import time +import pytest +from sagemaker.hyperpod.space.hyperpod_space import HPSpace +from hyperpod_space_template.v1_1.model import SpaceConfig, ResourceRequirements, AccessStrategyRef +from test.integration_tests.utils import get_time_str + +# --------- Config --------- +NAMESPACE = "default" +SPACE_NAME = "space-sdk-v1-1-integ-" + get_time_str() +DISPLAY_NAME = f"Space SDK V1.1 Integration Test {get_time_str()}" + +TIMEOUT_MINUTES = 2 +POLL_INTERVAL_SECONDS = 13 + + +@pytest.fixture(scope="module") +def space_config(): + """Create a v1.1 space configuration with new fields.""" + return SpaceConfig( + name=SPACE_NAME, + display_name=DISPLAY_NAME, + namespace=NAMESPACE, + queue_name="default-queue", + priority="high-priority", + env=[{"name": "TEST_VAR", "value": "test_value"}], + access_type="Public", + pod_security_context={"runAsUser": 1000}, + container_security_context={"allowPrivilegeEscalation": False}, + ) + + +@pytest.fixture(scope="module") +def space_obj(space_config): + """Create an HPSpace instance for testing.""" + return HPSpace(config=space_config) + + +@pytest.mark.dependency(name="create_v1_1") +def test_create_space(space_obj): + """Test creating a space with v1.1 fields.""" + space_obj.create() + assert space_obj.config.name == SPACE_NAME + + +@pytest.mark.dependency(depends=["create_v1_1"]) +def test_get_space_has_v1_1_fields(): + """Test that v1.1 fields are persisted and retrievable.""" + space = HPSpace.get(name=SPACE_NAME, namespace=NAMESPACE) + assert space.config.name == SPACE_NAME + assert space.config.display_name == DISPLAY_NAME + + # Verify kueue labels in raw resource + labels = space.raw_resource.get("metadata", {}).get("labels", {}) + assert labels.get("kueue.x-k8s.io/queue-name") == "default-queue" + assert labels.get("kueue.x-k8s.io/priority-class") == "high-priority" + + # Verify spec fields + spec = space.raw_resource.get("spec", {}) + assert spec.get("accessType") == "Public" + assert spec.get("env") == [{"name": "TEST_VAR", "value": "test_value"}] + assert spec.get("podSecurityContext") == {"runAsUser": 1000} + assert spec.get("containerSecurityContext") == {"allowPrivilegeEscalation": False} + + +@pytest.mark.dependency(depends=["create_v1_1"]) +def test_list_includes_v1_1_space(): + """Test that listing spaces includes the v1.1 space.""" + spaces = HPSpace.list(namespace=NAMESPACE) + names = [s.config.name for s in spaces] + assert SPACE_NAME in names + + +@pytest.mark.dependency(depends=["create_v1_1"]) +def test_update_env(): + """Test updating env field on a v1.1 space.""" + space = HPSpace.get(name=SPACE_NAME, namespace=NAMESPACE) + space.update(env=[{"name": "TEST_VAR", "value": "updated"}, {"name": "NEW_VAR", "value": "new"}]) + + updated = HPSpace.get(name=SPACE_NAME, namespace=NAMESPACE) + spec = updated.raw_resource.get("spec", {}) + assert {"name": "TEST_VAR", "value": "updated"} in spec.get("env", []) + assert {"name": "NEW_VAR", "value": "new"} in spec.get("env", []) + + +@pytest.mark.dependency(depends=["create_v1_1"]) +def test_delete_space(): + """Test deleting the v1.1 space.""" + space = HPSpace.get(name=SPACE_NAME, namespace=NAMESPACE) + space.delete() + + time.sleep(60) + spaces = HPSpace.list(namespace=NAMESPACE) + names = [s.config.name for s in spaces] + assert SPACE_NAME not in names diff --git a/test/unit_tests/cli/test_ray_dashboard_connection.py b/test/unit_tests/cli/test_ray_dashboard_connection.py new file mode 100644 index 00000000..4b8fc8f7 --- /dev/null +++ b/test/unit_tests/cli/test_ray_dashboard_connection.py @@ -0,0 +1,182 @@ +import pytest +from click.testing import CliRunner +from unittest.mock import Mock, patch, MagicMock + +from kubernetes.client.rest import ApiException + +from sagemaker.hyperpod.cli.commands.ray_dashboard_connection import create_ray_dashboard_connection + + +class TestRayDashboardConnectionCommand: + """Test cases for ray-dashboard-connection command""" + + def setup_method(self): + self.runner = CliRunner() + + @patch('sagemaker.hyperpod.cli.commands.ray_dashboard_connection._load_kube_config') + @patch('sagemaker.hyperpod.cli.commands.ray_dashboard_connection.client.CustomObjectsApi') + def test_create_success_returns_url(self, mock_custom_objects_api_class, mock_load_config): + """Test successful creation returns the connection URL""" + mock_api = Mock() + mock_api.create_namespaced_custom_object.return_value = { + "status": { + "connectionUrl": "https://my-cluster.spaces.example.com/bearer-auth?token=abc123" + } + } + mock_custom_objects_api_class.return_value = mock_api + + result = self.runner.invoke(create_ray_dashboard_connection, [ + '--cluster-name', 'my-raycluster', + '--namespace', 'team-a', + ]) + + assert result.exit_code == 0 + assert "https://my-cluster.spaces.example.com/bearer-auth?token=abc123" in result.output + mock_api.create_namespaced_custom_object.assert_called_once_with( + group="connection.access.sagemaker.amazonaws.com", + version="v1alpha1", + namespace="team-a", + plural="raydashboardconnections", + body={ + "apiVersion": "connection.access.sagemaker.amazonaws.com/v1alpha1", + "kind": "RayDashboardConnection", + "metadata": {"namespace": "team-a"}, + "spec": {"clusterName": "my-raycluster"}, + }, + ) + + @patch('sagemaker.hyperpod.cli.commands.ray_dashboard_connection._load_kube_config') + @patch('sagemaker.hyperpod.cli.commands.ray_dashboard_connection.client.CustomObjectsApi') + def test_create_default_namespace(self, mock_custom_objects_api_class, mock_load_config): + """Test namespace defaults to 'default' when not specified""" + mock_api = Mock() + mock_api.create_namespaced_custom_object.return_value = { + "status": {"connectionUrl": "https://example.com/dashboard"} + } + mock_custom_objects_api_class.return_value = mock_api + + result = self.runner.invoke(create_ray_dashboard_connection, [ + '--cluster-name', 'my-raycluster', + ]) + + assert result.exit_code == 0 + assert "https://example.com/dashboard" in result.output + call_kwargs = mock_api.create_namespaced_custom_object.call_args[1] + assert call_kwargs["namespace"] == "default" + + @patch('sagemaker.hyperpod.cli.commands.ray_dashboard_connection._load_kube_config') + @patch('sagemaker.hyperpod.cli.commands.ray_dashboard_connection.client.CustomObjectsApi') + def test_create_empty_url_raises_error(self, mock_custom_objects_api_class, mock_load_config): + """Test that empty connectionUrl raises an error""" + mock_api = Mock() + mock_api.create_namespaced_custom_object.return_value = { + "status": {"connectionUrl": ""} + } + mock_custom_objects_api_class.return_value = mock_api + + result = self.runner.invoke(create_ray_dashboard_connection, [ + '--cluster-name', 'my-raycluster', + '--namespace', 'default', + ]) + + assert result.exit_code != 0 + assert "Failed to get dashboard URL" in result.output + assert "contact your cluster administrator" in result.output + + @patch('sagemaker.hyperpod.cli.commands.ray_dashboard_connection._load_kube_config') + @patch('sagemaker.hyperpod.cli.commands.ray_dashboard_connection.client.CustomObjectsApi') + def test_create_no_status_raises_error(self, mock_custom_objects_api_class, mock_load_config): + """Test that missing status raises an error""" + mock_api = Mock() + mock_api.create_namespaced_custom_object.return_value = { + "metadata": {"name": "generated-name"} + } + mock_custom_objects_api_class.return_value = mock_api + + result = self.runner.invoke(create_ray_dashboard_connection, [ + '--cluster-name', 'my-raycluster', + ]) + + assert result.exit_code != 0 + assert "Failed to get dashboard URL" in result.output + + @patch('sagemaker.hyperpod.cli.commands.ray_dashboard_connection._load_kube_config') + @patch('sagemaker.hyperpod.cli.commands.ray_dashboard_connection.client.CustomObjectsApi') + def test_create_404_api_not_installed(self, mock_custom_objects_api_class, mock_load_config): + """Test 404 when operator is not installed shows install instructions""" + mock_api = Mock() + mock_api.create_namespaced_custom_object.side_effect = ApiException( + status=404, + reason="Not Found", + http_resp=Mock( + status=404, + reason="Not Found", + data=b'{"message":"the server could not find the requested resource","details":{"group":"connection.access.sagemaker.amazonaws.com","kind":"raydashboardconnections"}}' + ), + ) + mock_api.create_namespaced_custom_object.side_effect.body = ( + '{"message":"the server could not find the requested resource",' + '"details":{"group":"connection.access.sagemaker.amazonaws.com","kind":"raydashboardconnections"}}' + ) + mock_custom_objects_api_class.return_value = mock_api + + result = self.runner.invoke(create_ray_dashboard_connection, [ + '--cluster-name', 'my-raycluster', + '--namespace', 'default', + ]) + + assert result.exit_code != 0 + assert "RayDashboardConnection API is not available" in result.output + assert "hyperpod-ray-endpoint-operator" in result.output + + @patch('sagemaker.hyperpod.common.cli_decorators._namespace_exists', return_value=True) + @patch('sagemaker.hyperpod.cli.commands.ray_dashboard_connection._load_kube_config') + @patch('sagemaker.hyperpod.cli.commands.ray_dashboard_connection.client.CustomObjectsApi') + def test_create_404_namespace_not_found(self, mock_custom_objects_api_class, mock_load_config, mock_ns_exists): + """Test 404 for missing namespace shows raw error""" + mock_api = Mock() + mock_api.create_namespaced_custom_object.side_effect = ApiException( + status=404, + reason="Not Found", + http_resp=Mock(status=404, reason="Not Found", data=b'{"message":"namespaces not-exists not found"}'), + ) + mock_api.create_namespaced_custom_object.side_effect.body = '{"message":"namespaces not-exists not found"}' + mock_custom_objects_api_class.return_value = mock_api + + result = self.runner.invoke(create_ray_dashboard_connection, [ + '--cluster-name', 'my-raycluster', + '--namespace', 'not-exists', + ]) + + assert result.exit_code != 0 + assert "not found" in result.output.lower() or "not-exists" in result.output + + @patch('sagemaker.hyperpod.common.cli_decorators._namespace_exists', return_value=True) + @patch('sagemaker.hyperpod.cli.commands.ray_dashboard_connection._load_kube_config') + @patch('sagemaker.hyperpod.cli.commands.ray_dashboard_connection.client.CustomObjectsApi') + def test_create_403_raises_exception(self, mock_custom_objects_api_class, mock_load_config, mock_ns_exists): + """Test 403 forbidden is propagated as an error""" + mock_api = Mock() + exc = ApiException( + status=403, + reason="Forbidden", + http_resp=Mock(status=403, reason="Forbidden", data=b'{"message":"forbidden"}'), + ) + exc.body = '{"message":"forbidden"}' + mock_api.create_namespaced_custom_object.side_effect = exc + mock_custom_objects_api_class.return_value = mock_api + + result = self.runner.invoke(create_ray_dashboard_connection, [ + '--cluster-name', 'my-raycluster', + ]) + + assert result.exit_code != 0 + + def test_missing_cluster_name(self): + """Test that --cluster-name is required""" + result = self.runner.invoke(create_ray_dashboard_connection, [ + '--namespace', 'default', + ]) + + assert result.exit_code != 0 + assert "Missing option '--cluster-name'" in result.output diff --git a/test/unit_tests/cli/test_recipe_utils_template_rendering.py b/test/unit_tests/cli/test_recipe_utils_template_rendering.py new file mode 100644 index 00000000..93042620 --- /dev/null +++ b/test/unit_tests/cli/test_recipe_utils_template_rendering.py @@ -0,0 +1,338 @@ +"""Unit tests for recipe_utils template rendering. + +These tests verify that the Jinja2 template rendering in _render_k8s_template +uses a sandboxed environment that restricts template operations to safe +constructs while still allowing legitimate template functionality. +""" + +import pytest +from jinja2.exceptions import SecurityError + +from sagemaker.hyperpod.cli.recipe_utils import _render_k8s_template + + +class TestRenderK8sTemplate: + """Tests for _render_k8s_template sandboxed rendering.""" + + def test_variable_substitution(self): + """Normal variable substitution should work.""" + template = "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: {{ name }}\n namespace: {{ namespace }}" + config = {"name": "my-job", "namespace": "default"} + result = _render_k8s_template(template, config) + assert "name: my-job" in result + assert "namespace: default" in result + + def test_filter_usage(self): + """Jinja2 filters should work normally.""" + template = "name: {{ name | upper }}" + config = {"name": "my-job"} + result = _render_k8s_template(template, config) + assert "name: MY-JOB" in result + + def test_conditional(self): + """Jinja2 conditionals should work normally.""" + template = "{% if gpu %}accelerator: nvidia{% endif %}" + config = {"gpu": True} + result = _render_k8s_template(template, config) + assert "accelerator: nvidia" in result + + def test_loop(self): + """Jinja2 loops should work normally.""" + template = "{% for item in items %}{{ item }}\n{% endfor %}" + config = {"items": ["a", "b", "c"]} + result = _render_k8s_template(template, config) + assert "a" in result + assert "b" in result + assert "c" in result + + def test_blocks_os_popen_via_cycler(self): + """Access to os.popen through object traversal should be blocked.""" + template = "{{ cycler.__init__.__globals__.os.popen('whoami').read() }}" + config = {} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_blocks_dunder_init_access(self): + """Access to __init__ on objects should be blocked.""" + template = "{{ ''.__class__.__init__.__globals__ }}" + config = {} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_blocks_dunder_class_chaining(self): + """Chaining from __class__ to internal attributes should be blocked.""" + template = "{{ ''.__class__.__init__.__globals__ }}" + config = {} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_blocks_subclasses_access(self): + """Access to __subclasses__ should be blocked.""" + template = "{{ ''.__class__.__mro__[1].__subclasses__() }}" + config = {} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_blocks_globals_access(self): + """Access to __globals__ should be blocked.""" + template = "{{ config.__init__.__globals__['os'] }}" + config = {"config": {}} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_blocks_import_via_builtins(self): + """Attempt to access modules via __builtins__ should be blocked.""" + template = ( + "{{ ''.__class__.__init__.__globals__['__builtins__']['__import__']('os').popen('id').read() }}" + ) + config = {} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_blocks_mro_traversal(self): + """MRO traversal to reach internal classes should be blocked.""" + template = "{{ [].__class__.__mro__ }}" + config = {} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_realistic_k8s_template(self): + """A realistic Kubernetes manifest template should render correctly.""" + template = """apiVersion: batch/v1 +kind: Job +metadata: + name: {{ name }} + namespace: {{ namespace }} +spec: + template: + spec: + containers: + - name: training + image: {{ image }} + resources: + limits: + nvidia.com/gpu: {{ gpu_count }} + restartPolicy: Never""" + config = { + "name": "sft-llama-job", + "namespace": "hyperpod", + "image": "763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-training:2.0", + "gpu_count": 8, + } + result = _render_k8s_template(template, config) + assert "name: sft-llama-job" in result + assert "namespace: hyperpod" in result + assert "nvidia.com/gpu: 8" in result + assert "763104351884" in result + + def test_blocks_code_execution_in_template(self): + """Arbitrary code execution through template internals should be blocked.""" + template = ( + 'apiVersion: v1\nkind: ConfigMap\nmetadata:\n' + ' name: {{ cycler.__init__.__globals__.os.popen(\'echo pwned\').read() }}\n' + ' namespace: {{ namespace }}' + ) + config = {"namespace": "default"} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + # ------------------------------------------------------------------- + # Additional sandbox bypass vector tests + # + # The SandboxedEnvironment blocks unsafe attribute access via two mechanisms: + # 1. Dot notation (e.g., obj.__init__): raises SecurityError when the sandbox + # detects access to internal attributes on real objects. + # 2. |attr() filter (e.g., obj|attr("__init__")): returns Undefined (renders + # as empty string) for internal attributes. This is safe because Undefined + # cannot be called, iterated, or used to access further attributes. + # + # Both mechanisms prevent exploitation. Tests below verify neither path + # leaks internal state or enables code execution. + # ------------------------------------------------------------------- + + def test_blocks_attr_filter_returns_undefined(self): + """The |attr() filter returns Undefined for internal attributes. + + {{ ""|attr("__class__") }} uses the attr filter to access dunder + attributes indirectly. The sandbox intercepts this and returns Undefined + which renders as an empty string — no data leak occurs. + """ + template = '{{ ""|attr("__class__")|attr("__init__")|attr("__globals__") }}' + config = {} + result = _render_k8s_template(template, config) + # Should render as empty string (Undefined) — no internal data leaked + assert result == "" + assert "function" not in result + assert "module" not in result + assert "os" not in result + + def test_attr_filter_cannot_reach_callable(self): + """The attr() chain cannot produce a callable to execute code. + + Even though attr() doesn't raise, the resulting Undefined cannot be + called, preventing actual exploitation. + """ + from jinja2.exceptions import UndefinedError + # Attempting to call the result of attr() on an Undefined raises UndefinedError + template = '{{ ""|attr("__class__")|attr("__subclasses__")() }}' + config = {} + with pytest.raises(UndefinedError): + _render_k8s_template(template, config) + + def test_attr_filter_get_method_blocked(self): + """Cannot use .get() on Undefined returned by attr() to extract values.""" + from jinja2.exceptions import UndefinedError + template = '{% set g = ""|attr("__class__")|attr("__init__")|attr("__globals__") %}{{ g.get("os") }}' + config = {} + with pytest.raises(UndefinedError): + _render_k8s_template(template, config) + + def test_blocks_lipsum_globals_access(self): + """Access to __globals__ via lipsum builtin should be blocked. + + lipsum is a built-in Jinja2 global (like cycler, joiner, namespace); + it can be used as an entry point for traversal. + """ + template = "{{ lipsum.__globals__['os'].popen('id').read() }}" + config = {} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_blocks_joiner_globals_access(self): + """Access to __globals__ via joiner builtin should be blocked.""" + template = "{{ joiner.__init__.__globals__['os'].popen('id').read() }}" + config = {} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_blocks_namespace_init_globals(self): + """Access to __globals__ via namespace builtin should be blocked.""" + template = "{{ namespace.__init__.__globals__ }}" + config = {} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_blocks_config_data_object_traversal(self): + """Objects passed via config_data should not allow internal traversal.""" + template = "{{ obj.__class__.__init__.__globals__ }}" + config = {"obj": {"key": "value"}} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_format_string_attr_construction_is_safe(self): + """Format string tricks to construct dunder names are blocked by attr(). + + Even if you dynamically build "__class__" via string concat and pass it + to |attr(), the sandbox still returns Undefined — no data leak. + """ + template = '{% set name = "__cla" ~ "ss__" %}{{ ""|attr(name) }}' + config = {} + result = _render_k8s_template(template, config) + assert result == "" + assert "class" not in result.lower() or result == "" + + def test_format_constructed_full_chain_is_safe(self): + """Dynamically constructed attr names cannot bypass sandbox protection.""" + template = '{% set a = "__ini" %}{% set b = "t__" %}{{ ""|attr("__class__")|attr(a~b)|attr("__globals__") }}' + config = {} + result = _render_k8s_template(template, config) + assert result == "" + assert "os" not in result + assert "module" not in result + + def test_map_filter_attribute_returns_undefined(self): + """The |map(attribute=) filter returns Undefined for dunder access. + + {{ items|map(attribute="__class__")|list }} uses filter parameters + to perform attribute access, but sandbox blocks it returning Undefined. + """ + template = '{{ ["a","b"]|map(attribute="__class__")|list }}' + config = {} + result = _render_k8s_template(template, config) + # map with blocked attribute returns Undefined for each item + assert "str" not in result + assert "type" not in result + + def test_blocks_map_filter_deep_attribute(self): + """Deep attribute access via |map should be blocked.""" + template = '{{ ["a"]|map(attribute="__class__.__init__.__globals__")|list }}' + config = {} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_blocks_set_tag_namespace_globals(self): + """{% set %} assignment with namespace.__init__.__globals__ should be blocked. + + Self-referential trick using set to assign internal objects. + """ + template = "{% set x = namespace.__init__.__globals__ %}{{ x }}" + config = {} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_blocks_set_with_cycler_traversal(self): + """{% set %} combined with builtin object traversal should be blocked.""" + template = "{% set x = cycler.__init__.__globals__ %}{{ x }}" + config = {} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_join_filter_attr_construction_is_safe(self): + """Using |join to construct attribute names still returns Undefined.""" + template = '{% set parts = ["__","class","__"] %}{{ ""|attr(parts|join) }}' + config = {} + result = _render_k8s_template(template, config) + assert result == "" + + def test_getitem_dunder_returns_undefined(self): + """Accessing __class__ via bracket notation returns Undefined (safe). + + The sandbox intercepts __getitem__ access to internal attribute names + and returns Undefined rather than the actual attribute. + """ + template = '{{ ""["__class__"] }}' + config = {} + result = _render_k8s_template(template, config) + # Bracket notation for dunder attrs also returns Undefined (renders empty) + assert "str" not in result + assert "type" not in result + + def test_getitem_cannot_chain_to_globals(self): + """Bracket notation chaining to reach __globals__ raises SecurityError.""" + template = '{{ ""["__class__"]["__init__"]["__globals__"] }}' + config = {} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_blocks_request_like_object_traversal(self): + """If any complex objects are passed in config, traversal should be blocked.""" + class FakeRequest: + pass + + template = "{{ req.__class__.__init__.__globals__ }}" + config = {"req": FakeRequest()} + with pytest.raises(SecurityError): + _render_k8s_template(template, config) + + def test_attr_filter_no_data_leak_comprehensive(self): + """Comprehensive test that no attr() variant leaks internal Python state.""" + dangerous_templates = [ + '{{ ""|attr("__class__")|attr("__init__")|attr("__globals__") }}', + '{{ []|attr("__class__")|attr("__mro__") }}', + '{{ ""|attr("__class__")|attr("__subclasses__") }}', + '{% set x = ""|attr("__class__") %}{{ x|attr("__init__") }}', + ] + dangerous_indicators = ["function", "module", "os", "subprocess", "popen", + "builtins", " _parse_version("0.1.6")) + + @patch("sagemaker.hyperpod.space.utils.get_hyperpod_cluster_region", return_value="us-west-2") + @patch("sagemaker.hyperpod.space.utils.create_boto3_client") + def test_get_addon_version_parses_eksbuild_suffix(self, mock_client_factory, mock_region): + mock_client = Mock() + mock_client.describe_addon.return_value = { + "addon": {"addonVersion": "v0.1.6-eksbuild.1"} + } + mock_client_factory.return_value = mock_client + + result = get_spaces_addon_version("my-cluster") + self.assertEqual(result, "0.1.6") + mock_client.describe_addon.assert_called_once_with( + clusterName="my-cluster", addonName=SPACES_ADDON_NAME + ) + + @patch("sagemaker.hyperpod.space.utils.get_hyperpod_cluster_region", return_value="us-west-2") + @patch("sagemaker.hyperpod.space.utils.create_boto3_client") + def test_get_addon_version_without_v_prefix(self, mock_client_factory, mock_region): + mock_client = Mock() + mock_client.describe_addon.return_value = { + "addon": {"addonVersion": "0.1.1-eksbuild.2"} + } + mock_client_factory.return_value = mock_client + + result = get_spaces_addon_version("my-cluster") + self.assertEqual(result, "0.1.1") + + @patch("sagemaker.hyperpod.space.utils.get_hyperpod_cluster_region", return_value="us-west-2") + @patch("sagemaker.hyperpod.space.utils.create_boto3_client") + def test_get_addon_version_returns_none_on_exception(self, mock_client_factory, mock_region): + mock_client = Mock() + mock_client.describe_addon.side_effect = Exception("Not found") + mock_client_factory.return_value = mock_client + + result = get_spaces_addon_version("my-cluster") + self.assertIsNone(result) + + @patch("sagemaker.hyperpod.space.utils.get_hyperpod_cluster_region", return_value="us-west-2") + @patch("sagemaker.hyperpod.space.utils.create_boto3_client") + def test_get_addon_version_returns_none_on_unparseable(self, mock_client_factory, mock_region): + mock_client = Mock() + mock_client.describe_addon.return_value = { + "addon": {"addonVersion": "invalid-version"} + } + mock_client_factory.return_value = mock_client + + result = get_spaces_addon_version("my-cluster") + self.assertIsNone(result) + + def _make_decorated_class(self): + from hyperpod_space_template.v1_1.model import SpaceConfig + + class FakeSpace: + def __init__(self): + self.called = False + self.config = SpaceConfig(name="fake", display_name="Fake") + + @warn_if_addon_version_incompatible + def create(self): + self.called = True + return "created" + + return FakeSpace + + @patch("sagemaker.hyperpod.space.utils.get_spaces_addon_version", return_value="0.1.1") + @patch("sagemaker.hyperpod.space.utils.get_eks_cluster_name", return_value="my-cluster") + def test_decorator_warns_when_version_too_old(self, mock_cluster, mock_version): + space = self._make_decorated_class()() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = space.create() + + self.assertEqual(result, "created") + self.assertTrue(space.called) + self.assertEqual(len(w), 1) + self.assertIn("0.1.1", str(w[0].message)) + self.assertIn("0.1.6", str(w[0].message)) + + @patch("sagemaker.hyperpod.space.utils.get_spaces_addon_version", return_value="0.1.6") + @patch("sagemaker.hyperpod.space.utils.get_eks_cluster_name", return_value="my-cluster") + def test_decorator_no_warning_when_version_sufficient(self, mock_cluster, mock_version): + space = self._make_decorated_class()() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = space.create() + + self.assertEqual(result, "created") + self.assertEqual(len(w), 0) + + @patch("sagemaker.hyperpod.space.utils.get_spaces_addon_version", return_value=None) + @patch("sagemaker.hyperpod.space.utils.get_eks_cluster_name", return_value="my-cluster") + def test_decorator_no_warning_when_version_unknown(self, mock_cluster, mock_version): + space = self._make_decorated_class()() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = space.create() + + self.assertEqual(result, "created") + self.assertEqual(len(w), 0) + + @patch("sagemaker.hyperpod.space.utils.get_eks_cluster_name", side_effect=Exception("no context")) + def test_decorator_no_warning_when_cluster_unavailable(self, mock_cluster): + space = self._make_decorated_class()() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = space.create() + + self.assertEqual(result, "created") + self.assertTrue(space.called) + self.assertEqual(len(w), 0)