Skip to content

Commit 0064558

Browse files
authored
Merge pull request #80 from scaleapi/jason/environments
jason/environments
2 parents d73c1b0 + d24b0b2 commit 0064558

File tree

18 files changed

+773
-350
lines changed

18 files changed

+773
-350
lines changed

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

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
InputDeployOverrides,
2020
deploy_agent,
2121
)
22+
from agentex.lib.sdk.config.validation import (
23+
validate_manifest_and_environments,
24+
EnvironmentsValidationError,
25+
generate_helpful_error_message
26+
)
2227
from agentex.lib.cli.utils.cli_utils import handle_questionary_cancellation
2328
from agentex.lib.cli.utils.kubectl_utils import (
2429
check_and_switch_cluster_context,
@@ -243,18 +248,18 @@ def deploy(
243248
cluster: str = typer.Option(
244249
..., help="Target cluster name (must match kubectl context)"
245250
),
251+
environment: str = typer.Option(
252+
..., help="Environment name (dev, prod, etc.) - must be defined in environments.yaml"
253+
),
246254
manifest: str = typer.Option("manifest.yaml", help="Path to the manifest file"),
247255
namespace: str | None = typer.Option(
248256
None,
249-
help="Kubernetes namespace to deploy to (required in non-interactive mode)",
257+
help="Override Kubernetes namespace (defaults to namespace from environments.yaml)",
250258
),
251259
tag: str | None = typer.Option(None, help="Override the image tag for deployment"),
252260
repository: str | None = typer.Option(
253261
None, help="Override the repository for deployment"
254262
),
255-
override_file: str | None = typer.Option(
256-
None, help="Path to override configuration file"
257-
),
258263
interactive: bool = typer.Option(
259264
True, "--interactive/--no-interactive", help="Enable interactive prompts"
260265
),
@@ -272,45 +277,43 @@ def deploy(
272277
console.print(f"[red]Error:[/red] Manifest file not found: {manifest}")
273278
raise typer.Exit(1)
274279

275-
# In non-interactive mode, require namespace
276-
if not interactive and not namespace:
277-
console.print(
278-
"[red]Error:[/red] --namespace is required in non-interactive mode"
280+
# Validate manifest and environments configuration
281+
try:
282+
_, environments_config = validate_manifest_and_environments(
283+
str(manifest_path),
284+
required_environment=environment
279285
)
286+
agent_env_config = environments_config.get_config_for_env(environment)
287+
console.print(f"[green]✓[/green] Environment config validated: {environment}")
288+
289+
except EnvironmentsValidationError as e:
290+
error_msg = generate_helpful_error_message(e, "Environment validation failed")
291+
console.print(f"[red]Configuration Error:[/red]\n{error_msg}")
292+
raise typer.Exit(1)
293+
except Exception as e:
294+
console.print(f"[red]Error:[/red] Failed to validate configuration: {e}")
280295
raise typer.Exit(1)
281-
282-
# Get namespace if not provided (only in interactive mode)
283-
if not namespace:
284-
namespace = questionary.text(
285-
"Enter Kubernetes namespace:", default="default"
286-
).ask()
287-
namespace = handle_questionary_cancellation(namespace, "namespace input")
288-
289-
if not namespace:
290-
console.print("Deployment cancelled")
291-
raise typer.Exit(0)
292-
293-
# Validate override file exists if provided
294-
if override_file:
295-
override_path = Path(override_file)
296-
if not override_path.exists():
297-
console.print(
298-
f"[red]Error:[/red] Override file not found: {override_file}"
299-
)
300-
raise typer.Exit(1)
301296

302297
# Load manifest for credential validation
303298
manifest_obj = AgentManifest.from_yaml(str(manifest_path))
304299

300+
# Use namespace from environment config if not overridden
301+
if not namespace:
302+
namespace_from_config = agent_env_config.kubernetes.namespace if agent_env_config.kubernetes else None
303+
if namespace_from_config:
304+
console.print(f"[blue]ℹ[/blue] Using namespace from environments.yaml: {namespace_from_config}")
305+
namespace = namespace_from_config
306+
else:
307+
raise DeploymentError(f"No namespace found in environments.yaml for environment: {environment}, and not passed in as --namespace")
308+
305309
# Confirm deployment (only in interactive mode)
306310
console.print("\n[bold]Deployment Summary:[/bold]")
307311
console.print(f" Manifest: {manifest}")
312+
console.print(f" Environment: {environment}")
308313
console.print(f" Cluster: {cluster}")
309314
console.print(f" Namespace: {namespace}")
310315
if tag:
311316
console.print(f" Image Tag: {tag}")
312-
if override_file:
313-
console.print(f" Override File: {override_file}")
314317

315318
if interactive:
316319
proceed = questionary.confirm("Proceed with deployment?").ask()
@@ -339,7 +342,7 @@ def deploy(
339342
cluster_name=cluster,
340343
namespace=namespace,
341344
deploy_overrides=deploy_overrides,
342-
override_file_path=override_file,
345+
environment_name=environment,
343346
)
344347

345348
# Use the already loaded manifest object

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def create_project_structure(
6565
".dockerignore.j2": ".dockerignore",
6666
"manifest.yaml.j2": "manifest.yaml",
6767
"README.md.j2": "README.md",
68+
"environments.yaml.j2": "environments.yaml",
6869
}
6970

7071
# Add package management file based on uv choice

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

Lines changed: 47 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
from pydantic import BaseModel, Field
99
from rich.console import Console
1010

11-
from agentex.lib.cli.utils.auth_utils import _encode_principal_context
1211
from agentex.lib.cli.utils.exceptions import DeploymentError, HelmError
12+
from agentex.lib.sdk.config.environment_config import AgentEnvironmentConfig
1313
from agentex.lib.cli.utils.kubectl_utils import check_and_switch_cluster_context
1414
from agentex.lib.cli.utils.path_utils import calculate_docker_acp_module, PathResolutionError
1515
from agentex.lib.environment_variables import EnvVarKeys
1616
from agentex.lib.sdk.config.agent_config import AgentConfig
1717
from agentex.lib.sdk.config.agent_manifest import AgentManifest
18-
from agentex.lib.sdk.config.deployment_config import ClusterConfig
18+
1919
from agentex.lib.utils.logging import make_logger
2020

2121
logger = make_logger(__name__)
@@ -76,25 +76,6 @@ def add_helm_repo() -> None:
7676
raise HelmError(f"Failed to add helm repository: {e}") from e
7777

7878

79-
def load_override_config(override_file_path: str | None = None) -> ClusterConfig | None:
80-
"""Load override configuration from specified file path"""
81-
if not override_file_path:
82-
return None
83-
84-
override_path = Path(override_file_path)
85-
if not override_path.exists():
86-
raise DeploymentError(f"Override file not found: {override_file_path}")
87-
88-
try:
89-
with open(override_path) as f:
90-
config_data = yaml.safe_load(f)
91-
return ClusterConfig(**config_data) if config_data else None
92-
except Exception as e:
93-
raise DeploymentError(
94-
f"Failed to load override config from {override_file_path}: {e}"
95-
) from e
96-
97-
9879

9980
def convert_env_vars_dict_to_list(env_vars: dict[str, str]) -> list[dict[str, str]]:
10081
"""Convert a dictionary of environment variables to a list of dictionaries"""
@@ -116,13 +97,13 @@ def add_acp_command_to_helm_values(helm_values: dict[str, Any], manifest: AgentM
11697

11798
def merge_deployment_configs(
11899
manifest: AgentManifest,
119-
cluster_config: ClusterConfig | None,
100+
agent_env_config: AgentEnvironmentConfig | None,
120101
deploy_overrides: InputDeployOverrides,
121102
manifest_path: str,
122103
) -> dict[str, Any]:
123104
agent_config: AgentConfig = manifest.agent
124105

125-
"""Merge global deployment config with cluster-specific overrides into helm values"""
106+
"""Merge global deployment config with environment-specific overrides into helm values"""
126107
if not manifest.deployment:
127108
raise DeploymentError("No deployment configuration found in manifest")
128109

@@ -185,18 +166,27 @@ def merge_deployment_configs(
185166
"taskQueue": temporal_config.queue_name,
186167
}
187168

188-
# Collect all environment variables with conflict detection
169+
# Collect all environment variables with proper precedence
170+
# Priority: manifest -> environments.yaml -> secrets (highest)
189171
all_env_vars: dict[str, str] = {}
190172
secret_env_vars: list[dict[str, str]] = []
191173

192-
# Start with agent_config env vars
174+
# Start with agent_config env vars from manifest
193175
if agent_config.env:
194176
all_env_vars.update(agent_config.env)
177+
178+
# Override with environment config env vars if they exist
179+
if agent_env_config and agent_env_config.helm_overrides and "env" in agent_env_config.helm_overrides:
180+
env_overrides = agent_env_config.helm_overrides["env"]
181+
if isinstance(env_overrides, list):
182+
# Convert list format to dict for easier merging
183+
env_override_dict: dict[str, str] = {}
184+
for env_var in env_overrides:
185+
if isinstance(env_var, dict) and "name" in env_var and "value" in env_var:
186+
env_override_dict[str(env_var["name"])] = str(env_var["value"])
187+
all_env_vars.update(env_override_dict)
188+
195189

196-
# Add auth principal env var if manifest principal is set
197-
encoded_principal = _encode_principal_context(manifest)
198-
if encoded_principal:
199-
all_env_vars[EnvVarKeys.AUTH_PRINCIPAL_B64.value] = encoded_principal
200190

201191
# Handle credentials and check for conflicts
202192
if agent_config.credentials:
@@ -228,57 +218,23 @@ def merge_deployment_configs(
228218
}
229219
)
230220

231-
# Apply cluster-specific overrides
232-
if cluster_config:
233-
if cluster_config.image:
234-
if cluster_config.image.repository:
235-
helm_values["global"]["image"]["repository"] = (
236-
cluster_config.image.repository
237-
)
238-
if cluster_config.image.tag:
239-
helm_values["global"]["image"]["tag"] = cluster_config.image.tag
240-
241-
if cluster_config.replicaCount is not None:
242-
helm_values["replicaCount"] = cluster_config.replicaCount
243-
244-
if cluster_config.resources:
245-
if cluster_config.resources.requests:
246-
helm_values["resources"]["requests"].update(
247-
{
248-
"cpu": cluster_config.resources.requests.cpu,
249-
"memory": cluster_config.resources.requests.memory,
250-
}
251-
)
252-
if cluster_config.resources.limits:
253-
helm_values["resources"]["limits"].update(
254-
{
255-
"cpu": cluster_config.resources.limits.cpu,
256-
"memory": cluster_config.resources.limits.memory,
257-
}
258-
)
259-
260-
# Handle cluster env vars with conflict detection
261-
if cluster_config.env:
262-
# Convert cluster env list to dict for easier conflict detection
263-
cluster_env_dict = {env_var["name"]: env_var["value"] for env_var in cluster_config.env}
264-
265-
# Check for conflicts with secret env vars
266-
for secret_env_var in secret_env_vars:
267-
if secret_env_var["name"] in cluster_env_dict:
268-
logger.warning(
269-
f"Environment variable '{secret_env_var['name']}' is defined in both "
270-
f"cluster config env and secretEnvVars. The secret value will take precedence."
271-
)
272-
del cluster_env_dict[secret_env_var["name"]]
273-
274-
# Update all_env_vars with cluster overrides
275-
all_env_vars.update(cluster_env_dict)
276-
277-
# Apply additional arbitrary overrides
278-
if cluster_config.additional_overrides:
279-
_deep_merge(helm_values, cluster_config.additional_overrides)
221+
# Apply agent environment configuration overrides
222+
if agent_env_config:
223+
# Add auth principal env var if environment config is set
224+
if agent_env_config.auth:
225+
from agentex.lib.cli.utils.auth_utils import _encode_principal_context_from_env_config
226+
encoded_principal = _encode_principal_context_from_env_config(agent_env_config.auth)
227+
logger.info(f"Encoding auth principal from {agent_env_config.auth}")
228+
if encoded_principal:
229+
all_env_vars[EnvVarKeys.AUTH_PRINCIPAL_B64.value] = encoded_principal
230+
else:
231+
raise DeploymentError(f"Auth principal unable to be encoded for agent_env_config: {agent_env_config}")
232+
233+
if agent_env_config.helm_overrides:
234+
_deep_merge(helm_values, agent_env_config.helm_overrides)
280235

281236
# Set final environment variables
237+
# Environment variable precedence: manifest -> environments.yaml -> secrets (highest)
282238
if all_env_vars:
283239
helm_values["env"] = convert_env_vars_dict_to_list(all_env_vars)
284240

@@ -295,7 +251,7 @@ def merge_deployment_configs(
295251
# Handle image pull secrets
296252
if manifest.deployment and manifest.deployment.imagePullSecrets:
297253
pull_secrets = [
298-
pull_secret.to_dict()
254+
pull_secret.model_dump()
299255
for pull_secret in manifest.deployment.imagePullSecrets
300256
]
301257
helm_values["global"]["imagePullSecrets"] = pull_secrets
@@ -333,7 +289,7 @@ def deploy_agent(
333289
cluster_name: str,
334290
namespace: str,
335291
deploy_overrides: InputDeployOverrides,
336-
override_file_path: str | None = None,
292+
environment_name: str | None = None,
337293
) -> None:
338294
"""Deploy an agent using helm"""
339295

@@ -345,21 +301,23 @@ def deploy_agent(
345301
check_and_switch_cluster_context(cluster_name)
346302

347303
manifest = AgentManifest.from_yaml(file_path=manifest_path)
348-
override_config = load_override_config(override_file_path)
349304

350-
# Provide feedback about override configuration
351-
if override_config:
352-
console.print(f"[green]✓[/green] Using override config: {override_file_path}")
353-
else:
354-
console.print(
355-
"[yellow]ℹ[/yellow] No override config specified, using global defaults"
356-
)
305+
# Load agent environment configuration
306+
agent_env_config = None
307+
if environment_name:
308+
manifest_dir = Path(manifest_path).parent
309+
environments_config = manifest.load_environments_config(manifest_dir)
310+
if environments_config:
311+
agent_env_config = environments_config.get_config_for_env(environment_name)
312+
console.print(f"[green]✓[/green] Using environment config: {environment_name}")
313+
else:
314+
console.print(f"[yellow]⚠[/yellow] No environments.yaml found, skipping environment-specific config")
357315

358316
# Add helm repository/update
359317
add_helm_repo()
360318

361319
# Merge configurations
362-
helm_values = merge_deployment_configs(manifest, override_config, deploy_overrides, manifest_path)
320+
helm_values = merge_deployment_configs(manifest, agent_env_config, deploy_overrides, manifest_path)
363321

364322
# Create values file
365323
values_file = create_helm_values_file(helm_values)

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from rich.console import Console
77
from rich.panel import Panel
88

9-
from agentex.lib.cli.utils.auth_utils import _encode_principal_context
109
from agentex.lib.cli.handlers.cleanup_handlers import (
1110
cleanup_agent_workflows,
1211
should_cleanup_on_restart
@@ -374,10 +373,13 @@ def create_agent_environment(manifest: AgentManifest) -> dict[str, str]:
374373
"ACP_PORT": str(manifest.local_development.agent.port),
375374
}
376375

377-
# Add authorization principal if set
376+
# Add authorization principal if set - for local development, auth is optional
377+
from agentex.lib.cli.utils.auth_utils import _encode_principal_context
378378
encoded_principal = _encode_principal_context(manifest)
379379
if encoded_principal:
380380
env_vars[EnvVarKeys.AUTH_PRINCIPAL_B64] = encoded_principal
381+
else:
382+
logger.info("No auth principal configured - agent will run without authentication context")
381383

382384
# Add description if available
383385
if manifest.agent.description:

0 commit comments

Comments
 (0)