From fbff867bec2fb0ea9f49381672da038f385acd14 Mon Sep 17 00:00:00 2001 From: Arthur Ma Date: Fri, 12 Sep 2025 15:36:39 +0800 Subject: [PATCH 1/8] Using ARG query to support Aks resource read operations --- Directory.Packages.props | 1 - .../Azure/BaseAzureResourceService.cs | 24 +- docs/new-command.md | 1 - .../Check-Unused-ResourceProperties.ps1 | 297 ------------------ .../src/Azure.Mcp.Tools.Aks.csproj | 1 - .../src/Commands/AksJsonContext.cs | 7 + .../src/Commands/Cluster/ClusterGetCommand.cs | 2 + .../src/Services/AksService.cs | 111 +++---- .../Services/Models/AksAgentPoolProfile.cs | 16 + .../src/Services/Models/AksClusterData.cs | 40 +++ .../Services/Models/AksClusterProperties.cs | 32 ++ .../Services/Models/AksManagedClusterSku.cs | 17 + .../src/Services/Models/AksNetworkProfile.cs | 20 ++ .../src/Services/Models/AksPowerState.cs | 14 + .../AksCommandTests.cs | 2 +- .../Services/Models/SqlDatabaseProperties.cs | 2 - 16 files changed, 223 insertions(+), 364 deletions(-) delete mode 100644 eng/scripts/Check-Unused-ResourceProperties.ps1 create mode 100644 tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProfile.cs create mode 100644 tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterData.cs create mode 100644 tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterProperties.cs create mode 100644 tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksManagedClusterSku.cs create mode 100644 tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksNetworkProfile.cs create mode 100644 tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksPowerState.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 76f0b3f02..807fb4a2d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,7 +27,6 @@ - diff --git a/core/Azure.Mcp.Core/src/Services/Azure/BaseAzureResourceService.cs b/core/Azure.Mcp.Core/src/Services/Azure/BaseAzureResourceService.cs index 6b9f669df..52d4b75b1 100644 --- a/core/Azure.Mcp.Core/src/Services/Azure/BaseAzureResourceService.cs +++ b/core/Azure.Mcp.Core/src/Services/Azure/BaseAzureResourceService.cs @@ -53,7 +53,7 @@ private async Task GetTenantResourceAsync(Guid? tenantId, Cancel /// /// The type to convert each resource to /// The Azure resource type to query for (e.g., "Microsoft.Sql/servers/databases") - /// The resource group name to filter by + /// The resource group name to filter by (null to query all resource groups) /// The subscription ID or name /// Optional retry policy configuration /// Function to convert JsonElement to the target type @@ -63,7 +63,7 @@ private async Task GetTenantResourceAsync(Guid? tenantId, Cancel /// List of resources converted to the specified type protected async Task> ExecuteResourceQueryAsync( string resourceType, - string resourceGroup, + string? resourceGroup, string subscription, RetryPolicyOptions? retryPolicy, Func converter, @@ -71,7 +71,7 @@ protected async Task> ExecuteResourceQueryAsync( int limit = 50, CancellationToken cancellationToken = default) { - ValidateRequiredParameters(resourceType, resourceGroup, subscription); + ValidateRequiredParameters(resourceType, subscription); ArgumentNullException.ThrowIfNull(converter); var results = new List(); @@ -79,7 +79,11 @@ protected async Task> ExecuteResourceQueryAsync( var subscriptionResource = await _subscriptionService.GetSubscription(subscription, null, retryPolicy); var tenantResource = await GetTenantResourceAsync(subscriptionResource.Data.TenantId, cancellationToken); - var queryFilter = $"Resources | where type =~ '{EscapeKqlString(resourceType)}' and resourceGroup =~ '{EscapeKqlString(resourceGroup)}'"; + var queryFilter = $"Resources | where type =~ '{EscapeKqlString(resourceType)}'"; + if (!string.IsNullOrEmpty(resourceGroup)) + { + queryFilter += $" and resourceGroup =~ '{EscapeKqlString(resourceGroup)}'"; + } if (!string.IsNullOrEmpty(additionalFilter)) { queryFilter += $" and {additionalFilter}"; @@ -113,7 +117,7 @@ protected async Task> ExecuteResourceQueryAsync( /// /// The type to convert the resource to /// The Azure resource type to query for (e.g., "Microsoft.Sql/servers/databases") - /// The resource group name to filter by + /// The resource group name to filter by (null to query all resource groups) /// The subscription ID or name /// Optional retry policy configuration /// Function to convert JsonElement to the target type @@ -122,20 +126,24 @@ protected async Task> ExecuteResourceQueryAsync( /// Single resource converted to the specified type, or null if not found protected async Task ExecuteSingleResourceQueryAsync( string resourceType, - string resourceGroup, + string? resourceGroup, string subscription, RetryPolicyOptions? retryPolicy, Func converter, string? additionalFilter = null, CancellationToken cancellationToken = default) where T : class { - ValidateRequiredParameters(resourceType, resourceGroup, subscription); + ValidateRequiredParameters(resourceType, subscription); ArgumentNullException.ThrowIfNull(converter); var subscriptionResource = await _subscriptionService.GetSubscription(subscription, null, retryPolicy); var tenantResource = await GetTenantResourceAsync(subscriptionResource.Data.TenantId, cancellationToken); - var queryFilter = $"Resources | where type =~ '{EscapeKqlString(resourceType)}' and resourceGroup =~ '{EscapeKqlString(resourceGroup)}'"; + var queryFilter = $"Resources | where type =~ '{EscapeKqlString(resourceType)}'"; + if (!string.IsNullOrEmpty(resourceGroup)) + { + queryFilter += $" and resourceGroup =~ '{EscapeKqlString(resourceGroup)}'"; + } if (!string.IsNullOrEmpty(additionalFilter)) { queryFilter += $" and {additionalFilter}"; diff --git a/docs/new-command.md b/docs/new-command.md index add1145e1..ae7ddf3b7 100644 --- a/docs/new-command.md +++ b/docs/new-command.md @@ -1852,7 +1852,6 @@ Before submitting: - [ ] **AOT compilation verified** with `./eng/scripts/Build-Local.ps1 -BuildNative` - [ ] **Clean up unused using statements**: Run `dotnet format --include="tools/Azure.Mcp.Tools.{Toolset}/**/*.cs"` to remove unnecessary imports and ensure consistent formatting - [ ] Fix formatting issues with `dotnet format ./AzureMcp.sln` and ensure no warnings -- [ ] Identify unused properties for Azure Resource with `.\eng\scripts\Check-Unused-ResourceProperties.ps1` ### Azure SDK Integration - [ ] All Azure SDK property names verified and correct diff --git a/eng/scripts/Check-Unused-ResourceProperties.ps1 b/eng/scripts/Check-Unused-ResourceProperties.ps1 deleted file mode 100644 index efb00b045..000000000 --- a/eng/scripts/Check-Unused-ResourceProperties.ps1 +++ /dev/null @@ -1,297 +0,0 @@ -# PowerShell script to check for unused properties in Services/Models that are not used in Models mapping -param( - [string]$AreaName = "", - [switch]$Verbose = $false -) - -function Get-PropertyUsageFromMapping { - param( - [string]$ServiceFilePath, - [string]$ServicesModelClassName - ) - - if (-not (Test-Path $ServiceFilePath)) { - return @() - } - - $content = Get-Content $ServiceFilePath -Raw - - # Handle special cases based on class name patterns - if ($ServicesModelClassName -match "Properties$") { - # For Properties classes, we need to find the specific variable that uses this Properties type - # First, find the corresponding Data class name (e.g., SqlDatabaseProperties -> SqlDatabaseData) - $dataClassName = $ServicesModelClassName -replace "Properties$", "Data" - - # Look for variable declaration pattern: DataType? variableName = DataType.FromJson(...) - $variablePattern = "\b$dataClassName\?\s+(\w+)\s*=\s*$dataClassName\.FromJson" - $variableMatch = [regex]::Match($content, $variablePattern) - - if (-not $variableMatch.Success) { - # If no variable uses this Data class, then all Properties are unused - return @() - } - - $variableName = $variableMatch.Groups[1].Value - - # Look for nested property access: variableName.Properties?.PropertyName - $propertyPattern = "\b$variableName\.Properties\?\.\s*([A-Z][a-zA-Z0-9_]*)" - $propertyMatches = [regex]::Matches($content, $propertyPattern) - - $usedProperties = @() - foreach ($match in $propertyMatches) { - $propName = $match.Groups[1].Value - if ($propName -notmatch '^(ToString|GetHashCode|Equals|GetType|Split|LastOrDefault)$') { - $usedProperties += $propName - } - } - - return $usedProperties | Sort-Object -Unique - } - elseif ($ServicesModelClassName -eq "SqlSku") { - # Look for Sku property access: variable.Sku.PropertyName - $propertyPattern = "\w+\.Sku\.\s*([A-Z][a-zA-Z0-9_]*)" - $propertyMatches = [regex]::Matches($content, $propertyPattern) - - $usedProperties = @() - foreach ($match in $propertyMatches) { - $propName = $match.Groups[1].Value - if ($propName -notmatch '^(ToString|GetHashCode|Equals|GetType|Split|LastOrDefault)$') { - $usedProperties += $propName - } - } - - return $usedProperties | Sort-Object -Unique - } - elseif ($ServicesModelClassName -eq "SqlElasticPoolPerDatabaseSettings") { - # Look for PerDatabaseSettings property access: variable.Properties.PerDatabaseSettings.PropertyName - $propertyPattern = "\w+\.Properties\.PerDatabaseSettings\.\s*([A-Z][a-zA-Z0-9_]*)" - $propertyMatches = [regex]::Matches($content, $propertyPattern) - - $usedProperties = @() - foreach ($match in $propertyMatches) { - $propName = $match.Groups[1].Value - if ($propName -notmatch '^(ToString|GetHashCode|Equals|GetType|Split|LastOrDefault)$') { - $usedProperties += $propName - } - } - - return $usedProperties | Sort-Object -Unique - } - - # For regular classes, look for variable assignment pattern: Type? variableName = Type.FromJson(...) - $variablePattern = "\b$ServicesModelClassName\?\s+(\w+)\s*=\s*$ServicesModelClassName\.FromJson" - $variableMatch = [regex]::Match($content, $variablePattern) - - if (-not $variableMatch.Success) { - # If no variable uses this Services/Models class, then all its properties are unused - return @() - } - - $variableName = $variableMatch.Groups[1].Value - - # Look for usage of this variable's properties: variableName.PropertyName or variableName?.PropertyName - $propertyPattern = "\b$variableName(?:\.|->|\?\.)\s*([A-Z][a-zA-Z0-9_]*)" - $propertyMatches = [regex]::Matches($content, $propertyPattern) - - $usedProperties = @() - foreach ($match in $propertyMatches) { - $propName = $match.Groups[1].Value - if ($propName -notmatch '^(ToString|GetHashCode|Equals|GetType|Split|LastOrDefault)$') { - $usedProperties += $propName - } - } - - return $usedProperties | Sort-Object -Unique -} - -function Get-PropertiesFromServicesModel { - param( - [string]$FilePath - ) - - if (-not (Test-Path $FilePath)) { - return @() - } - - $content = Get-Content $FilePath -Raw - - # Extract public properties using regex - $propertyPattern = '\s*(?:public|internal)\s+[^{};]+?\s+([A-Z][a-zA-Z0-9_]*)\s*\{\s*get;?\s*set;?\s*\}' - $propertyMatches = [regex]::Matches($content, $propertyPattern) - - $properties = @() - foreach ($match in $propertyMatches) { - $properties += $match.Groups[1].Value - } - - return $properties | Sort-Object -Unique -} - -function Check-AreaPropertyUsage { - param( - [string]$AreaPath - ) - - $areaName = Split-Path $AreaPath -Leaf - Write-Host "`n=== Checking Area: $areaName ===" -ForegroundColor Cyan - - $srcPath = Join-Path $AreaPath "src" - if (-not (Test-Path $srcPath)) { - Write-Warning "No src folder found in $AreaPath" - return - } - - $projectFolders = Get-ChildItem $srcPath -Directory | Where-Object { $_.Name -like "AzureMcp.*" } - - if ($projectFolders.Count -eq 0) { - Write-Warning "No AzureMcp project folder found in $srcPath" - return - } - - $projectPath = $projectFolders[0].FullName - $servicesPath = Join-Path $projectPath "Services" - $modelsPath = Join-Path $projectPath "Models" - $servicesModelsPath = Join-Path $servicesPath "Models" - - if (-not (Test-Path $servicesModelsPath)) { - Write-Host "No Services/Models folder found in $projectPath" -ForegroundColor Yellow - return - } - - if (-not (Test-Path $modelsPath)) { - Write-Host "No Models folder found in $projectPath" -ForegroundColor Yellow - return - } - - # Find the service file - $serviceFiles = Get-ChildItem $servicesPath -Filter "*Service.cs" | Where-Object { $_.Name -notlike "I*Service.cs" } - if ($serviceFiles.Count -eq 0) { - Write-Warning "No service implementation file found in $servicesPath" - return - } - - $serviceFilePath = $serviceFiles[0].FullName - - # Get all model files in the Models folder - $modelFiles = Get-ChildItem $modelsPath -Filter "*.cs" - - $allUnusedProperties = @{} - $totalServicesModelFiles = 0 - $totalUnusedProperties = 0 - - # Get all Services/Models files - $servicesModelFiles = Get-ChildItem $servicesModelsPath -Filter "*.cs" - - foreach ($servicesModelFile in $servicesModelFiles) { - $totalServicesModelFiles++ - $fileName = $servicesModelFile.BaseName - - if ($Verbose) { - Write-Host " Checking Services/Models/$fileName.cs" -ForegroundColor Gray - } - - # Get properties from Services/Models file - $servicesProperties = Get-PropertiesFromServicesModel $servicesModelFile.FullName - - if ($servicesProperties.Count -eq 0) { - if ($Verbose) { - Write-Host " No properties found in $fileName" -ForegroundColor Gray - } - continue - } - - # Check usage for this specific Services/Models class - $usedProperties = Get-PropertyUsageFromMapping $serviceFilePath $fileName - - if ($Verbose) { - Write-Host " Properties in $fileName`: $($servicesProperties -join ', ')" -ForegroundColor Gray - Write-Host " Used properties for $fileName`: $($usedProperties -join ', ')" -ForegroundColor Gray - } - - # Find unused properties - $unusedProperties = $servicesProperties | Where-Object { $_ -notin $usedProperties } - - if ($unusedProperties.Count -gt 0) { - $allUnusedProperties[$fileName] = $unusedProperties - $totalUnusedProperties += $unusedProperties.Count - - Write-Host " $fileName.cs:" -ForegroundColor Yellow - foreach ($prop in $unusedProperties) { - Write-Host " - $prop" -ForegroundColor Red - } - } - elseif ($Verbose) { - Write-Host " All properties are used" -ForegroundColor Green - } - } - - # Summary for this area - Write-Host "`n Summary for ${areaName}:" -ForegroundColor Cyan - Write-Host " Services/Models files checked: $totalServicesModelFiles" - Write-Host " Files with unused properties: $($allUnusedProperties.Count)" - Write-Host " Total unused properties: $totalUnusedProperties" - - return @{ - AreaName = $areaName - UnusedProperties = $allUnusedProperties - TotalFiles = $totalServicesModelFiles - FilesWithUnused = $allUnusedProperties.Count - TotalUnused = $totalUnusedProperties - } -} - -# Main execution -$rootPath = Join-Path $PSScriptRoot ".." ".." -$areasPath = Join-Path $rootPath "areas" - -if (-not (Test-Path $areasPath)) { - Write-Error "Areas folder not found at $areasPath" - exit 1 -} - -$results = @() - -if ($AreaName) { - $areaPath = Join-Path $areasPath $AreaName - if (Test-Path $areaPath) { - $results += Check-AreaPropertyUsage $areaPath - } else { - Write-Error "Area '$AreaName' not found" - exit 1 - } -} else { - $areas = Get-ChildItem $areasPath -Directory | Sort-Object Name - - foreach ($area in $areas) { - $results += Check-AreaPropertyUsage $area.FullName - } -} - -# Overall summary -Write-Host "`n === OVERALL SUMMARY ===" -ForegroundColor Cyan - -$totalAreas = $results.Count -$totalServicesModelFiles = ($results | Measure-Object -Property TotalFiles -Sum).Sum -$totalFilesWithUnused = ($results | Measure-Object -Property FilesWithUnused -Sum).Sum -$totalUnusedProperties = ($results | Measure-Object -Property TotalUnused -Sum).Sum - -Write-Host "Areas checked: $totalAreas" -Write-Host "Total Services/Models files: $totalServicesModelFiles" -Write-Host "Files with unused properties: $totalFilesWithUnused" -Write-Host "Total unused properties: $totalUnusedProperties" - -if ($totalUnusedProperties -gt 0) { - Write-Host "`nAreas with unused properties:" -ForegroundColor Yellow - foreach ($result in $results | Where-Object { $_.TotalUnused -gt 0 }) { - Write-Host " $($result.AreaName): $($result.TotalUnused) unused properties in $($result.FilesWithUnused) files" -ForegroundColor Yellow - - foreach ($fileName in $result.UnusedProperties.Keys) { - Write-Host " $fileName.cs:" -ForegroundColor Gray - foreach ($prop in $result.UnusedProperties[$fileName]) { - Write-Host " - $prop" -ForegroundColor Red - } - } - } -} else { - Write-Host "`nAll properties are being used! ✅" -ForegroundColor Green -} diff --git a/tools/Azure.Mcp.Tools.Aks/src/Azure.Mcp.Tools.Aks.csproj b/tools/Azure.Mcp.Tools.Aks/src/Azure.Mcp.Tools.Aks.csproj index 516c62f99..374869120 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Azure.Mcp.Tools.Aks.csproj +++ b/tools/Azure.Mcp.Tools.Aks/src/Azure.Mcp.Tools.Aks.csproj @@ -12,7 +12,6 @@ - diff --git a/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs b/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs index d88942a13..f0744539b 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs @@ -3,11 +3,18 @@ using System.Text.Json.Serialization; using Azure.Mcp.Tools.Aks.Commands.Cluster; +using Azure.Mcp.Tools.Aks.Services.Models; namespace Azure.Mcp.Tools.Aks.Commands; [JsonSerializable(typeof(ClusterListCommand.ClusterListCommandResult))] [JsonSerializable(typeof(ClusterGetCommand.ClusterGetCommandResult))] [JsonSerializable(typeof(Models.Cluster))] +[JsonSerializable(typeof(AksAgentPoolProfile))] +[JsonSerializable(typeof(AksClusterData))] +[JsonSerializable(typeof(AksClusterProperties))] +[JsonSerializable(typeof(AksManagedClusterSku))] +[JsonSerializable(typeof(AksNetworkProfile))] +[JsonSerializable(typeof(AksPowerState))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] internal sealed partial class AksJsonContext : JsonSerializerContext; diff --git a/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterGetCommand.cs b/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterGetCommand.cs index 7c2b4c85a..02aa7c4df 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterGetCommand.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterGetCommand.cs @@ -80,6 +80,7 @@ public override async Task ExecuteAsync(CommandContext context, protected override string GetErrorMessage(Exception ex) => ex switch { + KeyNotFoundException => $"AKS cluster not found. Verify the cluster name, resource group, and that you have access.", RequestFailedException reqEx when reqEx.Status == 404 => "AKS cluster not found. Verify the cluster name, resource group, and subscription, and ensure you have access.", RequestFailedException reqEx when reqEx.Status == 403 => @@ -90,6 +91,7 @@ public override async Task ExecuteAsync(CommandContext context, protected override int GetStatusCode(Exception ex) => ex switch { + KeyNotFoundException => 404, RequestFailedException reqEx => reqEx.Status, _ => base.GetStatusCode(ex) }; diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs index c8817e2ef..e80a98c02 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs @@ -1,23 +1,27 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Text.Json; +using Azure.Core; using Azure.Mcp.Core.Options; using Azure.Mcp.Core.Services.Azure; using Azure.Mcp.Core.Services.Azure.Subscription; using Azure.Mcp.Core.Services.Azure.Tenant; using Azure.Mcp.Core.Services.Caching; using Azure.Mcp.Tools.Aks.Models; -using Azure.ResourceManager.ContainerService; +using Microsoft.Extensions.Logging; namespace Azure.Mcp.Tools.Aks.Services; public sealed class AksService( ISubscriptionService subscriptionService, ITenantService tenantService, - ICacheService cacheService) : BaseAzureService(tenantService), IAksService + ICacheService cacheService, + ILogger logger) : BaseAzureResourceService(subscriptionService, tenantService), IAksService { private readonly ISubscriptionService _subscriptionService = subscriptionService ?? throw new ArgumentNullException(nameof(subscriptionService)); private readonly ICacheService _cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService)); + private readonly ILogger _logger = logger; private const string CacheGroup = "aks"; private const string AksClustersCacheKey = "clusters"; @@ -42,28 +46,25 @@ public async Task> ListClusters( return cachedClusters; } - var subscriptionResource = await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy); - var clusters = new List(); - try { - await foreach (var cluster in subscriptionResource.GetContainerServiceManagedClustersAsync()) - { - if (cluster?.Data != null) - { - clusters.Add(ConvertToClusterModel(cluster)); - } - } + // Use Resource Graph to query AKS clusters + var clusters = await ExecuteResourceQueryAsync( + "Microsoft.ContainerService/managedClusters", + resourceGroup: null, // all resource groups + subscription, + retryPolicy, + ConvertToClusterModel, + cancellationToken: default); // Cache the results await _cacheService.SetAsync(CacheGroup, cacheKey, clusters, s_cacheDuration); + return clusters; } catch (Exception ex) { throw new Exception($"Error retrieving AKS clusters: {ex.Message}", ex); } - - return clusters; } public async Task GetCluster( @@ -87,29 +88,22 @@ public async Task> ListClusters( return cachedCluster; } - var subscriptionResource = await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy); - try { - var resourceGroupResource = await subscriptionResource - .GetResourceGroupAsync(resourceGroup); - - if (resourceGroupResource?.Value == null) + // Use Resource Graph to find the single cluster by name within the specified resource group + var cluster = await ExecuteSingleResourceQueryAsync( + "Microsoft.ContainerService/managedClusters", + resourceGroup, + subscription, + retryPolicy, + ConvertToClusterModel, + $"name =~ '{EscapeKqlString(clusterName)}'"); + + if (cluster == null) { - return null; + throw new KeyNotFoundException($"AKS cluster '{clusterName}' not found in resource group '{resourceGroup}' for subscription '{subscription}'."); } - var clusterResource = await resourceGroupResource.Value - .GetContainerServiceManagedClusters() - .GetAsync(clusterName); - - if (clusterResource?.Value?.Data == null) - { - return null; - } - - var cluster = ConvertToClusterModel(clusterResource.Value); - // Cache the result await _cacheService.SetAsync(CacheGroup, cacheKey, cluster, s_cacheDuration); @@ -117,36 +111,47 @@ public async Task> ListClusters( } catch (Exception ex) { - throw new Exception($"Error retrieving AKS cluster '{clusterName}': {ex.Message}", ex); + _logger.LogError(ex, + "Error retrieving AKS cluster '{ClusterName}' in resource group '{ResourceGroup}' for subscription '{Subscription}'", + clusterName, resourceGroup, subscription); + throw; } } - private static Cluster ConvertToClusterModel(ContainerServiceManagedClusterResource clusterResource) + // Overload for Resource Graph result + private static Cluster ConvertToClusterModel(System.Text.Json.JsonElement item) { - var data = clusterResource.Data; - var agentPool = data.AgentPoolProfiles?.FirstOrDefault(); + var data = Azure.Mcp.Tools.Aks.Services.Models.AksClusterData.FromJson(item); + if (data == null) + throw new InvalidOperationException("Failed to parse AKS cluster data"); + + // Resource identity + if (string.IsNullOrEmpty(data.ResourceId)) + throw new InvalidOperationException("Resource ID is missing"); + var id = new ResourceIdentifier(data.ResourceId); + var agentPool = data.Properties?.AgentPoolProfiles?.FirstOrDefault(); return new Cluster { - Name = data.Name, - SubscriptionId = clusterResource.Id.SubscriptionId, - ResourceGroupName = clusterResource.Id.ResourceGroupName, - Location = data.Location.ToString(), - KubernetesVersion = data.KubernetesVersion, - ProvisioningState = data.ProvisioningState?.ToString(), - PowerState = data.PowerStateCode?.ToString(), - DnsPrefix = data.DnsPrefix, - Fqdn = data.Fqdn, + Name = data.ResourceName ?? "Unknown", + SubscriptionId = id.SubscriptionId ?? "Unknown", + ResourceGroupName = id.ResourceGroupName ?? "Unknown", + Location = data.Location ?? "Unknown", + KubernetesVersion = data.Properties?.KubernetesVersion, + ProvisioningState = data.Properties?.ProvisioningState, + PowerState = data.Properties?.PowerState?.Code, + DnsPrefix = data.Properties?.DnsPrefix, + Fqdn = data.Properties?.Fqdn, NodeCount = agentPool?.Count, NodeVmSize = agentPool?.VmSize, - IdentityType = data.Identity?.ManagedServiceIdentityType.ToString(), - EnableRbac = data.EnableRbac, - NetworkPlugin = data.NetworkProfile?.NetworkPlugin?.ToString(), - NetworkPolicy = data.NetworkProfile?.NetworkPolicy?.ToString(), - ServiceCidr = data.NetworkProfile?.ServiceCidr, - DnsServiceIP = data.NetworkProfile?.DnsServiceIP?.ToString(), - SkuTier = data.Sku?.Tier?.ToString(), - Tags = data.Tags?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) + IdentityType = data.IdentityType, + EnableRbac = data.Properties?.EnableRbac, + NetworkPlugin = data.Properties?.NetworkProfile?.NetworkPlugin, + NetworkPolicy = data.Properties?.NetworkProfile?.NetworkPolicy, + ServiceCidr = data.Properties?.NetworkProfile?.ServiceCidr, + DnsServiceIP = data.Properties?.NetworkProfile?.DnsServiceIP, + SkuTier = data.Sku?.Tier, + Tags = data.Tags != null ? new Dictionary(data.Tags) : null }; } } diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProfile.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProfile.cs new file mode 100644 index 000000000..b6f62ba21 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProfile.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.Mcp.Tools.Aks.Commands; + +namespace Azure.Mcp.Tools.Aks.Services.Models; + +internal sealed class AksAgentPoolProfile +{ + /// Number of agents (VMs) to host docker containers. Allowed values must be in the range of 0 to 1000 (inclusive) for user pools and in the range of 1 to 1000 (inclusive) for system pools. The default value is 1. + public int? Count { get; set; } + /// VM size availability varies by region. If a node contains insufficient compute resources (memory, cpu, etc) pods might fail to run correctly. For more details on restricted VM sizes, see: https://docs.microsoft.com/azure/aks/quotas-skus-regions. + public string? VmSize { get; set; } +} \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterData.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterData.cs new file mode 100644 index 000000000..f725c0ecf --- /dev/null +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterData.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.Mcp.Tools.Aks.Commands; + +namespace Azure.Mcp.Tools.Aks.Services.Models; + +/// +/// A class representing the AKS Cluster data model for Resource Graph results. +/// +internal sealed class AksClusterData +{ + /// The resource ID for the resource. + [JsonPropertyName("id")] + public string? ResourceId { get; set; } + /// The type of the resource. + [JsonPropertyName("type")] + public string? ResourceType { get; set; } + /// The name of the resource. + [JsonPropertyName("name")] + public string? ResourceName { get; set; } + /// The location of the resource. + public string? Location { get; set; } + /// The SKU of the resource. + public AksManagedClusterSku? Sku { get; set; } + /// The identity type of the resource. + public string? IdentityType { get; set; } + /// The tags of the resource. + public IDictionary? Tags { get; set; } + /// The properties of the cluster. + public AksClusterProperties? Properties { get; set; } + + // Read the JSON response content and create a model instance from it. + public static AksClusterData? FromJson(JsonElement source) + { + return JsonSerializer.Deserialize(source, AksJsonContext.Default.AksClusterData); + } +} \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterProperties.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterProperties.cs new file mode 100644 index 000000000..7445caf13 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterProperties.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.Mcp.Tools.Aks.Commands; + +namespace Azure.Mcp.Tools.Aks.Services.Models; + +/// +/// A class representing the AksClusterProperties data model. +/// +internal sealed class AksClusterProperties +{ + /// The current provisioning state. + public string? ProvisioningState { get; set; } + /// The Power State of the cluster. + public AksPowerState? PowerState { get; set; } + /// Both patch version <major.minor.patch> (e.g. 1.20.13) and <major.minor> (e.g. 1.20) are supported. When <major.minor> is specified, the latest supported GA patch version is chosen automatically. Updating the cluster with the same <major.minor> once it has been created (e.g. 1.14.x -> 1.14) will not trigger an upgrade, even if a newer patch version is available. When you upgrade a supported AKS cluster, Kubernetes minor versions cannot be skipped. All upgrades must be performed sequentially by major version number. For example, upgrades between 1.14.x -> 1.15.x or 1.15.x -> 1.16.x are allowed, however 1.14.x -> 1.16.x is not allowed. See [upgrading an AKS cluster](https://docs.microsoft.com/azure/aks/upgrade-cluster) for more details. + public string? KubernetesVersion { get; set; } + /// This cannot be updated once the Managed Cluster has been created. + public string? DnsPrefix { get; set; } + /// The FQDN of the master pool. + public string? Fqdn { get; set; } + /// The agent pool properties. + public IList? AgentPoolProfiles { get; set; } + /// Whether to enable Kubernetes Role-Based Access Control. + [JsonPropertyName("enableRBAC")] + public bool? EnableRbac { get; set; } + /// The network configuration profile. + public AksNetworkProfile? NetworkProfile { get; set; } +} \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksManagedClusterSku.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksManagedClusterSku.cs new file mode 100644 index 000000000..8e661f0d1 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksManagedClusterSku.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.Mcp.Tools.Aks.Commands; + +namespace Azure.Mcp.Tools.Aks.Services.Models; + +/// The SKU of a Managed Cluster. +internal sealed class AksManagedClusterSku +{ + /// The name of a managed cluster SKU. + public string? Name { get; set; } + /// If not specified, the default is 'Free'. See [AKS Pricing Tier](https://learn.microsoft.com/azure/aks/free-standard-pricing-tiers) for more details. + public string? Tier { get; set; } +} \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksNetworkProfile.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksNetworkProfile.cs new file mode 100644 index 000000000..bf1d1dd7a --- /dev/null +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksNetworkProfile.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.Mcp.Tools.Aks.Commands; + +namespace Azure.Mcp.Tools.Aks.Services.Models; + +internal sealed class AksNetworkProfile +{ + /// Network plugin used for building the Kubernetes network. + public string? NetworkPlugin { get; set; } + /// Network policy used for building the Kubernetes network. + public string? NetworkPolicy { get; set; } + /// A CIDR notation IP range from which to assign service cluster IPs. It must not overlap with any Subnet IP ranges. + public string? ServiceCidr { get; set; } + /// An IP address assigned to the Kubernetes DNS service. It must be within the Kubernetes service address range specified in serviceCidr. + public string? DnsServiceIP { get; set; } +} \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksPowerState.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksPowerState.cs new file mode 100644 index 000000000..b24a30453 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksPowerState.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.Mcp.Tools.Aks.Commands; + +namespace Azure.Mcp.Tools.Aks.Services.Models; + +internal sealed class AksPowerState +{ + /// Tells whether the cluster is Running or Stopped. + public string? Code { get; set; } +} \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/AksCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/AksCommandTests.cs index e2c040fd8..261f7ea31 100644 --- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/AksCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/AksCommandTests.cs @@ -160,7 +160,7 @@ public async Task Should_handle_nonexistent_cluster_gracefully() var errorDetails = result.Value; Assert.True(errorDetails.TryGetProperty("message", out _)); Assert.True(errorDetails.TryGetProperty("type", out var typeProperty)); - Assert.Equal("Exception", typeProperty.GetString()); + Assert.Equal("KeyNotFoundException", typeProperty.GetString()); } [Fact] diff --git a/tools/Azure.Mcp.Tools.Sql/src/Services/Models/SqlDatabaseProperties.cs b/tools/Azure.Mcp.Tools.Sql/src/Services/Models/SqlDatabaseProperties.cs index e8b4f3e14..8c77c8d0f 100644 --- a/tools/Azure.Mcp.Tools.Sql/src/Services/Models/SqlDatabaseProperties.cs +++ b/tools/Azure.Mcp.Tools.Sql/src/Services/Models/SqlDatabaseProperties.cs @@ -23,8 +23,6 @@ internal sealed class SqlDatabaseProperties public DateTimeOffset? CreatedOn { get; set; } /// The current service level objective name of the database. public string? CurrentServiceObjectiveName { get; set; } - /// The license type to apply for this database. `LicenseIncluded` if you need a license, or `BasePrice` if you have a license and are eligible for the Azure Hybrid Benefit. - public string? LicenseType { get; set; } /// This records the earliest start date and time that restore is available for this database (ISO8601 format). [JsonPropertyName("earliestRestoreDate")] public DateTimeOffset? EarliestRestoreOn { get; set; } From 723101b36638a25f8d0a3d8453946d8049e3de46 Mon Sep 17 00:00:00 2001 From: Arthur Ma Date: Fri, 12 Sep 2025 16:49:18 +0800 Subject: [PATCH 2/8] Update --- servers/Azure.Mcp.Server/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/servers/Azure.Mcp.Server/CHANGELOG.md b/servers/Azure.Mcp.Server/CHANGELOG.md index 2f37d021b..64efd1f9c 100644 --- a/servers/Azure.Mcp.Server/CHANGELOG.md +++ b/servers/Azure.Mcp.Server/CHANGELOG.md @@ -12,6 +12,9 @@ The Azure MCP Server updates automatically by default whenever a new release com ### Other Changes +- Refactored AKS service implementation to use Azure Resource Graph queries instead of direct ARM API calls. + - Removed dependency on `Azure.ResourceManager.ContainerService` package by migrating to Azure Resource Graph queries, reducing package size and improving startup performance. + ## 0.6.0 (2025-09-11) ### Features Added From 273eb560f1af66da7ec7704586b6c284d4edd57f Mon Sep 17 00:00:00 2001 From: Arthur Ma Date: Fri, 12 Sep 2025 16:52:05 +0800 Subject: [PATCH 3/8] Update changelog --- servers/Azure.Mcp.Server/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/servers/Azure.Mcp.Server/CHANGELOG.md b/servers/Azure.Mcp.Server/CHANGELOG.md index 64efd1f9c..b47cae5a8 100644 --- a/servers/Azure.Mcp.Server/CHANGELOG.md +++ b/servers/Azure.Mcp.Server/CHANGELOG.md @@ -12,7 +12,7 @@ The Azure MCP Server updates automatically by default whenever a new release com ### Other Changes -- Refactored AKS service implementation to use Azure Resource Graph queries instead of direct ARM API calls. +- Refactored AKS service implementation to use Azure Resource Graph queries instead of direct ARM API calls. [[#424](https://github.com/microsoft/mcp/pull/424)] - Removed dependency on `Azure.ResourceManager.ContainerService` package by migrating to Azure Resource Graph queries, reducing package size and improving startup performance. ## 0.6.0 (2025-09-11) From 3d560ccdb94b82b86461f87b4b9a62213e24118b Mon Sep 17 00:00:00 2001 From: Arthur Ma Date: Fri, 12 Sep 2025 18:53:23 +0800 Subject: [PATCH 4/8] update --- .../src/Commands/AksJsonContext.cs | 2 - .../src/Services/AksService.cs | 77 +++++++++---------- .../src/Services/Models/AksAgentPoolData.cs | 32 -------- .../Services/Models/AksAgentPoolProfile.cs | 17 ++++ .../Services/Models/AksAgentPoolProperties.cs | 34 -------- .../NodepoolGetCommandTests.cs | 2 +- 6 files changed, 53 insertions(+), 111 deletions(-) delete mode 100644 tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolData.cs delete mode 100644 tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProperties.cs diff --git a/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs b/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs index ed5be5909..9d3583884 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs @@ -17,8 +17,6 @@ namespace Azure.Mcp.Tools.Aks.Commands; [JsonSerializable(typeof(AksManagedClusterSku))] [JsonSerializable(typeof(AksNetworkProfile))] [JsonSerializable(typeof(AksPowerState))] -[JsonSerializable(typeof(AksAgentPoolData))] -[JsonSerializable(typeof(AksAgentPoolProperties))] [JsonSerializable(typeof(NodepoolListCommand.NodepoolListCommandResult))] [JsonSerializable(typeof(NodepoolGetCommand.NodepoolGetCommandResult))] [JsonSerializable(typeof(Models.NodePool))] diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs index 231e5a073..0086c9f26 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs @@ -10,6 +10,7 @@ using Azure.Mcp.Core.Services.Caching; using Azure.Mcp.Tools.Aks.Models; using Microsoft.Extensions.Logging; +using System.Linq; namespace Azure.Mcp.Tools.Aks.Services; @@ -49,7 +50,6 @@ public async Task> ListClusters( try { - // Use Resource Graph to query AKS clusters var clusters = await ExecuteResourceQueryAsync( "Microsoft.ContainerService/managedClusters", resourceGroup: null, // all resource groups @@ -91,7 +91,6 @@ public async Task> ListClusters( try { - // Use Resource Graph to find the single cluster by name within the specified resource group var cluster = await ExecuteSingleResourceQueryAsync( "Microsoft.ContainerService/managedClusters", resourceGroup, @@ -142,16 +141,13 @@ public async Task> ListNodePools( try { - // Use Resource Graph to query AKS agent pools for the specified cluster - var filter = $"contains(id, '/managedClusters/{EscapeKqlString(clusterName)}/')"; - var nodePools = await ExecuteResourceQueryAsync( - "Microsoft.ContainerService/managedClusters/agentPools", + var nodePools = await ExecuteSingleResourceQueryAsync( + "Microsoft.ContainerService/managedClusters", resourceGroup, subscription, retryPolicy, - ConvertToNodePoolModel, - filter, - cancellationToken: default); + ConvertToClusterNodePoolModel, + $"name =~ '{EscapeKqlString(clusterName)}'") ?? new List(); // Cache the results await _cacheService.SetAsync(CacheGroup, cacheKey, nodePools, s_cacheDuration); @@ -187,24 +183,23 @@ public async Task> ListNodePools( try { - // Use Resource Graph to find the single node pool by name within the specified cluster and resource group - var nodePool = await ExecuteSingleResourceQueryAsync( - "Microsoft.ContainerService/managedClusters/agentPools", + var nodePools = await ExecuteSingleResourceQueryAsync( + "Microsoft.ContainerService/managedClusters", resourceGroup, subscription, retryPolicy, - ConvertToNodePoolModel, - $"name =~ '{EscapeKqlString(nodePoolName)}' and contains(id, '/managedClusters/{EscapeKqlString(clusterName)}/')"); + ConvertToClusterNodePoolModel, + $"name =~ '{EscapeKqlString(clusterName)}'") ?? new List(); - if (nodePool == null) + var nodePool = nodePools.FirstOrDefault(np => np.Name == nodePoolName); + if (nodePool != null) { - throw new KeyNotFoundException($"AKS node pool '{nodePoolName}' not found in cluster '{clusterName}' in resource group '{resourceGroup}' for subscription '{subscription}'."); + // Cache the result + await _cacheService.SetAsync(CacheGroup, cacheKey, nodePool, s_cacheDuration); + return nodePool; } - - // Cache the result - await _cacheService.SetAsync(CacheGroup, cacheKey, nodePool, s_cacheDuration); - - return nodePool; + + throw new KeyNotFoundException($"AKS node pool '{nodePoolName}' not found in cluster '{clusterName}' in resource group '{resourceGroup}' for subscription '{subscription}'."); } catch (Exception ex) { @@ -217,9 +212,7 @@ public async Task> ListNodePools( private static Cluster ConvertToClusterModel(JsonElement item) { - var data = Models.AksClusterData.FromJson(item); - if (data == null) - throw new InvalidOperationException("Failed to parse AKS cluster data"); + var data = Models.AksClusterData.FromJson(item) ?? throw new InvalidOperationException("Failed to parse AKS cluster data"); // Resource identity if (string.IsNullOrEmpty(data.ResourceId)) @@ -251,25 +244,25 @@ private static Cluster ConvertToClusterModel(JsonElement item) }; } - // JsonElement-based converter for Resource Graph results representing agent pool resources - private static NodePool ConvertToNodePoolModel(JsonElement item) + private static List ConvertToClusterNodePoolModel(JsonElement item) { - var data = Models.AksAgentPoolData.FromJson(item); - if (data == null) - throw new InvalidOperationException("Failed to parse AKS agent pool data"); + var data = Models.AksClusterData.FromJson(item) ?? throw new InvalidOperationException("Failed to parse AKS cluster data"); - return new NodePool - { - Name = data.ResourceName ?? "Unknown", - NodeCount = data.Properties?.Count, - NodeVmSize = data.Properties?.VmSize?.ToString(), - OsType = data.Properties?.OSType?.ToString(), - Mode = data.Properties?.Mode?.ToString(), - OrchestratorVersion = data.Properties?.OrchestratorVersion, - EnableAutoScaling = data.Properties?.EnableAutoScaling, - MinCount = data.Properties?.MinCount, - MaxCount = data.Properties?.MaxCount, - ProvisioningState = data.Properties?.ProvisioningState - }; + return data.Properties?.AgentPoolProfiles? + .Select(node => new NodePool + { + Name = node.Name ?? "Unknown", + NodeCount = node.Count, + NodeVmSize = node.VmSize, + OsType = node.OSType, + Mode = node.Mode, + OrchestratorVersion = node.OrchestratorVersion, + EnableAutoScaling = node.EnableAutoScaling, + MinCount = node.MinCount, + MaxCount = node.MaxCount, + ProvisioningState = node.ProvisioningState + }) + .ToList() + ?? new List(); } } diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolData.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolData.cs deleted file mode 100644 index ea9e3133e..000000000 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolData.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text.Json; -using System.Text.Json.Serialization; -using Azure.Mcp.Tools.Aks.Commands; - -namespace Azure.Mcp.Tools.Aks.Services.Models; - -/// -/// A class representing the AksAgentPoolData data model. -/// -internal sealed class AksAgentPoolData -{ - /// The resource ID for the resource. - [JsonPropertyName("id")] - public string? ResourceId { get; set; } - /// The type of the resource. - [JsonPropertyName("type")] - public string? ResourceType { get; set; } - /// The name of the resource. - [JsonPropertyName("name")] - public string? ResourceName { get; set; } - /// The properties of the agent pool. - public AksAgentPoolProperties? Properties { get; set; } - - // Read the JSON response content and create a model instance from it. - public static AksAgentPoolData? FromJson(JsonElement source) - { - return JsonSerializer.Deserialize(source, AksJsonContext.Default.AksAgentPoolData); - } -} diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProfile.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProfile.cs index b6f62ba21..c335959ed 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProfile.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProfile.cs @@ -9,8 +9,25 @@ namespace Azure.Mcp.Tools.Aks.Services.Models; internal sealed class AksAgentPoolProfile { + /// Windows agent pool names must be 6 characters or less. + public string? Name { get; set; } /// Number of agents (VMs) to host docker containers. Allowed values must be in the range of 0 to 1000 (inclusive) for user pools and in the range of 1 to 1000 (inclusive) for system pools. The default value is 1. public int? Count { get; set; } /// VM size availability varies by region. If a node contains insufficient compute resources (memory, cpu, etc) pods might fail to run correctly. For more details on restricted VM sizes, see: https://docs.microsoft.com/azure/aks/quotas-skus-regions. public string? VmSize { get; set; } + /// The operating system type. The default is Linux. + [JsonPropertyName("osType")] + public string? OSType { get; set; } + /// A cluster must have at least one 'System' Agent Pool at all times. For additional information on agent pool restrictions and best practices, see: https://docs.microsoft.com/azure/aks/use-system-pools. + public string? Mode { get; set; } + /// Both patch version <major.minor.patch> (e.g. 1.20.13) and <major.minor> (e.g. 1.20) are supported. When <major.minor> is specified, the latest supported GA patch version is chosen automatically. Updating the cluster with the same <major.minor> once it has been created (e.g. 1.14.x -> 1.14) will not trigger an upgrade, even if a newer patch version is available. As a best practice, you should upgrade all node pools in an AKS cluster to the same Kubernetes version. The node pool version must have the same major version as the control plane. The node pool minor version must be within two minor versions of the control plane version. The node pool version cannot be greater than the control plane version. For more information see [upgrading a node pool](https://docs.microsoft.com/azure/aks/use-multiple-node-pools#upgrade-a-node-pool). + public string? OrchestratorVersion { get; set; } + /// Whether to enable auto-scaler. + public bool? EnableAutoScaling { get; set; } + /// The maximum number of nodes for auto-scaling. + public int? MaxCount { get; set; } + /// The minimum number of nodes for auto-scaling. + public int? MinCount { get; set; } + /// The current deployment or provisioning state. + public string? ProvisioningState { get; set; } } \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProperties.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProperties.cs deleted file mode 100644 index 84344a682..000000000 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProperties.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text.Json; -using System.Text.Json.Serialization; -using Azure.Mcp.Tools.Aks.Commands; - -namespace Azure.Mcp.Tools.Aks.Services.Models; - -/// -/// A class representing the AksAgentPoolProperties data model. -/// -internal sealed class AksAgentPoolProperties -{ - /// Number of agents (VMs) to host docker containers. Allowed values must be in the range of 0 to 1000 (inclusive) for user pools and in the range of 1 to 1000 (inclusive) for system pools. The default value is 1. - public int? Count { get; set; } - /// VM size availability varies by region. If a node contains insufficient compute resources (memory, cpu, etc) pods might fail to run correctly. For more details on restricted VM sizes, see: https://docs.microsoft.com/azure/aks/quotas-skus-regions. - public string? VmSize { get; set; } - /// The operating system type. The default is Linux. - [JsonPropertyName("osType")] - public string? OSType { get; set; } - /// A cluster must have at least one 'System' Agent Pool at all times. For additional information on agent pool restrictions and best practices, see: https://docs.microsoft.com/azure/aks/use-system-pools. - public string? Mode { get; set; } - /// Both patch version <major.minor.patch> (e.g. 1.20.13) and <major.minor> (e.g. 1.20) are supported. When <major.minor> is specified, the latest supported GA patch version is chosen automatically. Updating the cluster with the same <major.minor> once it has been created (e.g. 1.14.x -> 1.14) will not trigger an upgrade, even if a newer patch version is available. As a best practice, you should upgrade all node pools in an AKS cluster to the same Kubernetes version. The node pool version must have the same major version as the control plane. The node pool minor version must be within two minor versions of the control plane version. The node pool version cannot be greater than the control plane version. For more information see [upgrading a node pool](https://docs.microsoft.com/azure/aks/use-multiple-node-pools#upgrade-a-node-pool). - public string? OrchestratorVersion { get; set; } - /// Whether to enable auto-scaler. - public bool? EnableAutoScaling { get; set; } - /// The maximum number of nodes for auto-scaling. - public int? MaxCount { get; set; } - /// The minimum number of nodes for auto-scaling. - public int? MinCount { get; set; } - /// The current deployment or provisioning state. - public string? ProvisioningState { get; } -} diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolGetCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolGetCommandTests.cs index 710a8cee9..fde81ff70 100644 --- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolGetCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.LiveTests/NodepoolGetCommandTests.cs @@ -89,7 +89,7 @@ public async Task Should_handle_nonexistent_nodepool_gracefully() var errorDetails = result.Value; Assert.True(errorDetails.TryGetProperty("message", out _)); Assert.True(errorDetails.TryGetProperty("type", out var typeProperty)); - Assert.Equal("Exception", typeProperty.GetString()); + Assert.Equal("KeyNotFoundException", typeProperty.GetString()); } [Fact] From d7e6dd3b37219cb98d0a0a54d3e9cdbae976e482 Mon Sep 17 00:00:00 2001 From: Arthur Ma Date: Fri, 12 Sep 2025 18:58:38 +0800 Subject: [PATCH 5/8] update --- .../src/Commands/Cluster/ClusterGetCommand.cs | 2 +- .../src/Commands/Cluster/ClusterListCommand.cs | 2 +- .../src/Commands/Nodepool/NodepoolGetCommand.cs | 2 +- .../src/Commands/Nodepool/NodepoolListCommand.cs | 2 +- tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs | 9 ++++----- tools/Azure.Mcp.Tools.Aks/src/Services/IAksService.cs | 8 ++++---- .../src/Services/Models/AksAgentPoolProfile.cs | 4 +--- .../src/Services/Models/AksClusterProperties.cs | 4 +--- .../src/Services/Models/AksManagedClusterSku.cs | 6 +----- .../src/Services/Models/AksNetworkProfile.cs | 6 +----- .../src/Services/Models/AksPowerState.cs | 6 +----- 11 files changed, 17 insertions(+), 34 deletions(-) diff --git a/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterGetCommand.cs b/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterGetCommand.cs index 6e43d9cbf..d7b5f0655 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterGetCommand.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterGetCommand.cs @@ -63,7 +63,7 @@ public override async Task ExecuteAsync(CommandContext context, try { var aksService = context.GetService(); - var cluster = await aksService.GetCluster( + var cluster = await aksService.GetClusterAsync( options.Subscription!, options.ClusterName!, options.ResourceGroup!, diff --git a/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterListCommand.cs b/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterListCommand.cs index 4d19bd3ef..8e87aa8d8 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterListCommand.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterListCommand.cs @@ -45,7 +45,7 @@ public override async Task ExecuteAsync(CommandContext context, try { var aksService = context.GetService(); - var clusters = await aksService.ListClusters( + var clusters = await aksService.ListClustersAsync( options.Subscription!, options.Tenant, options.RetryPolicy); diff --git a/tools/Azure.Mcp.Tools.Aks/src/Commands/Nodepool/NodepoolGetCommand.cs b/tools/Azure.Mcp.Tools.Aks/src/Commands/Nodepool/NodepoolGetCommand.cs index 02a69cb31..491c6e80b 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Commands/Nodepool/NodepoolGetCommand.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Commands/Nodepool/NodepoolGetCommand.cs @@ -57,7 +57,7 @@ public override async Task ExecuteAsync(CommandContext context, try { var aksService = context.GetService(); - var nodePool = await aksService.GetNodePool( + var nodePool = await aksService.GetNodePoolAsync( options.Subscription!, options.ResourceGroup!, options.ClusterName!, diff --git a/tools/Azure.Mcp.Tools.Aks/src/Commands/Nodepool/NodepoolListCommand.cs b/tools/Azure.Mcp.Tools.Aks/src/Commands/Nodepool/NodepoolListCommand.cs index 923aa0173..8bc6585f5 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Commands/Nodepool/NodepoolListCommand.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Commands/Nodepool/NodepoolListCommand.cs @@ -54,7 +54,7 @@ public override async Task ExecuteAsync(CommandContext context, try { var aksService = context.GetService(); - var nodePools = await aksService.ListNodePools( + var nodePools = await aksService.ListNodePoolsAsync( options.Subscription!, options.ResourceGroup!, options.ClusterName!, diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs index 0086c9f26..f4cbf3b32 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs @@ -10,7 +10,6 @@ using Azure.Mcp.Core.Services.Caching; using Azure.Mcp.Tools.Aks.Models; using Microsoft.Extensions.Logging; -using System.Linq; namespace Azure.Mcp.Tools.Aks.Services; @@ -29,7 +28,7 @@ public sealed class AksService( private const string AksNodePoolsCacheKey = "nodepools"; private static readonly TimeSpan s_cacheDuration = TimeSpan.FromHours(1); - public async Task> ListClusters( + public async Task> ListClustersAsync( string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null) @@ -68,7 +67,7 @@ public async Task> ListClusters( } } - public async Task GetCluster( + public async Task GetClusterAsync( string subscription, string clusterName, string resourceGroup, @@ -118,7 +117,7 @@ public async Task> ListClusters( } } - public async Task> ListNodePools( + public async Task> ListNodePoolsAsync( string subscription, string resourceGroup, string clusterName, @@ -159,7 +158,7 @@ public async Task> ListNodePools( } } - public async Task GetNodePool( + public async Task GetNodePoolAsync( string subscription, string resourceGroup, string clusterName, diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/IAksService.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/IAksService.cs index c1c5729a3..56bcb1bae 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/IAksService.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/IAksService.cs @@ -8,26 +8,26 @@ namespace Azure.Mcp.Tools.Aks.Services; public interface IAksService { - Task> ListClusters( + Task> ListClustersAsync( string subscription, string? tenant = null, RetryPolicyOptions? retryPolicy = null); - Task GetCluster( + Task GetClusterAsync( string subscription, string clusterName, string resourceGroup, string? tenant = null, RetryPolicyOptions? retryPolicy = null); - Task> ListNodePools( + Task> ListNodePoolsAsync( string subscription, string resourceGroup, string clusterName, string? tenant = null, RetryPolicyOptions? retryPolicy = null); - Task GetNodePool( + Task GetNodePoolAsync( string subscription, string resourceGroup, string clusterName, diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProfile.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProfile.cs index c335959ed..2575a030e 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProfile.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksAgentPoolProfile.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json; using System.Text.Json.Serialization; -using Azure.Mcp.Tools.Aks.Commands; namespace Azure.Mcp.Tools.Aks.Services.Models; @@ -30,4 +28,4 @@ internal sealed class AksAgentPoolProfile public int? MinCount { get; set; } /// The current deployment or provisioning state. public string? ProvisioningState { get; set; } -} \ No newline at end of file +} diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterProperties.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterProperties.cs index 7445caf13..d52705c1d 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterProperties.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterProperties.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json; using System.Text.Json.Serialization; -using Azure.Mcp.Tools.Aks.Commands; namespace Azure.Mcp.Tools.Aks.Services.Models; @@ -29,4 +27,4 @@ internal sealed class AksClusterProperties public bool? EnableRbac { get; set; } /// The network configuration profile. public AksNetworkProfile? NetworkProfile { get; set; } -} \ No newline at end of file +} diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksManagedClusterSku.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksManagedClusterSku.cs index 8e661f0d1..b15d2de2c 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksManagedClusterSku.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksManagedClusterSku.cs @@ -1,10 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json; -using System.Text.Json.Serialization; -using Azure.Mcp.Tools.Aks.Commands; - namespace Azure.Mcp.Tools.Aks.Services.Models; /// The SKU of a Managed Cluster. @@ -14,4 +10,4 @@ internal sealed class AksManagedClusterSku public string? Name { get; set; } /// If not specified, the default is 'Free'. See [AKS Pricing Tier](https://learn.microsoft.com/azure/aks/free-standard-pricing-tiers) for more details. public string? Tier { get; set; } -} \ No newline at end of file +} diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksNetworkProfile.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksNetworkProfile.cs index bf1d1dd7a..fb86d9cf6 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksNetworkProfile.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksNetworkProfile.cs @@ -1,10 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json; -using System.Text.Json.Serialization; -using Azure.Mcp.Tools.Aks.Commands; - namespace Azure.Mcp.Tools.Aks.Services.Models; internal sealed class AksNetworkProfile @@ -17,4 +13,4 @@ internal sealed class AksNetworkProfile public string? ServiceCidr { get; set; } /// An IP address assigned to the Kubernetes DNS service. It must be within the Kubernetes service address range specified in serviceCidr. public string? DnsServiceIP { get; set; } -} \ No newline at end of file +} diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksPowerState.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksPowerState.cs index b24a30453..49c85a8cc 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksPowerState.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksPowerState.cs @@ -1,14 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.Json; -using System.Text.Json.Serialization; -using Azure.Mcp.Tools.Aks.Commands; - namespace Azure.Mcp.Tools.Aks.Services.Models; internal sealed class AksPowerState { /// Tells whether the cluster is Running or Stopped. public string? Code { get; set; } -} \ No newline at end of file +} From 7efa66a6f8056737fcb88cca97bb2d40f3f795d2 Mon Sep 17 00:00:00 2001 From: Arthur Ma Date: Tue, 16 Sep 2025 08:05:48 +0800 Subject: [PATCH 6/8] fixed format errors --- tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs | 2 +- tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs | 2 +- tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterData.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs b/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs index 9d3583884..4d6a0625b 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs @@ -3,8 +3,8 @@ using System.Text.Json.Serialization; using Azure.Mcp.Tools.Aks.Commands.Cluster; -using Azure.Mcp.Tools.Aks.Services.Models; using Azure.Mcp.Tools.Aks.Commands.Nodepool; +using Azure.Mcp.Tools.Aks.Services.Models; namespace Azure.Mcp.Tools.Aks.Commands; diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs index f4cbf3b32..ab0ffb70d 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs @@ -197,7 +197,7 @@ public async Task> ListNodePoolsAsync( await _cacheService.SetAsync(CacheGroup, cacheKey, nodePool, s_cacheDuration); return nodePool; } - + throw new KeyNotFoundException($"AKS node pool '{nodePoolName}' not found in cluster '{clusterName}' in resource group '{resourceGroup}' for subscription '{subscription}'."); } catch (Exception ex) diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterData.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterData.cs index f725c0ecf..8a384656e 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterData.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/Models/AksClusterData.cs @@ -37,4 +37,4 @@ internal sealed class AksClusterData { return JsonSerializer.Deserialize(source, AksJsonContext.Default.AksClusterData); } -} \ No newline at end of file +} From 9c1857b8e45e4f95a75cf4d85efd052584ee0315 Mon Sep 17 00:00:00 2001 From: Arthur Ma Date: Tue, 16 Sep 2025 08:42:29 +0800 Subject: [PATCH 7/8] Normalized cspell settings. Fixed aks tests. --- .vscode/cspell.json | 167 ++++++++++-------- .../Cluster/ClusterGetCommandTests.cs | 12 +- .../Cluster/ClusterListCommandTests.cs | 10 +- .../Nodepool/NodepoolGetCommandTests.cs | 10 +- .../Nodepool/NodepoolListCommandTests.cs | 10 +- 5 files changed, 111 insertions(+), 98 deletions(-) diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 69be63e20..6ee6d86e7 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -6,7 +6,7 @@ "csharp", "node", "powershell", - "softwareTerms", + "softwareterms", "typescript", "users" ], @@ -60,25 +60,10 @@ { "name": "baseline", "words": [ - "%2Fmcp", - "AADSTS", - "ACCESSTOKEN", - "Commmand", - "Commitish", - "Fmcp", - "Groq", - "HKCU", - "HKEY_CURRENT_USER", - "Hyperscale", - "Intune", - "LASTEXITCODE", - "LPUTF8Str", - "Ollama", - "Roboto", - "Segoe", - "VSTEST", - "Xunit", + "%2fmcp", + "aadsts", "accessibilities", + "accesstoken", "adadmin", "addattachment", "adminprovider", @@ -108,14 +93,16 @@ "cobertura", "codeql", "codesign", + "commitish", + "commmand", "contentfiles", "credscan", "cslschema", "cutover", "datatable", + "datetime", "datistemplate", "datname", - "datetime", "descired", "devcert", "deviceid", @@ -128,17 +115,25 @@ "entraid", "existingaccount", "filestorage", + "fmcp", "fname", "funkyfoo", "gdnbaselines", "globaltool", "glsl", + "groq", + "hkcu", + "hkey_current_user", "hotmail", + "hyperscale", + "intune", "kcsb", + "lastexitcode", "libc", "libgcc", "locproj", "logissue", + "lputf8str", "maxdepth", "mcptestadmin", "mgmt", @@ -164,6 +159,7 @@ "notcontains", "notlike", "nslookup", + "ollama", "otel", "otlp", "pipefail", @@ -172,9 +168,11 @@ "quickstart", "reportgenerator", "reporttypes", - "resx", "resourcegroups", + "resx", + "roboto", "securestring", + "segoe", "setvariable", "skus", "storageaccount", @@ -198,32 +196,35 @@ "vectorizable", "vectorizer", "vsix", - "vsixtarget" + "vsixtarget", + "vstest", + "xunit" ] } ], "words": [ "1espt", "aarch", - "accesspolicy", "acaenvironment", - "ADMINPROVIDER", + "accesspolicy", + "adminprovider", "agentic", "aisearch", "akscluster", "aksservice", "alcoop", - "AOAI", "amlfs", - "Apim", + "aoai", + "apim", "appconfig", "applens", "appservice", - "ASPNETCORE", + "aspnetcore", "australiacentral", "australiaeast", "australiasoutheast", - "Autorenewable", + "autorenewable", + "autoscaler", "azapi", "azcli", "azext", @@ -272,12 +273,13 @@ "bdylan", "bestpractices", "bicepschema", - "BINLOG", + "binlog", "binutils", + "blazor", "brazilsouth", "brazilsoutheast", "breathability", - "Byol", + "byol", "canadacentral", "canadaeast", "centralindia", @@ -287,18 +289,18 @@ "cloudarchitect", "codegen", "codeium", - "CODEOWNERS", + "codeowners", "codesign", - "Codespace", + "codespace", "cognitiveservices", "containerapp", "containerapps", - "CONTENTAZUREFILECONNECTIONSTRING", - "CONTENTSHARE", + "contentazurefileconnectionstring", + "contentshare", "contoso", - "CONV", + "conv", "copilotmd", - "Cosell", + "cosell", "csdevkit", "cslschema", "cvzf", @@ -309,22 +311,22 @@ "dataverse", "dbforpostgresql", "deallocate", - "DEBUGTELEMETRY", + "debugtelemetry", "devbox", "devcontainers", "discoverability", - "Distributedtask", - "dotnettools", + "distributedtask", "dotenv", + "dotnettools", "drawcord", - "DUMPFILE", + "dumpfile", "eastasia", "eastus", "eastus2euap", "enumerables", "eslintcache", "esrp", - "ESRPRELPACMANTEST", + "esrprelpacmantest", "eventgrid", "eventhouse", "exfiltration", @@ -342,9 +344,9 @@ "germanynorth", "gethealth", "grpcio", - "Gsaascend", - "Gsamas", - "GZRS", + "gsaascend", + "gsamas", + "gzrs", "healthmodels", "heatmaps", "hnsw", @@ -354,7 +356,9 @@ "idempotency", "idtyp", "indonesiacentral", - "INFILE", + "infile", + "intelli", + "intellij", "israelcentral", "italynorth", "japaneast", @@ -367,69 +371,73 @@ "keyvault", "koreacentral", "koreasouth", - "Kusto", + "kusto", "kvps", "lakehouse", + "laskewitz", "ligar", "linkedservices", - "Linq", - "LINUXOS", - "LINUXPOOL", - "LINUXVMIMAGE", - "LLM", + "linq", + "linuxos", + "linuxpool", + "linuxvmimage", + "llm", "loadtest", "loadtesting", "loadtestrun", "loadtests", "lucene", - "MACOS", - "MACPOOL", - "MACVMIMAGE", + "macos", + "macpool", + "macvmimage", "malaysiawest", "markitdown", "mcpserver", "mcptmp", "mexicocentral", - "midsole", - "Microbundle", + "microbundle", "microsoftdocs", + "midsole", "monitoredresources", "msal", - "MSRP", + "msrp", "myaccount", "myacr", "myapp", "mycluster", "myfilesystem", "mygroup", - "myworkbook", "mysvc", + "myworkbook", "netstandard", + "newtonsoft", "newzealandnorth", - "Newtonsoft", - "Npgsql", - "nupkg", + "nodepool", + "nodepools", "norequired", "northcentralus", "northeurope", "norwayeast", "norwaywest", + "npgsql", "npmjs", + "nugets", + "nupkg", "nuxt", - "Occured", + "occured", "odata", "oidc", "onboarded", "openai", "operationalinsights", - "OUTFILE", + "outfile", "packability", "pageable", "payg", "paygo", "pgrep", - "piechart", "pids", + "piechart", "polandcentral", "portalsettings", "predeploy", @@ -438,17 +446,19 @@ "pscore", "pscustomobject", "pullrequest", + "ragrs", + "ragzrs", "rainfly", - "RAGRS", - "RAGZRS", - "RediSearch", + "redisearch", "resourcegroup", "resourcegroups", "resourcehealth", + "rhtest", "rhvm", - "Runtimes", + "runtimes", "searchdocs", "serverfarms", + "serverjson", "servicebus", "sessionhost", "setparam", @@ -462,11 +472,13 @@ "southeastasia", "southindia", "spaincentral", + "systempool", "staticwebapp", "staticwebapps", "storageaccount", "storageaccounts", "submode", + "subresource", "swedencentral", "swedensouth", "switzerlandnorth", @@ -489,9 +501,10 @@ "uaenorth", "uksouth", "ukwest", - "UNCOMPRESS", - "UNHEX", - "Upns", + "uncompress", + "unhex", + "upns", + "userpool", "usersession", "vectorizable", "vectorizer", @@ -499,7 +512,7 @@ "versionsuffix", "virtualdesktop", "virtualmachines", - "Vnet", + "vnet", "vscodeignore", "vsmarketplace", "vsts", @@ -509,12 +522,12 @@ "westus", "westus2", "westus3", + "windowsos", + "windowspool", + "windowsvmimage", "winget", - "WINDOWSOS", - "WINDOWSPOOL", - "WINDOWSVMIMAGE", "wscript", - "xvfb", - "Xunit" + "xunit", + "xvfb" ] } diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Cluster/ClusterGetCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Cluster/ClusterGetCommandTests.cs index b4809aa03..117088b55 100644 --- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Cluster/ClusterGetCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Cluster/ClusterGetCommandTests.cs @@ -61,7 +61,7 @@ public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldS Location = "East US" }; - _aksService.GetCluster( + _aksService.GetClusterAsync( Arg.Any(), Arg.Any(), Arg.Any(), @@ -102,7 +102,7 @@ public async Task ExecuteAsync_ReturnsClusterWhenFound() ProvisioningState = "Succeeded" }; - _aksService.GetCluster("test-subscription", "test-cluster", "test-rg", null, Arg.Any()) + _aksService.GetClusterAsync("test-subscription", "test-cluster", "test-rg", null, Arg.Any()) .Returns(expectedCluster); var parseResult = _commandDefinition.Parse(["--subscription", "test-subscription", "--resource-group", "test-rg", "--cluster", "test-cluster"]); @@ -120,7 +120,7 @@ public async Task ExecuteAsync_ReturnsClusterWhenFound() public async Task ExecuteAsync_ReturnsNullWhenClusterNotFound() { // Arrange - _aksService.GetCluster("test-subscription", "nonexistent-cluster", "test-rg", null, Arg.Any()) + _aksService.GetClusterAsync("test-subscription", "nonexistent-cluster", "test-rg", null, Arg.Any()) .Returns((Models.Cluster?)null); var parseResult = _commandDefinition.Parse(["--subscription", "test-subscription", "--resource-group", "test-rg", "--cluster", "nonexistent-cluster"]); @@ -138,7 +138,7 @@ public async Task ExecuteAsync_ReturnsNullWhenClusterNotFound() public async Task ExecuteAsync_HandlesServiceErrors() { // Arrange - _aksService.GetCluster(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.GetClusterAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromException(new Exception("Test error"))); var parseResult = _commandDefinition.Parse(["--subscription", "test-subscription", "--resource-group", "test-rg", "--cluster", "test-cluster"]); @@ -157,7 +157,7 @@ public async Task ExecuteAsync_Handles404NotFound() { // Arrange var notFoundException = new RequestFailedException(404, "Not Found"); - _aksService.GetCluster(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.GetClusterAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromException(notFoundException)); var parseResult = _commandDefinition.Parse(["--subscription", "test-subscription", "--resource-group", "test-rg", "--cluster", "test-cluster"]); @@ -175,7 +175,7 @@ public async Task ExecuteAsync_Handles403Forbidden() { // Arrange var forbiddenException = new RequestFailedException(403, "Forbidden"); - _aksService.GetCluster(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.GetClusterAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromException(forbiddenException)); var parseResult = _commandDefinition.Parse(["--subscription", "test-subscription", "--resource-group", "test-rg", "--cluster", "test-cluster"]); diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Cluster/ClusterListCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Cluster/ClusterListCommandTests.cs index 16be62ade..aa007247c 100644 --- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Cluster/ClusterListCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Cluster/ClusterListCommandTests.cs @@ -56,7 +56,7 @@ public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldS new() { Name = "cluster1", Location = "eastus" }, new() { Name = "cluster2", Location = "westus" } }; - _aksService.ListClusters(Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.ListClustersAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(testClusters); } @@ -89,7 +89,7 @@ public async Task ExecuteAsync_ReturnsClustersList() new() { Name = "cluster2", Location = "westus", KubernetesVersion = "1.29.0" }, new() { Name = "cluster3", Location = "centralus", KubernetesVersion = "1.28.5" } }; - _aksService.ListClusters(Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.ListClustersAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(expectedClusters); var context = new CommandContext(_serviceProvider); @@ -101,7 +101,7 @@ public async Task ExecuteAsync_ReturnsClustersList() Assert.NotNull(response.Results); // Verify the mock was called - await _aksService.Received(1).ListClusters(Arg.Any(), Arg.Any(), Arg.Any()); + await _aksService.Received(1).ListClustersAsync(Arg.Any(), Arg.Any(), Arg.Any()); var json = JsonSerializer.Serialize(response.Results); // Debug: Output the actual JSON to understand the structure @@ -120,7 +120,7 @@ public async Task ExecuteAsync_ReturnsClustersList() public async Task ExecuteAsync_ReturnsNullWhenNoClusters() { // Arrange - _aksService.ListClusters(Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.ListClustersAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new List()); var context = new CommandContext(_serviceProvider); @@ -138,7 +138,7 @@ public async Task ExecuteAsync_ReturnsNullWhenNoClusters() public async Task ExecuteAsync_HandlesServiceErrors() { // Arrange - _aksService.ListClusters(Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.ListClustersAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromException>(new Exception("Test error"))); var context = new CommandContext(_serviceProvider); diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Nodepool/NodepoolGetCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Nodepool/NodepoolGetCommandTests.cs index d22ea5854..25149ed89 100644 --- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Nodepool/NodepoolGetCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Nodepool/NodepoolGetCommandTests.cs @@ -62,7 +62,7 @@ public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldS NodeVmSize = "Standard_DS2_v2", Mode = "System" }; - _aksService.GetNodePool(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.GetNodePoolAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(testNodePool); } @@ -95,7 +95,7 @@ public async Task ExecuteAsync_ReturnsNodePool() NodeVmSize = "Standard_D4s_v5", Mode = "User" }; - _aksService.GetNodePool(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.GetNodePoolAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(expectedNodePool); var context = new CommandContext(_serviceProvider); @@ -108,7 +108,7 @@ public async Task ExecuteAsync_ReturnsNodePool() Assert.Equal(200, response.Status); Assert.NotNull(response.Results); - await _aksService.Received(1).GetNodePool(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _aksService.Received(1).GetNodePoolAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); var json = JsonSerializer.Serialize(response.Results); var result = JsonSerializer.Deserialize(json, AksJsonContext.Default.NodepoolGetCommandResult); @@ -123,7 +123,7 @@ public async Task ExecuteAsync_ReturnsNodePool() public async Task ExecuteAsync_ReturnsNullWhenNodePoolNotFound() { // Arrange - _aksService.GetNodePool(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.GetNodePoolAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns((Models.NodePool?)null); var context = new CommandContext(_serviceProvider); @@ -141,7 +141,7 @@ public async Task ExecuteAsync_ReturnsNullWhenNodePoolNotFound() public async Task ExecuteAsync_HandlesServiceErrors() { // Arrange - _aksService.GetNodePool(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.GetNodePoolAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromException(new Exception("Test error"))); var context = new CommandContext(_serviceProvider); diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Nodepool/NodepoolListCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Nodepool/NodepoolListCommandTests.cs index 2ab98da2f..598efea2d 100644 --- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Nodepool/NodepoolListCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.UnitTests/Nodepool/NodepoolListCommandTests.cs @@ -59,7 +59,7 @@ public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldS new() { Name = "np1", NodeCount = 3, NodeVmSize = "Standard_DS2_v2" }, new() { Name = "np2", NodeCount = 5, NodeVmSize = "Standard_D4s_v5" } }; - _aksService.ListNodePools(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.ListNodePoolsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(testNodePools); } @@ -91,7 +91,7 @@ public async Task ExecuteAsync_ReturnsNodePoolsList() new() { Name = "systempool", NodeCount = 3, NodeVmSize = "Standard_DS2_v2", Mode = "System" }, new() { Name = "userpool", NodeCount = 5, NodeVmSize = "Standard_D4s_v5", Mode = "User" } }; - _aksService.ListNodePools(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.ListNodePoolsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(expectedNodePools); var context = new CommandContext(_serviceProvider); @@ -105,7 +105,7 @@ public async Task ExecuteAsync_ReturnsNodePoolsList() Assert.NotNull(response.Results); // Verify the mock was called - await _aksService.Received(1).ListNodePools(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _aksService.Received(1).ListNodePoolsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); var json = JsonSerializer.Serialize(response.Results); var result = JsonSerializer.Deserialize(json, AksJsonContext.Default.NodepoolListCommandResult); @@ -121,7 +121,7 @@ public async Task ExecuteAsync_ReturnsNodePoolsList() public async Task ExecuteAsync_ReturnsNullWhenNoNodePools() { // Arrange - _aksService.ListNodePools(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.ListNodePoolsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new List()); var context = new CommandContext(_serviceProvider); @@ -139,7 +139,7 @@ public async Task ExecuteAsync_ReturnsNullWhenNoNodePools() public async Task ExecuteAsync_HandlesServiceErrors() { // Arrange - _aksService.ListNodePools(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _aksService.ListNodePoolsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromException>(new Exception("Test error"))); var context = new CommandContext(_serviceProvider); From 5f10fe27e874b36523ac64508ef6bea69df3d882 Mon Sep 17 00:00:00 2001 From: Arthur Ma Date: Tue, 16 Sep 2025 17:44:04 +0800 Subject: [PATCH 8/8] update --- .../Azure/BaseAzureResourceService.cs | 23 +++++++++++++++++++ .../src/Services/AksService.cs | 7 ++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/core/Azure.Mcp.Core/src/Services/Azure/BaseAzureResourceService.cs b/core/Azure.Mcp.Core/src/Services/Azure/BaseAzureResourceService.cs index 52d4b75b1..77223fa4b 100644 --- a/core/Azure.Mcp.Core/src/Services/Azure/BaseAzureResourceService.cs +++ b/core/Azure.Mcp.Core/src/Services/Azure/BaseAzureResourceService.cs @@ -4,6 +4,7 @@ using Azure.Mcp.Core.Options; using Azure.Mcp.Core.Services.Azure.Subscription; using Azure.Mcp.Core.Services.Azure.Tenant; +using Azure.ResourceManager; using Azure.ResourceManager.ResourceGraph; using Azure.ResourceManager.ResourceGraph.Models; using Azure.ResourceManager.Resources; @@ -48,6 +49,20 @@ private async Task GetTenantResourceAsync(Guid? tenantId, Cancel return tenantResource; } + /// + /// Validates that the specified resource group exists within the given subscription. + /// + /// The subscription resource to check against. + /// The name of the resource group to validate. + /// Cancellation token. + /// True if the resource group exists; otherwise, false. + private async Task ValidateResourceGroupExistsAsync(SubscriptionResource subscriptionResource, string resourceGroupName, CancellationToken cancellationToken = default) + { + var resourceGroupCollection = subscriptionResource.GetResourceGroups(); + var result = await resourceGroupCollection.ExistsAsync(resourceGroupName, cancellationToken).ConfigureAwait(false); + return result.Value; + } + /// /// Executes a Resource Graph query and returns a list of resources of the specified type. /// @@ -82,6 +97,10 @@ protected async Task> ExecuteResourceQueryAsync( var queryFilter = $"Resources | where type =~ '{EscapeKqlString(resourceType)}'"; if (!string.IsNullOrEmpty(resourceGroup)) { + if (!await ValidateResourceGroupExistsAsync(subscriptionResource, resourceGroup, cancellationToken)) + { + throw new KeyNotFoundException($"Resource group '{resourceGroup}' does not exist in subscription '{subscriptionResource.Data.SubscriptionId}'"); + } queryFilter += $" and resourceGroup =~ '{EscapeKqlString(resourceGroup)}'"; } if (!string.IsNullOrEmpty(additionalFilter)) @@ -142,6 +161,10 @@ protected async Task> ExecuteResourceQueryAsync( var queryFilter = $"Resources | where type =~ '{EscapeKqlString(resourceType)}'"; if (!string.IsNullOrEmpty(resourceGroup)) { + if (!await ValidateResourceGroupExistsAsync(subscriptionResource, resourceGroup, cancellationToken)) + { + throw new KeyNotFoundException($"Resource group '{resourceGroup}' does not exist in subscription '{subscriptionResource.Data.SubscriptionId}'"); + } queryFilter += $" and resourceGroup =~ '{EscapeKqlString(resourceGroup)}'"; } if (!string.IsNullOrEmpty(additionalFilter)) diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs index ab0ffb70d..8ae55ae1e 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs @@ -146,8 +146,11 @@ public async Task> ListNodePoolsAsync( subscription, retryPolicy, ConvertToClusterNodePoolModel, - $"name =~ '{EscapeKqlString(clusterName)}'") ?? new List(); - + $"name =~ '{EscapeKqlString(clusterName)}'"); + if (nodePools == null) + { + throw new KeyNotFoundException($"No node pools found for cluster '{clusterName}' in resource group '{resourceGroup}' for subscription '{subscription}'"); + } // Cache the results await _cacheService.SetAsync(CacheGroup, cacheKey, nodePools, s_cacheDuration); return nodePools;