diff --git a/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs b/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs index 40a0c0daff0..b942153032b 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs @@ -273,6 +273,72 @@ 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<(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 047c72ccfd8..a26ea36c96e 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/DefaultArmClientProvider.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/DefaultArmClientProvider.cs @@ -108,6 +108,32 @@ 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); + } + + 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 8b1b11dcb94..3ab579c37a0 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/IProvisioningServices.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/IProvisioningServices.cs @@ -90,6 +90,16 @@ 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); + + /// + /// 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 f4c72d460f5..33262a846ae 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs @@ -312,9 +312,104 @@ private async Task PromptForSubscriptionAsync(CancellationToken cancellationToke } private async Task PromptForLocationAndResourceGroupAsync(CancellationToken cancellationToken) + { + List<(string Name, string Location)>? resourceGroupOptions = null; + var resourceGroupFetchSucceeded = false; + + var step = await activityReporter.CreateStepAsync( + "fetch-resource-groups", + cancellationToken).ConfigureAwait(false); + + await using (step.ConfigureAwait(false)) + { + try + { + var task = await step.CreateTaskAsync("Fetching resource groups", cancellationToken).ConfigureAwait(false); + + await using (task.ConfigureAwait(false)) + { + (resourceGroupOptions, resourceGroupFetchSucceeded) = await TryGetResourceGroupsWithLocationAsync(_options.SubscriptionId!, cancellationToken).ConfigureAwait(false); + } + + if (resourceGroupFetchSucceeded && resourceGroupOptions is not null) + { + await step.SucceedAsync($"Found {resourceGroupOptions.Count} resource group(s)", cancellationToken).ConfigureAwait(false); + } + else + { + await step.WarnAsync("Failed to fetch resource groups", cancellationToken).ConfigureAwait(false); + } + } + catch (Exception ex) + { + _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; + } + } + + // First, prompt for resource group selection + var resourceGroupInput = 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.Name, rg.Name)).ToList() + : [] + }; + + var resourceGroupResult = await _interactionService.PromptInputsAsync( + AzureProvisioningStrings.ResourceGroupDialogTitle, + AzureProvisioningStrings.ResourceGroupSelectionMessage, + [resourceGroupInput], + new InputsDialogInteractionOptions + { + EnableMessageMarkdown = false, + ValidationCallback = static (validationContext) => + { + var resourceGroupInput = validationContext.Inputs[ResourceGroupName]; + if (!IsValidResourceGroupName(resourceGroupInput.Value)) + { + validationContext.AddValidationError(resourceGroupInput, AzureProvisioningStrings.ValidationResourceGroupNameInvalid); + } + return Task.CompletedTask; + } + }, + cancellationToken).ConfigureAwait(false); + + 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 fetchSucceeded = false; + var locationFetchSucceeded = false; var step = await activityReporter.CreateStepAsync( "fetch-regions", @@ -328,12 +423,12 @@ private async Task PromptForLocationAndResourceGroupAsync(CancellationToken canc await using (task.ConfigureAwait(false)) { - (locationOptions, fetchSucceeded) = await TryGetLocationsAsync(_options.SubscriptionId!, cancellationToken).ConfigureAwait(false); + (locationOptions, locationFetchSucceeded) = await TryGetLocationsAsync(_options.SubscriptionId!, cancellationToken).ConfigureAwait(false); } - if (fetchSucceeded) + if (locationFetchSucceeded) { - await step.SucceedAsync($"Found {locationOptions!.Count} available region(s)", cancellationToken).ConfigureAwait(false); + await step.SucceedAsync($"Found {locationOptions!.Count} region(s)", cancellationToken).ConfigureAwait(false); } else { @@ -348,46 +443,28 @@ private async Task PromptForLocationAndResourceGroupAsync(CancellationToken canc } } - var result = await _interactionService.PromptInputsAsync( + var locationResult = 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() - } + { + Name = LocationName, + InputType = InputType.Choice, + Label = AzureProvisioningStrings.LocationLabel, + Required = true, + Options = [..locationOptions] + } ], new InputsDialogInteractionOptions { - EnableMessageMarkdown = false, - ValidationCallback = static (validationContext) => - { - var resourceGroupInput = validationContext.Inputs[ResourceGroupName]; - if (!IsValidResourceGroupName(resourceGroupInput.Value)) - { - validationContext.AddValidationError(resourceGroupInput, AzureProvisioningStrings.ValidationResourceGroupNameInvalid); - } - return Task.CompletedTask; - } + EnableMessageMarkdown = false }, cancellationToken).ConfigureAwait(false); - if (!result.Canceled) + 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 5ffb3b6d721..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,10 +223,42 @@ private async Task RetrieveAzureProvisioningOptions(CancellationToken cancellati inputs.Add(new InteractionInput { - Name = ResourceGroupName, - InputType = InputType.Text, - Label = AzureProvisioningStrings.ResourceGroupLabel, - Value = GetDefaultResourceGroupName() + Name = LocationName, + InputType = InputType.Choice, + 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; + + // 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) + { + 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, ResourceGroupName] + } }); 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..695b8f56856 100644 --- a/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.Designer.cs +++ b/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.Designer.cs @@ -268,5 +268,32 @@ 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); + } + } + + /// + /// 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 8ce03f15d66..61b5cdbcde2 100644 --- a/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.resx +++ b/src/Aspire.Hosting.Azure/Resources/AzureProvisioningStrings.resx @@ -189,4 +189,13 @@ 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 + + + 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 c6e941522c9..0b0bf17dd8d 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.cs.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.cs.xlf @@ -51,11 +51,26 @@ 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ů + + Select or enter resource group + 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 8b56b473ae9..d3abd12acdc 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.de.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.de.xlf @@ -51,11 +51,26 @@ Weitere Informationen finden Sie in der [Azure-Bereitstellungsdokumentation](htt Azure-Bereitstellung + + Azure resource group + Azure resource group + + Resource group Ressourcengruppe + + Select or enter resource group + 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 bfa50ecde2a..1346790dc4e 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.es.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.es.xlf @@ -51,11 +51,26 @@ 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 + + Select or enter resource group + 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 17be6f53547..eb9b9807b00 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.fr.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.fr.xlf @@ -51,11 +51,26 @@ Pour en savoir plus, consultez la [documentation d’approvisionnement Azure](ht Approvisionnement Azure + + Azure resource group + Azure resource group + + Resource group Groupe de ressources + + Select or enter resource group + 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 c375a09e047..b51a8d4c842 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.it.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.it.xlf @@ -51,11 +51,26 @@ Per altre informazioni, vedere la [documentazione sul provisioning di Azure](htt Provisioning Azure + + Azure resource group + Azure resource group + + Resource group Gruppo di risorse + + Select or enter resource group + 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 6425e3d26cd..e8b836a4305 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ja.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ja.xlf @@ -51,11 +51,26 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Azure のプロビジョニング + + Azure resource group + Azure resource group + + Resource group リソース グループ + + Select or enter resource group + 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 aaf3c3823f7..e9bf40690dd 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ko.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ko.xlf @@ -51,11 +51,26 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Azure 프로비전 + + Azure resource group + Azure resource group + + Resource group 리소스 그룹 + + Select or enter resource group + 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 2fb8e819863..56acae75301 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pl.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.pl.xlf @@ -51,11 +51,26 @@ Aby dowiedzieć się więcej, zobacz [dokumentację aprowizacji platformy Azure] Aprowizowanie platformy Azure + + Azure resource group + Azure resource group + + Resource group Grupa zasobów + + Select or enter resource group + 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 0642392f426..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,11 +51,26 @@ 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 + + Select or enter resource group + 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 f7bbf002cac..bdd4a5173ab 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ru.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.ru.xlf @@ -51,11 +51,26 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Подготовка Azure + + Azure resource group + Azure resource group + + Resource group Группа ресурсов + + Select or enter resource group + 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 019eb1edb01..d2f6c16dd5a 100644 --- a/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.tr.xlf +++ b/src/Aspire.Hosting.Azure/Resources/xlf/AzureProvisioningStrings.tr.xlf @@ -51,11 +51,26 @@ 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 + + Select or enter resource group + 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 97e4c240b2e..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,11 +51,26 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Azure 预配 + + Azure resource group + Azure resource group + + Resource group 资源组 + + Select or enter resource group + 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 ccb77145a18..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,11 +51,26 @@ To learn more, see the [Azure provisioning docs](https://aka.ms/dotnet/aspire/az Azure 佈建 + + Azure resource group + Azure resource group + + Resource group 資源群組 + + Select or enter resource group + 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 43ad4d333ec..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.Text, 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 992680d2c71..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(InputType.Text, input.InputType); - Assert.False(input.Required); + Assert.Equal(BaseProvisioningContextProvider.LocationName, input.Name); + Assert.Equal("Location", input.Label); + Assert.Equal(InputType.Choice, input.InputType); + 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(InputType.Text, input.InputType); - Assert.False(input.Required); + Assert.Equal(BaseProvisioningContextProvider.LocationName, input.Name); + Assert.Equal("Location", input.Label); + Assert.Equal(InputType.Choice, input.InputType); + 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, @@ -524,4 +548,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); + } } diff --git a/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs b/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs index 946038c9918..6b3fccf817d 100644 --- a/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs +++ b/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs @@ -248,6 +248,28 @@ 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); + } + + 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); + } } ///