Skip to content

Commit f4a5676

Browse files
committed
refactor: nest OCI fields into sub-model and extract resolve_chart helper
Address PR #255 review comments: - Create OciRegistryConfig sub-model replacing flat helm_oci_* fields - Remove use_oci boolean, branch on oci_registry presence directly - Extract resolve_chart() combining chart reference + version resolution - Add tests for nested OCI config YAML parsing
1 parent 257ca6c commit f4a5676

File tree

3 files changed

+120
-83
lines changed

3 files changed

+120
-83
lines changed

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

Lines changed: 51 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from agentex.lib.cli.utils.kubectl_utils import check_and_switch_cluster_context
1818
from agentex.lib.sdk.config.agent_config import AgentConfig
1919
from agentex.lib.sdk.config.agent_manifest import AgentManifest
20-
from agentex.lib.sdk.config.environment_config import AgentEnvironmentConfig
20+
from agentex.lib.sdk.config.environment_config import OciRegistryConfig, AgentEnvironmentConfig
2121

2222
logger = make_logger(__name__)
2323
console = Console()
@@ -113,8 +113,7 @@ def login_to_gar_registry(oci_registry: str) -> None:
113113
) from e
114114
except FileNotFoundError:
115115
raise HelmError(
116-
"gcloud CLI not found. Please install the Google Cloud SDK: "
117-
"https://cloud.google.com/sdk/docs/install"
116+
"gcloud CLI not found. Please install the Google Cloud SDK: https://cloud.google.com/sdk/docs/install"
118117
) from None
119118

120119

@@ -170,9 +169,7 @@ def get_latest_gar_chart_version(oci_registry: str, chart_name: str = "agentex-a
170169

171170
output = result.stdout.strip()
172171
if not output:
173-
raise HelmError(
174-
f"No tags found for chart '{chart_name}' in {oci_registry}"
175-
)
172+
raise HelmError(f"No tags found for chart '{chart_name}' in {oci_registry}")
176173

177174
# The output is the tag name (semantic version)
178175
version = output
@@ -181,43 +178,55 @@ def get_latest_gar_chart_version(oci_registry: str, chart_name: str = "agentex-a
181178

182179
except subprocess.CalledProcessError as e:
183180
raise HelmError(
184-
f"Failed to fetch chart tags from GAR: {e.stderr}\n"
185-
"Ensure you have access to the Artifact Registry."
181+
f"Failed to fetch chart tags from GAR: {e.stderr}\nEnsure you have access to the Artifact Registry."
186182
) from e
187183
except FileNotFoundError:
188184
raise HelmError(
189-
"gcloud CLI not found. Please install the Google Cloud SDK: "
190-
"https://cloud.google.com/sdk/docs/install"
185+
"gcloud CLI not found. Please install the Google Cloud SDK: https://cloud.google.com/sdk/docs/install"
191186
) from None
192187

193188

194-
def get_chart_reference(
195-
use_oci: bool,
196-
helm_repository_name: str | None = None,
197-
oci_registry: str | None = None,
189+
def resolve_chart(
190+
oci_registry: OciRegistryConfig | None,
191+
helm_repository_name: str | None,
192+
use_latest_chart: bool,
198193
chart_name: str = "agentex-agent",
199-
) -> str:
200-
"""Get the chart reference based on the deployment mode.
194+
) -> tuple[str, str]:
195+
"""Resolve the chart reference and version based on the deployment mode.
201196
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
197+
For OCI mode, builds an oci:// reference and resolves version from:
198+
--use-latest-chart (GAR only) > oci_registry.chart_version > default.
199+
For classic mode, builds a repo/chart reference and uses default version.
207200
208201
Returns:
209-
Chart reference string for helm install/upgrade commands
202+
(chart_reference, chart_version)
210203
"""
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}"
204+
if oci_registry:
205+
chart_reference = f"oci://{oci_registry.url}/{chart_name}"
206+
207+
if use_latest_chart:
208+
if oci_registry.provider != "gar":
209+
console.print(
210+
"[yellow]⚠[/yellow] --use-latest-chart only works with GAR provider (provider: gar), using default version"
211+
)
212+
chart_version = DEFAULT_HELM_CHART_VERSION
213+
else:
214+
chart_version = get_latest_gar_chart_version(oci_registry.url)
215+
elif oci_registry.chart_version:
216+
chart_version = oci_registry.chart_version
217+
else:
218+
chart_version = DEFAULT_HELM_CHART_VERSION
216219
else:
217220
if not helm_repository_name:
218221
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}"
222+
chart_reference = f"{helm_repository_name}/{chart_name}"
223+
224+
if use_latest_chart:
225+
console.print("[yellow]⚠[/yellow] --use-latest-chart only works with OCI registries, using default version")
226+
chart_version = DEFAULT_HELM_CHART_VERSION
227+
228+
console.print(f"[blue]ℹ[/blue] Using Helm chart version: {chart_version}")
229+
return chart_reference, chart_version
221230

222231

223232
def convert_env_vars_dict_to_list(env_vars: dict[str, str]) -> list[dict[str, str]]:
@@ -465,24 +474,20 @@ def deploy_agent(
465474
else:
466475
console.print(f"[yellow]⚠[/yellow] No environments.yaml found, skipping environment-specific config")
467476

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
477+
# Determine deployment mode: OCI registry or classic helm repo
478+
oci_registry = agent_env_config.oci_registry if agent_env_config else None
470479
helm_repository_name: str | None = None
471-
oci_registry: str | None = None
472480

473-
# Track OCI provider for provider-specific features
474-
oci_provider: str | None = None
475-
476-
if use_oci:
477-
oci_registry = agent_env_config.helm_oci_registry # type: ignore[union-attr]
478-
oci_provider = agent_env_config.helm_oci_provider # type: ignore[union-attr]
479-
console.print(f"[blue]ℹ[/blue] Using OCI Helm registry: {oci_registry}")
481+
if oci_registry:
482+
console.print(f"[blue]ℹ[/blue] Using OCI Helm registry: {oci_registry.url}")
480483

481484
# Only auto-authenticate for GAR provider
482-
if oci_provider == "gar":
483-
login_to_gar_registry(oci_registry) # type: ignore[arg-type]
485+
if oci_registry.provider == "gar":
486+
login_to_gar_registry(oci_registry.url)
484487
else:
485-
console.print("[blue]ℹ[/blue] Skipping auto-authentication (no provider specified, assuming already authenticated)")
488+
console.print(
489+
"[blue]ℹ[/blue] Skipping auto-authentication (no provider specified, assuming already authenticated)"
490+
)
486491
else:
487492
if agent_env_config:
488493
helm_repository_name = agent_env_config.helm_repository_name
@@ -493,31 +498,13 @@ def deploy_agent(
493498
# Add helm repository/update (classic mode only)
494499
add_helm_repo(helm_repository_name, helm_repository_url)
495500

496-
# Get the chart reference based on deployment mode
497-
chart_reference = get_chart_reference(
498-
use_oci=use_oci,
499-
helm_repository_name=helm_repository_name,
501+
# Resolve chart reference and version in one step
502+
chart_reference, chart_version = resolve_chart(
500503
oci_registry=oci_registry,
504+
helm_repository_name=helm_repository_name,
505+
use_latest_chart=use_latest_chart,
501506
)
502507

503-
# Determine chart version
504-
# Priority: --use-latest-chart > env config > default
505-
if use_latest_chart:
506-
if not use_oci:
507-
console.print("[yellow]⚠[/yellow] --use-latest-chart only works with OCI registries, using default version")
508-
chart_version = DEFAULT_HELM_CHART_VERSION
509-
elif oci_provider != "gar":
510-
console.print("[yellow]⚠[/yellow] --use-latest-chart only works with GAR provider (helm_oci_provider: gar), using default version")
511-
chart_version = DEFAULT_HELM_CHART_VERSION
512-
else:
513-
chart_version = get_latest_gar_chart_version(oci_registry) # type: ignore[arg-type]
514-
elif agent_env_config and agent_env_config.helm_chart_version:
515-
chart_version = agent_env_config.helm_chart_version
516-
else:
517-
chart_version = DEFAULT_HELM_CHART_VERSION
518-
519-
console.print(f"[blue]ℹ[/blue] Using Helm chart version: {chart_version}")
520-
521508
# Merge configurations
522509
helm_values = merge_deployment_configs(manifest, agent_env_config, deploy_overrides, manifest_path)
523510

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

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,25 @@ def validate_namespace_format(cls, v: str) -> str:
5555
return namespace
5656

5757

58+
class OciRegistryConfig(BaseModel):
59+
"""OCI registry configuration for Helm chart deployments."""
60+
61+
url: str = Field(
62+
...,
63+
description="OCI registry URL for Helm charts (e.g., 'us-west1-docker.pkg.dev/project/repo'). "
64+
"When set, OCI mode is used instead of classic helm repo.",
65+
)
66+
provider: Literal["gar"] | None = Field(
67+
default=None,
68+
description="OCI registry provider for provider-specific features. "
69+
"Set to 'gar' for Google Artifact Registry to enable auto-authentication via gcloud "
70+
"and latest version fetching. When not set, assumes user has already authenticated.",
71+
)
72+
chart_version: str | None = Field(
73+
default=None, description="Helm chart version to deploy. If not set, uses the default version from the CLI."
74+
)
75+
76+
5877
class AgentEnvironmentConfig(BaseModel):
5978
"""Complete configuration for an agent in a specific environment."""
6079

@@ -67,31 +86,15 @@ class AgentEnvironmentConfig(BaseModel):
6786
helm_repository_name: str = Field(default="scale-egp", description="Helm repository name for the environment")
6887
helm_repository_url: str = Field(
6988
default="https://scale-egp-helm-charts-us-west-2.s3.amazonaws.com/charts",
70-
description="Helm repository url for the environment (classic mode)"
89+
description="Helm repository url for the environment (classic mode)",
7190
)
72-
helm_oci_registry: str | None = Field(
73-
default=None,
74-
description="OCI registry URL for Helm charts (e.g., 'us-west1-docker.pkg.dev/project/repo'). "
75-
"When set, OCI mode is used instead of classic helm repo."
76-
)
77-
helm_oci_provider: Literal["gar"] | None = Field(
78-
default=None,
79-
description="OCI registry provider for provider-specific features. "
80-
"Set to 'gar' for Google Artifact Registry to enable auto-authentication via gcloud "
81-
"and latest version fetching. When not set, assumes user has already authenticated."
82-
)
83-
helm_chart_version: str | None = Field(
84-
default=None,
85-
description="Helm chart version to deploy. If not set, uses the default version from the CLI."
91+
oci_registry: OciRegistryConfig | None = Field(
92+
default=None, description="OCI registry configuration. When set, OCI mode is used instead of classic helm repo."
8693
)
8794
helm_overrides: Dict[str, Any] = Field(
8895
default_factory=dict, description="Helm chart value overrides for environment-specific tuning"
8996
)
9097

91-
def uses_oci_registry(self) -> bool:
92-
"""Check if this environment uses OCI registry for Helm charts."""
93-
return self.helm_oci_registry is not None
94-
9598

9699
class AgentEnvironmentsConfig(UtilsBaseModel):
97100
"""All environment configurations for an agent."""

tests/lib/cli/test_environment_config.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,3 +296,50 @@ def test_load_yaml_missing_required_auth_raises_error(self):
296296

297297
with pytest.raises(ValueError, match="Failed to load"):
298298
AgentEnvironmentsConfig.from_yaml(f.name)
299+
300+
def test_load_yaml_with_oci_registry(self):
301+
"""Test loading YAML with nested oci_registry configuration."""
302+
yaml_content = """
303+
schema_version: v1
304+
environments:
305+
dev:
306+
kubernetes:
307+
namespace: dev-namespace
308+
auth:
309+
principal:
310+
user_id: "user-123"
311+
oci_registry:
312+
url: us-west1-docker.pkg.dev/my-project/my-repo
313+
provider: gar
314+
chart_version: "0.2.0"
315+
"""
316+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
317+
f.write(yaml_content)
318+
f.flush()
319+
320+
config = AgentEnvironmentsConfig.from_yaml(f.name)
321+
322+
env = config.environments["dev"]
323+
assert env.oci_registry is not None
324+
assert env.oci_registry.url == "us-west1-docker.pkg.dev/my-project/my-repo"
325+
assert env.oci_registry.provider == "gar"
326+
assert env.oci_registry.chart_version == "0.2.0"
327+
328+
def test_load_yaml_without_oci_registry(self):
329+
"""Test that oci_registry is None when not specified in YAML."""
330+
yaml_content = """
331+
schema_version: v1
332+
environments:
333+
dev:
334+
kubernetes:
335+
namespace: dev-namespace
336+
auth:
337+
principal:
338+
user_id: "user-123"
339+
"""
340+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
341+
f.write(yaml_content)
342+
f.flush()
343+
344+
config = AgentEnvironmentsConfig.from_yaml(f.name)
345+
assert config.environments["dev"].oci_registry is None

0 commit comments

Comments
 (0)