|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure.Provisioning;
using Aspire.Hosting.Azure.Provisioning.Internal;
using Aspire.Hosting.Eventing;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Aspire.Hosting.Azure;
// Provisions azure resources for development purposes
internal sealed class AzureProvisioner(
DistributedApplicationExecutionContext executionContext,
IConfiguration configuration,
IServiceProvider serviceProvider,
BicepProvisioner bicepProvisioner,
ResourceNotificationService notificationService,
ResourceLoggerService loggerService,
IDistributedApplicationEventing eventing,
IProvisioningContextProvider provisioningContextProvider,
IUserSecretsManager userSecretsManager
) : IDistributedApplicationLifecycleHook
{
internal const string AspireResourceNameTag = "aspire-resource-name";
private ILookup<IResource, IResourceWithParent>? _parentChildLookup;
public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
// AzureProvisioner only applies to RunMode
if (executionContext.IsPublishMode)
{
return;
}
var azureResources = AzureResourcePreparer.GetAzureResourcesFromAppModel(appModel);
if (azureResources.Count == 0)
{
return;
}
// Create a map of parents to their children used to propagate state changes later.
_parentChildLookup = appModel.Resources.OfType<IResourceWithParent>().ToLookup(r => r.Parent);
// Sets the state of the resource and all of its children
async Task UpdateStateAsync((IResource Resource, IAzureResource AzureResource) resource, Func<CustomResourceSnapshot, CustomResourceSnapshot> stateFactory)
{
await notificationService.PublishUpdateAsync(resource.AzureResource, stateFactory).ConfigureAwait(false);
// Some IAzureResource instances are a surrogate for for another resource in the app model
// to ensure that resource events are published for the resource that the user expects
// we lookup the resource in the app model here and publish the update to it as well.
if (resource.Resource != resource.AzureResource)
{
await notificationService.PublishUpdateAsync(resource.Resource, stateFactory).ConfigureAwait(false);
}
// We basically want child resources to be moved into the same state as their parent resources whenever
// there is a state update. This is done for us in DCP so we replicate the behavior here in the Azure Provisioner.
var childResources = _parentChildLookup[resource.Resource].ToList();
for (var i = 0; i < childResources.Count; i++)
{
var child = childResources[i];
// Add any level of children
foreach (var grandChild in _parentChildLookup[child])
{
if (!childResources.Contains(grandChild))
{
childResources.Add(grandChild);
}
}
await notificationService.PublishUpdateAsync(child, stateFactory).ConfigureAwait(false);
}
}
// After the resource is provisioned, set its state
async Task AfterProvisionAsync((IResource Resource, IAzureResource AzureResource) resource)
{
try
{
await resource.AzureResource.ProvisioningTaskCompletionSource!.Task.ConfigureAwait(false);
var rolesFailed = await WaitForRoleAssignments(resource).ConfigureAwait(false);
if (!rolesFailed)
{
await UpdateStateAsync(resource, s => s with
{
State = new("Running", KnownResourceStateStyles.Success)
})
.ConfigureAwait(false);
}
}
catch (MissingConfigurationException)
{
await UpdateStateAsync(resource, s => s with
{
State = new("Missing subscription configuration", KnownResourceStateStyles.Error)
})
.ConfigureAwait(false);
}
catch (Exception)
{
await UpdateStateAsync(resource, s => s with
{
State = new("Failed to Provision", KnownResourceStateStyles.Error)
})
.ConfigureAwait(false);
}
}
async Task<bool> WaitForRoleAssignments((IResource Resource, IAzureResource AzureResource) resource)
{
var rolesFailed = false;
if (resource.AzureResource.TryGetAnnotationsOfType<RoleAssignmentResourceAnnotation>(out var roleAssignments))
{
try
{
foreach (var roleAssignment in roleAssignments)
{
await roleAssignment.RolesResource.ProvisioningTaskCompletionSource!.Task.ConfigureAwait(false);
}
}
catch (Exception)
{
rolesFailed = true;
await UpdateStateAsync(resource, s => s with
{
State = new("Failed to Provision Roles", KnownResourceStateStyles.Error)
})
.ConfigureAwait(false);
}
}
return rolesFailed;
}
// Mark all resources as starting
foreach (var r in azureResources)
{
r.AzureResource!.ProvisioningTaskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
await UpdateStateAsync(r, s => s with
{
State = new("Starting", KnownResourceStateStyles.Info)
})
.ConfigureAwait(false);
// After the resource is provisioned, set its state
_ = AfterProvisionAsync(r);
}
// This is fully async so we can just fire and forget
_ = Task.Run(() => ProvisionAzureResources(
configuration,
azureResources,
cancellationToken), cancellationToken);
}
private async Task ProvisionAzureResources(
IConfiguration configuration,
IList<(IResource Resource, IAzureResource AzureResource)> azureResources,
CancellationToken cancellationToken)
{
// Load user secrets first so they can be passed to the provisioning context
var userSecrets = await userSecretsManager.LoadUserSecretsAsync(cancellationToken).ConfigureAwait(false);
// Make resources wait on the same provisioning context
var provisioningContextLazy = new Lazy<Task<ProvisioningContext>>(() => provisioningContextProvider.CreateProvisioningContextAsync(userSecrets, cancellationToken));
var tasks = new List<Task>();
foreach (var resource in azureResources)
{
tasks.Add(ProcessResourceAsync(configuration, provisioningContextLazy, resource, cancellationToken));
}
var task = Task.WhenAll(tasks);
// Suppress throwing so that we can save the user secrets even if the task fails
await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
// If we created any resources then save the user secrets
await userSecretsManager.SaveUserSecretsAsync(userSecrets, cancellationToken).ConfigureAwait(false);
// Set the completion source for all resources
foreach (var resource in azureResources)
{
resource.AzureResource.ProvisioningTaskCompletionSource?.TrySetResult();
}
}
private async Task ProcessResourceAsync(IConfiguration configuration, Lazy<Task<ProvisioningContext>> provisioningContextLazy, (IResource Resource, IAzureResource AzureResource) resource, CancellationToken cancellationToken)
{
var beforeResourceStartedEvent = new BeforeResourceStartedEvent(resource.Resource, serviceProvider);
await eventing.PublishAsync(beforeResourceStartedEvent, cancellationToken).ConfigureAwait(false);
var resourceLogger = loggerService.GetLogger(resource.AzureResource);
// Only process AzureBicepResource resources
if (resource.AzureResource is not AzureBicepResource bicepResource)
{
resource.AzureResource.ProvisioningTaskCompletionSource?.TrySetResult();
resourceLogger.LogInformation("Skipping {resourceName} because it is not a Bicep resource.", resource.AzureResource.Name);
return;
}
if (bicepResource.IsContainer())
{
resource.AzureResource.ProvisioningTaskCompletionSource?.TrySetResult();
resourceLogger.LogInformation("Skipping {resourceName} because it is not configured to be provisioned.", resource.AzureResource.Name);
}
else if (await bicepProvisioner.ConfigureResourceAsync(configuration, bicepResource, cancellationToken).ConfigureAwait(false))
{
resource.AzureResource.ProvisioningTaskCompletionSource?.TrySetResult();
resourceLogger.LogInformation("Using connection information stored in user secrets for {resourceName}.", resource.AzureResource.Name);
await PublishConnectionStringAvailableEventAsync().ConfigureAwait(false);
}
else
{
if (resource.AzureResource.IsExisting())
{
resourceLogger.LogInformation("Resolving {resourceName} as existing resource...", resource.AzureResource.Name);
}
else
{
resourceLogger.LogInformation("Provisioning {resourceName}...", resource.AzureResource.Name);
}
try
{
var provisioningContext = await provisioningContextLazy.Value.ConfigureAwait(false);
await bicepProvisioner.GetOrCreateResourceAsync(
bicepResource,
provisioningContext,
cancellationToken).ConfigureAwait(false);
resource.AzureResource.ProvisioningTaskCompletionSource?.TrySetResult();
await PublishConnectionStringAvailableEventAsync().ConfigureAwait(false);
}
catch (AzureCliNotOnPathException ex)
{
resourceLogger.LogCritical("Using Azure resources during local development requires the installation of the Azure CLI. See https://aka.ms/dotnet/aspire/azcli for instructions.");
resource.AzureResource.ProvisioningTaskCompletionSource?.TrySetException(ex);
}
catch (MissingConfigurationException ex)
{
resourceLogger.LogCritical("Resource could not be provisioned because Azure subscription, location, and resource group information is missing. See https://aka.ms/dotnet/aspire/azure/provisioning for more details.");
resource.AzureResource.ProvisioningTaskCompletionSource?.TrySetException(ex);
}
catch (Exception ex)
{
resourceLogger.LogError(ex, "Error provisioning {ResourceName}.", resource.AzureResource.Name);
resource.AzureResource.ProvisioningTaskCompletionSource?.TrySetException(new InvalidOperationException($"Unable to resolve references from {resource.AzureResource.Name}"));
}
}
async Task PublishConnectionStringAvailableEventAsync()
{
await PublishConnectionStringAvailableEventRecursiveAsync(resource.Resource).ConfigureAwait(false);
}
async Task PublishConnectionStringAvailableEventRecursiveAsync(IResource targetResource)
{
// If the resource itself has a connection string then publish that the connection string is available.
if (targetResource is IResourceWithConnectionString)
{
var connectionStringAvailableEvent = new ConnectionStringAvailableEvent(targetResource, serviceProvider);
await eventing.PublishAsync(connectionStringAvailableEvent, cancellationToken).ConfigureAwait(false);
}
// Sometimes the container/executable itself does not have a connection string, and in those cases
// we need to dispatch the event for the children.
if (_parentChildLookup![targetResource] is { } children)
{
// only dispatch the event for children that have a connection string and are IResourceWithParent, not parented by annotations.
foreach (var child in children.OfType<IResourceWithConnectionString>().Where(c => c is IResourceWithParent))
{
await PublishConnectionStringAvailableEventRecursiveAsync(child).ConfigureAwait(false);
}
}
}
}
}
|