diff --git a/src/aks-agent/HISTORY.rst b/src/aks-agent/HISTORY.rst index d6de8f0fc5d..e8e77545933 100644 --- a/src/aks-agent/HISTORY.rst +++ b/src/aks-agent/HISTORY.rst @@ -12,6 +12,13 @@ To release a new version, please select a new version number (usually plus 1 to Pending +++++++ +1.0.0b22 +++++++++ +* Bump aks-agent to v0.7.1 + * Suppress litellm debug logs +* Feature: Separate Azure OpenAI provider into API Key and Microsoft Entra ID (keyless) providers +* Feature: Add --yes/-y flag to agent-cleanup command to skip confirmation prompt + 1.0.0b21 ++++++++ * Bump aks-agent to v0.6.0 diff --git a/src/aks-agent/azext_aks_agent/_consts.py b/src/aks-agent/azext_aks_agent/_consts.py index 4d121032dd5..a60a4368074 100644 --- a/src/aks-agent/azext_aks_agent/_consts.py +++ b/src/aks-agent/azext_aks_agent/_consts.py @@ -52,7 +52,7 @@ AKS_MCP_LABEL_SELECTOR = "app.kubernetes.io/name=aks-mcp" # AKS Agent Version (shared by helm chart and docker image) -AKS_AGENT_VERSION = "0.6.0" +AKS_AGENT_VERSION = "0.7.1" # Helm Configuration HELM_VERSION = "3.16.0" diff --git a/src/aks-agent/azext_aks_agent/_params.py b/src/aks-agent/azext_aks_agent/_params.py index c5d34d75613..b96c193ea49 100644 --- a/src/aks-agent/azext_aks_agent/_params.py +++ b/src/aks-agent/azext_aks_agent/_params.py @@ -111,3 +111,9 @@ def load_arguments(self, _): help="The mode decides how the agent is deployed.", default="cluster", ) + c.argument( + "yes", + options_list=["--yes", "-y"], + action="store_true", + help="Do not prompt for confirmation.", + ) diff --git a/src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py b/src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py index 5f2deb7e7fe..79e54a4afe8 100644 --- a/src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py +++ b/src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py @@ -232,7 +232,7 @@ def _populate_api_keys_from_secret(self): ) if not secret.data: - logger.warning("Secret '%s' exists but has no data", self.llm_secret_name) + logger.debug("Secret '%s' exists but has no data", self.llm_secret_name) return # Decode secret data (base64 encoded) @@ -927,6 +927,22 @@ def _create_helm_values(self): "create": False, } + # Configure aks-agent pod to use the same service account as aks-mcp for workload identity + helm_values["workloadIdentity"] = { + "enabled": True, + } + helm_values["serviceAccount"] = { + "create": False, + "name": self.aks_mcp_service_account_name, + } + + has_empty_api_key = any( + not model_config.get("api_key") or not model_config.get("api_key").strip() + for model_config in self.llm_config_manager.model_list.values() + ) + if has_empty_api_key: + helm_values["azureADTokenAuth"] = True + return helm_values def save_llm_config(self, provider: LLMProvider, params: dict) -> None: diff --git a/src/aks-agent/azext_aks_agent/agent/llm_config_manager.py b/src/aks-agent/azext_aks_agent/agent/llm_config_manager.py index 8461ac931b1..375cd299c1b 100644 --- a/src/aks-agent/azext_aks_agent/agent/llm_config_manager.py +++ b/src/aks-agent/azext_aks_agent/agent/llm_config_manager.py @@ -50,8 +50,10 @@ def get_env_vars(self, secret_name: str) -> List[Dict[str, str]]: """ env_vars_list = [] for _, model_config in self.model_list.items(): - env_var = LLMProvider.to_env_vars(secret_name, model_config) - env_vars_list.append(env_var) + api_key = model_config.get("api_key") + if api_key and api_key.strip(): + env_var = LLMProvider.to_env_vars(secret_name, model_config) + env_vars_list.append(env_var) return env_vars_list diff --git a/src/aks-agent/azext_aks_agent/agent/llm_providers/__init__.py b/src/aks-agent/azext_aks_agent/agent/llm_providers/__init__.py index c98dc8e52db..04d932f8322 100644 --- a/src/aks-agent/azext_aks_agent/agent/llm_providers/__init__.py +++ b/src/aks-agent/azext_aks_agent/agent/llm_providers/__init__.py @@ -10,6 +10,7 @@ from .anthropic_provider import AnthropicProvider from .azure_provider import AzureProvider +from .azure_entraid_provider import AzureEntraIDProvider from .base import LLMProvider from .gemini_provider import GeminiProvider from .openai_compatible_provider import OpenAICompatibleProvider @@ -19,11 +20,11 @@ _PROVIDER_CLASSES: List[LLMProvider] = [ AzureProvider, + AzureEntraIDProvider, OpenAIProvider, AnthropicProvider, GeminiProvider, OpenAICompatibleProvider, - # Add new providers here ] PROVIDER_REGISTRY = {} @@ -49,8 +50,9 @@ def _get_provider_by_index(idx: int) -> LLMProvider: Raises ValueError if index is out of range. """ if 1 <= idx <= len(_PROVIDER_CLASSES): - console.print("You selected provider:", _PROVIDER_CLASSES[idx - 1]().readable_name, style=f"bold {HELP_COLOR}") - return _PROVIDER_CLASSES[idx - 1]() + provider = _PROVIDER_CLASSES[idx - 1]() + console.print("You selected provider:", provider.readable_name, style=f"bold {HELP_COLOR}") + return provider raise ValueError(f"Invalid provider index: {idx}") diff --git a/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_entraid_provider.py b/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_entraid_provider.py new file mode 100644 index 00000000000..117b5e989fe --- /dev/null +++ b/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_entraid_provider.py @@ -0,0 +1,58 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +from typing import Tuple + +from .base import LLMProvider, is_valid_url, non_empty + + +def is_valid_api_base(v: str) -> bool: + if not v.startswith("https://"): + return False + return is_valid_url(v) + + +class AzureEntraIDProvider(LLMProvider): + @property + def readable_name(self) -> str: + return "Azure OpenAI (Microsoft Entra ID)" + + @property + def model_route(self) -> str: + return "azure" + + @property + def parameter_schema(self): + return { + "model": { + "secret": False, + "default": None, + "hint": "ensure your deployment name is the same as the model name, e.g., gpt-5", + "validator": non_empty, + "alias": "deployment_name" + }, + "api_base": { + "secret": False, + "default": None, + "validator": is_valid_api_base + }, + "api_version": { + "secret": False, + "default": "2025-04-01-preview", + "hint": None, + "validator": non_empty + } + } + + def validate_connection(self, params: dict) -> Tuple[str, str]: + api_base = params.get("api_base") + api_version = params.get("api_version") + deployment_name = params.get("model") + + if not all([api_base, api_version, deployment_name]): + return "Missing required Azure parameters.", "retry_input" + + return None, "save" diff --git a/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_provider.py b/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_provider.py index 658b8441e99..60f7a44cd13 100644 --- a/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_provider.py +++ b/src/aks-agent/azext_aks_agent/agent/llm_providers/azure_provider.py @@ -24,7 +24,7 @@ def is_valid_api_base(v: str) -> bool: class AzureProvider(LLMProvider): @property def readable_name(self) -> str: - return "Azure OpenAI" + return "Azure OpenAI (API Key)" @property def model_route(self) -> str: diff --git a/src/aks-agent/azext_aks_agent/agent/llm_providers/base.py b/src/aks-agent/azext_aks_agent/agent/llm_providers/base.py index 3e4c1135c26..9b35fca1a39 100644 --- a/src/aks-agent/azext_aks_agent/agent/llm_providers/base.py +++ b/src/aks-agent/azext_aks_agent/agent/llm_providers/base.py @@ -175,6 +175,8 @@ def to_k8s_secret_data(cls, params: dict): """ secret_key = cls.sanitize_k8s_secret_key(params) secret_value = params.get("api_key") + if not secret_value or not secret_value.strip(): + return {} secret_data = { secret_key: base64.b64encode(secret_value.encode("utf-8")).decode("utf-8"), } @@ -206,9 +208,13 @@ def to_secured_model_list_config(cls, params: dict) -> Dict[str, dict]: """Create a model config dictionary for the model list from the provider parameters. Returns a copy of params with the api_key replaced by environment variable reference. """ - secret_key = cls.sanitize_k8s_secret_key(params) secured_params = params.copy() - secured_params.update({"api_key": f"{{{{ env.{secret_key} }}}}"}) + api_key = params.get("api_key") + if api_key and api_key.strip(): + secret_key = cls.sanitize_k8s_secret_key(params) + secured_params.update({"api_key": f"{{{{ env.{secret_key} }}}}"}) + else: + secured_params.pop("api_key", None) return secured_params @classmethod diff --git a/src/aks-agent/azext_aks_agent/custom.py b/src/aks-agent/azext_aks_agent/custom.py index f7e15e9b7ad..e3e6e670e1a 100644 --- a/src/aks-agent/azext_aks_agent/custom.py +++ b/src/aks-agent/azext_aks_agent/custom.py @@ -179,6 +179,19 @@ def _setup_helm_deployment(console, aks_agent_manager: AKSAgentManager): f"\n👤 Current service account in namespace '{aks_agent_manager.namespace}': {service_account_name}", style="cyan") + # Check if using Azure Entra ID provider and show role assignment reminder + model_list = aks_agent_manager.get_llm_config() + if model_list and any("azure/" in model_name and not model_config.get("api_key") for model_name, model_config in model_list.items()): + console.print( + f"\n⚠️ IMPORTANT: If using keyless authentication with Azure OpenAI, ensure the 'Cognitive Services OpenAI User' or 'Azure AI Developer' role " + f"is assigned to the workload identity (service account: {service_account_name}).", + style=f"bold {INFO_COLOR}" + ) + console.print( + "Learn more: https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/managed-identity\n", + style=INFO_COLOR + ) + elif helm_status == "not_found": console.print( f"Helm chart not deployed (status: {helm_status}). Setting up deployment...", @@ -197,6 +210,19 @@ def _setup_helm_deployment(console, aks_agent_manager: AKSAgentManager): "'azure.workload.identity/client-id: '.", style=WARNING_COLOR) + # Check if using Azure Entra ID provider and show role assignment note + model_list = aks_agent_manager.get_llm_config() + if model_list and any("azure/" in model_name and not model_config.get("api_key") for model_name, model_config in model_list.items()): + console.print( + "\n⚠️ NOTE: You are using keyless authentication with Azure OpenAI. " + "Ensure the 'Cognitive Services OpenAI User' or 'Azure AI Developer' role is assigned to the workload identity.", + style=f"bold {INFO_COLOR}" + ) + console.print( + "Learn more: https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/managed-identity", + style=INFO_COLOR + ) + # Prompt user for service account name (required) while True: user_input = console.input( @@ -422,6 +448,7 @@ def aks_agent_cleanup( cluster_name, namespace, mode=None, + yes=False, ): """Cleanup and uninstall the AKS agent.""" with CLITelemetryClient(event_type="cleanup") as telemetry_client: @@ -442,16 +469,17 @@ def aks_agent_cleanup( f"⚠️ Warning: --namespace '{namespace}' is specified but will be ignored in client mode.", style=WARNING_COLOR) - console.print( - "\n⚠️ Warning: This will uninstall the AKS agent and delete all associated resources.", - style=WARNING_COLOR) + if not yes: + console.print( + "\n⚠️ Warning: This will uninstall the AKS agent and delete all associated resources.", + style=WARNING_COLOR) - user_confirmation = console.input( - f"\n[{WARNING_COLOR}]Are you sure you want to proceed with cleanup? (y/N): [/]").strip().lower() + user_confirmation = console.input( + f"\n[{WARNING_COLOR}]Are you sure you want to proceed with cleanup? (y/N): [/]").strip().lower() - if user_confirmation not in ['y', 'yes']: - console.print("❌ Cleanup cancelled.", style=INFO_COLOR) - return + if user_confirmation not in ['y', 'yes']: + console.print("❌ Cleanup cancelled.", style=INFO_COLOR) + return console.print("\n🗑️ Starting cleanup (this typically takes a few seconds)...", style=INFO_COLOR) diff --git a/src/aks-agent/setup.py b/src/aks-agent/setup.py index db249ebd4e5..da915508c4f 100644 --- a/src/aks-agent/setup.py +++ b/src/aks-agent/setup.py @@ -9,7 +9,7 @@ from setuptools import find_packages, setup -VERSION = "1.0.0b21" +VERSION = "1.0.0b22" CLASSIFIERS = [ "Development Status :: 4 - Beta",