-
Notifications
You must be signed in to change notification settings - Fork 760
Add Azure provisioning command handling and settings configuration #10038
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
8474f7f
a30a954
212649b
8c61642
3886110
b5cc50a
22ae194
c04d37d
cb4f845
ff44a96
3eb91a7
a9f86e2
cd84fce
e9d5c92
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,9 @@ | ||
| #pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. | ||
|
|
||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Reflection; | ||
| using System.Security.Cryptography; | ||
| using System.Text.Json.Nodes; | ||
| using Aspire.Hosting.Azure.Utils; | ||
|
|
@@ -17,6 +20,7 @@ namespace Aspire.Hosting.Azure.Provisioning.Internal; | |
| /// Default implementation of <see cref="IProvisioningContextProvider"/>. | ||
| /// </summary> | ||
| internal sealed class DefaultProvisioningContextProvider( | ||
| IInteractionService interactionService, | ||
| IOptions<AzureProvisionerOptions> options, | ||
| IHostEnvironment environment, | ||
| ILogger<DefaultProvisioningContextProvider> logger, | ||
|
|
@@ -26,8 +30,106 @@ internal sealed class DefaultProvisioningContextProvider( | |
| { | ||
| private readonly AzureProvisionerOptions _options = options.Value; | ||
|
|
||
| private readonly TaskCompletionSource _provisioningOptionsAvailable = new(); | ||
|
|
||
| private void EnsureProvisioningOptions(JsonObject userSecrets) | ||
| { | ||
| if (!string.IsNullOrEmpty(_options.Location) && !string.IsNullOrEmpty(_options.SubscriptionId)) | ||
| { | ||
| // If both options are already set, we can skip the prompt | ||
| _provisioningOptionsAvailable.TrySetResult(); | ||
| return; | ||
| } | ||
|
|
||
| if (interactionService.IsAvailable) | ||
| { | ||
| // Start the loop that will allow the user to specify the Azure provisioning options | ||
| _ = Task.Run(async () => | ||
| { | ||
| try | ||
| { | ||
| await RetrieveAzureProvisioningOptions(userSecrets).ConfigureAwait(false); | ||
|
|
||
| logger.LogDebug("Azure provisioning options have been handled successfully."); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| logger.LogError(ex, "Failed to retrieve Azure provisioning options."); | ||
eerhardt marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| }); | ||
| } | ||
| } | ||
| private async Task RetrieveAzureProvisioningOptions(JsonObject userSecrets, CancellationToken cancellationToken = default) | ||
| { | ||
| while (_options.Location == null || _options.SubscriptionId == null) | ||
| { | ||
| var locations = typeof(AzureLocation).GetProperties(BindingFlags.Public | BindingFlags.Static) | ||
|
||
| .Where(p => p.PropertyType == typeof(AzureLocation)) | ||
| .Select(p => (AzureLocation)p.GetValue(null)!) | ||
| .Select(location => KeyValuePair.Create(location.Name, location.DisplayName ?? location.Name)) | ||
| .OrderBy(kvp => kvp.Value) | ||
| .ToList(); | ||
|
|
||
| var messageBarResult = await interactionService.PromptMessageBarAsync( | ||
| "Azure Provisioning", | ||
eerhardt marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| "The model contains Azure resources that require an Azure Subscription.", | ||
| new MessageBarInteractionOptions | ||
| { | ||
| Intent = MessageIntent.Warning, | ||
| PrimaryButtonText = "Enter values" | ||
| }, | ||
| cancellationToken) | ||
| .ConfigureAwait(false); | ||
|
|
||
| if (messageBarResult.Canceled) | ||
| { | ||
| // User canceled the prompt, so we exit the loop | ||
| _provisioningOptionsAvailable.SetException(new MissingConfigurationException("Azure provisioning options were not provided.")); | ||
| return; | ||
| } | ||
|
|
||
| if (messageBarResult.Data) | ||
| { | ||
| var result = await interactionService.PromptInputsAsync( | ||
| "Azure Provisioning", | ||
eerhardt marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """ | ||
| The model contains Azure resources that require an Azure Subscription. | ||
| Please provide the required Azure settings. | ||
|
|
||
| If you do not have an Azure subscription, you can create a [free account](https://azure.com/free). | ||
| """, | ||
| [ | ||
| new InteractionInput { InputType = InputType.Choice, Label = "Location", Placeholder = "Select Location", Required = true, Options = [..locations] }, | ||
davidfowl marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| new InteractionInput { InputType = InputType.SecretText, Label = "Subscription ID", Placeholder = "Select Subscription ID", Required = true }, | ||
davidfowl marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| new InteractionInput { InputType = InputType.Text, Label = "Resource Group", Value = GetDefaultResourceGroupName()}, | ||
eerhardt marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ], | ||
| new InputsDialogInteractionOptions { ShowDismiss = false, EnableMessageMarkdown = true }, | ||
|
||
| cancellationToken).ConfigureAwait(false); | ||
|
|
||
| if (!result.Canceled) | ||
| { | ||
| _options.Location = result.Data?[0].Value; | ||
| _options.SubscriptionId = result.Data?[1].Value; | ||
| _options.ResourceGroup = result.Data?[2].Value; | ||
eerhardt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| _options.AllowResourceGroupCreation = true; // Allow the creation of the resource group if it does not exist. | ||
|
|
||
| // Persist the parameter value to user secrets so they can be reused in the future | ||
| userSecrets.Prop("Azure")["Location"] = _options.Location; | ||
davidfowl marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| userSecrets.Prop("Azure")["SubscriptionId"] = _options.SubscriptionId; | ||
| userSecrets.Prop("Azure")["ResourceGroup"] = _options.ResourceGroup; | ||
|
|
||
| _provisioningOptionsAvailable.SetResult(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public async Task<ProvisioningContext> CreateProvisioningContextAsync(JsonObject userSecrets, CancellationToken cancellationToken = default) | ||
| { | ||
| EnsureProvisioningOptions(userSecrets); | ||
|
|
||
| await _provisioningOptionsAvailable.Task.ConfigureAwait(false); | ||
|
|
||
| var subscriptionId = _options.SubscriptionId ?? throw new MissingConfigurationException("An Azure subscription id is required. Set the Azure:SubscriptionId configuration value."); | ||
|
|
||
| var credential = tokenCredentialProvider.TokenCredential; | ||
|
|
@@ -57,26 +159,8 @@ public async Task<ProvisioningContext> CreateProvisioningContextAsync(JsonObject | |
| if (string.IsNullOrEmpty(_options.ResourceGroup)) | ||
| { | ||
| // Generate an resource group name since none was provided | ||
|
|
||
| var prefix = "rg-aspire"; | ||
|
|
||
| if (!string.IsNullOrWhiteSpace(_options.ResourceGroupPrefix)) | ||
| { | ||
| prefix = _options.ResourceGroupPrefix; | ||
| } | ||
|
|
||
| var suffix = RandomNumberGenerator.GetHexString(8, lowercase: true); | ||
|
|
||
| var maxApplicationNameSize = ResourceGroupNameHelpers.MaxResourceGroupNameLength - prefix.Length - suffix.Length - 2; // extra '-'s | ||
|
|
||
| var normalizedApplicationName = ResourceGroupNameHelpers.NormalizeResourceGroupName(environment.ApplicationName.ToLowerInvariant()); | ||
| if (normalizedApplicationName.Length > maxApplicationNameSize) | ||
| { | ||
| normalizedApplicationName = normalizedApplicationName[..maxApplicationNameSize]; | ||
| } | ||
|
|
||
| // Create a unique resource group name and save it in user secrets | ||
| resourceGroupName = $"{prefix}-{normalizedApplicationName}-{suffix}"; | ||
| resourceGroupName = GetDefaultResourceGroupName(); | ||
|
|
||
| createIfAbsent = true; | ||
|
|
||
|
|
@@ -131,4 +215,26 @@ public async Task<ProvisioningContext> CreateProvisioningContextAsync(JsonObject | |
| principal, | ||
| userSecrets); | ||
| } | ||
| } | ||
|
|
||
| private string GetDefaultResourceGroupName() | ||
| { | ||
| var prefix = "rg-aspire"; | ||
|
|
||
| if (!string.IsNullOrWhiteSpace(_options.ResourceGroupPrefix)) | ||
| { | ||
| prefix = _options.ResourceGroupPrefix; | ||
| } | ||
|
|
||
| var suffix = RandomNumberGenerator.GetHexString(8, lowercase: true); | ||
|
|
||
| var maxApplicationNameSize = ResourceGroupNameHelpers.MaxResourceGroupNameLength - prefix.Length - suffix.Length - 2; // extra '-'s | ||
|
|
||
| var normalizedApplicationName = ResourceGroupNameHelpers.NormalizeResourceGroupName(environment.ApplicationName.ToLowerInvariant()); | ||
| if (normalizedApplicationName.Length > maxApplicationNameSize) | ||
| { | ||
| normalizedApplicationName = normalizedApplicationName[..maxApplicationNameSize]; | ||
| } | ||
|
|
||
| return $"{prefix}-{normalizedApplicationName}-{suffix}"; | ||
| } | ||
| } | ||

Uh oh!
There was an error while loading. Please reload this page.