Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/agentex/lib/cli/commands/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,9 @@ def deploy(
),
tag: str | None = typer.Option(None, help="Override the image tag for deployment"),
repository: str | None = typer.Option(None, help="Override the repository for deployment"),
use_latest_chart: bool = typer.Option(
False, "--use-latest-chart", help="Fetch and use the latest Helm chart version from OCI registry"
),
interactive: bool = typer.Option(True, "--interactive/--no-interactive", help="Enable interactive prompts"),
):
"""Deploy an agent to a Kubernetes cluster using Helm"""
Expand Down Expand Up @@ -396,6 +399,8 @@ def deploy(
console.print(f" Namespace: {namespace}")
if tag:
console.print(f" Image Tag: {tag}")
if use_latest_chart:
console.print(" Chart Version: [cyan]latest (will be fetched)[/cyan]")

if interactive:
proceed = questionary.confirm("Proceed with deployment?").ask()
Expand All @@ -421,6 +426,7 @@ def deploy(
namespace=namespace,
deploy_overrides=deploy_overrides,
environment_name=environment,
use_latest_chart=use_latest_chart,
)

# Use the already loaded manifest object
Expand Down
233 changes: 219 additions & 14 deletions src/agentex/lib/cli/handlers/deploy_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand All @@ -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,
Copy link
Collaborator

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?

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()]
Expand Down Expand Up @@ -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():
Expand All @@ -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
Copy link
Collaborator

@RoxyFarhad RoxyFarhad Feb 4, 2026

Choose a reason for hiding this comment

The 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)
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
23 changes: 21 additions & 2 deletions src/agentex/lib/sdk/config/environment_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Copy link
Collaborator

Choose a reason for hiding this comment

The 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."""
Expand Down