From 210ac52a082537032090617837e094017339a407 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 07:26:11 +0000 Subject: [PATCH 1/5] Initial plan From 104aa9aa73f2dde0fb4d92304a476153813c171c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 07:42:27 +0000 Subject: [PATCH 2/5] Add resource group selection to Azure provisioning prompts Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../BaseProvisioningContextProvider.cs | 33 ++++++++++ .../Internal/DefaultArmClientProvider.cs | 13 ++++ .../Internal/IProvisioningServices.cs | 5 ++ .../PublishModeProvisioningContextProvider.cs | 65 ++++++++++++------- .../RunModeProvisioningContextProvider.cs | 27 +++++++- .../AzureProvisioningStrings.Designer.cs | 9 +++ .../Resources/AzureProvisioningStrings.resx | 3 + .../xlf/AzureProvisioningStrings.cs.xlf | 5 ++ .../xlf/AzureProvisioningStrings.de.xlf | 5 ++ .../xlf/AzureProvisioningStrings.es.xlf | 5 ++ .../xlf/AzureProvisioningStrings.fr.xlf | 5 ++ .../xlf/AzureProvisioningStrings.it.xlf | 5 ++ .../xlf/AzureProvisioningStrings.ja.xlf | 5 ++ .../xlf/AzureProvisioningStrings.ko.xlf | 5 ++ .../xlf/AzureProvisioningStrings.pl.xlf | 5 ++ .../xlf/AzureProvisioningStrings.pt-BR.xlf | 5 ++ .../xlf/AzureProvisioningStrings.ru.xlf | 5 ++ .../xlf/AzureProvisioningStrings.tr.xlf | 5 ++ .../xlf/AzureProvisioningStrings.zh-Hans.xlf | 5 ++ .../xlf/AzureProvisioningStrings.zh-Hant.xlf | 5 ++ .../ProvisioningTestHelpers.cs | 11 ++++ 21 files changed, 204 insertions(+), 27 deletions(-) diff --git a/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs b/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs index 40a0c0daff0..d7a7b70281b 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs @@ -273,6 +273,39 @@ public virtual async Task CreateProvisioningContextAsync(Ca return await TryGetSubscriptionsAsync(_options.TenantId, cancellationToken).ConfigureAwait(false); } + protected async Task<(List? resourceGroupOptions, bool fetchSucceeded)> TryGetResourceGroupsAsync(string subscriptionId, CancellationToken cancellationToken) + { + List? 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> locationOptions, bool fetchSucceeded)> TryGetLocationsAsync(string subscriptionId, CancellationToken cancellationToken) { List>? locationOptions = null; diff --git a/src/Aspire.Hosting.Azure/Provisioning/Internal/DefaultArmClientProvider.cs b/src/Aspire.Hosting.Azure/Provisioning/Internal/DefaultArmClientProvider.cs index 047c72ccfd8..252b93b977a 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/DefaultArmClientProvider.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/DefaultArmClientProvider.cs @@ -108,6 +108,19 @@ public async Task> GetAvailableSubscriptionsA return locations.OrderBy(l => l.DisplayName); } + public async Task> GetAvailableResourceGroupsAsync(string subscriptionId, CancellationToken cancellationToken = default) + { + var subscription = await armClient.GetSubscriptions().GetAsync(subscriptionId, cancellationToken).ConfigureAwait(false); + var resourceGroups = new List(); + + 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; diff --git a/src/Aspire.Hosting.Azure/Provisioning/Internal/IProvisioningServices.cs b/src/Aspire.Hosting.Azure/Provisioning/Internal/IProvisioningServices.cs index 8b1b11dcb94..cf613b55079 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/IProvisioningServices.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/IProvisioningServices.cs @@ -90,6 +90,11 @@ internal interface IArmClient /// Gets all available locations for the specified subscription. /// Task> GetAvailableLocationsAsync(string subscriptionId, CancellationToken cancellationToken = default); + + /// + /// Gets all available resource groups for the specified subscription. + /// + Task> GetAvailableResourceGroupsAsync(string subscriptionId, CancellationToken cancellationToken = default); } /// diff --git a/src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs b/src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs index f4c72d460f5..1fd8cc3ca54 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs @@ -314,26 +314,33 @@ private async Task PromptForSubscriptionAsync(CancellationToken cancellationToke private async Task PromptForLocationAndResourceGroupAsync(CancellationToken cancellationToken) { List>? locationOptions = null; - var fetchSucceeded = false; + List? 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 { @@ -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 + { + 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, diff --git a/src/Aspire.Hosting.Azure/Provisioning/Internal/RunModeProvisioningContextProvider.cs b/src/Aspire.Hosting.Azure/Provisioning/Internal/RunModeProvisioningContextProvider.cs index 5ffb3b6d721..a47c9e11c10 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/RunModeProvisioningContextProvider.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/RunModeProvisioningContextProvider.cs @@ -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( diff --git a/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.Designer.cs b/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.Designer.cs index b6e0b80a8d5..e013a4a8ebe 100644 --- a/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.Designer.cs +++ b/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.Designer.cs @@ -268,5 +268,14 @@ internal static string ValidationTenantIdInvalid { return ResourceManager.GetString("ValidationTenantIdInvalid", resourceCulture); } } + + /// + /// Looks up a localized string similar to Select or enter resource group. + /// + internal static string ResourceGroupPlaceholder { + get { + return ResourceManager.GetString("ResourceGroupPlaceholder", resourceCulture); + } + } } } \ No newline at end of file diff --git a/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.resx b/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.resx index 8ce03f15d66..556d8cbb3cd 100644 --- a/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.resx +++ b/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.resx @@ -189,4 +189,7 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Tenant ID must be a valid GUID. + + Select or enter resource group + \ No newline at end of file diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.cs.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.cs.xlf index c6e941522c9..37b75016fa4 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.cs.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.cs.xlf @@ -56,6 +56,11 @@ Zjistěte více v [dokumentaci k nasazení Azure](https://aka.ms/dotnet/aspire/a Skupina prostředků + + Select or enter resource group + Select or enter resource group + + Azure subscription Předplatné Azure diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.de.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.de.xlf index 8b56b473ae9..1fc18a6571b 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.de.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.de.xlf @@ -56,6 +56,11 @@ Weitere Informationen finden Sie in der [Azure-Bereitstellungsdokumentation](htt Ressourcengruppe + + Select or enter resource group + Select or enter resource group + + Azure subscription Azure-Abonnement diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.es.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.es.xlf index bfa50ecde2a..57d85f6c6e0 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.es.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.es.xlf @@ -56,6 +56,11 @@ Para más información, consulte la [documentación de aprovisionamiento de Azur Grupo de recursos + + Select or enter resource group + Select or enter resource group + + Azure subscription Suscripción de Azure diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.fr.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.fr.xlf index 17be6f53547..6b36c00acc4 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.fr.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.fr.xlf @@ -56,6 +56,11 @@ Pour en savoir plus, consultez la [documentation d’approvisionnement Azure](ht Groupe de ressources + + Select or enter resource group + Select or enter resource group + + Azure subscription Abonnement Azure diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.it.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.it.xlf index c375a09e047..f5060eedc0a 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.it.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.it.xlf @@ -56,6 +56,11 @@ Per altre informazioni, vedere la [documentazione sul provisioning di Azure](htt Gruppo di risorse + + Select or enter resource group + Select or enter resource group + + Azure subscription Sottoscrizione di Azure diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ja.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ja.xlf index 6425e3d26cd..b5d0e8c3d4f 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ja.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ja.xlf @@ -56,6 +56,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az リソース グループ + + Select or enter resource group + Select or enter resource group + + Azure subscription Azure サブスクリプション diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ko.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ko.xlf index aaf3c3823f7..8f4b1efb25d 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ko.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ko.xlf @@ -56,6 +56,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az 리소스 그룹 + + Select or enter resource group + Select or enter resource group + + Azure subscription Azure 구독 diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pl.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pl.xlf index 2fb8e819863..50ce100e7e0 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pl.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pl.xlf @@ -56,6 +56,11 @@ Aby dowiedzieć się więcej, zobacz [dokumentację aprowizacji platformy Azure] Grupa zasobów + + Select or enter resource group + Select or enter resource group + + Azure subscription Subskrypcja platformy Azure diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pt-BR.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pt-BR.xlf index 0642392f426..0c38a31624e 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pt-BR.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pt-BR.xlf @@ -56,6 +56,11 @@ Para saber mais, veja a [documentação de provisionamento do Azure](https://aka Grupo de recursos + + Select or enter resource group + Select or enter resource group + + Azure subscription Assinatura do Azure diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ru.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ru.xlf index f7bbf002cac..08e46e2b278 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ru.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ru.xlf @@ -56,6 +56,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Группа ресурсов + + Select or enter resource group + Select or enter resource group + + Azure subscription Подписка Azure diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.tr.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.tr.xlf index 019eb1edb01..29b74c945dd 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.tr.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.tr.xlf @@ -56,6 +56,11 @@ Daha fazla bilgi için [Azure sağlama belgelerine](https://aka.ms/dotnet/aspire Kaynak grubu + + Select or enter resource group + Select or enter resource group + + Azure subscription Azure aboneliği diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hans.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hans.xlf index 97e4c240b2e..54bdef00473 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hans.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hans.xlf @@ -56,6 +56,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az 资源组 + + Select or enter resource group + Select or enter resource group + + Azure subscription Azure 订阅 diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hant.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hant.xlf index ccb77145a18..8091c59ddfc 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hant.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hant.xlf @@ -56,6 +56,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az 資源群組 + + Select or enter resource group + Select or enter resource group + + Azure subscription Azure 訂用帳戶 diff --git a/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs b/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs index 946038c9918..2c063267cf8 100644 --- a/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs +++ b/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs @@ -248,6 +248,17 @@ public Task> GetAvailableSubscriptionsAsync(s }; return Task.FromResult>(locations); } + + public Task> GetAvailableResourceGroupsAsync(string subscriptionId, CancellationToken cancellationToken = default) + { + var resourceGroups = new List + { + "rg-test-1", + "rg-test-2", + "rg-aspire-dev" + }; + return Task.FromResult>(resourceGroups); + } } /// From ecf4cb72babd8fbb3548390c0c795219e0b73e78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 07:46:14 +0000 Subject: [PATCH 3/5] Add test for resource group fetching and fix existing tests Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../ProvisioningContextProviderTests.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.Azure.Tests/ProvisioningContextProviderTests.cs b/tests/Aspire.Hosting.Azure.Tests/ProvisioningContextProviderTests.cs index 992680d2c71..2153c45013e 100644 --- a/tests/Aspire.Hosting.Azure.Tests/ProvisioningContextProviderTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/ProvisioningContextProviderTests.cs @@ -303,7 +303,7 @@ public async Task CreateProvisioningContextAsync_PromptsIfNoOptions() { Assert.Equal(BaseProvisioningContextProvider.ResourceGroupName, input.Name); Assert.Equal("Resource group", input.Label); - Assert.Equal(InputType.Text, input.InputType); + Assert.Equal(InputType.Choice, input.InputType); Assert.False(input.Required); }); @@ -456,7 +456,7 @@ public async Task CreateProvisioningContextAsync_DoesNotPromptForTenantWhenSubsc { Assert.Equal(BaseProvisioningContextProvider.ResourceGroupName, input.Name); Assert.Equal("Resource group", input.Label); - Assert.Equal(InputType.Text, input.InputType); + Assert.Equal(InputType.Choice, input.InputType); Assert.False(input.Required); }); @@ -524,4 +524,26 @@ public async Task PublishMode_CreateProvisioningContextAsync_ReturnsValidContext Assert.NotNull(context.Principal); Assert.Equal("westus2", context.Location.Name); } + + [Fact] + public async Task GetAvailableResourceGroupsAsync_ReturnsResourceGroups() + { + // Arrange + var armClientProvider = ProvisioningTestHelpers.CreateArmClientProvider(); + var tokenCredentialProvider = ProvisioningTestHelpers.CreateTokenCredentialProvider(); + var credential = tokenCredentialProvider.TokenCredential; + var armClient = armClientProvider.GetArmClient(credential); + var subscriptionId = "12345678-1234-1234-1234-123456789012"; + + // Act + var resourceGroups = await armClient.GetAvailableResourceGroupsAsync(subscriptionId, CancellationToken.None); + + // Assert + Assert.NotNull(resourceGroups); + var resourceGroupList = resourceGroups.ToList(); + Assert.NotEmpty(resourceGroupList); + Assert.Contains("rg-test-1", resourceGroupList); + Assert.Contains("rg-test-2", resourceGroupList); + Assert.Contains("rg-aspire-dev", resourceGroupList); + } } From 392015678ab129ac06a89815e97aed61bec35932 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 08:03:43 +0000 Subject: [PATCH 4/5] Fix AzureDeployerTests to expect Choice input type for resource group Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs index 43ad4d333ec..eeb800fe6f9 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs @@ -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); }); From 9fb6926dcac49d60e6193c2d7f42ec0fa1ac8af5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 08:30:33 +0000 Subject: [PATCH 5/5] Change UX to prompt for resource group before location - Resource group selection now comes before location selection - Location is only prompted when creating a new resource group - When selecting an existing resource group, its location is automatically used - Added GetAvailableResourceGroupsWithLocationAsync to fetch RG details - Updated both RunMode and PublishMode providers - Added new localization strings for resource group dialog - Updated all tests to reflect new UX flow Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../BaseProvisioningContextProvider.cs | 33 ++++ .../Internal/DefaultArmClientProvider.cs | 13 ++ .../Internal/IProvisioningServices.cs | 5 + .../PublishModeProvisioningContextProvider.cs | 152 ++++++++++++------ .../RunModeProvisioningContextProvider.cs | 55 ++++--- .../AzureProvisioningStrings.Designer.cs | 18 +++ .../Resources/AzureProvisioningStrings.resx | 6 + .../xlf/AzureProvisioningStrings.cs.xlf | 10 ++ .../xlf/AzureProvisioningStrings.de.xlf | 10 ++ .../xlf/AzureProvisioningStrings.es.xlf | 10 ++ .../xlf/AzureProvisioningStrings.fr.xlf | 10 ++ .../xlf/AzureProvisioningStrings.it.xlf | 10 ++ .../xlf/AzureProvisioningStrings.ja.xlf | 10 ++ .../xlf/AzureProvisioningStrings.ko.xlf | 10 ++ .../xlf/AzureProvisioningStrings.pl.xlf | 10 ++ .../xlf/AzureProvisioningStrings.pt-BR.xlf | 10 ++ .../xlf/AzureProvisioningStrings.ru.xlf | 10 ++ .../xlf/AzureProvisioningStrings.tr.xlf | 10 ++ .../xlf/AzureProvisioningStrings.zh-Hans.xlf | 10 ++ .../xlf/AzureProvisioningStrings.zh-Hant.xlf | 10 ++ .../AzureDeployerTests.cs | 29 ++-- .../ProvisioningContextProviderTests.cs | 52 ++++-- .../ProvisioningTestHelpers.cs | 11 ++ 23 files changed, 417 insertions(+), 87 deletions(-) diff --git a/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs b/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs index d7a7b70281b..b942153032b 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs @@ -306,6 +306,39 @@ public virtual async Task CreateProvisioningContextAsync(Ca return (resourceGroupOptions, false); } + protected async Task<(List<(string Name, string Location)>? resourceGroupOptions, bool fetchSucceeded)> TryGetResourceGroupsWithLocationAsync(string subscriptionId, CancellationToken cancellationToken) + { + List<(string Name, string Location)>? 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.GetAvailableResourceGroupsWithLocationAsync(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 with locations."); + } + } + 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> locationOptions, bool fetchSucceeded)> TryGetLocationsAsync(string subscriptionId, CancellationToken cancellationToken) { List>? locationOptions = null; diff --git a/src/Aspire.Hosting.Azure/Provisioning/Internal/DefaultArmClientProvider.cs b/src/Aspire.Hosting.Azure/Provisioning/Internal/DefaultArmClientProvider.cs index 252b93b977a..a26ea36c96e 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/DefaultArmClientProvider.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/DefaultArmClientProvider.cs @@ -121,6 +121,19 @@ public async Task> GetAvailableResourceGroupsAsync(string su return resourceGroups.OrderBy(rg => rg); } + public async Task> GetAvailableResourceGroupsWithLocationAsync(string subscriptionId, CancellationToken cancellationToken = default) + { + var subscription = await armClient.GetSubscriptions().GetAsync(subscriptionId, cancellationToken).ConfigureAwait(false); + var resourceGroups = new List<(string Name, string Location)>(); + + await foreach (var resourceGroup in subscription.Value.GetResourceGroups().GetAllAsync(cancellationToken: cancellationToken).ConfigureAwait(false)) + { + resourceGroups.Add((resourceGroup.Data.Name, resourceGroup.Data.Location.Name)); + } + + return resourceGroups.OrderBy(rg => rg.Name); + } + private sealed class DefaultTenantResource(TenantResource tenantResource) : ITenantResource { public Guid? TenantId => tenantResource.Data.TenantId; diff --git a/src/Aspire.Hosting.Azure/Provisioning/Internal/IProvisioningServices.cs b/src/Aspire.Hosting.Azure/Provisioning/Internal/IProvisioningServices.cs index cf613b55079..3ab579c37a0 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/IProvisioningServices.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/IProvisioningServices.cs @@ -95,6 +95,11 @@ internal interface IArmClient /// Gets all available resource groups for the specified subscription. /// Task> GetAvailableResourceGroupsAsync(string subscriptionId, CancellationToken cancellationToken = default); + + /// + /// Gets detailed information about available resource groups including their locations. + /// + Task> GetAvailableResourceGroupsWithLocationAsync(string subscriptionId, CancellationToken cancellationToken = default); } /// diff --git a/src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs b/src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs index 1fd8cc3ca54..33262a846ae 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs @@ -313,76 +313,59 @@ private async Task PromptForSubscriptionAsync(CancellationToken cancellationToke private async Task PromptForLocationAndResourceGroupAsync(CancellationToken cancellationToken) { - List>? locationOptions = null; - List? resourceGroupOptions = null; - var locationFetchSucceeded = false; + List<(string Name, string Location)>? resourceGroupOptions = null; var resourceGroupFetchSucceeded = false; var step = await activityReporter.CreateStepAsync( - "fetch-regions-and-resource-groups", + "fetch-resource-groups", cancellationToken).ConfigureAwait(false); await using (step.ConfigureAwait(false)) { try { - var task = await step.CreateTaskAsync("Fetching supported regions and resource groups", cancellationToken).ConfigureAwait(false); + var task = await step.CreateTaskAsync("Fetching resource groups", cancellationToken).ConfigureAwait(false); await using (task.ConfigureAwait(false)) { - (locationOptions, locationFetchSucceeded) = await TryGetLocationsAsync(_options.SubscriptionId!, cancellationToken).ConfigureAwait(false); - (resourceGroupOptions, resourceGroupFetchSucceeded) = await TryGetResourceGroupsAsync(_options.SubscriptionId!, cancellationToken).ConfigureAwait(false); + (resourceGroupOptions, resourceGroupFetchSucceeded) = await TryGetResourceGroupsWithLocationAsync(_options.SubscriptionId!, cancellationToken).ConfigureAwait(false); } - if (locationFetchSucceeded && resourceGroupFetchSucceeded) + if (resourceGroupFetchSucceeded && resourceGroupOptions is not null) { - 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); + await step.SucceedAsync($"Found {resourceGroupOptions.Count} resource group(s)", cancellationToken).ConfigureAwait(false); } else { - await step.WarnAsync("Failed to fetch regions, falling back to manual entry", cancellationToken).ConfigureAwait(false); + await step.WarnAsync("Failed to fetch resource groups", cancellationToken).ConfigureAwait(false); } } catch (Exception ex) { - _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); + _logger.LogError(ex, "Failed to retrieve Azure resource group information."); + await step.FailAsync($"Failed to retrieve resource group information: {ex.Message}", cancellationToken).ConfigureAwait(false); throw; } } - var inputs = new List + // First, prompt for resource group selection + var resourceGroupInput = new 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() - : [] - } + 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.Name, rg.Name)).ToList() + : [] }; - var result = await _interactionService.PromptInputsAsync( - AzureProvisioningStrings.LocationDialogTitle, - AzureProvisioningStrings.LocationSelectionMessage, - inputs, + var resourceGroupResult = await _interactionService.PromptInputsAsync( + AzureProvisioningStrings.ResourceGroupDialogTitle, + AzureProvisioningStrings.ResourceGroupSelectionMessage, + [resourceGroupInput], new InputsDialogInteractionOptions { EnableMessageMarkdown = false, @@ -398,11 +381,90 @@ private async Task PromptForLocationAndResourceGroupAsync(CancellationToken canc }, cancellationToken).ConfigureAwait(false); - if (!result.Canceled) + if (resourceGroupResult.Canceled) + { + return; + } + + var selectedResourceGroup = resourceGroupResult.Data[ResourceGroupName].Value; + _options.ResourceGroup = selectedResourceGroup; + _options.AllowResourceGroupCreation = true; + + // Check if the selected resource group is an existing one + var existingResourceGroup = resourceGroupOptions?.FirstOrDefault(rg => rg.Name.Equals(selectedResourceGroup, StringComparison.OrdinalIgnoreCase)); + + if (existingResourceGroup != null && !string.IsNullOrEmpty(existingResourceGroup.Value.Name)) + { + // Use the location from the existing resource group + _options.Location = existingResourceGroup.Value.Location; + _logger.LogInformation("Using location {location} from existing resource group {resourceGroup}", _options.Location, selectedResourceGroup); + } + else + { + // This is a new resource group, prompt for location + await PromptForLocationAsync(cancellationToken).ConfigureAwait(false); + } + } + + private async Task PromptForLocationAsync(CancellationToken cancellationToken) + { + List>? locationOptions = null; + var locationFetchSucceeded = false; + + var step = await activityReporter.CreateStepAsync( + "fetch-regions", + cancellationToken).ConfigureAwait(false); + + await using (step.ConfigureAwait(false)) + { + try + { + var task = await step.CreateTaskAsync("Fetching supported regions", cancellationToken).ConfigureAwait(false); + + await using (task.ConfigureAwait(false)) + { + (locationOptions, locationFetchSucceeded) = await TryGetLocationsAsync(_options.SubscriptionId!, cancellationToken).ConfigureAwait(false); + } + + if (locationFetchSucceeded) + { + await step.SucceedAsync($"Found {locationOptions!.Count} region(s)", cancellationToken).ConfigureAwait(false); + } + else + { + await step.WarnAsync("Failed to fetch regions, falling back to manual entry", cancellationToken).ConfigureAwait(false); + } + } + 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); + throw; + } + } + + var locationResult = await _interactionService.PromptInputsAsync( + AzureProvisioningStrings.LocationDialogTitle, + AzureProvisioningStrings.LocationSelectionMessage, + [ + new InteractionInput + { + Name = LocationName, + InputType = InputType.Choice, + Label = AzureProvisioningStrings.LocationLabel, + Required = true, + Options = [..locationOptions] + } + ], + new InputsDialogInteractionOptions + { + EnableMessageMarkdown = false + }, + cancellationToken).ConfigureAwait(false); + + if (!locationResult.Canceled) { - _options.Location = result.Data[LocationName].Value; - _options.ResourceGroup = result.Data[ResourceGroupName].Value; - _options.AllowResourceGroupCreation = true; + _options.Location = locationResult.Data[LocationName].Value; } } } diff --git a/src/Aspire.Hosting.Azure/Provisioning/Internal/RunModeProvisioningContextProvider.cs b/src/Aspire.Hosting.Azure/Provisioning/Internal/RunModeProvisioningContextProvider.cs index a47c9e11c10..a65b278b1a5 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/RunModeProvisioningContextProvider.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/RunModeProvisioningContextProvider.cs @@ -192,11 +192,12 @@ private async Task RetrieveAzureProvisioningOptions(CancellationToken cancellati inputs.Add(new InteractionInput { - Name = LocationName, + Name = ResourceGroupName, InputType = InputType.Choice, - Label = AzureProvisioningStrings.LocationLabel, - Placeholder = AzureProvisioningStrings.LocationPlaceholder, - Required = true, + Label = AzureProvisioningStrings.ResourceGroupLabel, + Placeholder = AzureProvisioningStrings.ResourceGroupPlaceholder, + Value = GetDefaultResourceGroupName(), + AllowCustomChoice = true, Disabled = true, DynamicLoading = new InputLoadOptions { @@ -204,9 +205,16 @@ private async Task RetrieveAzureProvisioningOptions(CancellationToken cancellati { var subscriptionId = context.AllInputs[SubscriptionIdName].Value ?? string.Empty; - var (locationOptions, _) = await TryGetLocationsAsync(subscriptionId, cancellationToken).ConfigureAwait(false); + var (resourceGroupOptions, fetchSucceeded) = await TryGetResourceGroupsWithLocationAsync(subscriptionId, cancellationToken).ConfigureAwait(false); - context.Input.Options = locationOptions; + if (fetchSucceeded && resourceGroupOptions is not null) + { + context.Input.Options = resourceGroupOptions.Select(rg => KeyValuePair.Create(rg.Name, rg.Name)).ToList(); + } + else + { + context.Input.Options = []; + } context.Input.Disabled = false; }, DependsOnInputs = [SubscriptionIdName] @@ -215,32 +223,41 @@ private async Task RetrieveAzureProvisioningOptions(CancellationToken cancellati inputs.Add(new InteractionInput { - Name = ResourceGroupName, + Name = LocationName, InputType = InputType.Choice, - Label = AzureProvisioningStrings.ResourceGroupLabel, - Placeholder = AzureProvisioningStrings.ResourceGroupPlaceholder, - Value = GetDefaultResourceGroupName(), - AllowCustomChoice = true, + Label = AzureProvisioningStrings.LocationLabel, + Placeholder = AzureProvisioningStrings.LocationPlaceholder, + Required = true, Disabled = true, DynamicLoading = new InputLoadOptions { LoadCallback = async (context) => { var subscriptionId = context.AllInputs[SubscriptionIdName].Value ?? string.Empty; + var resourceGroupName = context.AllInputs[ResourceGroupName].Value ?? string.Empty; - var (resourceGroupOptions, fetchSucceeded) = await TryGetResourceGroupsAsync(subscriptionId, cancellationToken).ConfigureAwait(false); - + // Check if the selected resource group is an existing one + var (resourceGroupOptions, fetchSucceeded) = await TryGetResourceGroupsWithLocationAsync(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 = []; + var existingResourceGroup = resourceGroupOptions.FirstOrDefault(rg => rg.Name.Equals(resourceGroupName, StringComparison.OrdinalIgnoreCase)); + if (existingResourceGroup != default) + { + // Use location from existing resource group + context.Input.Options = [KeyValuePair.Create(existingResourceGroup.Location, existingResourceGroup.Location)]; + context.Input.Value = existingResourceGroup.Location; + context.Input.Disabled = true; // Make it read-only since it's from existing RG + return; + } } + + // For new resource groups, load all locations + var (locationOptions, _) = await TryGetLocationsAsync(subscriptionId, cancellationToken).ConfigureAwait(false); + context.Input.Options = locationOptions; context.Input.Disabled = false; }, - DependsOnInputs = [SubscriptionIdName] + DependsOnInputs = [SubscriptionIdName, ResourceGroupName] } }); diff --git a/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.Designer.cs b/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.Designer.cs index e013a4a8ebe..695b8f56856 100644 --- a/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.Designer.cs +++ b/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.Designer.cs @@ -277,5 +277,23 @@ internal static string ResourceGroupPlaceholder { return ResourceManager.GetString("ResourceGroupPlaceholder", resourceCulture); } } + + /// + /// Looks up a localized string similar to Azure resource group. + /// + internal static string ResourceGroupDialogTitle { + get { + return ResourceManager.GetString("ResourceGroupDialogTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select your Azure resource group or enter a new name:. + /// + internal static string ResourceGroupSelectionMessage { + get { + return ResourceManager.GetString("ResourceGroupSelectionMessage", resourceCulture); + } + } } } \ No newline at end of file diff --git a/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.resx b/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.resx index 556d8cbb3cd..61b5cdbcde2 100644 --- a/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.resx +++ b/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.resx @@ -192,4 +192,10 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Select or enter resource group + + Azure resource group + + + Select your Azure resource group or enter a new name: + \ No newline at end of file diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.cs.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.cs.xlf index 37b75016fa4..0b0bf17dd8d 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.cs.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.cs.xlf @@ -51,6 +51,11 @@ Zjistěte více v [dokumentaci k nasazení Azure](https://aka.ms/dotnet/aspire/a Zřizování Azure + + Azure resource group + Azure resource group + + Resource group Skupina prostředků @@ -61,6 +66,11 @@ Zjistěte více v [dokumentaci k nasazení Azure](https://aka.ms/dotnet/aspire/a Select or enter resource group + + Select your Azure resource group or enter a new name: + Select your Azure resource group or enter a new name: + + Azure subscription Předplatné Azure diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.de.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.de.xlf index 1fc18a6571b..d3abd12acdc 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.de.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.de.xlf @@ -51,6 +51,11 @@ Weitere Informationen finden Sie in der [Azure-Bereitstellungsdokumentation](htt Azure-Bereitstellung + + Azure resource group + Azure resource group + + Resource group Ressourcengruppe @@ -61,6 +66,11 @@ Weitere Informationen finden Sie in der [Azure-Bereitstellungsdokumentation](htt Select or enter resource group + + Select your Azure resource group or enter a new name: + Select your Azure resource group or enter a new name: + + Azure subscription Azure-Abonnement diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.es.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.es.xlf index 57d85f6c6e0..1346790dc4e 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.es.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.es.xlf @@ -51,6 +51,11 @@ Para más información, consulte la [documentación de aprovisionamiento de Azur Aprovisionamiento de Azure + + Azure resource group + Azure resource group + + Resource group Grupo de recursos @@ -61,6 +66,11 @@ Para más información, consulte la [documentación de aprovisionamiento de Azur Select or enter resource group + + Select your Azure resource group or enter a new name: + Select your Azure resource group or enter a new name: + + Azure subscription Suscripción de Azure diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.fr.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.fr.xlf index 6b36c00acc4..eb9b9807b00 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.fr.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.fr.xlf @@ -51,6 +51,11 @@ Pour en savoir plus, consultez la [documentation d’approvisionnement Azure](ht Approvisionnement Azure + + Azure resource group + Azure resource group + + Resource group Groupe de ressources @@ -61,6 +66,11 @@ Pour en savoir plus, consultez la [documentation d’approvisionnement Azure](ht Select or enter resource group + + Select your Azure resource group or enter a new name: + Select your Azure resource group or enter a new name: + + Azure subscription Abonnement Azure diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.it.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.it.xlf index f5060eedc0a..b51a8d4c842 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.it.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.it.xlf @@ -51,6 +51,11 @@ Per altre informazioni, vedere la [documentazione sul provisioning di Azure](htt Provisioning Azure + + Azure resource group + Azure resource group + + Resource group Gruppo di risorse @@ -61,6 +66,11 @@ Per altre informazioni, vedere la [documentazione sul provisioning di Azure](htt Select or enter resource group + + Select your Azure resource group or enter a new name: + Select your Azure resource group or enter a new name: + + Azure subscription Sottoscrizione di Azure diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ja.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ja.xlf index b5d0e8c3d4f..e8b836a4305 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ja.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ja.xlf @@ -51,6 +51,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Azure のプロビジョニング + + Azure resource group + Azure resource group + + Resource group リソース グループ @@ -61,6 +66,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Select or enter resource group + + Select your Azure resource group or enter a new name: + Select your Azure resource group or enter a new name: + + Azure subscription Azure サブスクリプション diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ko.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ko.xlf index 8f4b1efb25d..e9bf40690dd 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ko.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ko.xlf @@ -51,6 +51,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Azure 프로비전 + + Azure resource group + Azure resource group + + Resource group 리소스 그룹 @@ -61,6 +66,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Select or enter resource group + + Select your Azure resource group or enter a new name: + Select your Azure resource group or enter a new name: + + Azure subscription Azure 구독 diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pl.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pl.xlf index 50ce100e7e0..56acae75301 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pl.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pl.xlf @@ -51,6 +51,11 @@ Aby dowiedzieć się więcej, zobacz [dokumentację aprowizacji platformy Azure] Aprowizowanie platformy Azure + + Azure resource group + Azure resource group + + Resource group Grupa zasobów @@ -61,6 +66,11 @@ Aby dowiedzieć się więcej, zobacz [dokumentację aprowizacji platformy Azure] Select or enter resource group + + Select your Azure resource group or enter a new name: + Select your Azure resource group or enter a new name: + + Azure subscription Subskrypcja platformy Azure diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pt-BR.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pt-BR.xlf index 0c38a31624e..6eafbd07b10 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pt-BR.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pt-BR.xlf @@ -51,6 +51,11 @@ Para saber mais, veja a [documentação de provisionamento do Azure](https://aka Provisionamento do Azure + + Azure resource group + Azure resource group + + Resource group Grupo de recursos @@ -61,6 +66,11 @@ Para saber mais, veja a [documentação de provisionamento do Azure](https://aka Select or enter resource group + + Select your Azure resource group or enter a new name: + Select your Azure resource group or enter a new name: + + Azure subscription Assinatura do Azure diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ru.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ru.xlf index 08e46e2b278..bdd4a5173ab 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ru.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ru.xlf @@ -51,6 +51,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Подготовка Azure + + Azure resource group + Azure resource group + + Resource group Группа ресурсов @@ -61,6 +66,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Select or enter resource group + + Select your Azure resource group or enter a new name: + Select your Azure resource group or enter a new name: + + Azure subscription Подписка Azure diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.tr.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.tr.xlf index 29b74c945dd..d2f6c16dd5a 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.tr.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.tr.xlf @@ -51,6 +51,11 @@ Daha fazla bilgi için [Azure sağlama belgelerine](https://aka.ms/dotnet/aspire Azure sağlama + + Azure resource group + Azure resource group + + Resource group Kaynak grubu @@ -61,6 +66,11 @@ Daha fazla bilgi için [Azure sağlama belgelerine](https://aka.ms/dotnet/aspire Select or enter resource group + + Select your Azure resource group or enter a new name: + Select your Azure resource group or enter a new name: + + Azure subscription Azure aboneliği diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hans.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hans.xlf index 54bdef00473..021eb9af4da 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hans.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hans.xlf @@ -51,6 +51,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Azure 预配 + + Azure resource group + Azure resource group + + Resource group 资源组 @@ -61,6 +66,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Select or enter resource group + + Select your Azure resource group or enter a new name: + Select your Azure resource group or enter a new name: + + Azure subscription Azure 订阅 diff --git a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hant.xlf b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hant.xlf index 8091c59ddfc..6cb21e3d752 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hant.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.zh-Hant.xlf @@ -51,6 +51,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Azure 佈建 + + Azure resource group + Azure resource group + + Resource group 資源群組 @@ -61,6 +66,11 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Select or enter resource group + + Select your Azure resource group or enter a new name: + Select your Azure resource group or enter a new name: + + Azure subscription Azure 訂用帳戶 diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs index eeb800fe6f9..c27dfb5721f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs @@ -102,29 +102,40 @@ public async Task DeployAsync_PromptsViaInteractionService() subscriptionInteraction.Inputs[0].Value = "12345678-1234-1234-1234-123456789012"; subscriptionInteraction.CompletionTcs.SetResult(InteractionResult.Ok(subscriptionInteraction.Inputs)); - // Wait for the second interaction (location and resource group selection) + // Wait for the resource group selection interaction + var resourceGroupInteraction = await testInteractionService.Interactions.Reader.ReadAsync(); + Assert.Equal("Azure resource group", resourceGroupInteraction.Title); + Assert.False(resourceGroupInteraction.Options!.EnableMessageMarkdown); + + // Verify the expected input for resource group selection + Assert.Collection(resourceGroupInteraction.Inputs, + input => + { + Assert.Equal("Resource group", input.Label); + Assert.Equal(InputType.Choice, input.InputType); + Assert.False(input.Required); + }); + + // Complete the resource group interaction with a new resource group name + resourceGroupInteraction.Inputs[0].Value = "test-rg"; + resourceGroupInteraction.CompletionTcs.SetResult(InteractionResult.Ok(resourceGroupInteraction.Inputs)); + + // Wait for the location selection interaction (only shown for new resource groups) var locationInteraction = await testInteractionService.Interactions.Reader.ReadAsync(); Assert.Equal("Azure location and resource group", locationInteraction.Title); Assert.False(locationInteraction.Options!.EnableMessageMarkdown); - // Verify the expected inputs for location and resource group (fallback to manual entry) + // Verify the expected input for location selection Assert.Collection(locationInteraction.Inputs, input => { Assert.Equal("Location", input.Label); Assert.Equal(InputType.Choice, input.InputType); Assert.True(input.Required); - }, - input => - { - Assert.Equal("Resource group", input.Label); - Assert.Equal(InputType.Choice, input.InputType); - Assert.False(input.Required); }); // Complete the location interaction locationInteraction.Inputs[0].Value = "westus2"; - locationInteraction.Inputs[1].Value = "test-rg"; locationInteraction.CompletionTcs.SetResult(InteractionResult.Ok(locationInteraction.Inputs)); // Wait for the run task to complete (or timeout) diff --git a/tests/Aspire.Hosting.Azure.Tests/ProvisioningContextProviderTests.cs b/tests/Aspire.Hosting.Azure.Tests/ProvisioningContextProviderTests.cs index 2153c45013e..c41961d2320 100644 --- a/tests/Aspire.Hosting.Azure.Tests/ProvisioningContextProviderTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/ProvisioningContextProviderTests.cs @@ -294,22 +294,34 @@ public async Task CreateProvisioningContextAsync_PromptsIfNoOptions() }, input => { - Assert.Equal(BaseProvisioningContextProvider.LocationName, input.Name); - Assert.Equal("Location", input.Label); + Assert.Equal(BaseProvisioningContextProvider.ResourceGroupName, input.Name); + Assert.Equal("Resource group", input.Label); Assert.Equal(InputType.Choice, input.InputType); - Assert.True(input.Required); + Assert.False(input.Required); }, input => { - Assert.Equal(BaseProvisioningContextProvider.ResourceGroupName, input.Name); - Assert.Equal("Resource group", input.Label); + Assert.Equal(BaseProvisioningContextProvider.LocationName, input.Name); + Assert.Equal("Location", input.Label); Assert.Equal(InputType.Choice, input.InputType); - Assert.False(input.Required); + Assert.True(input.Required); }); inputsInteraction.Inputs[BaseProvisioningContextProvider.SubscriptionIdName].Value = "12345678-1234-1234-1234-123456789012"; - // Trigger dynamic update of locations based on subscription. + // Trigger dynamic update of resource groups based on subscription. + await inputsInteraction.Inputs[BaseProvisioningContextProvider.ResourceGroupName].DynamicLoading!.LoadCallback(new LoadInputContext + { + AllInputs = inputsInteraction.Inputs, + CancellationToken = CancellationToken.None, + Input = inputsInteraction.Inputs[BaseProvisioningContextProvider.ResourceGroupName], + ServiceProvider = new ServiceCollection().BuildServiceProvider() + }); + + // Set a custom resource group name (new resource group) + inputsInteraction.Inputs[BaseProvisioningContextProvider.ResourceGroupName].Value = "test-new-rg"; + + // Trigger dynamic update of locations based on subscription and resource group. await inputsInteraction.Inputs[BaseProvisioningContextProvider.LocationName].DynamicLoading!.LoadCallback(new LoadInputContext { AllInputs = inputsInteraction.Inputs, @@ -447,20 +459,32 @@ public async Task CreateProvisioningContextAsync_DoesNotPromptForTenantWhenSubsc }, input => { - Assert.Equal(BaseProvisioningContextProvider.LocationName, input.Name); - Assert.Equal("Location", input.Label); + Assert.Equal(BaseProvisioningContextProvider.ResourceGroupName, input.Name); + Assert.Equal("Resource group", input.Label); Assert.Equal(InputType.Choice, input.InputType); - Assert.True(input.Required); + Assert.False(input.Required); }, input => { - Assert.Equal(BaseProvisioningContextProvider.ResourceGroupName, input.Name); - Assert.Equal("Resource group", input.Label); + Assert.Equal(BaseProvisioningContextProvider.LocationName, input.Name); + Assert.Equal("Location", input.Label); Assert.Equal(InputType.Choice, input.InputType); - Assert.False(input.Required); + Assert.True(input.Required); }); - // Trigger dynamic update of locations based on subscription. + // Trigger dynamic update of resource groups based on subscription. + await inputsInteraction.Inputs[BaseProvisioningContextProvider.ResourceGroupName].DynamicLoading!.LoadCallback(new LoadInputContext + { + AllInputs = inputsInteraction.Inputs, + CancellationToken = CancellationToken.None, + Input = inputsInteraction.Inputs[BaseProvisioningContextProvider.ResourceGroupName], + ServiceProvider = new ServiceCollection().BuildServiceProvider() + }); + + // Set a custom resource group name + inputsInteraction.Inputs[BaseProvisioningContextProvider.ResourceGroupName].Value = "test-new-rg"; + + // Trigger dynamic update of locations based on subscription and resource group. await inputsInteraction.Inputs[BaseProvisioningContextProvider.LocationName].DynamicLoading!.LoadCallback(new LoadInputContext { AllInputs = inputsInteraction.Inputs, diff --git a/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs b/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs index 2c063267cf8..6b3fccf817d 100644 --- a/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs +++ b/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs @@ -259,6 +259,17 @@ public Task> GetAvailableResourceGroupsAsync(string subscrip }; return Task.FromResult>(resourceGroups); } + + public Task> GetAvailableResourceGroupsWithLocationAsync(string subscriptionId, CancellationToken cancellationToken = default) + { + var resourceGroups = new List<(string Name, string Location)> + { + ("rg-test-1", "eastus"), + ("rg-test-2", "westus"), + ("rg-aspire-dev", "westus2") + }; + return Task.FromResult>(resourceGroups); + } } ///