Skip to content

Commit 15bbb26

Browse files
authored
Merge branch 'main' into lima2/0330-bumo
2 parents 0d45d31 + db11aca commit 15bbb26

File tree

79 files changed

+4324
-654
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+4324
-654
lines changed

src/aks-agent/HISTORY.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ To release a new version, please select a new version number (usually plus 1 to
1212
Pending
1313
+++++++
1414

15+
1.0.0b22
16+
++++++++
17+
* Bump aks-agent to v0.7.1
18+
* Suppress litellm debug logs
19+
* Feature: Separate Azure OpenAI provider into API Key and Microsoft Entra ID (keyless) providers
20+
* Feature: Add --yes/-y flag to agent-cleanup command to skip confirmation prompt
21+
1522
1.0.0b21
1623
++++++++
1724
* Bump aks-agent to v0.6.0

src/aks-agent/azext_aks_agent/_consts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
AKS_MCP_LABEL_SELECTOR = "app.kubernetes.io/name=aks-mcp"
5353

5454
# AKS Agent Version (shared by helm chart and docker image)
55-
AKS_AGENT_VERSION = "0.6.0"
55+
AKS_AGENT_VERSION = "0.7.1"
5656

5757
# Helm Configuration
5858
HELM_VERSION = "3.16.0"

src/aks-agent/azext_aks_agent/_params.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,9 @@ def load_arguments(self, _):
111111
help="The mode decides how the agent is deployed.",
112112
default="cluster",
113113
)
114+
c.argument(
115+
"yes",
116+
options_list=["--yes", "-y"],
117+
action="store_true",
118+
help="Do not prompt for confirmation.",
119+
)

src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ def _populate_api_keys_from_secret(self):
232232
)
233233

234234
if not secret.data:
235-
logger.warning("Secret '%s' exists but has no data", self.llm_secret_name)
235+
logger.debug("Secret '%s' exists but has no data", self.llm_secret_name)
236236
return
237237

238238
# Decode secret data (base64 encoded)
@@ -927,6 +927,22 @@ def _create_helm_values(self):
927927
"create": False,
928928
}
929929

930+
# Configure aks-agent pod to use the same service account as aks-mcp for workload identity
931+
helm_values["workloadIdentity"] = {
932+
"enabled": True,
933+
}
934+
helm_values["serviceAccount"] = {
935+
"create": False,
936+
"name": self.aks_mcp_service_account_name,
937+
}
938+
939+
has_empty_api_key = any(
940+
not model_config.get("api_key") or not model_config.get("api_key").strip()
941+
for model_config in self.llm_config_manager.model_list.values()
942+
)
943+
if has_empty_api_key:
944+
helm_values["azureADTokenAuth"] = True
945+
930946
return helm_values
931947

932948
def save_llm_config(self, provider: LLMProvider, params: dict) -> None:

src/aks-agent/azext_aks_agent/agent/llm_config_manager.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ def get_env_vars(self, secret_name: str) -> List[Dict[str, str]]:
5050
"""
5151
env_vars_list = []
5252
for _, model_config in self.model_list.items():
53-
env_var = LLMProvider.to_env_vars(secret_name, model_config)
54-
env_vars_list.append(env_var)
53+
api_key = model_config.get("api_key")
54+
if api_key and api_key.strip():
55+
env_var = LLMProvider.to_env_vars(secret_name, model_config)
56+
env_vars_list.append(env_var)
5557
return env_vars_list
5658

5759

src/aks-agent/azext_aks_agent/agent/llm_providers/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from .anthropic_provider import AnthropicProvider
1212
from .azure_provider import AzureProvider
13+
from .azure_entraid_provider import AzureEntraIDProvider
1314
from .base import LLMProvider
1415
from .gemini_provider import GeminiProvider
1516
from .openai_compatible_provider import OpenAICompatibleProvider
@@ -19,11 +20,11 @@
1920

2021
_PROVIDER_CLASSES: List[LLMProvider] = [
2122
AzureProvider,
23+
AzureEntraIDProvider,
2224
OpenAIProvider,
2325
AnthropicProvider,
2426
GeminiProvider,
2527
OpenAICompatibleProvider,
26-
# Add new providers here
2728
]
2829

2930
PROVIDER_REGISTRY = {}
@@ -49,8 +50,9 @@ def _get_provider_by_index(idx: int) -> LLMProvider:
4950
Raises ValueError if index is out of range.
5051
"""
5152
if 1 <= idx <= len(_PROVIDER_CLASSES):
52-
console.print("You selected provider:", _PROVIDER_CLASSES[idx - 1]().readable_name, style=f"bold {HELP_COLOR}")
53-
return _PROVIDER_CLASSES[idx - 1]()
53+
provider = _PROVIDER_CLASSES[idx - 1]()
54+
console.print("You selected provider:", provider.readable_name, style=f"bold {HELP_COLOR}")
55+
return provider
5456
raise ValueError(f"Invalid provider index: {idx}")
5557

5658

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
7+
from typing import Tuple
8+
9+
from .base import LLMProvider, is_valid_url, non_empty
10+
11+
12+
def is_valid_api_base(v: str) -> bool:
13+
if not v.startswith("https://"):
14+
return False
15+
return is_valid_url(v)
16+
17+
18+
class AzureEntraIDProvider(LLMProvider):
19+
@property
20+
def readable_name(self) -> str:
21+
return "Azure OpenAI (Microsoft Entra ID)"
22+
23+
@property
24+
def model_route(self) -> str:
25+
return "azure"
26+
27+
@property
28+
def parameter_schema(self):
29+
return {
30+
"model": {
31+
"secret": False,
32+
"default": None,
33+
"hint": "ensure your deployment name is the same as the model name, e.g., gpt-5",
34+
"validator": non_empty,
35+
"alias": "deployment_name"
36+
},
37+
"api_base": {
38+
"secret": False,
39+
"default": None,
40+
"validator": is_valid_api_base
41+
},
42+
"api_version": {
43+
"secret": False,
44+
"default": "2025-04-01-preview",
45+
"hint": None,
46+
"validator": non_empty
47+
}
48+
}
49+
50+
def validate_connection(self, params: dict) -> Tuple[str, str]:
51+
api_base = params.get("api_base")
52+
api_version = params.get("api_version")
53+
deployment_name = params.get("model")
54+
55+
if not all([api_base, api_version, deployment_name]):
56+
return "Missing required Azure parameters.", "retry_input"
57+
58+
return None, "save"

src/aks-agent/azext_aks_agent/agent/llm_providers/azure_provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def is_valid_api_base(v: str) -> bool:
2424
class AzureProvider(LLMProvider):
2525
@property
2626
def readable_name(self) -> str:
27-
return "Azure OpenAI"
27+
return "Azure OpenAI (API Key)"
2828

2929
@property
3030
def model_route(self) -> str:

src/aks-agent/azext_aks_agent/agent/llm_providers/base.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ def to_k8s_secret_data(cls, params: dict):
175175
"""
176176
secret_key = cls.sanitize_k8s_secret_key(params)
177177
secret_value = params.get("api_key")
178+
if not secret_value or not secret_value.strip():
179+
return {}
178180
secret_data = {
179181
secret_key: base64.b64encode(secret_value.encode("utf-8")).decode("utf-8"),
180182
}
@@ -206,9 +208,13 @@ def to_secured_model_list_config(cls, params: dict) -> Dict[str, dict]:
206208
"""Create a model config dictionary for the model list from the provider parameters.
207209
Returns a copy of params with the api_key replaced by environment variable reference.
208210
"""
209-
secret_key = cls.sanitize_k8s_secret_key(params)
210211
secured_params = params.copy()
211-
secured_params.update({"api_key": f"{{{{ env.{secret_key} }}}}"})
212+
api_key = params.get("api_key")
213+
if api_key and api_key.strip():
214+
secret_key = cls.sanitize_k8s_secret_key(params)
215+
secured_params.update({"api_key": f"{{{{ env.{secret_key} }}}}"})
216+
else:
217+
secured_params.pop("api_key", None)
212218
return secured_params
213219

214220
@classmethod

src/aks-agent/azext_aks_agent/custom.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,19 @@ def _setup_helm_deployment(console, aks_agent_manager: AKSAgentManager):
179179
f"\n👤 Current service account in namespace '{aks_agent_manager.namespace}': {service_account_name}",
180180
style="cyan")
181181

182+
# Check if using Azure Entra ID provider and show role assignment reminder
183+
model_list = aks_agent_manager.get_llm_config()
184+
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()):
185+
console.print(
186+
f"\n⚠️ IMPORTANT: If using keyless authentication with Azure OpenAI, ensure the 'Cognitive Services OpenAI User' or 'Azure AI Developer' role "
187+
f"is assigned to the workload identity (service account: {service_account_name}).",
188+
style=f"bold {INFO_COLOR}"
189+
)
190+
console.print(
191+
"Learn more: https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/managed-identity\n",
192+
style=INFO_COLOR
193+
)
194+
182195
elif helm_status == "not_found":
183196
console.print(
184197
f"Helm chart not deployed (status: {helm_status}). Setting up deployment...",
@@ -197,6 +210,19 @@ def _setup_helm_deployment(console, aks_agent_manager: AKSAgentManager):
197210
"'azure.workload.identity/client-id: <managed-identity-client-id>'.",
198211
style=WARNING_COLOR)
199212

213+
# Check if using Azure Entra ID provider and show role assignment note
214+
model_list = aks_agent_manager.get_llm_config()
215+
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()):
216+
console.print(
217+
"\n⚠️ NOTE: You are using keyless authentication with Azure OpenAI. "
218+
"Ensure the 'Cognitive Services OpenAI User' or 'Azure AI Developer' role is assigned to the workload identity.",
219+
style=f"bold {INFO_COLOR}"
220+
)
221+
console.print(
222+
"Learn more: https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/managed-identity",
223+
style=INFO_COLOR
224+
)
225+
200226
# Prompt user for service account name (required)
201227
while True:
202228
user_input = console.input(
@@ -422,6 +448,7 @@ def aks_agent_cleanup(
422448
cluster_name,
423449
namespace,
424450
mode=None,
451+
yes=False,
425452
):
426453
"""Cleanup and uninstall the AKS agent."""
427454
with CLITelemetryClient(event_type="cleanup") as telemetry_client:
@@ -442,16 +469,17 @@ def aks_agent_cleanup(
442469
f"⚠️ Warning: --namespace '{namespace}' is specified but will be ignored in client mode.",
443470
style=WARNING_COLOR)
444471

445-
console.print(
446-
"\n⚠️ Warning: This will uninstall the AKS agent and delete all associated resources.",
447-
style=WARNING_COLOR)
472+
if not yes:
473+
console.print(
474+
"\n⚠️ Warning: This will uninstall the AKS agent and delete all associated resources.",
475+
style=WARNING_COLOR)
448476

449-
user_confirmation = console.input(
450-
f"\n[{WARNING_COLOR}]Are you sure you want to proceed with cleanup? (y/N): [/]").strip().lower()
477+
user_confirmation = console.input(
478+
f"\n[{WARNING_COLOR}]Are you sure you want to proceed with cleanup? (y/N): [/]").strip().lower()
451479

452-
if user_confirmation not in ['y', 'yes']:
453-
console.print("❌ Cleanup cancelled.", style=INFO_COLOR)
454-
return
480+
if user_confirmation not in ['y', 'yes']:
481+
console.print("❌ Cleanup cancelled.", style=INFO_COLOR)
482+
return
455483

456484
console.print("\n🗑️ Starting cleanup (this typically takes a few seconds)...", style=INFO_COLOR)
457485

0 commit comments

Comments
 (0)