Skip to content

Commit e3778c1

Browse files
committed
feat: add OCI Helm registry support for agent deployments
Add support for deploying agents using OCI-based Helm registries (e.g., Google Artifact Registry) as an alternative to classic Helm repositories. Changes: - Add `helm_oci_registry` and `helm_chart_version` fields to AgentEnvironmentConfig - Implement auto-login to Google Artifact Registry using gcloud credentials - Add `--use-latest-chart` CLI flag to fetch the latest chart version from OCI registry - Support both classic Helm repo mode and OCI registry mode based on environment config
1 parent 939ab0d commit e3778c1

File tree

3 files changed

+234
-22
lines changed

3 files changed

+234
-22
lines changed

src/agentex/lib/cli/commands/agents.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ def deploy(
262262
repository: str | None = typer.Option(
263263
None, help="Override the repository for deployment"
264264
),
265+
use_latest_chart: bool = typer.Option(
266+
False, "--use-latest-chart", help="Fetch and use the latest Helm chart version from OCI registry"
267+
),
265268
interactive: bool = typer.Option(
266269
True, "--interactive/--no-interactive", help="Enable interactive prompts"
267270
),
@@ -322,6 +325,8 @@ def deploy(
322325
console.print(f" Namespace: {namespace}")
323326
if tag:
324327
console.print(f" Image Tag: {tag}")
328+
if use_latest_chart:
329+
console.print(" Chart Version: [cyan]latest (will be fetched)[/cyan]")
325330

326331
if interactive:
327332
proceed = questionary.confirm("Proceed with deployment?").ask()
@@ -351,6 +356,7 @@ def deploy(
351356
namespace=namespace,
352357
deploy_overrides=deploy_overrides,
353358
environment_name=environment,
359+
use_latest_chart=use_latest_chart,
354360
)
355361

356362
# Use the already loaded manifest object

src/agentex/lib/cli/handlers/deploy_handlers.py

Lines changed: 207 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
console = Console()
2424

2525
TEMPORAL_WORKER_KEY = "temporal-worker"
26-
AGENTEX_AGENTS_HELM_CHART_VERSION = "0.1.9"
26+
DEFAULT_HELM_CHART_VERSION = "0.1.9"
2727

2828

2929
class InputDeployOverrides(BaseModel):
@@ -42,7 +42,7 @@ def check_helm_installed() -> bool:
4242

4343

4444
def add_helm_repo(helm_repository_name: str, helm_repository_url: str) -> None:
45-
"""Add the agentex helm repository if not already added"""
45+
"""Add the agentex helm repository if not already added (classic mode)"""
4646
try:
4747
# Check if repo already exists
4848
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:
6969
raise HelmError(f"Failed to add helm repository: {e}") from e
7070

7171

72+
def login_to_gar_registry(oci_registry: str) -> None:
73+
"""Auto-login to Google Artifact Registry using gcloud credentials.
74+
75+
Args:
76+
oci_registry: The GAR registry URL (e.g., 'us-west1-docker.pkg.dev/project-id/repo-name')
77+
"""
78+
try:
79+
# Extract the registry host (e.g., 'us-west1-docker.pkg.dev')
80+
registry_host = oci_registry.split("/")[0]
81+
82+
# Get access token from gcloud
83+
console.print(f"[blue]ℹ[/blue] Authenticating with Google Artifact Registry: {registry_host}")
84+
result = subprocess.run(
85+
["gcloud", "auth", "print-access-token"],
86+
capture_output=True,
87+
text=True,
88+
check=True,
89+
)
90+
access_token = result.stdout.strip()
91+
92+
# Login to helm registry using the access token
93+
subprocess.run(
94+
[
95+
"helm",
96+
"registry",
97+
"login",
98+
registry_host,
99+
"--username",
100+
"oauth2accesstoken",
101+
"--password-stdin",
102+
],
103+
input=access_token,
104+
text=True,
105+
check=True,
106+
)
107+
console.print(f"[green]✓[/green] Authenticated with GAR: {registry_host}")
108+
109+
except subprocess.CalledProcessError as e:
110+
raise HelmError(
111+
f"Failed to authenticate with Google Artifact Registry: {e}\n"
112+
"Ensure you are logged in with 'gcloud auth login' and have access to the registry."
113+
) from e
114+
except FileNotFoundError:
115+
raise HelmError(
116+
"gcloud CLI not found. Please install the Google Cloud SDK: "
117+
"https://cloud.google.com/sdk/docs/install"
118+
) from None
119+
120+
121+
def get_latest_gar_chart_version(oci_registry: str, chart_name: str = "agentex-agent") -> str:
122+
"""Fetch the latest version of a Helm chart from Google Artifact Registry.
123+
124+
GAR stores Helm chart versions as tags (e.g., '0.1.9'), not as versions (which are SHA digests).
125+
This function lists tags sorted by creation time and returns the most recent one.
126+
127+
Args:
128+
oci_registry: The GAR registry URL (e.g., 'us-west1-docker.pkg.dev/project-id/repo-name')
129+
chart_name: Name of the Helm chart
130+
131+
Returns:
132+
The latest version string (e.g., '0.2.0')
133+
"""
134+
try:
135+
# Parse the OCI registry URL to extract components
136+
# Format: REGION-docker.pkg.dev/PROJECT/REPOSITORY
137+
parts = oci_registry.split("/")
138+
if len(parts) < 3:
139+
raise HelmError(
140+
f"Invalid OCI registry format: {oci_registry}. "
141+
"Expected format: REGION-docker.pkg.dev/PROJECT/REPOSITORY"
142+
)
143+
144+
location = parts[0].replace("-docker.pkg.dev", "")
145+
project = parts[1]
146+
repository = parts[2]
147+
148+
console.print(f"[blue]ℹ[/blue] Fetching latest chart version from GAR...")
149+
150+
# Use gcloud to list tags (not versions - versions are SHA digests)
151+
# Tags contain the semantic versions like '0.1.9'
152+
result = subprocess.run(
153+
[
154+
"gcloud",
155+
"artifacts",
156+
"tags",
157+
"list",
158+
f"--repository={repository}",
159+
f"--location={location}",
160+
f"--project={project}",
161+
f"--package={chart_name}",
162+
"--sort-by=~createTime",
163+
"--limit=1",
164+
"--format=value(tag)",
165+
],
166+
capture_output=True,
167+
text=True,
168+
check=True,
169+
)
170+
171+
output = result.stdout.strip()
172+
if not output:
173+
raise HelmError(
174+
f"No tags found for chart '{chart_name}' in {oci_registry}"
175+
)
176+
177+
# The output is the tag name (semantic version)
178+
version = output
179+
console.print(f"[green]✓[/green] Latest chart version: {version}")
180+
return version
181+
182+
except subprocess.CalledProcessError as e:
183+
raise HelmError(
184+
f"Failed to fetch chart tags from GAR: {e.stderr}\n"
185+
"Ensure you have access to the Artifact Registry."
186+
) from e
187+
except FileNotFoundError:
188+
raise HelmError(
189+
"gcloud CLI not found. Please install the Google Cloud SDK: "
190+
"https://cloud.google.com/sdk/docs/install"
191+
) from None
192+
193+
194+
def get_chart_reference(
195+
use_oci: bool,
196+
helm_repository_name: str | None = None,
197+
oci_registry: str | None = None,
198+
chart_name: str = "agentex-agent",
199+
) -> str:
200+
"""Get the chart reference based on the deployment mode.
201+
202+
Args:
203+
use_oci: Whether to use OCI registry mode
204+
helm_repository_name: Name of the classic helm repo (required if use_oci=False)
205+
oci_registry: OCI registry URL (required if use_oci=True)
206+
chart_name: Name of the helm chart
207+
208+
Returns:
209+
Chart reference string for helm install/upgrade commands
210+
"""
211+
if use_oci:
212+
if not oci_registry:
213+
raise HelmError("OCI registry URL is required for OCI mode")
214+
# OCI format: oci://registry/path/chart-name
215+
return f"oci://{oci_registry}/{chart_name}"
216+
else:
217+
if not helm_repository_name:
218+
raise HelmError("Helm repository name is required for classic mode")
219+
# Classic format: repo-name/chart-name
220+
return f"{helm_repository_name}/{chart_name}"
221+
222+
72223
def convert_env_vars_dict_to_list(env_vars: dict[str, str]) -> list[dict[str, str]]:
73224
"""Convert a dictionary of environment variables to a list of dictionaries"""
74225
return [{"name": key, "value": value} for key, value in env_vars.items()]
@@ -281,8 +432,18 @@ def deploy_agent(
281432
namespace: str,
282433
deploy_overrides: InputDeployOverrides,
283434
environment_name: str | None = None,
435+
use_latest_chart: bool = False,
284436
) -> None:
285-
"""Deploy an agent using helm"""
437+
"""Deploy an agent using helm
438+
439+
Args:
440+
manifest_path: Path to the agent manifest file
441+
cluster_name: Target Kubernetes cluster name
442+
namespace: Kubernetes namespace to deploy to
443+
deploy_overrides: Image repository/tag overrides
444+
environment_name: Environment name from environments.yaml
445+
use_latest_chart: If True, fetch and use the latest chart version from OCI registry (OCI mode only)
446+
"""
286447

287448
# Validate prerequisites
288449
if not check_helm_installed():
@@ -304,14 +465,46 @@ def deploy_agent(
304465
else:
305466
console.print(f"[yellow]⚠[/yellow] No environments.yaml found, skipping environment-specific config")
306467

307-
if agent_env_config:
308-
helm_repository_name = agent_env_config.helm_repository_name
309-
helm_repository_url = agent_env_config.helm_repository_url
468+
# Determine if using OCI or classic helm repo mode
469+
use_oci = agent_env_config.uses_oci_registry() if agent_env_config else False
470+
helm_repository_name: str | None = None
471+
oci_registry: str | None = None
472+
473+
if use_oci:
474+
oci_registry = agent_env_config.helm_oci_registry # type: ignore[union-attr]
475+
console.print(f"[blue]ℹ[/blue] Using OCI Helm registry: {oci_registry}")
476+
login_to_gar_registry(oci_registry) # type: ignore[arg-type]
477+
else:
478+
if agent_env_config:
479+
helm_repository_name = agent_env_config.helm_repository_name
480+
helm_repository_url = agent_env_config.helm_repository_url
481+
else:
482+
helm_repository_name = "scale-egp"
483+
helm_repository_url = "https://scale-egp-helm-charts-us-west-2.s3.amazonaws.com/charts"
484+
# Add helm repository/update (classic mode only)
485+
add_helm_repo(helm_repository_name, helm_repository_url)
486+
487+
# Get the chart reference based on deployment mode
488+
chart_reference = get_chart_reference(
489+
use_oci=use_oci,
490+
helm_repository_name=helm_repository_name,
491+
oci_registry=oci_registry,
492+
)
493+
494+
# Determine chart version
495+
# Priority: --use-latest-chart > env config > default
496+
if use_latest_chart:
497+
if not use_oci:
498+
console.print("[yellow]⚠[/yellow] --use-latest-chart only works with OCI registries, using default version")
499+
chart_version = DEFAULT_HELM_CHART_VERSION
500+
else:
501+
chart_version = get_latest_gar_chart_version(oci_registry) # type: ignore[arg-type]
502+
elif agent_env_config and agent_env_config.helm_chart_version:
503+
chart_version = agent_env_config.helm_chart_version
310504
else:
311-
helm_repository_name = "scale-egp"
312-
helm_repository_url = "https://scale-egp-helm-charts-us-west-2.s3.amazonaws.com/charts"
313-
# Add helm repository/update
314-
add_helm_repo(helm_repository_name, helm_repository_url)
505+
chart_version = DEFAULT_HELM_CHART_VERSION
506+
507+
console.print(f"[blue]ℹ[/blue] Using Helm chart version: {chart_version}")
315508

316509
# Merge configurations
317510
helm_values = merge_deployment_configs(manifest, agent_env_config, deploy_overrides, manifest_path)
@@ -341,9 +534,9 @@ def deploy_agent(
341534
"helm",
342535
"upgrade",
343536
release_name,
344-
f"{helm_repository_name}/agentex-agent",
537+
chart_reference,
345538
"--version",
346-
AGENTEX_AGENTS_HELM_CHART_VERSION,
539+
chart_version,
347540
"-f",
348541
values_file,
349542
"-n",
@@ -363,9 +556,9 @@ def deploy_agent(
363556
"helm",
364557
"install",
365558
release_name,
366-
f"{helm_repository_name}/agentex-agent",
559+
chart_reference,
367560
"--version",
368-
AGENTEX_AGENTS_HELM_CHART_VERSION,
561+
chart_version,
369562
"-f",
370563
values_file,
371564
"-n",

src/agentex/lib/sdk/config/environment_config.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,28 +64,41 @@ def validate_namespace_format(cls, v: str) -> str:
6464

6565
class AgentEnvironmentConfig(BaseModel):
6666
"""Complete configuration for an agent in a specific environment."""
67-
67+
6868
kubernetes: AgentKubernetesConfig | None = Field(
69-
default=None,
69+
default=None,
7070
description="Kubernetes deployment configuration"
7171
)
7272
auth: AgentAuthConfig = Field(
73-
...,
73+
...,
7474
description="Authentication and authorization configuration"
7575
)
7676
helm_repository_name: str = Field(
77-
default="scale-egp",
78-
description="Helm repository name for the environment"
77+
default="scale-egp",
78+
description="Helm repository name for the environment (classic mode)"
7979
)
8080
helm_repository_url: str = Field(
81-
default="https://scale-egp-helm-charts-us-west-2.s3.amazonaws.com/charts",
82-
description="Helm repository url for the environment"
81+
default="https://scale-egp-helm-charts-us-west-2.s3.amazonaws.com/charts",
82+
description="Helm repository url for the environment (classic mode)"
83+
)
84+
helm_oci_registry: str | None = Field(
85+
default=None,
86+
description="OCI registry URL for Helm charts (e.g., 'us-west1-docker.pkg.dev/project/repo'). "
87+
"When set, OCI mode is used instead of classic helm repo."
88+
)
89+
helm_chart_version: str | None = Field(
90+
default=None,
91+
description="Helm chart version to deploy. If not set, uses the default version from the CLI."
8392
)
8493
helm_overrides: Dict[str, Any] = Field(
85-
default_factory=dict,
94+
default_factory=dict,
8695
description="Helm chart value overrides for environment-specific tuning"
8796
)
8897

98+
def uses_oci_registry(self) -> bool:
99+
"""Check if this environment uses OCI registry for Helm charts."""
100+
return self.helm_oci_registry is not None
101+
89102

90103
class AgentEnvironmentsConfig(UtilsBaseModel):
91104
"""All environment configurations for an agent."""

0 commit comments

Comments
 (0)