-
Notifications
You must be signed in to change notification settings - Fork 5
feat: add OCI Helm registry support for agent deployments #255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,7 @@ | |
| console = Console() | ||
|
|
||
| TEMPORAL_WORKER_KEY = "temporal-worker" | ||
| AGENTEX_AGENTS_HELM_CHART_VERSION = "0.1.9" | ||
| DEFAULT_HELM_CHART_VERSION = "0.1.9" | ||
|
|
||
|
|
||
| class InputDeployOverrides(BaseModel): | ||
|
|
@@ -42,7 +42,7 @@ def check_helm_installed() -> bool: | |
|
|
||
|
|
||
| def add_helm_repo(helm_repository_name: str, helm_repository_url: str) -> None: | ||
| """Add the agentex helm repository if not already added""" | ||
| """Add the agentex helm repository if not already added (classic mode)""" | ||
| try: | ||
| # Check if repo already exists | ||
| result = subprocess.run(["helm", "repo", "list"], capture_output=True, text=True, check=True) | ||
|
|
@@ -69,6 +69,157 @@ def add_helm_repo(helm_repository_name: str, helm_repository_url: str) -> None: | |
| raise HelmError(f"Failed to add helm repository: {e}") from e | ||
|
|
||
|
|
||
| def login_to_gar_registry(oci_registry: str) -> None: | ||
| """Auto-login to Google Artifact Registry using gcloud credentials. | ||
|
|
||
| Args: | ||
| oci_registry: The GAR registry URL (e.g., 'us-west1-docker.pkg.dev/project-id/repo-name') | ||
| """ | ||
| try: | ||
| # Extract the registry host (e.g., 'us-west1-docker.pkg.dev') | ||
| registry_host = oci_registry.split("/")[0] | ||
|
|
||
| # Get access token from gcloud | ||
| console.print(f"[blue]ℹ[/blue] Authenticating with Google Artifact Registry: {registry_host}") | ||
| result = subprocess.run( | ||
| ["gcloud", "auth", "print-access-token"], | ||
| capture_output=True, | ||
| text=True, | ||
| check=True, | ||
| ) | ||
| access_token = result.stdout.strip() | ||
|
|
||
| # Login to helm registry using the access token | ||
| subprocess.run( | ||
| [ | ||
| "helm", | ||
| "registry", | ||
| "login", | ||
| registry_host, | ||
| "--username", | ||
| "oauth2accesstoken", | ||
| "--password-stdin", | ||
| ], | ||
| input=access_token, | ||
| text=True, | ||
| check=True, | ||
| ) | ||
| console.print(f"[green]✓[/green] Authenticated with GAR: {registry_host}") | ||
|
|
||
| except subprocess.CalledProcessError as e: | ||
| raise HelmError( | ||
| f"Failed to authenticate with Google Artifact Registry: {e}\n" | ||
| "Ensure you are logged in with 'gcloud auth login' and have access to the registry." | ||
| ) from e | ||
| except FileNotFoundError: | ||
| raise HelmError( | ||
| "gcloud CLI not found. Please install the Google Cloud SDK: " | ||
| "https://cloud.google.com/sdk/docs/install" | ||
| ) from None | ||
|
|
||
|
|
||
| def get_latest_gar_chart_version(oci_registry: str, chart_name: str = "agentex-agent") -> str: | ||
| """Fetch the latest version of a Helm chart from Google Artifact Registry. | ||
|
|
||
| GAR stores Helm chart versions as tags (e.g., '0.1.9'), not as versions (which are SHA digests). | ||
| This function lists tags sorted by creation time and returns the most recent one. | ||
|
|
||
| Args: | ||
| oci_registry: The GAR registry URL (e.g., 'us-west1-docker.pkg.dev/project-id/repo-name') | ||
| chart_name: Name of the Helm chart | ||
|
|
||
| Returns: | ||
| The latest version string (e.g., '0.2.0') | ||
| """ | ||
| try: | ||
| # Parse the OCI registry URL to extract components | ||
| # Format: REGION-docker.pkg.dev/PROJECT/REPOSITORY | ||
| parts = oci_registry.split("/") | ||
| if len(parts) < 3: | ||
| raise HelmError( | ||
| f"Invalid OCI registry format: {oci_registry}. " | ||
| "Expected format: REGION-docker.pkg.dev/PROJECT/REPOSITORY" | ||
| ) | ||
|
|
||
| location = parts[0].replace("-docker.pkg.dev", "") | ||
| project = parts[1] | ||
| repository = parts[2] | ||
|
|
||
| console.print(f"[blue]ℹ[/blue] Fetching latest chart version from GAR...") | ||
|
|
||
| # Use gcloud to list tags (not versions - versions are SHA digests) | ||
| # Tags contain the semantic versions like '0.1.9' | ||
| result = subprocess.run( | ||
| [ | ||
| "gcloud", | ||
| "artifacts", | ||
| "tags", | ||
| "list", | ||
| f"--repository={repository}", | ||
| f"--location={location}", | ||
| f"--project={project}", | ||
| f"--package={chart_name}", | ||
| "--sort-by=~createTime", | ||
| "--limit=1", | ||
| "--format=value(tag)", | ||
| ], | ||
| capture_output=True, | ||
| text=True, | ||
| check=True, | ||
| ) | ||
|
|
||
| output = result.stdout.strip() | ||
| if not output: | ||
| raise HelmError( | ||
| f"No tags found for chart '{chart_name}' in {oci_registry}" | ||
| ) | ||
|
|
||
| # The output is the tag name (semantic version) | ||
| version = output | ||
| console.print(f"[green]✓[/green] Latest chart version: {version}") | ||
| return version | ||
|
|
||
| except subprocess.CalledProcessError as e: | ||
| raise HelmError( | ||
| f"Failed to fetch chart tags from GAR: {e.stderr}\n" | ||
| "Ensure you have access to the Artifact Registry." | ||
| ) from e | ||
| except FileNotFoundError: | ||
| raise HelmError( | ||
| "gcloud CLI not found. Please install the Google Cloud SDK: " | ||
| "https://cloud.google.com/sdk/docs/install" | ||
| ) from None | ||
|
|
||
|
|
||
| def get_chart_reference( | ||
| use_oci: bool, | ||
| helm_repository_name: str | None = None, | ||
| oci_registry: str | None = None, | ||
| chart_name: str = "agentex-agent", | ||
| ) -> str: | ||
| """Get the chart reference based on the deployment mode. | ||
|
|
||
| Args: | ||
| use_oci: Whether to use OCI registry mode | ||
| helm_repository_name: Name of the classic helm repo (required if use_oci=False) | ||
| oci_registry: OCI registry URL (required if use_oci=True) | ||
| chart_name: Name of the helm chart | ||
|
|
||
| Returns: | ||
| Chart reference string for helm install/upgrade commands | ||
| """ | ||
| if use_oci: | ||
| if not oci_registry: | ||
| raise HelmError("OCI registry URL is required for OCI mode") | ||
| # OCI format: oci://registry/path/chart-name | ||
| return f"oci://{oci_registry}/{chart_name}" | ||
| else: | ||
| if not helm_repository_name: | ||
| raise HelmError("Helm repository name is required for classic mode") | ||
| # Classic format: repo-name/chart-name | ||
| return f"{helm_repository_name}/{chart_name}" | ||
|
|
||
|
|
||
| def convert_env_vars_dict_to_list(env_vars: dict[str, str]) -> list[dict[str, str]]: | ||
| """Convert a dictionary of environment variables to a list of dictionaries""" | ||
| return [{"name": key, "value": value} for key, value in env_vars.items()] | ||
|
|
@@ -281,8 +432,18 @@ def deploy_agent( | |
| namespace: str, | ||
| deploy_overrides: InputDeployOverrides, | ||
| environment_name: str | None = None, | ||
| use_latest_chart: bool = False, | ||
| ) -> None: | ||
| """Deploy an agent using helm""" | ||
| """Deploy an agent using helm | ||
|
|
||
| Args: | ||
| manifest_path: Path to the agent manifest file | ||
| cluster_name: Target Kubernetes cluster name | ||
| namespace: Kubernetes namespace to deploy to | ||
| deploy_overrides: Image repository/tag overrides | ||
| environment_name: Environment name from environments.yaml | ||
| use_latest_chart: If True, fetch and use the latest chart version from OCI registry (OCI mode only) | ||
| """ | ||
|
|
||
| # Validate prerequisites | ||
| if not check_helm_installed(): | ||
|
|
@@ -304,14 +465,58 @@ def deploy_agent( | |
| else: | ||
| console.print(f"[yellow]⚠[/yellow] No environments.yaml found, skipping environment-specific config") | ||
|
|
||
| if agent_env_config: | ||
| helm_repository_name = agent_env_config.helm_repository_name | ||
| helm_repository_url = agent_env_config.helm_repository_url | ||
| # Determine if using OCI or classic helm repo mode | ||
| use_oci = agent_env_config.uses_oci_registry() if agent_env_config else False | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you make this a helper function that returns chart_reference, chart_version just so its clear that it is always set? Feel like that gets kind of lost here and can cause helm deploy issues |
||
| helm_repository_name: str | None = None | ||
| oci_registry: str | None = None | ||
|
|
||
| # Track OCI provider for provider-specific features | ||
| oci_provider: str | None = None | ||
|
|
||
| if use_oci: | ||
| oci_registry = agent_env_config.helm_oci_registry # type: ignore[union-attr] | ||
| oci_provider = agent_env_config.helm_oci_provider # type: ignore[union-attr] | ||
| console.print(f"[blue]ℹ[/blue] Using OCI Helm registry: {oci_registry}") | ||
|
|
||
| # Only auto-authenticate for GAR provider | ||
| if oci_provider == "gar": | ||
| login_to_gar_registry(oci_registry) # type: ignore[arg-type] | ||
| else: | ||
| console.print("[blue]ℹ[/blue] Skipping auto-authentication (no provider specified, assuming already authenticated)") | ||
| else: | ||
| if agent_env_config: | ||
| helm_repository_name = agent_env_config.helm_repository_name | ||
| helm_repository_url = agent_env_config.helm_repository_url | ||
| else: | ||
| helm_repository_name = "scale-egp" | ||
| helm_repository_url = "https://scale-egp-helm-charts-us-west-2.s3.amazonaws.com/charts" | ||
| # Add helm repository/update (classic mode only) | ||
| add_helm_repo(helm_repository_name, helm_repository_url) | ||
|
|
||
| # Get the chart reference based on deployment mode | ||
| chart_reference = get_chart_reference( | ||
| use_oci=use_oci, | ||
| helm_repository_name=helm_repository_name, | ||
| oci_registry=oci_registry, | ||
| ) | ||
|
|
||
| # Determine chart version | ||
| # Priority: --use-latest-chart > env config > default | ||
| if use_latest_chart: | ||
| if not use_oci: | ||
| console.print("[yellow]⚠[/yellow] --use-latest-chart only works with OCI registries, using default version") | ||
| chart_version = DEFAULT_HELM_CHART_VERSION | ||
| elif oci_provider != "gar": | ||
| console.print("[yellow]⚠[/yellow] --use-latest-chart only works with GAR provider (helm_oci_provider: gar), using default version") | ||
| chart_version = DEFAULT_HELM_CHART_VERSION | ||
| else: | ||
| chart_version = get_latest_gar_chart_version(oci_registry) # type: ignore[arg-type] | ||
| elif agent_env_config and agent_env_config.helm_chart_version: | ||
| chart_version = agent_env_config.helm_chart_version | ||
| else: | ||
| helm_repository_name = "scale-egp" | ||
| helm_repository_url = "https://scale-egp-helm-charts-us-west-2.s3.amazonaws.com/charts" | ||
| # Add helm repository/update | ||
| add_helm_repo(helm_repository_name, helm_repository_url) | ||
| chart_version = DEFAULT_HELM_CHART_VERSION | ||
|
|
||
| console.print(f"[blue]ℹ[/blue] Using Helm chart version: {chart_version}") | ||
|
|
||
| # Merge configurations | ||
| helm_values = merge_deployment_configs(manifest, agent_env_config, deploy_overrides, manifest_path) | ||
|
|
@@ -341,9 +546,9 @@ def deploy_agent( | |
| "helm", | ||
| "upgrade", | ||
| release_name, | ||
| f"{helm_repository_name}/agentex-agent", | ||
| chart_reference, | ||
| "--version", | ||
| AGENTEX_AGENTS_HELM_CHART_VERSION, | ||
| chart_version, | ||
| "-f", | ||
| values_file, | ||
| "-n", | ||
|
|
@@ -363,9 +568,9 @@ def deploy_agent( | |
| "helm", | ||
| "install", | ||
| release_name, | ||
| f"{helm_repository_name}/agentex-agent", | ||
| chart_reference, | ||
| "--version", | ||
| AGENTEX_AGENTS_HELM_CHART_VERSION, | ||
| chart_version, | ||
| "-f", | ||
| values_file, | ||
| "-n", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,7 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any, Dict, override | ||
| from typing import Any, Dict, Literal, override | ||
| from pathlib import Path | ||
|
|
||
| import yaml | ||
|
|
@@ -67,12 +67,31 @@ class AgentEnvironmentConfig(BaseModel): | |
| helm_repository_name: str = Field(default="scale-egp", description="Helm repository name for the environment") | ||
| helm_repository_url: str = Field( | ||
| default="https://scale-egp-helm-charts-us-west-2.s3.amazonaws.com/charts", | ||
| description="Helm repository url for the environment", | ||
| description="Helm repository url for the environment (classic mode)" | ||
| ) | ||
| helm_oci_registry: str | None = Field( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what about making an oci_registry map and then having these as nested values? just so it looks more like an optional config? |
||
| default=None, | ||
| description="OCI registry URL for Helm charts (e.g., 'us-west1-docker.pkg.dev/project/repo'). " | ||
| "When set, OCI mode is used instead of classic helm repo." | ||
| ) | ||
| helm_oci_provider: Literal["gar"] | None = Field( | ||
| default=None, | ||
| description="OCI registry provider for provider-specific features. " | ||
| "Set to 'gar' for Google Artifact Registry to enable auto-authentication via gcloud " | ||
| "and latest version fetching. When not set, assumes user has already authenticated." | ||
| ) | ||
| helm_chart_version: str | None = Field( | ||
| default=None, | ||
| description="Helm chart version to deploy. If not set, uses the default version from the CLI." | ||
| ) | ||
| helm_overrides: Dict[str, Any] = Field( | ||
| default_factory=dict, description="Helm chart value overrides for environment-specific tuning" | ||
| ) | ||
|
|
||
| def uses_oci_registry(self) -> bool: | ||
| """Check if this environment uses OCI registry for Helm charts.""" | ||
| return self.helm_oci_registry is not None | ||
|
|
||
|
|
||
| class AgentEnvironmentsConfig(UtilsBaseModel): | ||
| """All environment configurations for an agent.""" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need this use_oi ? isn't this implicitly true if oci_registry is not true?