|
// 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 ASPIREPIPELINES001
#pragma warning disable ASPIREAZURE001
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Pipelines;
using Azure.Provisioning;
using Azure.Provisioning.AppService;
using Azure.Provisioning.Expressions;
using Azure.Provisioning.Primitives;
namespace Aspire.Hosting.Azure;
/// <summary>
/// Represents an Azure App Service Environment resource.
/// </summary>
public class AzureAppServiceEnvironmentResource :
AzureProvisioningResource,
IAzureComputeEnvironmentResource,
IAzureContainerRegistry
{
/// <summary>
/// Initializes a new instance of the <see cref="AzureAppServiceEnvironmentResource"/> class.
/// </summary>
/// <param name="name">The name of the Azure App Service Environment.</param>
/// <param name="configureInfrastructure">The callback to configure the Azure infrastructure for this resource.</param>
public AzureAppServiceEnvironmentResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure)
: base(name, configureInfrastructure)
{
// Add pipeline step annotation to create steps and expand deployment target steps
Annotations.Add(new PipelineStepAnnotation(async (factoryContext) =>
{
var model = factoryContext.PipelineContext.Model;
var steps = new List<PipelineStep>();
var loginToAcrStep = new PipelineStep
{
Name = $"login-to-acr-{name}",
Action = context => AzureEnvironmentResourceHelpers.LoginToRegistryAsync(this, context),
Tags = ["acr-login"]
};
// Add print-dashboard-url step
var printDashboardUrlStep = new PipelineStep
{
Name = $"print-dashboard-url-{name}",
Action = ctx => PrintDashboardUrlAsync(ctx),
Tags = ["print-summary"],
DependsOnSteps = [AzureEnvironmentResource.ProvisionInfrastructureStepName],
RequiredBySteps = [WellKnownPipelineSteps.Deploy]
};
steps.Add(loginToAcrStep);
steps.Add(printDashboardUrlStep);
// Expand deployment target steps for all compute resources
// This ensures the push/provision steps from deployment targets are included in the pipeline
foreach (var computeResource in model.GetComputeResources())
{
var deploymentTarget = computeResource.GetDeploymentTargetAnnotation(this)?.DeploymentTarget;
if (deploymentTarget != null && deploymentTarget.TryGetAnnotationsOfType<PipelineStepAnnotation>(out var annotations))
{
// Resolve the deployment target's PipelineStepAnnotation and expand its steps
// We do this because the deployment target is not in the model
foreach (var annotation in annotations)
{
var childFactoryContext = new PipelineStepFactoryContext
{
PipelineContext = factoryContext.PipelineContext,
Resource = deploymentTarget
};
var deploymentTargetSteps = await annotation.CreateStepsAsync(childFactoryContext).ConfigureAwait(false);
foreach (var step in deploymentTargetSteps)
{
// Ensure the step is associated with the deployment target resource
step.Resource ??= deploymentTarget;
}
steps.AddRange(deploymentTargetSteps);
}
}
}
return steps;
}));
// Add pipeline configuration annotation to wire up dependencies
// This is where we wire up the build steps created by the resources
Annotations.Add(new PipelineConfigurationAnnotation(context =>
{
var acrLoginSteps = context.GetSteps(this, "acr-login");
// Wire up build step dependencies
// Build steps are created by ProjectResource and ContainerResource
foreach (var computeResource in context.Model.GetComputeResources())
{
var deploymentTarget = computeResource.GetDeploymentTargetAnnotation(this)?.DeploymentTarget;
if (deploymentTarget is null)
{
continue;
}
// Execute the PipelineConfigurationAnnotation callbacks on the deployment target
if (deploymentTarget.TryGetAnnotationsOfType<PipelineConfigurationAnnotation>(out var annotations))
{
foreach (var annotation in annotations)
{
annotation.Callback(context);
}
}
context.GetSteps(deploymentTarget, WellKnownPipelineTags.PushContainerImage)
.DependsOn(acrLoginSteps);
}
// This ensures that resources that have to be built before deployments are handled
foreach (var computeResource in context.Model.GetBuildResources())
{
context.GetSteps(computeResource, WellKnownPipelineTags.BuildCompute)
.RequiredBy(WellKnownPipelineSteps.Deploy)
.DependsOn(WellKnownPipelineSteps.DeployPrereq);
}
// Make print-summary step depend on provisioning of this environment
var printSummarySteps = context.GetSteps(this, "print-summary");
var provisionSteps = context.GetSteps(this, WellKnownPipelineTags.ProvisionInfrastructure);
printSummarySteps.DependsOn(provisionSteps);
acrLoginSteps.DependsOn(provisionSteps);
}));
}
private async Task PrintDashboardUrlAsync(PipelineStepContext context)
{
var dashboardUri = await DashboardUriReference.GetValueAsync(context.CancellationToken).ConfigureAwait(false);
await context.ReportingStep.CompleteAsync(
$"Dashboard available at [dashboard URL]({dashboardUri})",
CompletionState.Completed,
context.CancellationToken).ConfigureAwait(false);
}
// We don't want these to be public if we end up with an app service
// per compute resource.
internal BicepOutputReference PlanIdOutputReference => new("planId", this);
internal BicepOutputReference ContainerRegistryUrl => new("AZURE_CONTAINER_REGISTRY_ENDPOINT", this);
internal BicepOutputReference ContainerRegistryName => new("AZURE_CONTAINER_REGISTRY_NAME", this);
internal BicepOutputReference ContainerRegistryManagedIdentityId => new("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", this);
internal BicepOutputReference ContainerRegistryClientId => new("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID", this);
internal BicepOutputReference WebsiteContributorManagedIdentityId => new("AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_ID", this);
internal BicepOutputReference WebsiteContributorManagedIdentityPrincipalId => new("AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_PRINCIPAL_ID", this);
/// <summary>
/// Gets the suffix added to each web app created in this App Service Environment.
/// </summary>
internal BicepOutputReference WebSiteSuffix => new("webSiteSuffix", this);
/// <summary>
/// Gets or sets a value indicating whether the Aspire dashboard should be included in the container app environment.
/// Default is true.
/// </summary>
internal bool EnableDashboard { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether Application Insights telemetry should be enabled in the app service environment.
/// </summary>
internal bool EnableApplicationInsights { get; set; }
/// <summary>
/// Gets the location for the Application Insights resource. If <c>null</c>, the resource group location is used.
/// </summary>
internal string? ApplicationInsightsLocation { get; set; }
/// <summary>
/// Parameter resource for the Application Insights location.
/// </summary>
internal ParameterResource? ApplicationInsightsLocationParameter { get; set; }
/// <summary>
/// Application Insights resource.
/// </summary>
internal AzureApplicationInsightsResource? ApplicationInsightsResource { get; set; }
/// <summary>
/// Enables or disables automatic scaling for the App Service Plan.
/// </summary>
internal bool EnableAutomaticScaling { get; set; }
/// <summary>
/// Gets the name of the App Service Plan.
/// </summary>
public BicepOutputReference NameOutputReference => new("name", this);
/// <summary>
/// Gets the URI of the App Service Environment dashboard.
/// </summary>
public BicepOutputReference DashboardUriReference => new("AZURE_APP_SERVICE_DASHBOARD_URI", this);
/// <summary>
/// Gets the Application Insights Instrumentation Key.
/// </summary>
public BicepOutputReference AzureAppInsightsInstrumentationKeyReference =>
new("AZURE_APPLICATION_INSIGHTS_INSTRUMENTATIONKEY", this);
/// <summary>
/// Gets the Application Insights Connection String.
/// </summary>
public BicepOutputReference AzureAppInsightsConnectionStringReference =>
new("AZURE_APPLICATION_INSIGHTS_CONNECTION_STRING", this);
internal static BicepValue<string> GetWebSiteSuffixBicep() =>
BicepFunction.GetUniqueString(BicepFunction.GetResourceGroup().Id);
ReferenceExpression IAzureContainerRegistry.ManagedIdentityId =>
ReferenceExpression.Create($"{ContainerRegistryManagedIdentityId}");
ReferenceExpression IContainerRegistry.Name =>
ReferenceExpression.Create($"{ContainerRegistryName}");
ReferenceExpression IContainerRegistry.Endpoint =>
ReferenceExpression.Create($"{ContainerRegistryUrl}");
ReferenceExpression IComputeEnvironmentResource.GetHostAddressExpression(EndpointReference endpointReference)
{
var resource = endpointReference.Resource;
return ReferenceExpression.Create($"{resource.Name.ToLowerInvariant()}-{WebSiteSuffix}.azurewebsites.net");
}
/// <inheritdoc/>
public override ProvisionableResource AddAsExistingResource(AzureResourceInfrastructure infra)
{
var bicepIdentifier = this.GetBicepIdentifier();
var resources = infra.GetProvisionableResources();
// Check if an AppServicePlan with the same identifier already exists
var existingPlan = resources.OfType<AppServicePlan>().SingleOrDefault(plan => plan.BicepIdentifier == bicepIdentifier);
if (existingPlan is not null)
{
return existingPlan;
}
// Create and add new resource if it doesn't exist
var plan = AppServicePlan.FromExisting(bicepIdentifier);
if (!TryApplyExistingResourceAnnotation(
this,
infra,
plan))
{
plan.Name = NameOutputReference.AsProvisioningParameter(infra);
}
infra.Add(plan);
return plan;
}
}
|