-
Notifications
You must be signed in to change notification settings - Fork 423
Read default subscription from Azure CLI profile for all subscription-scoped commands #1974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
11804a9
1c6ebbd
5c04535
7a6be82
7c6c198
bf5fbae
2c640d6
0a655a6
48d3b98
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Azure.Mcp.Core.Areas.Subscription.Models; | ||
| using Azure.Mcp.Core.Areas.Subscription.Options; | ||
| using Azure.Mcp.Core.Commands; | ||
| using Azure.Mcp.Core.Services.Azure.Subscription; | ||
|
|
@@ -21,7 +22,11 @@ public sealed class SubscriptionListCommand(ILogger<SubscriptionListCommand> log | |
| public override string Name => "list"; | ||
|
|
||
| public override string Description => | ||
| "List all or current subscriptions for an account in Azure; returns subscriptionId, displayName, state, tenantId, and isDefault. Use for scope selection in governance, policy, access, cost management, or deployment."; | ||
| "List all Azure subscriptions for the current account. Returns subscriptionId, displayName, state, tenantId, and isDefault for each subscription. " + | ||
| "The isDefault field indicates the user's default subscription as resolved from the Azure CLI profile (configured via 'az account set') or, if not set there, from the AZURE_SUBSCRIPTION_ID environment variable. " + | ||
| "When the user has not specified a subscription, prefer the subscription where isDefault is true. " + | ||
| "If no default can be determined from either source and multiple subscriptions exist, ask the user which subscription to use."; | ||
|
|
||
| public override string Title => CommandTitle; | ||
|
|
||
| public override ToolMetadata Metadata => new() | ||
|
|
@@ -48,8 +53,11 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context, | |
| var subscriptionService = context.GetService<ISubscriptionService>(); | ||
| var subscriptions = await subscriptionService.GetSubscriptions(options.Tenant, options.RetryPolicy, cancellationToken); | ||
|
|
||
| var defaultSubscriptionId = subscriptionService.GetDefaultSubscriptionId(); | ||
| var subscriptionInfos = MapToSubscriptionInfos(subscriptions, defaultSubscriptionId); | ||
|
|
||
| context.Response.Results = ResponseResult.Create( | ||
| new SubscriptionListCommandResult(subscriptions), | ||
| new SubscriptionListCommandResult(subscriptionInfos), | ||
| SubscriptionJsonContext.Default.SubscriptionListCommandResult); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Breaking change: The result payload changed from This should at minimum be documented in a changelog entry (none was added). Consider whether wrapping |
||
| } | ||
| catch (Exception ex) | ||
|
|
@@ -61,5 +69,26 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context, | |
| return context.Response; | ||
| } | ||
|
|
||
| internal record SubscriptionListCommandResult(List<SubscriptionData> Subscriptions); | ||
| internal static List<SubscriptionInfo> MapToSubscriptionInfos(List<SubscriptionData> subscriptions, string? defaultSubscriptionId) | ||
| { | ||
| var hasDefault = !string.IsNullOrEmpty(defaultSubscriptionId); | ||
|
|
||
| var infos = subscriptions.Select(s => new SubscriptionInfo( | ||
| s.SubscriptionId, | ||
| s.DisplayName, | ||
| s.State?.ToString(), | ||
| s.TenantId?.ToString(), | ||
| hasDefault && s.SubscriptionId.Equals(defaultSubscriptionId, StringComparison.OrdinalIgnoreCase) | ||
| )).ToList(); | ||
|
|
||
| // Sort so the default subscription appears first | ||
| if (hasDefault) | ||
| { | ||
| infos = [.. infos.OrderByDescending(s => s.IsDefault)]; | ||
| } | ||
|
|
||
| return infos; | ||
| } | ||
|
|
||
| internal record SubscriptionListCommandResult(List<SubscriptionInfo> Subscriptions); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| namespace Azure.Mcp.Core.Areas.Subscription.Models; | ||
|
|
||
| /// <summary> | ||
| /// Represents an Azure subscription with default subscription indicator. | ||
| /// </summary> | ||
| /// <param name="SubscriptionId">The subscription ID.</param> | ||
| /// <param name="DisplayName">The display name of the subscription.</param> | ||
| /// <param name="State">The subscription state (e.g., Enabled, Disabled).</param> | ||
| /// <param name="TenantId">The tenant ID associated with the subscription.</param> | ||
| /// <param name="IsDefault">Whether this subscription is the default as configured via 'az account set' in the Azure CLI profile, or via the AZURE_SUBSCRIPTION_ID environment variable.</param> | ||
| internal record SubscriptionInfo( | ||
| string SubscriptionId, | ||
| string DisplayName, | ||
| string? State, | ||
| string? TenantId, | ||
| bool IsDefault); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Azure.Mcp.Core.Helpers; | ||
| using Xunit; | ||
|
|
||
| namespace Azure.Mcp.Core.UnitTests.Areas.Subscription; | ||
|
|
||
| public class AzureCliProfileHelperTests | ||
| { | ||
| [Fact] | ||
| public void ParseDefaultSubscriptionId_ValidProfile_ReturnsDefaultId() | ||
| { | ||
| var profileJson = """ | ||
| { | ||
| "subscriptions": [ | ||
| { | ||
| "id": "sub-1111-1111", | ||
| "name": "Subscription One", | ||
| "state": "Enabled", | ||
| "tenantId": "tenant-1111", | ||
| "isDefault": false | ||
| }, | ||
| { | ||
| "id": "sub-2222-2222", | ||
| "name": "Subscription Two", | ||
| "state": "Enabled", | ||
| "tenantId": "tenant-2222", | ||
| "isDefault": true | ||
| }, | ||
| { | ||
| "id": "sub-3333-3333", | ||
| "name": "Subscription Three", | ||
| "state": "Enabled", | ||
| "tenantId": "tenant-3333", | ||
| "isDefault": false | ||
| } | ||
| ] | ||
| } | ||
| """; | ||
|
|
||
| var result = AzureCliProfileHelper.ParseDefaultSubscriptionId(profileJson); | ||
|
|
||
| Assert.Equal("sub-2222-2222", result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ParseDefaultSubscriptionId_NoDefaultInProfile_ReturnsNull() | ||
| { | ||
| var profileJson = """ | ||
| { | ||
| "subscriptions": [ | ||
| { | ||
| "id": "sub-1111-1111", | ||
| "name": "Subscription One", | ||
| "state": "Enabled", | ||
| "tenantId": "tenant-1111", | ||
| "isDefault": false | ||
| } | ||
| ] | ||
| } | ||
| """; | ||
|
|
||
| var result = AzureCliProfileHelper.ParseDefaultSubscriptionId(profileJson); | ||
|
|
||
| Assert.Null(result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ParseDefaultSubscriptionId_EmptySubscriptions_ReturnsNull() | ||
| { | ||
| var profileJson = """ | ||
| { | ||
| "subscriptions": [] | ||
| } | ||
| """; | ||
|
|
||
| var result = AzureCliProfileHelper.ParseDefaultSubscriptionId(profileJson); | ||
|
|
||
| Assert.Null(result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ParseDefaultSubscriptionId_NoSubscriptionsProperty_ReturnsNull() | ||
| { | ||
| var profileJson = """ | ||
| { | ||
| "installationId": "some-id" | ||
| } | ||
| """; | ||
|
|
||
| var result = AzureCliProfileHelper.ParseDefaultSubscriptionId(profileJson); | ||
|
|
||
| Assert.Null(result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ParseDefaultSubscriptionId_MissingIdOnDefault_ReturnsNull() | ||
| { | ||
| var profileJson = """ | ||
| { | ||
| "subscriptions": [ | ||
| { | ||
| "name": "Subscription One", | ||
| "isDefault": true | ||
| } | ||
| ] | ||
| } | ||
| """; | ||
|
|
||
| var result = AzureCliProfileHelper.ParseDefaultSubscriptionId(profileJson); | ||
|
|
||
| Assert.Null(result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void GetAzureProfilePath_WhenUserProfileAvailable_ReturnsExpectedPath() | ||
| { | ||
| var result = AzureCliProfileHelper.GetAzureProfilePath(); | ||
|
|
||
| // In containerized/CI environments, user profile may not be available | ||
| if (string.IsNullOrEmpty(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile))) | ||
| { | ||
| Assert.Null(result); | ||
| } | ||
| else | ||
| { | ||
| Assert.NotNull(result); | ||
| Assert.Contains(".azure", result); | ||
| Assert.EndsWith("azureProfile.json", result); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public void GetAzureProfilePath_ReturnsNullOrValidPath() | ||
| { | ||
| var result = AzureCliProfileHelper.GetAzureProfilePath(); | ||
|
|
||
| // The method must either return null (empty user profile) or a valid absolute path | ||
| if (result != null) | ||
| { | ||
| Assert.False(string.IsNullOrWhiteSpace(result)); | ||
| Assert.Contains(".azure", result); | ||
| Assert.EndsWith("azureProfile.json", result); | ||
| Assert.True(Path.IsPathRooted(result), "Path should be absolute, not relative"); | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.