Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,39 @@ public virtual async Task<ProvisioningContext> CreateProvisioningContextAsync(Ca
return await TryGetSubscriptionsAsync(_options.TenantId, cancellationToken).ConfigureAwait(false);
}

protected async Task<(List<string>? resourceGroupOptions, bool fetchSucceeded)> TryGetResourceGroupsAsync(string subscriptionId, CancellationToken cancellationToken)
{
List<string>? resourceGroupOptions = null;

// SubscriptionId is always a GUID. Check if we have a valid GUID before trying to use it.
if (Guid.TryParse(subscriptionId, out _))
{
try
{
var credential = _tokenCredentialProvider.TokenCredential;
var armClient = _armClientProvider.GetArmClient(credential);
var availableResourceGroups = await armClient.GetAvailableResourceGroupsAsync(subscriptionId, cancellationToken).ConfigureAwait(false);
var resourceGroupList = availableResourceGroups.ToList();

if (resourceGroupList.Count > 0)
{
resourceGroupOptions = resourceGroupList;
return (resourceGroupOptions, true);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to enumerate available resource groups.");
}
}
else
{
_logger.LogDebug("SubscriptionId '{SubscriptionId}' isn't a valid GUID. Skipping getting available resource groups from client.", subscriptionId);
}

return (resourceGroupOptions, false);
}

protected async Task<(List<KeyValuePair<string, string>> locationOptions, bool fetchSucceeded)> TryGetLocationsAsync(string subscriptionId, CancellationToken cancellationToken)
{
List<KeyValuePair<string, string>>? locationOptions = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,19 @@ public async Task<IEnumerable<ISubscriptionResource>> GetAvailableSubscriptionsA
return locations.OrderBy(l => l.DisplayName);
}

public async Task<IEnumerable<string>> GetAvailableResourceGroupsAsync(string subscriptionId, CancellationToken cancellationToken = default)
{
var subscription = await armClient.GetSubscriptions().GetAsync(subscriptionId, cancellationToken).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

In many cases we would have already queried for the subscription ID or gotten the subscription list, can we cache the result or do we want to fetch each time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The subscription resource is already obtained earlier in the flow (via GetSubscriptionAndTenantAsync), so we're already making that call. The resource groups are fetched once when the subscription is selected and used to populate the dropdown. For the current implementation, I've kept it simple without additional caching since the fetch happens only once per provisioning session. If we need cross-session caching, we could consider adding it to the deployment state manager in a future update.

var resourceGroups = new List<string>();

await foreach (var resourceGroup in subscription.Value.GetResourceGroups().GetAllAsync(cancellationToken: cancellationToken).ConfigureAwait(false))
{
resourceGroups.Add(resourceGroup.Data.Name);
}

return resourceGroups.OrderBy(rg => rg);
}

private sealed class DefaultTenantResource(TenantResource tenantResource) : ITenantResource
{
public Guid? TenantId => tenantResource.Data.TenantId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ internal interface IArmClient
/// Gets all available locations for the specified subscription.
/// </summary>
Task<IEnumerable<(string Name, string DisplayName)>> GetAvailableLocationsAsync(string subscriptionId, CancellationToken cancellationToken = default);

/// <summary>
/// Gets all available resource groups for the specified subscription.
/// </summary>
Task<IEnumerable<string>> GetAvailableResourceGroupsAsync(string subscriptionId, CancellationToken cancellationToken = default);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,26 +314,33 @@ private async Task PromptForSubscriptionAsync(CancellationToken cancellationToke
private async Task PromptForLocationAndResourceGroupAsync(CancellationToken cancellationToken)
{
List<KeyValuePair<string, string>>? locationOptions = null;
var fetchSucceeded = false;
List<string>? resourceGroupOptions = null;
var locationFetchSucceeded = false;
var resourceGroupFetchSucceeded = false;

var step = await activityReporter.CreateStepAsync(
"fetch-regions",
"fetch-regions-and-resource-groups",
cancellationToken).ConfigureAwait(false);

await using (step.ConfigureAwait(false))
{
try
{
var task = await step.CreateTaskAsync("Fetching supported regions", cancellationToken).ConfigureAwait(false);
var task = await step.CreateTaskAsync("Fetching supported regions and resource groups", cancellationToken).ConfigureAwait(false);

await using (task.ConfigureAwait(false))
{
(locationOptions, fetchSucceeded) = await TryGetLocationsAsync(_options.SubscriptionId!, cancellationToken).ConfigureAwait(false);
(locationOptions, locationFetchSucceeded) = await TryGetLocationsAsync(_options.SubscriptionId!, cancellationToken).ConfigureAwait(false);
(resourceGroupOptions, resourceGroupFetchSucceeded) = await TryGetResourceGroupsAsync(_options.SubscriptionId!, cancellationToken).ConfigureAwait(false);
}

if (fetchSucceeded)
if (locationFetchSucceeded && resourceGroupFetchSucceeded)
{
await step.SucceedAsync($"Found {locationOptions!.Count} available region(s)", cancellationToken).ConfigureAwait(false);
await step.SucceedAsync($"Found {locationOptions!.Count} region(s) and {resourceGroupOptions!.Count} resource group(s)", cancellationToken).ConfigureAwait(false);
}
else if (locationFetchSucceeded)
{
await step.SucceedAsync($"Found {locationOptions!.Count} region(s)", cancellationToken).ConfigureAwait(false);
}
else
{
Expand All @@ -342,32 +349,40 @@ private async Task PromptForLocationAndResourceGroupAsync(CancellationToken canc
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to retrieve Azure region information.");
await step.FailAsync($"Failed to retrieve region information: {ex.Message}", cancellationToken).ConfigureAwait(false);
_logger.LogError(ex, "Failed to retrieve Azure region and resource group information.");
await step.FailAsync($"Failed to retrieve region and resource group information: {ex.Message}", cancellationToken).ConfigureAwait(false);
throw;
}
}

var inputs = new List<InteractionInput>
{
new InteractionInput
{
Name = LocationName,
InputType = InputType.Choice,
Label = AzureProvisioningStrings.LocationLabel,
Required = true,
Options = [..locationOptions]
},
new InteractionInput
{
Name = ResourceGroupName,
InputType = InputType.Choice,
Label = AzureProvisioningStrings.ResourceGroupLabel,
Placeholder = AzureProvisioningStrings.ResourceGroupPlaceholder,
Value = GetDefaultResourceGroupName(),
AllowCustomChoice = true,
Options = resourceGroupFetchSucceeded && resourceGroupOptions is not null
? resourceGroupOptions.Select(rg => KeyValuePair.Create(rg, rg)).ToList()
: []
}
};

var result = await _interactionService.PromptInputsAsync(
AzureProvisioningStrings.LocationDialogTitle,
AzureProvisioningStrings.LocationSelectionMessage,
[
new InteractionInput
{
Name = LocationName,
InputType = InputType.Choice,
Label = AzureProvisioningStrings.LocationLabel,
Required = true,
Options = [..locationOptions]
},
new InteractionInput
{
Name = ResourceGroupName,
InputType = InputType.Text,
Label = AzureProvisioningStrings.ResourceGroupLabel,
Value = GetDefaultResourceGroupName()
}
],
inputs,
new InputsDialogInteractionOptions
{
EnableMessageMarkdown = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,32 @@ private async Task RetrieveAzureProvisioningOptions(CancellationToken cancellati
inputs.Add(new InteractionInput
{
Name = ResourceGroupName,
InputType = InputType.Text,
InputType = InputType.Choice,
Label = AzureProvisioningStrings.ResourceGroupLabel,
Value = GetDefaultResourceGroupName()
Placeholder = AzureProvisioningStrings.ResourceGroupPlaceholder,
Value = GetDefaultResourceGroupName(),
AllowCustomChoice = true,
Disabled = true,
DynamicLoading = new InputLoadOptions
{
LoadCallback = async (context) =>
{
var subscriptionId = context.AllInputs[SubscriptionIdName].Value ?? string.Empty;

var (resourceGroupOptions, fetchSucceeded) = await TryGetResourceGroupsAsync(subscriptionId, cancellationToken).ConfigureAwait(false);

if (fetchSucceeded && resourceGroupOptions is not null)
{
context.Input.Options = resourceGroupOptions.Select(rg => KeyValuePair.Create(rg, rg)).ToList();
}
else
{
context.Input.Options = [];
}
context.Input.Disabled = false;
},
DependsOnInputs = [SubscriptionIdName]
}
});

var result = await _interactionService.PromptInputsAsync(
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,7 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az
<data name="ValidationTenantIdInvalid" xml:space="preserve">
<value>Tenant ID must be a valid GUID.</value>
</data>
<data name="ResourceGroupPlaceholder" xml:space="preserve">
<value>Select or enter resource group</value>
</data>
</root>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public async Task DeployAsync_PromptsViaInteractionService()
input =>
{
Assert.Equal("Resource group", input.Label);
Assert.Equal(InputType.Text, input.InputType);
Assert.Equal(InputType.Choice, input.InputType);
Assert.False(input.Required);
});

Expand Down
Loading
Loading