diff --git a/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs b/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs index 4af52fc34a..8500be36b4 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Commands/AksJsonContext.cs @@ -4,6 +4,7 @@ using System.Text.Json.Serialization; using Azure.Mcp.Tools.Aks.Commands.Cluster; using Azure.Mcp.Tools.Aks.Commands.Nodepool; +using Azure.Mcp.Tools.Aks.Services; namespace Azure.Mcp.Tools.Aks.Commands; @@ -46,5 +47,14 @@ namespace Azure.Mcp.Tools.Aks.Commands; [JsonSerializable(typeof(Models.WorkloadAutoScalerProfile))] [JsonSerializable(typeof(Models.WorkloadAutoScalerKeda))] [JsonSerializable(typeof(Models.WorkloadAutoScalerVerticalPodAutoscaler))] +[JsonSerializable(typeof(AksClusterResourceGraphResponse))] +[JsonSerializable(typeof(AksClusterSkuJson))] +[JsonSerializable(typeof(AksClusterPropertiesJson))] +[JsonSerializable(typeof(AksPowerStateJson))] +[JsonSerializable(typeof(AksClusterNetworkProfileJson))] +[JsonSerializable(typeof(AksNetworkLoadBalancerProfileJson))] +[JsonSerializable(typeof(AksManagedOutboundIPsJson))] +[JsonSerializable(typeof(AksAddonProfileJson))] +[JsonSerializable(typeof(AksAddonIdentityJson))] [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 666b4a3ce3..a2dd81fb94 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterGetCommand.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Commands/Cluster/ClusterGetCommand.cs @@ -33,15 +33,6 @@ protected override void RegisterOptions(Command command) base.RegisterOptions(command); command.Options.Add(OptionDefinitions.Common.ResourceGroup); command.Options.Add(AksOptionDefinitions.Cluster); - command.Validators.Add(commandResults => - { - var clusterName = commandResults.GetValueOrDefault(AksOptionDefinitions.Cluster); - var resourceGroup = commandResults.GetValueOrDefault(OptionDefinitions.Common.ResourceGroup); - if (!string.IsNullOrEmpty(clusterName) && string.IsNullOrEmpty(resourceGroup)) - { - commandResults.AddError("When specifying a cluster name, the --resource-group option is required."); - } - }); } protected override ClusterGetOptions BindOptions(ParseResult parseResult) diff --git a/tools/Azure.Mcp.Tools.Aks/src/Models/NodePool.cs b/tools/Azure.Mcp.Tools.Aks/src/Models/NodePool.cs index 05063053bc..d8ec04bed8 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Models/NodePool.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Models/NodePool.cs @@ -155,7 +155,10 @@ public sealed class NodePoolNetworkProfile public sealed class PortRange { + [System.Text.Json.Serialization.JsonPropertyName("portStart")] public int? StartPort { get; set; } + + [System.Text.Json.Serialization.JsonPropertyName("portEnd")] public int? EndPort { get; set; } } diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/AksClusterResourceGraphResponse.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/AksClusterResourceGraphResponse.cs new file mode 100644 index 0000000000..e94457d440 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/AksClusterResourceGraphResponse.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.Mcp.Tools.Aks.Models; + +namespace Azure.Mcp.Tools.Aks.Services; + +/// +/// Intermediate type that mirrors the Azure Resource Graph JSON structure for a managed cluster +/// resource. Used only for deserialization; the public model is then +/// populated from this type via AksService.MapToCluster. +/// +internal sealed class AksClusterResourceGraphResponse +{ + public string? Id { get; set; } + public string? Name { get; set; } + public string? SubscriptionId { get; set; } + + [JsonPropertyName("resourceGroup")] + public string? ResourceGroup { get; set; } + + public string? Location { get; set; } + public Dictionary? Tags { get; set; } + public AksClusterSkuJson? Sku { get; set; } + public ResourceIdentity? Identity { get; set; } + public AksClusterPropertiesJson? Properties { get; set; } +} + +internal sealed class AksClusterSkuJson +{ + public string? Name { get; set; } + public string? Tier { get; set; } +} + +internal sealed class AksClusterPropertiesJson +{ + public string? KubernetesVersion { get; set; } + public string? ProvisioningState { get; set; } + public string? DnsPrefix { get; set; } + public string? Fqdn { get; set; } + public string? NodeResourceGroup { get; set; } + public string? SupportPlan { get; set; } + + [JsonPropertyName("resourceUID")] + public string? ResourceUid { get; set; } + + [JsonPropertyName("enableRBAC")] + public bool? EnableRbac { get; set; } + + public bool? DisableLocalAccounts { get; set; } + public int? MaxAgentPools { get; set; } + public AksPowerStateJson? PowerState { get; set; } + public AksClusterNetworkProfileJson? NetworkProfile { get; set; } + public OidcIssuerProfile? OidcIssuerProfile { get; set; } + public AutoUpgradeProfile? AutoUpgradeProfile { get; set; } + public ClusterSecurityProfile? SecurityProfile { get; set; } + public ClusterStorageProfile? StorageProfile { get; set; } + public WorkloadAutoScalerProfile? WorkloadAutoScalerProfile { get; set; } + public Dictionary? AddonProfiles { get; set; } + public Dictionary? IdentityProfile { get; set; } + public List? AgentPoolProfiles { get; set; } +} + +internal sealed class AksPowerStateJson +{ + public string? Code { get; set; } +} + +/// +/// Intermediate network profile type. The public model +/// has a flat ManagedOutboundIPCount, while the Resource Graph JSON nests this value +/// under loadBalancerProfile.managedOutboundIPs.count, requiring a separate type. +/// +internal sealed class AksClusterNetworkProfileJson +{ + public string? NetworkPlugin { get; set; } + public string? NetworkPluginMode { get; set; } + public string? NetworkPolicy { get; set; } + public string? NetworkDataplane { get; set; } + public string? LoadBalancerSku { get; set; } + public AksNetworkLoadBalancerProfileJson? LoadBalancerProfile { get; set; } + public string? PodCidr { get; set; } + public string? ServiceCidr { get; set; } + public string? DnsServiceIP { get; set; } + public string? OutboundType { get; set; } + public List? PodCidrs { get; set; } + public List? ServiceCidrs { get; set; } + public List? IpFamilies { get; set; } +} + +internal sealed class AksNetworkLoadBalancerProfileJson +{ + public AksManagedOutboundIPsJson? ManagedOutboundIPs { get; set; } + public List? EffectiveOutboundIPs { get; set; } + public string? BackendPoolType { get; set; } +} + +internal sealed class AksManagedOutboundIPsJson +{ + public int? Count { get; set; } +} + +/// +/// Intermediate add-on profile type used to deserialize each entry in +/// properties.addonProfiles before flattening to the config.* / +/// identity.* key convention used by . +/// +internal sealed class AksAddonProfileJson +{ + public bool? Enabled { get; set; } + public Dictionary? Config { get; set; } + public AksAddonIdentityJson? Identity { get; set; } +} + +internal sealed class AksAddonIdentityJson +{ + public string? ClientId { get; set; } + public string? ObjectId { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs index 7207106f80..dae5805c8c 100644 --- a/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs +++ b/tools/Azure.Mcp.Tools.Aks/src/Services/AksService.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Text.Json; using Azure.Mcp.Core.Services.Azure; using Azure.Mcp.Core.Services.Azure.Subscription; using Azure.Mcp.Core.Services.Azure.Tenant; +using Azure.Mcp.Tools.Aks.Commands; using Azure.Mcp.Tools.Aks.Models; using Azure.ResourceManager.ContainerService; using Azure.ResourceManager.ContainerService.Models; @@ -17,7 +19,7 @@ public sealed class AksService( ISubscriptionService subscriptionService, ITenantService tenantService, ICacheService cacheService, - ILogger logger) : BaseAzureService(tenantService), IAksService + 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)); @@ -56,36 +58,16 @@ public async Task> GetClusters( return cachedClusters; } - var subscriptionResource = await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken); - - var clusters = new List(); - if (string.IsNullOrEmpty(resourceGroup)) - { - - await foreach (var cluster in subscriptionResource.GetContainerServiceManagedClustersAsync(cancellationToken)) - { - if (cluster?.Data != null) - { - clusters.Add(ConvertToClusterModel(cluster)); - } - } - } - else - { - var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); - if (resourceGroupResource?.Value == null) - { - return clusters; - } + var result = await ExecuteResourceQueryAsync( + "Microsoft.ContainerService/managedClusters", + resourceGroup, + subscription, + retryPolicy, + ConvertToClusterFromJson, + tenant: tenant, + cancellationToken: cancellationToken); - await foreach (var cluster in resourceGroupResource.Value.GetContainerServiceManagedClusters().GetAllAsync(cancellationToken)) - { - if (cluster?.Data != null) - { - clusters.Add(ConvertToClusterModel(cluster)); - } - } - } + var clusters = result.Results; // Cache the results await _cacheService.SetAsync(CacheGroup, cacheKey, clusters, s_cacheDuration, cancellationToken); @@ -94,12 +76,16 @@ public async Task> GetClusters( } else { - ValidateRequiredParameters((nameof(resourceGroup), resourceGroup), (nameof(clusterName), clusterName)); + ValidateRequiredParameters((nameof(clusterName), clusterName)); // Create cache key - var cacheKey = string.IsNullOrEmpty(tenant) - ? CacheKeyBuilder.Build("cluster", subscription, resourceGroup!, clusterName) - : CacheKeyBuilder.Build("cluster", subscription, resourceGroup!, clusterName, tenant); + var cacheKey = (string.IsNullOrEmpty(resourceGroup), string.IsNullOrEmpty(tenant)) switch + { + (true, true) => CacheKeyBuilder.Build("cluster", subscription, clusterName), + (false, true) => CacheKeyBuilder.Build("cluster", subscription, resourceGroup, clusterName), + (true, false) => CacheKeyBuilder.Build("cluster", subscription, clusterName, tenant), + (false, false) => CacheKeyBuilder.Build("cluster", subscription, resourceGroup, clusterName, tenant) + }; // Try to get from cache first var cachedCluster = await _cacheService.GetAsync>(CacheGroup, cacheKey, s_cacheDuration, cancellationToken); @@ -108,24 +94,22 @@ public async Task> GetClusters( return cachedCluster; } - var subscriptionResource = await _subscriptionService.GetSubscription(subscription, tenant, retryPolicy, cancellationToken); - var resourceGroupResource = await subscriptionResource.GetResourceGroupAsync(resourceGroup, cancellationToken); - - if (resourceGroupResource?.Value == null) - { - return []; - } - - var clusterResource = await resourceGroupResource.Value - .GetContainerServiceManagedClusters() - .GetAsync(clusterName, cancellationToken); - - if (clusterResource?.Value?.Data == null) + var cluster = await ExecuteSingleResourceQueryAsync( + "Microsoft.ContainerService/managedClusters", + resourceGroup: resourceGroup, + subscription: subscription, + retryPolicy: retryPolicy, + converter: ConvertToClusterFromJson, + additionalFilter: $"name =~ '{EscapeKqlString(clusterName!)}'" , + tenant: tenant, + cancellationToken: cancellationToken); + + if (cluster == null) { return []; } - var clusters = new List() { ConvertToClusterModel(clusterResource.Value) }; + var clusters = new List() { cluster }; // Cache the result await _cacheService.SetAsync(CacheGroup, cacheKey, clusters, s_cacheDuration, cancellationToken); @@ -242,176 +226,121 @@ public async Task> GetNodePools( } } - private static Cluster ConvertToClusterModel(ContainerServiceManagedClusterResource clusterResource) + private static Cluster ConvertToClusterFromJson(JsonElement item) + { + var response = JsonSerializer.Deserialize(item, AksJsonContext.Default.AksClusterResourceGraphResponse); + return response is null ? new Cluster() : MapToCluster(response); + } + + private static Cluster MapToCluster(AksClusterResourceGraphResponse response) { - var data = clusterResource.Data; - var agentPool = data.AgentPoolProfiles?.FirstOrDefault(); + var props = response.Properties; + var np = props?.NetworkProfile; var cluster = new Cluster { - Id = clusterResource.Id.ToString(), - 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, - NodeCount = agentPool?.Count, - NodeVmSize = agentPool?.VmSize, - IdentityType = data.Identity?.ManagedServiceIdentityType.ToString(), - Identity = new() - { - Type = data.Identity?.ManagedServiceIdentityType.ToString(), - PrincipalId = data.Identity?.PrincipalId?.ToString(), - TenantId = data.Identity?.TenantId?.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(), - SkuName = data.Sku?.Name?.ToString(), - NodeResourceGroup = data.NodeResourceGroup, - MaxAgentPools = data.MaxAgentPools, - SupportPlan = data.SupportPlan?.ToString(), - NetworkProfile = new() - { - NetworkPlugin = data.NetworkProfile?.NetworkPlugin?.ToString(), - NetworkPluginMode = data.NetworkProfile?.NetworkPluginMode?.ToString(), - NetworkPolicy = data.NetworkProfile?.NetworkPolicy?.ToString(), - NetworkDataplane = data.NetworkProfile?.NetworkDataplane?.ToString(), - LoadBalancerSku = data.NetworkProfile?.LoadBalancerSku?.ToString(), - LoadBalancerProfile = data.NetworkProfile?.LoadBalancerProfile is null ? null : new() - { - ManagedOutboundIPCount = data.NetworkProfile?.LoadBalancerProfile?.ManagedOutboundIPs?.Count, - EffectiveOutboundIPs = data.NetworkProfile?.LoadBalancerProfile?.EffectiveOutboundIPs?.Select(e => new EffectiveOutboundIPReference() { Id = e.Id?.ToString() }).ToList(), - BackendPoolType = data.NetworkProfile?.LoadBalancerProfile?.BackendPoolType?.ToString() - }, - PodCidr = data.NetworkProfile?.PodCidr, - ServiceCidr = data.NetworkProfile?.ServiceCidr, - DnsServiceIP = data.NetworkProfile?.DnsServiceIP?.ToString(), - OutboundType = data.NetworkProfile?.OutboundType?.ToString(), - PodCidrs = data.NetworkProfile?.PodCidrs?.ToList(), - ServiceCidrs = data.NetworkProfile?.ServiceCidrs?.ToList(), - IpFamilies = data.NetworkProfile?.IPFamilies?.Select(f => f.ToString()).ToList() - }, - WindowsProfile = data.WindowsProfile is null ? null : new() { AdminUsername = data.WindowsProfile.AdminUsername }, - ServicePrincipalProfile = data.ServicePrincipalProfile is null ? null : new() { ClientId = data.ServicePrincipalProfile.ClientId }, - AutoUpgradeProfile = data.AutoUpgradeProfile is null ? null : new() - { - UpgradeChannel = data.AutoUpgradeProfile.UpgradeChannel?.ToString(), - NodeOSUpgradeChannel = data.AutoUpgradeProfile.NodeOSUpgradeChannel?.ToString() - }, - // OIDC Issuer Profile - OidcIssuerProfile = data.OidcIssuerProfile is null ? null : new() - { - Enabled = data.OidcIssuerProfile.IsEnabled, - IssuerUrl = data.OidcIssuerProfile.IssuerUriInfo - }, - AddonProfiles = data.AddonProfiles?.ToDictionary( - kvp => kvp.Key, - static kvp => - { - IDictionary map = new Dictionary(); - if (kvp.Value != null) - { - if (kvp.Value.Config != null) - { - foreach (var c in kvp.Value.Config) - { - map[$"config.{c.Key}"] = c.Value; - } - } - if (kvp.Value.Identity != null) - { - if (kvp.Value.Identity.ClientId != null) - map.Add("identity.clientId", kvp.Value.Identity.ClientId.ToString()!); - if (kvp.Value.Identity.ObjectId != null) - map.Add("identity.objectId", kvp.Value.Identity.ObjectId.ToString()!); - } - } - return map; - }), - IdentityProfile = data.IdentityProfile?.ToDictionary( - kvp => kvp.Key, - kvp => new ManagedIdentityReference - { - ResourceId = kvp.Value?.ResourceId?.ToString(), - ClientId = kvp.Value?.ClientId?.ToString(), - ObjectId = kvp.Value?.ObjectId?.ToString() - }), - DisableLocalAccounts = data.DisableLocalAccounts, - // Security Profile - SecurityProfile = data.SecurityProfile is null ? null : new() - { - AzureKeyVaultKms = data.SecurityProfile.AzureKeyVaultKms is null ? null : new() - { - Enabled = data.SecurityProfile.AzureKeyVaultKms.IsEnabled, - KeyId = data.SecurityProfile.AzureKeyVaultKms.KeyId?.ToString() - }, - Defender = data.SecurityProfile.Defender is null ? null : new() - { - LogAnalyticsWorkspaceResourceId = data.SecurityProfile.Defender.LogAnalyticsWorkspaceResourceId?.ToString(), - SecurityMonitoring = new() { Enabled = data.SecurityProfile.Defender.IsSecurityMonitoringEnabled } - }, - ImageCleaner = data.SecurityProfile.ImageCleaner is null ? null : new() - { - Enabled = data.SecurityProfile.ImageCleaner.IsEnabled, - IntervalHours = data.SecurityProfile.ImageCleaner.IntervalHours - }, - WorkloadIdentity = data.SecurityProfile.IsWorkloadIdentityEnabled is null - ? null - : new() { Enabled = data.SecurityProfile.IsWorkloadIdentityEnabled } - }, - // Storage Profile - StorageProfile = data.StorageProfile is null ? null : new() - { - BlobCSIDriver = data.StorageProfile.IsBlobCsiDriverEnabled is null ? null : new() { Enabled = data.StorageProfile.IsBlobCsiDriverEnabled }, - DiskCSIDriver = data.StorageProfile.IsDiskCsiDriverEnabled is null ? null : new() { Enabled = data.StorageProfile.IsDiskCsiDriverEnabled }, - FileCSIDriver = data.StorageProfile.IsFileCsiDriverEnabled is null ? null : new() { Enabled = data.StorageProfile.IsFileCsiDriverEnabled }, - SnapshotController = data.StorageProfile.IsSnapshotControllerEnabled is null ? null : new() { Enabled = data.StorageProfile.IsSnapshotControllerEnabled } - }, - // Metrics profile (no 1.2.5 SDK match for our CostAnalysis model) - MetricsProfile = null, - // Node provisioning and bootstrap profiles are not exposed in SDK 1.2.5 - NodeProvisioningProfile = null, - BootstrapProfile = null, - // Workload Auto-scaler profile - WorkloadAutoScalerProfile = data.WorkloadAutoScalerProfile is null ? null : new() + Id = response.Id, + Name = response.Name, + SubscriptionId = response.SubscriptionId, + ResourceGroupName = response.ResourceGroup, + Location = response.Location, + Tags = response.Tags, + SkuName = response.Sku?.Name, + SkuTier = response.Sku?.Tier, + IdentityType = response.Identity?.Type, + Identity = response.Identity, + KubernetesVersion = props?.KubernetesVersion, + ProvisioningState = props?.ProvisioningState, + DnsPrefix = props?.DnsPrefix, + Fqdn = props?.Fqdn, + NodeResourceGroup = props?.NodeResourceGroup, + SupportPlan = props?.SupportPlan, + ResourceUid = props?.ResourceUid, + EnableRbac = props?.EnableRbac, + DisableLocalAccounts = props?.DisableLocalAccounts, + MaxAgentPools = props?.MaxAgentPools, + PowerState = props?.PowerState?.Code, + NetworkPlugin = np?.NetworkPlugin, + NetworkPolicy = np?.NetworkPolicy, + ServiceCidr = np?.ServiceCidr, + DnsServiceIP = np?.DnsServiceIP, + NetworkProfile = MapToNetworkProfile(np), + OidcIssuerProfile = props?.OidcIssuerProfile, + AutoUpgradeProfile = props?.AutoUpgradeProfile, + SecurityProfile = props?.SecurityProfile, + StorageProfile = props?.StorageProfile, + WorkloadAutoScalerProfile = props?.WorkloadAutoScalerProfile, + AddonProfiles = MapAddonProfiles(props?.AddonProfiles), + IdentityProfile = props?.IdentityProfile, + AgentPoolProfiles = props?.AgentPoolProfiles, + NodeCount = props?.AgentPoolProfiles?.Count > 0 ? props.AgentPoolProfiles[0].Count : null, + NodeVmSize = props?.AgentPoolProfiles?.Count > 0 ? props.AgentPoolProfiles[0].VmSize : null, + }; + + return cluster; + } + + private static ClusterNetworkProfile? MapToNetworkProfile(AksClusterNetworkProfileJson? np) + { + if (np is null) + return null; + + ClusterNetworkLoadBalancerProfile? lbProfile = null; + if (np.LoadBalancerProfile is { } lb) + { + lbProfile = new() { - Keda = data.WorkloadAutoScalerProfile.IsKedaEnabled is null ? null : new() { Enabled = data.WorkloadAutoScalerProfile.IsKedaEnabled }, - VerticalPodAutoscaler = data.WorkloadAutoScalerProfile.IsVpaEnabled is null ? null : new() { Enabled = data.WorkloadAutoScalerProfile.IsVpaEnabled } - }, - // AI toolchain operator profile not exposed in SDK 1.2.5 - AiToolchainOperatorProfile = null, - // Unique resource UID - ResourceUid = data.ResourceId, - Tags = data.Tags?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) + ManagedOutboundIPCount = lb.ManagedOutboundIPs?.Count, + EffectiveOutboundIPs = lb.EffectiveOutboundIPs, + BackendPoolType = lb.BackendPoolType, + }; + } + + return new() + { + NetworkPlugin = np.NetworkPlugin, + NetworkPluginMode = np.NetworkPluginMode, + NetworkPolicy = np.NetworkPolicy, + NetworkDataplane = np.NetworkDataplane, + LoadBalancerSku = np.LoadBalancerSku, + LoadBalancerProfile = lbProfile, + PodCidr = np.PodCidr, + ServiceCidr = np.ServiceCidr, + DnsServiceIP = np.DnsServiceIP, + OutboundType = np.OutboundType, + PodCidrs = np.PodCidrs, + ServiceCidrs = np.ServiceCidrs, + IpFamilies = np.IpFamilies, }; + } - // Map agent pool profiles from the cluster resource data when available - if (data.AgentPoolProfiles is not null) + private static IDictionary>? MapAddonProfiles( + Dictionary? addonProfiles) + { + if (addonProfiles is null) + return null; + + var result = new Dictionary>(); + foreach (var (name, addon) in addonProfiles) { - try + var map = new Dictionary(); + if (addon.Config is not null) { - cluster.AgentPoolProfiles = data.AgentPoolProfiles.Select(ConvertToNodePoolModel).ToList(); + foreach (var (key, value) in addon.Config) + map[$"config.{key}"] = value; } - catch + if (addon.Identity is not null) { - // If SDK shape differs, fall back to minimal projection - cluster.AgentPoolProfiles = data.AgentPoolProfiles - .Select(p => new NodePool { Name = p.Name, Count = p.Count, VmSize = p.VmSize?.ToString(), Mode = p.Mode?.ToString() }) - .ToList(); + if (addon.Identity.ClientId is not null) + map["identity.clientId"] = addon.Identity.ClientId; + if (addon.Identity.ObjectId is not null) + map["identity.objectId"] = addon.Identity.ObjectId; } + result[name] = map; } - return cluster; + return result.Count > 0 ? result : null; } - private static NodePool ConvertToNodePoolModel(ContainerServiceAgentPoolResource agentPoolResource) { var data = agentPoolResource.Data; diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/AksCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/AksCommandTests.cs index 63b6a71f79..efc58fc6a0 100644 --- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/AksCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/AksCommandTests.cs @@ -178,21 +178,36 @@ public async Task Should_get_specific_aks_cluster() [Fact] public async Task Should_handle_nonexistent_cluster_gracefully() { + // First, get a list of clusters to find a resource group to test against + var listResult = await CallToolAsync( + "aks_cluster_get", + new() + { + { "subscription", Settings.SubscriptionId } + }); + + var clusters = listResult.AssertProperty("clusters"); + Assert.True(clusters.GetArrayLength() > 0, "Expected at least one AKS cluster for testing get command"); + + // Get the first cluster's resource group + var firstCluster = clusters.EnumerateArray().First(); + var resourceGroupName = RegisterOrRetrieveVariable("firstResourceGroupName", firstCluster.GetProperty("resourceGroupName").GetString()!); + + // Attempt to get a non-existent cluster from that resource group var result = await CallToolAsync( "aks_cluster_get", new() { { "subscription", Settings.SubscriptionId }, - { "resource-group", "nonexistent-rg" }, + { "resource-group", resourceGroupName }, { "cluster", "nonexistent-cluster" } }); - // Should return runtime error response with error details + // Should return list with zero clusters Assert.True(result.HasValue); - var errorDetails = result.Value; - errorDetails.AssertProperty("message"); - var typeProperty = errorDetails.AssertProperty("type"); - Assert.Equal("RequestFailedException", typeProperty.GetString()); + var results = result.Value; + var resultsClusters = results.AssertProperty("clusters"); + Assert.True(resultsClusters.GetArrayLength() == 0, "Expected no clusters for nonexistent cluster request"); } [Fact] @@ -353,6 +368,7 @@ public async Task Should_handle_nonexistent_nodepool_gracefully() var errorDetails = result.Value; errorDetails.AssertProperty("message"); var typeProperty = errorDetails.AssertProperty("type"); + Assert.Equal("RequestFailedException", typeProperty.GetString()); } diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Cluster/ClusterGetCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Cluster/ClusterGetCommandTests.cs index e99b43ceb5..ce5ddbe031 100644 --- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Cluster/ClusterGetCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Cluster/ClusterGetCommandTests.cs @@ -26,9 +26,9 @@ public void Constructor_InitializesCommandCorrectly() [Theory] [InlineData("--subscription sub1 --resource-group rg1 --cluster cluster1", true)] - [InlineData("--subscription sub1 --cluster cluster1", false)] // Missing resource-group - [InlineData("--resource-group rg1 --cluster cluster1", false)] // Missing subscription - [InlineData("", false)] // Missing all required options + [InlineData("--subscription sub1 --cluster cluster1", true)] // Resource group is optional with ARG queries + [InlineData("--resource-group rg1 --cluster cluster1", true)] // Subscription is no longer required + [InlineData("", true)] // Valid scenario now that resource querying is being utilized public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) { // Arrange diff --git a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Nodepool/NodepoolGetCommandTests.cs b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Nodepool/NodepoolGetCommandTests.cs index b54108c949..c89e69d767 100644 --- a/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Nodepool/NodepoolGetCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Aks/tests/Azure.Mcp.Tools.Aks.Tests/Nodepool/NodepoolGetCommandTests.cs @@ -29,7 +29,7 @@ public void Constructor_InitializesCommandCorrectly() [InlineData("--subscription sub123 --resource-group rg1 --cluster c1 --nodepool np1 --tenant t1", true)] [InlineData("--subscription sub123 --resource-group rg1 --nodepool np1", false)] // missing cluster [InlineData("--subscription sub123 --cluster c1 --nodepool np1", false)] // missing rg - [InlineData("--resource-group rg1 --cluster c1 --nodepool np1", false)] // missing subscription + [InlineData("--resource-group rg1 --cluster c1 --nodepool np1", true)] // ARG querying doesn't require subscription [InlineData("", false)] public async Task ExecuteAsync_ValidatesInputCorrectly(string args, bool shouldSucceed) {