Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f97ce1f
add osdisk full cache feature
May 1, 2026
1f6c0ab
Updated existing parameter "identity" to type string[] to configure M…
Balashivaram May 4, 2026
8b370c8
[Release] Update index.json for extension [ azure-firewall-2.2.0 ]
azclibot May 4, 2026
69d46e5
Managed Network Fabric CLI updates for new 2026-01-15-preview API (#9…
idanielsteven May 5, 2026
588ff77
Add Microsoft.Maintenance ScheduledEvents List acknowledge command (#…
rbandi124 May 5, 2026
6413e31
[Release] Update index.json for extension [ managednetworkfabric-10.0…
azclibot May 5, 2026
1aa518d
[Release] Update index.json for extension [ maintenance-2.0.0b1 ]
azclibot May 5, 2026
eede753
{CI} Sync resourceManagement.yml according To ADO Wiki Page - Service…
azclibot May 5, 2026
963f8fd
[aks-agent] Fix client mode when Microsoft Entra ID (keyless) provide…
mainred May 5, 2026
142f8c7
[Release] Update index.json for extension [ aks-agent-1.0.0b23 ]
azclibot May 5, 2026
09043f3
[Core] raw githubusercontent urls are updated to refer azcli blob to …
msarfraz May 6, 2026
7351534
Azure Clean Room: CLI for 2026-04-30-preview (#9848)
anrdesai May 7, 2026
5c6ef7b
[Release] Update index.json for extension [ managedcleanroom-1.0.0b6 ]
azclibot May 7, 2026
674e5f6
Adding a prompt to regenerate new SSH Keys (#9846)
DevanshG1 May 7, 2026
dcdc61d
WO CLI Release/april2026 (#9854)
atharvau May 7, 2026
20c4381
[Release] Update index.json for extension [ sftp-1.0.0b3 ]
azclibot May 7, 2026
5689fd0
[Release] Update index.json for extension [ workload-orchestration-5.…
azclibot May 7, 2026
43d2c03
[ManagedCleanroom] Add --subdirectory parameter for dataset publish (…
imddanish May 8, 2026
36b7b38
[Release] Update index.json for extension [ managedcleanroom-1.0.0b7 ]
azclibot May 8, 2026
ec976be
Migrate cdn from az cli to extensions (#9786)
Ptnan7 May 8, 2026
3c412f5
[Release] Update index.json for extension [ cdn-1.0.0b1 ]
azclibot May 8, 2026
5a7ca89
{confcom}: Simplify logic in map_image_from_tar(_compatibility)?, new…
micromaomao May 12, 2026
9b3e30b
[Release] Update index.json for extension [ confcom ]
azclibot May 12, 2026
e732db8
[dataprotection] Add autoprotection support for blob backup instances…
Komla-Ansah May 12, 2026
4b8c6bb
[Release] Update index.json for extension [ dataprotection-1.10.0 ]
azclibot May 12, 2026
e862dbb
[HORIZONDB] `az horizondb create/show/delete`: Introduce commands for…
nasc17 May 13, 2026
2e68d3a
[Release] Update index.json for extension [ horizondb-1.0.0b1 ]
azclibot May 13, 2026
c21ec22
Fleet 2026-03-02-preview API (clustermesh) (#9647)
frantran May 14, 2026
85a505d
az aks nodepool update supports crg id (#9811)
zjpjack-github May 14, 2026
d2d44b8
[Release] Update index.json for extension [ fleet-1.10.0 ]
azclibot May 14, 2026
9f50e8f
[confcom]: Fix accidental docker dependency even when using tar (#9863)
micromaomao May 14, 2026
88a80d3
[Release] Update index.json for extension [ aks-preview-20.0.0b7 ]
azclibot May 14, 2026
10a6950
[Release] Update index.json for extension [ confcom ]
azclibot May 14, 2026
9977dc8
[front-door] Add WAF rule create examples (#9860)
Ptnan7 May 14, 2026
36142eb
[AKS] Mixed sku autoscaling support for VMs agentpool (#9808)
reneeli123 May 14, 2026
231e89c
[Release] Update index.json for extension [ aks-preview-20.0.0b8 ]
azclibot May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 0 additions & 1 deletion .github/policies/resourceManagement.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1717,7 +1717,6 @@ configuration:
- mentionUsers:
mentionees:
- dkershaw10
- baywet

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may I ask why we need to modify this file?

- SteveMutungi254
replyTemplate: Thanks for the feedback! We are routing this to the appropriate team for follow-up. cc ${mentionees}.
assignMentionees: False
Expand Down
18 changes: 18 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,24 @@ jobs:
ADO_PULL_REQUEST_LATEST_COMMIT: HEAD
ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch)

- job: CheckExternalUrls
displayName: "Check External Source URLs"
condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
pool:
name: ${{ variables.ubuntu_pool }}
steps:
- task: UsePythonVersion@0
displayName: 'Use Python 3.13'
inputs:
versionSpec: 3.13
- bash: |
#!/usr/bin/env bash
set -ev
# External URL exclusions are maintained in scripts/ci/external_url_exclusions.json.
git fetch origin --depth=1 $(System.PullRequest.TargetBranch)
python scripts/ci/validate_external_source_urls.py --src=HEAD --tgt=origin/$(System.PullRequest.TargetBranch)
displayName: 'Validate External Source URLs'

- job: AzdevLinterModifiedExtensions
displayName: "azdev linter on Modified Extensions"
condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
Expand Down
14 changes: 14 additions & 0 deletions scripts/ci/external_url_exclusions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"tool": "External URL Validation",
"scope": {
"include": ["src/**/*.py"],
"exclude": [
"**/tests/**",
"**/vendored_sdks/**",
"**/*help.py"
]
}
}


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may I ask why we need to modify this file?


219 changes: 219 additions & 0 deletions scripts/ci/validate_external_source_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
#!/usr/bin/env python

# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

"""Fail CI if forbidden raw GitHub URL is introduced in new diff lines."""

import argparse

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may I ask why we need to modify this file?

import fnmatch
import json
import re
import subprocess
import sys
from pathlib import Path


GITHUB_URL_PATTERN = re.compile(
r"https?://raw\.githubusercontent\.com/[^\s\"'`,)}\]]*"
)
INLINE_SUPPRESSION_PATTERN = re.compile(
r"#\s*external-url-exempt:\s*\S"
)
_FILENAME_PATTERN = re.compile(r"^[A-Za-z0-9_\-]+\.[A-Za-z0-9]{1,10}$")
RECOMMENDED_INTERNAL_URL = "https://azcliprod.blob.core.windows.net/cli"
SCOPE_CONFIG_PATH = Path(__file__).with_name("external_url_exclusions.json")

# Scope configuration loaded from external_url_exclusions.json.
# Contains optional "include" and "exclude" glob-pattern lists.
_SCOPE_CONFIG = None


def _load_scope_config():
"""Load scope configuration (include/exclude patterns) from the JSON file."""
try:
with SCOPE_CONFIG_PATH.open(encoding="utf-8") as input_file:
config = json.load(input_file)
except (OSError, ValueError) as ex:
raise RuntimeError(f"Unable to load scope config from '{SCOPE_CONFIG_PATH}': {ex}") from ex

if not isinstance(config, dict):
raise RuntimeError(
f"Invalid scope configuration in '{SCOPE_CONFIG_PATH}': expected a JSON object"
)

scope = config.get("scope", {})
if not isinstance(scope, dict):
raise RuntimeError(
f"Invalid scope configuration in '{SCOPE_CONFIG_PATH}': 'scope' must be a JSON object"
)

include = scope.get("include", [])
exclude = scope.get("exclude", [])

if isinstance(include, str):
include = [include]
if isinstance(exclude, str):
exclude = [exclude]

if not isinstance(include, list) or not all(isinstance(p, str) for p in include):
raise RuntimeError(
f"Invalid scope configuration in '{SCOPE_CONFIG_PATH}': 'include' must be a string or array of strings"
)
if not isinstance(exclude, list) or not all(isinstance(p, str) for p in exclude):
raise RuntimeError(
f"Invalid scope configuration in '{SCOPE_CONFIG_PATH}': 'exclude' must be a string or array of strings"
)

return (
[p.replace("\\", "/") for p in include],
[p.replace("\\", "/") for p in exclude],
)


def _get_scope_config():
"""Return cached (include_patterns, exclude_patterns) tuple."""
global _SCOPE_CONFIG # pylint: disable=global-statement

if _SCOPE_CONFIG is None:
_SCOPE_CONFIG = _load_scope_config()

return _SCOPE_CONFIG


def _matches_any(file_path: str, patterns: list) -> bool:
"""Return True if *file_path* matches any of the given glob patterns."""
return any(fnmatch.fnmatch(file_path, p) for p in patterns)



def _extract_filename_from_url(line: str) -> str:
"""Extract the file name from the first GitHub URL found in *line*.

Returns the basename (e.g. ``map.json``) or ``"xxx.xxx"`` when no
recognisable file name is present.
"""
match = GITHUB_URL_PATTERN.search(line)
if match:
url_path = match.group(0).rstrip("/")
basename = url_path.rsplit("/", 1)[-1] if "/" in url_path else ""
if _FILENAME_PATTERN.match(basename):
return basename
return "xxx.xxx"


def _should_flag(file_path: str) -> bool:
"""Decide whether *file_path* should be checked for forbidden URLs.

An entry is included when there is no include list (empty means
"entire codebase") or when it matches at least one include pattern.
A included entry is then flagged unless it also matches an exclude pattern.
"""
include_patterns, exclude_patterns = _get_scope_config()

included = (not include_patterns) or _matches_any(file_path, include_patterns)
return included and not _matches_any(file_path, exclude_patterns)


def _run_diff(src: str, tgt: str, cached: bool = False) -> str:
cmd = ["git", "diff", "--unified=0", "--no-color"]
if cached:
cmd.append("--cached")
else:
cmd.append(f"{tgt}...{src}")

proc = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or "git diff failed")
return proc.stdout


def _find_violations(diff_text: str):
violations = []
current_file = ""
prev_added_line = ""

for line in diff_text.splitlines():
if line.startswith("+++ b/"):
current_file = line[6:]
prev_added_line = ""
continue

if not line.startswith("+") or line.startswith("+++"):
prev_added_line = ""
continue

added_line = line[1:]
if GITHUB_URL_PATTERN.search(added_line) and _should_flag(current_file):
# Skip if the current line or the previous added line has a suppression comment
if not (INLINE_SUPPRESSION_PATTERN.search(added_line)
or INLINE_SUPPRESSION_PATTERN.search(prev_added_line)):
violations.append((current_file or "<unknown>", added_line.strip()))

prev_added_line = added_line

return violations


def main() -> int:
parser = argparse.ArgumentParser(description="Check diff for forbidden raw GitHub URL usage.")
parser.add_argument("--src", default="HEAD", help="Source ref/commit for git diff.")
parser.add_argument("--tgt", default="HEAD~1", help="Target ref/commit for git diff.")
parser.add_argument("--cached", action="store_true", help="Check staged changes in git index.")
args = parser.parse_args()

try:
_get_scope_config()
diff_text = _run_diff(src=args.src, tgt=args.tgt, cached=args.cached)
except Exception as ex: # pylint: disable=broad-except
if args.cached:
print(f"Unable to evaluate staged diff: {ex}", file=sys.stderr)
else:
print(f"Unable to evaluate diff between '{args.tgt}' and '{args.src}': {ex}", file=sys.stderr)
return 1

violations = _find_violations(diff_text)
if not violations:
print("No forbidden external GitHub URL found in added lines.")
return 0

print("ERROR: Found forbidden external GitHub URL(s) in this change:\n", file=sys.stderr)
for file_path, content in violations:
filename = _extract_filename_from_url(content)
print(
f" {file_path}: {content}\n"
"\n"
" To fix, follow one of the options below (in priority order):\n"
"\n"
" Option 1 (Preferred) — Host the file in the AME storage account\n"
" ---------------------------------------------------------------\n"
" Reach out to the Platform squad to upload the file to the shared\n"
" Azure CLI storage account. Once uploaded, replace the raw GitHub\n"
" URL with the internal blob URL. The resulting URL should look like:\n"
"\n"
f" {RECOMMENDED_INTERNAL_URL}/<module>/{filename}\n"
"\n"
" Option 2 (Fallback) — Suppress with an inline comment\n"
" -----------------------------------------------------\n"
" Only if the GitHub URL is required by design (e.g. the upstream\n"
" repo IS the authoritative source), add an inline suppression\n"
" comment on the line before or on the same line like:\n"
"\n"
" # external-url-exempt: <reason>\n"
f" {content} \n",
file=sys.stderr,
)
return 1


if __name__ == "__main__":
sys.exit(main())

4 changes: 4 additions & 0 deletions src/aks-agent/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ To release a new version, please select a new version number (usually plus 1 to
Pending
+++++++

1.0.0b23
++++++++
* Fix: client mode when Microsoft Entra ID (keyless) provider is selected

1.0.0b22
++++++++
* Bump aks-agent to v0.7.1
Expand Down
10 changes: 10 additions & 0 deletions src/aks-agent/azext_aks_agent/agent/k8s/aks_agent_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,16 @@ def exec_aks_agent(self, command_flags: str = "") -> bool:
"-e", "AZURE_TOKEN_CREDENTIALS=AzureCLICredential"
]

# When no API key is configured for an Azure model, enable Azure AD token authentication
model_list = self.llm_config_manager.model_list
if model_list and any(
"azure/" in model_name and (
not model_config.get("api_key") or not model_config.get("api_key").strip()
)
for model_name, model_config in model_list.items()
):
env_vars.extend(["-e", "AZURE_AD_TOKEN_AUTH=True"])

# Prepare the command
exec_command = [
"docker", "run",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,72 @@ def test_init_command_flags(self):
expected = f"-n {self.cluster_name} -g {self.resource_group}"
self.assertEqual(result, expected)

def _make_client_manager(self, model_list):
"""Helper: create an AKSAgentManagerClient with a mocked llm_config_manager and config_dir."""
manager = AKSAgentManagerClient(
resource_group_name=self.resource_group,
cluster_name=self.cluster_name,
subscription_id=self.subscription_id,
kubeconfig_path=self.kubeconfig_path,
)
manager.llm_config_manager = MagicMock()
manager.llm_config_manager.model_list = model_list

# Make config_dir look like it exists and contains the required files
mock_config_dir = MagicMock()
mock_config_dir.exists.return_value = True
mock_file = MagicMock()
mock_file.exists.return_value = True
mock_config_dir.__truediv__ = lambda self, other: mock_file
manager.config_dir = mock_config_dir

return manager

@patch("subprocess.run")
@patch("os.path.exists", return_value=False)
def test_exec_aks_agent_sets_azure_ad_token_auth_when_no_api_key(self, _mock_exists, mock_run):
"""AZURE_AD_TOKEN_AUTH=True must be injected when an azure/ model has no api_key."""
mock_run.return_value = MagicMock(returncode=0)

manager = self._make_client_manager({
"azure/gpt-4o": {"model": "azure/gpt-4o"} # no api_key
})

manager.exec_aks_agent("")

docker_cmd = mock_run.call_args[0][0]
self.assertIn("AZURE_AD_TOKEN_AUTH=True", docker_cmd)

@patch("subprocess.run")
@patch("os.path.exists", return_value=False)
def test_exec_aks_agent_sets_azure_ad_token_auth_when_api_key_is_whitespace(self, _mock_exists, mock_run):
"""AZURE_AD_TOKEN_AUTH=True must be injected when api_key is whitespace-only."""
mock_run.return_value = MagicMock(returncode=0)

manager = self._make_client_manager({
"azure/gpt-4o": {"model": "azure/gpt-4o", "api_key": " "}
})

manager.exec_aks_agent("")

docker_cmd = mock_run.call_args[0][0]
self.assertIn("AZURE_AD_TOKEN_AUTH=True", docker_cmd)

@patch("subprocess.run")
@patch("os.path.exists", return_value=False)
def test_exec_aks_agent_omits_azure_ad_token_auth_when_api_key_present(self, _mock_exists, mock_run):
"""AZURE_AD_TOKEN_AUTH must NOT be injected when a valid api_key is configured."""
mock_run.return_value = MagicMock(returncode=0)

manager = self._make_client_manager({
"azure/gpt-4o": {"model": "azure/gpt-4o", "api_key": "my-secret-key"}
})

manager.exec_aks_agent("")

docker_cmd = mock_run.call_args[0][0]
self.assertNotIn("AZURE_AD_TOKEN_AUTH=True", docker_cmd)


if __name__ == '__main__':
unittest.main()
2 changes: 1 addition & 1 deletion src/aks-agent/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from setuptools import find_packages, setup

VERSION = "1.0.0b22"
VERSION = "1.0.0b23"

CLASSIFIERS = [
"Development Status :: 4 - Beta",
Expand Down
Loading
Loading