// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#pragma warning disable ASPIREAZURE003
#pragma warning disable ASPIREAZURE001
#pragma warning disable ASPIREPIPELINES002
using Aspire.Dashboard.Model;
using Aspire.Hosting.ApplicationModel;
using Azure.Provisioning;
using Azure.Provisioning.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Aspire.Hosting.Azure;
/// <summary>
/// Prepares Azure resources for provisioning and publish.
///
/// This includes preparing role assignment annotations for Azure resources.
/// </summary>
internal sealed class AzureResourcePreparer(
IOptions<AzureProvisioningOptions> options,
DistributedApplicationExecutionContext executionContext)
{
internal Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken)
{
return PrepareResourcesAsync(@event.Model, cancellationToken);
}
internal async Task PrepareResourcesAsync(DistributedApplicationModel model, CancellationToken cancellationToken)
{
var azureResources = GetAzureResourcesFromAppModel(model);
if (azureResources.Count == 0)
{
// AddAzureProvisioning creates the environment before resources are configured, so wait until
// preparation to hide it when the final run-mode model contains only local emulators.
if (executionContext.IsRunMode &&
model.Resources.OfType<AzureEnvironmentResource>().SingleOrDefault() is { } environmentResource &&
!environmentResource.HasAnnotationOfType<HiddenAnnotation>())
{
environmentResource.Annotations.Add(new HiddenAnnotation(HiddenBehavior.Always));
}
return;
}
if (!EnvironmentSupportsIdentitiesAndAssignments())
{
// If the app infrastructure does not support targeted identities and role assignments, then we need to ensure that
// there are no identity or role assignment annotations in the app model because they won't be honored otherwise.
EnsureNoIdentityOrRoleAssignmentAnnotations(model);
}
await BuildRoleAssignmentAnnotations(model, azureResources, cancellationToken).ConfigureAwait(false);
PropagateReferencedDeploymentPrerequisites(model);
if (executionContext.IsRunMode)
{
AddPerResourceCommands(azureResources);
}
// set the ProvisioningBuildOptions on the resource, if necessary
foreach (var r in azureResources)
{
if (r.AzureResource is AzureProvisioningResource provisioningResource)
{
provisioningResource.ProvisioningBuildOptions = options.Value.ProvisioningBuildOptions;
}
}
}
private static void AddPerResourceCommands(List<(IResource Resource, IAzureResource AzureResource)> azureResources)
{
foreach (var resource in azureResources)
{
if (resource.AzureResource is not AzureBicepResource bicepResource ||
bicepResource.IsContainer() ||
bicepResource.IsEmulator())
{
continue;
}
foreach (var command in AzureProvisioningController.ResourceCommandDefinitions)
{
AddOrReplaceCommand(
resource.Resource,
command.Name,
command.DisplayName,
executeCommand: context => command.ExecuteCommand(context.Services.GetRequiredService<AzureProvisioningController>(), resource.Resource.Name, context),
new CommandOptions
{
Description = command.Description,
ConfirmationMessage = command.ConfirmationMessage,
IconName = command.IconName,
IconVariant = command.IconVariant,
IsHighlighted = command.IsHighlighted,
Arguments = command.Command == AzureProvisioningController.AzureResourceCommand.ChangeLocation
? AzureProvisioningController.CreateChangeLocationCommandArguments(GetDeploymentStateResourceName(resource))
: command.Arguments ?? [],
ValidateArguments = command.ValidateArguments,
UpdateState = context => context.Services.GetRequiredService<AzureProvisioningController>().GetResourceCommandState(resource.Resource.Name, command.Command, context)
});
}
}
}
private static string GetDeploymentStateResourceName((IResource Resource, IAzureResource AzureResource) resource)
=> resource.AzureResource is AzureBicepResource bicepResource ? bicepResource.Name : resource.Resource.Name;
private static void AddOrReplaceCommand(
IResource resource,
string name,
string displayName,
Func<ExecuteCommandContext, Task<ExecuteCommandResult>> executeCommand,
CommandOptions commandOptions)
{
if (resource.Annotations.OfType<ResourceCommandAnnotation>().SingleOrDefault(annotation => annotation.Name == name) is { } existingAnnotation)
{
resource.Annotations.Remove(existingAnnotation);
}
resource.Annotations.Add(new ResourceCommandAnnotation(
name,
displayName,
commandOptions.UpdateState ?? (_ => ResourceCommandState.Enabled),
executeCommand,
commandOptions.Description,
commandOptions.Arguments,
commandOptions.ConfirmationMessage,
commandOptions.IconName,
commandOptions.IconVariant,
commandOptions.IsHighlighted,
commandOptions.Visibility,
commandOptions.ValidateArguments));
}
internal static List<(IResource Resource, IAzureResource AzureResource)> GetAzureResourcesFromAppModel(DistributedApplicationModel appModel)
{
// Some resources do not derive from IAzureResource but can be handled
// by the Azure provisioner because they have the AzureBicepResourceAnnotation
// which holds a reference to the surrogate AzureBicepResource which implements
// IAzureResource and can be used by the Azure Bicep Provisioner.
var azureResources = new List<(IResource, IAzureResource)>();
foreach (var resource in appModel.Resources)
{
if (resource.IsExcludedFromPublish() || resource.IsContainer() || resource.IsEmulator())
{
continue;
}
else if (resource is IAzureResource azureResource)
{
// If we are dealing with an Azure resource then we just return it.
azureResources.Add((resource, azureResource));
}
else if (resource.Annotations.OfType<AzureBicepResourceAnnotation>().SingleOrDefault() is { } annotation)
{
// If we aren't an Azure resource and there is no surrogate, return null for
// the Azure resource in the tuple (we'll filter it out later.
azureResources.Add((resource, annotation.Resource));
}
}
return azureResources;
}
private bool EnvironmentSupportsIdentitiesAndAssignments()
{
// run mode always supports targeted role assignments
// publish mode only supports targeted role assignments if the environment supports it
return executionContext.IsRunMode || options.Value.SupportsTargetedRoleAssignments;
}
private static void EnsureNoIdentityOrRoleAssignmentAnnotations(DistributedApplicationModel appModel)
{
foreach (var resource in appModel.Resources)
{
if (resource.HasAnnotationOfType<RoleAssignmentAnnotation>())
{
throw new InvalidOperationException("The application model does not support role assignments. Ensure you are using an environment that supports role assignments, for example AddAzureContainerAppEnvironment.");
}
if (resource.HasAnnotationOfType<AppIdentityAnnotation>())
{
throw new InvalidOperationException("The application model does not support using explicit managed identities. Ensure you are using an environment that supports managed identities, for example AddAzureContainerAppEnvironment.");
}
}
}
private async Task BuildRoleAssignmentAnnotations(DistributedApplicationModel appModel, List<(IResource Resource, IAzureResource AzureResource)> azureResources, CancellationToken cancellationToken)
{
var globalRoleAssignments = new Dictionary<AzureProvisioningResource, HashSet<RoleDefinition>>();
if (!EnvironmentSupportsIdentitiesAndAssignments())
{
// when the app infrastructure doesn't support targeted role assignments, just copy all the default role assignments to applied role assignments
foreach (var resource in azureResources.Select(r => r.AzureResource).OfType<AzureProvisioningResource>())
{
if (resource.TryGetLastAnnotation<DefaultRoleAssignmentsAnnotation>(out var defaultRoleAssignments))
{
AppendGlobalRoleAssignments(globalRoleAssignments, resource, defaultRoleAssignments.Roles);
}
}
}
else
{
// when the app infrastructure supports targeted role assignments, walk the resource graph and
// - if in RunMode
// - if a compute resource has RoleAssignmentAnnotations, add them to globalRoleAssignments on the referenced Azure resource
// - if the resource doesn't, copy the DefaultRoleAssignments to globalRoleAssignments
//
// - if in PublishMode
// - if a compute resource has RoleAssignmentAnnotations, use them
// - if the resource doesn't, copy the DefaultRoleAssignments to RoleAssignmentAnnotations to apply the defaults
var resourceSnapshot = appModel.GetComputeResources()
.Concat(appModel.Resources
.OfType<AzureUserAssignedIdentityResource>()
.Where(r => !r.IsExcludedFromPublish()))
.ToArray(); // avoid modifying the collection while iterating
foreach (var resource in resourceSnapshot)
{
var prerequisiteResources = new HashSet<AzureBicepResource>();
var directDependencies = await resource.GetResourceDependenciesAsync(executionContext, ResourceDependencyDiscoveryMode.DirectOnly, cancellationToken).ConfigureAwait(false);
var azureReferences = new HashSet<IAzureResource>(directDependencies.OfType<IAzureResource>());
var azureReferencesWithRoleAssignments =
(resource.TryGetAnnotationsOfType<RoleAssignmentAnnotation>(out var annotations)
? annotations
: [])
.ToLookup(a => a.Target);
foreach (var azureReference in azureReferences.OfType<AzureProvisioningResource>())
{
if (azureReference.IsContainer() || azureReference.IsEmulator())
{
// Skip emulators
continue;
}
var roleAssignments = azureReferencesWithRoleAssignments[azureReference];
if (roleAssignments.Any())
{
if (executionContext.IsRunMode)
{
// in RunMode, we need to add the role assignments to the resource
AppendGlobalRoleAssignments(globalRoleAssignments, azureReference, roleAssignments.SelectMany(a => a.Roles));
}
// in PublishMode, this is a no-op since GetAllRoleAssignments will handle the role assignments
}
else if (azureReference.TryGetLastAnnotation<DefaultRoleAssignmentsAnnotation>(out var defaults))
{
if (executionContext.IsRunMode)
{
// in RunMode, we copy the default role assignments to the Azure reference,
// even if the roles are empty, since empty roles are used by some resources - like databases
AppendGlobalRoleAssignments(globalRoleAssignments, azureReference, defaults.Roles);
}
else
{
// in PublishMode, we copy the default role assignments to the compute resource
resource.Annotations.Add(new RoleAssignmentAnnotation(azureReference, defaults.Roles));
}
}
// Find private endpoints that target Azure resources referenced by this compute resource.
// These must be provisioned before the compute resource is deployed.
if (azureReference.TryGetAnnotationsOfType<PrivateEndpointTargetAnnotation>(out var peAnnotations))
{
foreach (var peAnnotation in peAnnotations)
{
prerequisiteResources.Add(peAnnotation.PrivateEndpointResource);
}
}
}
// A direct dependency that is not itself an Azure resource can still "front" one
// (e.g. a Foundry hosted agent's node app fronts its owning Foundry account). Such a
// resource carries ReferenceRoleAssignmentAnnotation(s) declaring that any resource
// referencing it should be granted roles on a transitive Azure target the normal
// IAzureResource-only reference walk above cannot reach. Fold those implied targets
// into the same role-assignment path so the consumer gets an identity + role bicep
// exactly as it would for a direct Azure reference.
foreach (var dependency in directDependencies)
{
if (!dependency.TryGetAnnotationsOfType<ReferenceRoleAssignmentAnnotation>(out var impliedRoleAssignments))
{
continue;
}
foreach (var impliedRoleAssignment in impliedRoleAssignments)
{
var target = impliedRoleAssignment.Target;
if (target.IsContainer() || target.IsEmulator())
{
continue;
}
if (executionContext.IsRunMode)
{
AppendGlobalRoleAssignments(globalRoleAssignments, target, impliedRoleAssignment.Roles);
}
else
{
// In PublishMode, materialize as an explicit RoleAssignmentAnnotation so
// GetAllRoleAssignments (which groups by target and unions roles) picks it
// up alongside any roles the consumer already declares for the same target.
resource.Annotations.Add(new RoleAssignmentAnnotation(target, impliedRoleAssignment.Roles));
}
}
}
// in PublishMode with SupportsTargetedRoleAssignments, we need to create the identity and role assignment resources
// if the resource references any Azure resources, or has role assignments to Azure resources
if (executionContext.IsPublishMode)
{
var roleAssignments = GetAllRoleAssignments(resource);
if (roleAssignments.Count > 0)
{
var (identityResource, roleAssignmentResources) = CreateIdentityAndRoleAssignmentResources(resource, roleAssignments);
if (resource != identityResource)
{
// Only add the AppIdentityAnnotation if the resource doesn't already have one
if (!resource.TryGetLastAnnotation<AppIdentityAnnotation>(out var existingAppIdentityAnnotation) ||
existingAppIdentityAnnotation.IdentityResource != identityResource)
{
resource.Annotations.Add(new AppIdentityAnnotation(identityResource));
}
// add the identity resource to the resource collection so it can be provisioned
// but only if it's not already there
if (!appModel.Resources.Contains(identityResource))
{
appModel.Resources.Add(identityResource);
}
}
foreach (var roleAssignmentResource in roleAssignmentResources)
{
prerequisiteResources.Add(AddOrGetRoleAssignmentResource(appModel, roleAssignmentResource));
}
}
}
// Add prerequisite infrastructure resources on the compute resource.
// Deployment infrastructure subscribers will transfer these to deployment target References
// so AzureBicepResource dependency wiring can apply provision ordering.
AddDeploymentPrerequisitesAnnotation(resource, prerequisiteResources);
}
if (executionContext.IsRunMode)
{
// in RunMode, any Azure resources that are not referenced by a compute resource should have their default role assignments applied
foreach (var azureResource in azureResources.Select(r => r.AzureResource).OfType<AzureProvisioningResource>())
{
if (!globalRoleAssignments.TryGetValue(azureResource, out _) &&
azureResource.TryGetLastAnnotation<DefaultRoleAssignmentsAnnotation>(out var defaultRoleAssignments))
{
AppendGlobalRoleAssignments(globalRoleAssignments, azureResource, defaultRoleAssignments.Roles);
}
}
}
}
if (globalRoleAssignments.Count > 0)
{
CreateGlobalRoleAssignments(appModel, globalRoleAssignments);
}
}
private static Dictionary<AzureProvisioningResource, IEnumerable<RoleDefinition>> GetAllRoleAssignments(IResource resource)
{
var result = new Dictionary<AzureProvisioningResource, IEnumerable<RoleDefinition>>();
if (resource.TryGetAnnotationsOfType<RoleAssignmentAnnotation>(out var roleAssignments))
{
foreach (var g in roleAssignments.GroupBy(r => r.Target))
{
// Deduplicate roles per target. A target can accumulate multiple RoleAssignmentAnnotations
// (e.g. an implied ReferenceRoleAssignmentAnnotation from two hosted agents on the same
// Foundry account, plus a direct reference). Emitting the same RoleDefinition twice would
// produce two RoleAssignment bicep resources with the same identifier ("{prefix}_{roleName}")
// and fail bicep compilation. This mirrors the RunMode path, which unions into a HashSet.
result[g.Key] = g.SelectMany(r => r.Roles).Distinct();
}
}
return result;
}
private (AzureUserAssignedIdentityResource IdentityResource, List<AzureRoleAssignmentResource> RoleAssignmentResources) CreateIdentityAndRoleAssignmentResources(
IResource resource,
Dictionary<AzureProvisioningResource, IEnumerable<RoleDefinition>> roleAssignments)
{
AzureUserAssignedIdentityResource identityResource;
// If we're currently targeting an AzureUserAssignedIdentityResource, we can use it as the identity resource
// for the role assignments. If we are targeting a compute resource that has an AppIdentityAnnotation, we can
// use the identity resource from that annotation. Otherwise, create a new identity resource to use for role assignments.
if (resource is AzureUserAssignedIdentityResource existingIdentityResource)
{
identityResource = existingIdentityResource;
}
else if (resource.TryGetLastAnnotation<AppIdentityAnnotation>(out var appIdentityAnnotation) &&
appIdentityAnnotation.IdentityResource is AzureUserAssignedIdentityResource existingAppIdentity)
{
identityResource = existingAppIdentity;
}
else
{
identityResource = new AzureUserAssignedIdentityResource($"{resource.Name}-identity")
{
ProvisioningBuildOptions = options.Value.ProvisioningBuildOptions
};
}
var roleAssignmentResources = CreateRoleAssignmentsResources(resource, roleAssignments, identityResource);
return (identityResource, roleAssignmentResources);
}
private List<AzureRoleAssignmentResource> CreateRoleAssignmentsResources(
IResource resource,
Dictionary<AzureProvisioningResource, IEnumerable<RoleDefinition>> roleAssignments,
AzureUserAssignedIdentityResource appIdentityResource)
{
var roleAssignmentResources = new List<AzureRoleAssignmentResource>();
foreach (var (targetResource, roles) in roleAssignments)
{
var roleAssignmentResource = new AzureRoleAssignmentResource(
$"{resource.Name}-roles-{targetResource.Name}",
targetResource,
resource,
appIdentityResource,
infra => AddRoleAssignmentsInfrastructure(infra, targetResource, roles, appIdentityResource))
{
ProvisioningBuildOptions = options.Value.ProvisioningBuildOptions,
};
ApplyExistingResourceScope(roleAssignmentResource, targetResource);
roleAssignmentResources.Add(roleAssignmentResource);
}
return roleAssignmentResources;
}
private void AddRoleAssignmentsInfrastructure(
AzureResourceInfrastructure infra,
AzureProvisioningResource azureResource,
IEnumerable<RoleDefinition> roles,
AzureUserAssignedIdentityResource appIdentityResource)
{
var context = new AddRoleAssignmentsContext(
infra,
executionContext,
roles,
new(() => RoleManagementPrincipalType.ServicePrincipal),
new(() => appIdentityResource.PrincipalId.AsProvisioningParameter(infra, parameterName: AzureBicepResource.KnownParameters.PrincipalId)),
new(() => appIdentityResource.PrincipalName.AsProvisioningParameter(infra, parameterName: AzureBicepResource.KnownParameters.PrincipalName)));
azureResource.AddRoleAssignments(context);
}
/// <summary>
/// Context for adding role assignments to an Azure resource.
/// </summary>
private sealed class AddRoleAssignmentsContext(
AzureResourceInfrastructure infrastructure,
DistributedApplicationExecutionContext executionContext,
IEnumerable<RoleDefinition> roles,
Lazy<BicepValue<RoleManagementPrincipalType>> getPrincipalType,
Lazy<BicepValue<Guid>> getPrincipalId,
Lazy<BicepValue<string>> getPrincipalName) : IAddRoleAssignmentsContext
{
public AzureResourceInfrastructure Infrastructure { get; } = infrastructure;
public IEnumerable<RoleDefinition> Roles { get; } = roles;
public BicepValue<RoleManagementPrincipalType> PrincipalType => getPrincipalType.Value;
public BicepValue<Guid> PrincipalId => getPrincipalId.Value;
public BicepValue<string> PrincipalName => getPrincipalName.Value;
public DistributedApplicationExecutionContext ExecutionContext => executionContext;
}
private static void AppendGlobalRoleAssignments(Dictionary<AzureProvisioningResource, HashSet<RoleDefinition>> globalRoleAssignments, AzureProvisioningResource azureResource, IEnumerable<RoleDefinition> newRoles)
{
if (!globalRoleAssignments.TryGetValue(azureResource, out var existingRoles))
{
existingRoles = new HashSet<RoleDefinition>();
globalRoleAssignments[azureResource] = existingRoles;
}
existingRoles.UnionWith(newRoles);
}
private void CreateGlobalRoleAssignments(DistributedApplicationModel appModel, Dictionary<AzureProvisioningResource, HashSet<RoleDefinition>> globalRoleAssignments)
{
foreach (var (azureResource, roles) in globalRoleAssignments)
{
var roleAssignmentResource = CreateGlobalRoleAssignmentsResource(azureResource, roles);
roleAssignmentResource = AddOrGetRoleAssignmentResource(appModel, roleAssignmentResource);
if (!azureResource.Annotations.OfType<RoleAssignmentResourceAnnotation>().Any(a => a.RolesResource == roleAssignmentResource))
{
azureResource.Annotations.Add(new RoleAssignmentResourceAnnotation(roleAssignmentResource));
}
if (!roleAssignmentResource.Annotations.OfType<ResourceRelationshipAnnotation>().Any(a => a.Resource == azureResource && a.Type == KnownRelationshipTypes.Parent))
{
roleAssignmentResource.Annotations.Add(new ResourceRelationshipAnnotation(azureResource, KnownRelationshipTypes.Parent));
}
}
}
private static AzureRoleAssignmentResource AddOrGetRoleAssignmentResource(DistributedApplicationModel appModel, AzureRoleAssignmentResource roleAssignmentResource)
{
if (!appModel.Resources.TryGetByName(roleAssignmentResource.Name, out var existingResource))
{
appModel.Resources.Add(roleAssignmentResource);
return roleAssignmentResource;
}
if (existingResource is AzureRoleAssignmentResource existingRoleAssignmentResource &&
existingRoleAssignmentResource.TargetAzureResource == roleAssignmentResource.TargetAzureResource &&
existingRoleAssignmentResource.OwnerResource == roleAssignmentResource.OwnerResource &&
existingRoleAssignmentResource.IdentityResource == roleAssignmentResource.IdentityResource)
{
return existingRoleAssignmentResource;
}
appModel.Resources.Add(roleAssignmentResource);
return roleAssignmentResource;
}
private static void AddDeploymentPrerequisitesAnnotation(IResource resource, HashSet<AzureBicepResource> prerequisiteResources)
{
if (prerequisiteResources.Count == 0)
{
return;
}
if (resource.TryGetAnnotationsOfType<DeploymentPrerequisitesAnnotation>(out var existingAnnotations))
{
prerequisiteResources.ExceptWith(existingAnnotations.SelectMany(a => a.Resources));
}
if (prerequisiteResources.Count > 0)
{
resource.Annotations.Add(new DeploymentPrerequisitesAnnotation(prerequisiteResources));
}
}
private static void PropagateReferencedDeploymentPrerequisites(DistributedApplicationModel appModel)
{
foreach (var resource in appModel.Resources.OfType<AzureBicepResource>().ToArray())
{
var prerequisiteResources = resource.GetExplicitAzureReferences()
.SelectMany(reference => reference.Annotations.OfType<DeploymentPrerequisitesAnnotation>())
.SelectMany(annotation => annotation.Resources)
.Where(prerequisite => prerequisite != resource)
.ToHashSet();
// References drive pipeline provision ordering. Keep the annotation too so compute
// environment publishers can transfer these prerequisites to workload resources.
resource.References.UnionWith(prerequisiteResources);
AddDeploymentPrerequisitesAnnotation(resource, prerequisiteResources);
}
}
private AzureRoleAssignmentResource CreateGlobalRoleAssignmentsResource(
AzureProvisioningResource targetResource,
IEnumerable<RoleDefinition> roles)
{
var roleAssignmentResource = new AzureRoleAssignmentResource(
$"{targetResource.Name}-roles",
targetResource,
ownerResource: null,
identityResource: null,
infra => AddGlobalRoleAssignmentsInfrastructure(infra, targetResource, roles))
{
ProvisioningBuildOptions = options.Value.ProvisioningBuildOptions,
};
ApplyExistingResourceScope(roleAssignmentResource, targetResource);
return roleAssignmentResource;
}
private static void ApplyExistingResourceScope(AzureBicepResource roleAssignmentResource, AzureProvisioningResource targetResource)
{
if (targetResource.TryGetLastAnnotation<ExistingAzureResourceAnnotation>(out var existingAnnotation) &&
AzureBicepResourceScope.FromExistingResourceAnnotation(existingAnnotation) is { } scope)
{
roleAssignmentResource.Scope = scope;
}
}
private void AddGlobalRoleAssignmentsInfrastructure(
AzureResourceInfrastructure infra,
AzureProvisioningResource azureResource,
IEnumerable<RoleDefinition> roles)
{
ProvisioningParameter CreatePrincipalParam(string name)
{
var param = new ProvisioningParameter(name, typeof(string));
infra.Add(param);
return param;
}
var context = new AddRoleAssignmentsContext(
infra,
executionContext,
roles,
new(() => CreatePrincipalParam(AzureBicepResource.KnownParameters.PrincipalType)),
new(() => CreatePrincipalParam(AzureBicepResource.KnownParameters.PrincipalId)),
new(() => CreatePrincipalParam(AzureBicepResource.KnownParameters.PrincipalName)));
azureResource.AddRoleAssignments(context);
}
}