Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/Aspire.Cli/Utils/ConsoleActivityLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,14 +283,14 @@ public void SetFinalResult(bool succeeded)
if (succeeded)
{
_finalStatusHeader = _enableColor
? $"[green]{SuccessSymbol} DEPLOYMENT SUCCEEDED[/]"
: $"{SuccessSymbol} DEPLOYMENT SUCCEEDED";
? $"[green]{SuccessSymbol} PIPELINE SUCCEEDED[/]"
: $"{SuccessSymbol} PIPELINE SUCCEEDED";
}
else
{
_finalStatusHeader = _enableColor
? $"[red]{FailureSymbol} DEPLOYMENT FAILED[/]"
: $"{FailureSymbol} DEPLOYMENT FAILED";
? $"[red]{FailureSymbol} PIPELINE FAILED[/]"
: $"{FailureSymbol} PIPELINE FAILED";
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// 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
#pragma warning disable ASPIRECOMPUTE001
#pragma warning disable ASPIREPUBLISHERS001

using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Pipelines;
using Azure.Provisioning;
using Azure.Provisioning.AppContainers;
using Azure.Provisioning.Primitives;
Expand All @@ -11,13 +17,135 @@ namespace Aspire.Hosting.Azure.AppContainers;
/// <summary>
/// Represents an Azure Container App Environment resource.
/// </summary>
/// <param name="name">The name of the Container App Environment.</param>
/// <param name="configureInfrastructure">The callback to configure the Azure infrastructure for this resource.</param>
public class AzureContainerAppEnvironmentResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure) :
#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
AzureProvisioningResource(name, configureInfrastructure), IAzureComputeEnvironmentResource, IAzureContainerRegistry
#pragma warning restore ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
public class AzureContainerAppEnvironmentResource :
AzureProvisioningResource, IAzureComputeEnvironmentResource, IAzureContainerRegistry
{
/// <summary>
/// Initializes a new instance of the <see cref="AzureContainerAppEnvironmentResource"/> class.
/// </summary>
/// <param name="name">The name of the Container App Environment.</param>
/// <param name="configureInfrastructure">The callback to configure the Azure infrastructure for this resource.</param>
public AzureContainerAppEnvironmentResource(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 provisionSteps = context.GetSteps(this, WellKnownPipelineTags.ProvisionInfrastructure);
var printDashboardUrlSteps = context.GetSteps(this, "print-summary");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think we want to model a pattern where these kinds of "local tags" are constants/statics on the class.

printDashboardUrlSteps.DependsOn(provisionSteps);

acrLoginSteps.DependsOn(provisionSteps);
}));
}

private async Task PrintDashboardUrlAsync(PipelineStepContext context)
{
var domainValue = await ContainerAppDomain.GetValueAsync(context.CancellationToken).ConfigureAwait(false);

var dashboardUrl = $"https://aspire-dashboard.ext.{domainValue}";

await context.ReportingStep.CompleteAsync(
$"Dashboard available at [dashboard URL]({dashboardUrl})",
CompletionState.Completed,
context.CancellationToken).ConfigureAwait(false);
}
internal bool UseAzdNamingConvention { get; set; }

/// <summary>
Expand Down
140 changes: 134 additions & 6 deletions src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppResource.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,149 @@
// 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 ASPIRECOMPUTE001
#pragma warning disable ASPIREPIPELINES001
#pragma warning disable ASPIREPUBLISHERS001
#pragma warning disable ASPIREAZURE001
#pragma warning disable ASPIREPIPELINES003

using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Pipelines;
using Aspire.Hosting.Publishing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Aspire.Hosting.Azure.AppContainers;

/// <summary>
/// Represents an Azure Container App resource.
/// </summary>
/// <param name="name">The name of the resource in the Aspire application model.</param>
/// <param name="configureInfrastructure">Callback to configure the Azure resources.</param>
/// <param name="targetResource">The target compute resource that this Azure Container App is being created for.</param>
public class AzureContainerAppResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure, IResource targetResource)
: AzureProvisioningResource(name, configureInfrastructure)
public class AzureContainerAppResource : AzureProvisioningResource
{
/// <summary>
/// Initializes a new instance of the <see cref="AzureContainerAppResource"/> class.
/// </summary>
/// <param name="name">The name of the resource in the Aspire application model.</param>
/// <param name="configureInfrastructure">Callback to configure the Azure resources.</param>
/// <param name="targetResource">The target compute resource that this Azure Container App is being created for.</param>
public AzureContainerAppResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure, IResource targetResource)
: base(name, configureInfrastructure)
{
TargetResource = targetResource;

// Add pipeline step annotation for push
Annotations.Add(new PipelineStepAnnotation((factoryContext) =>
{
// Get the registry from the target resource's deployment target annotation
var deploymentTargetAnnotation = targetResource.GetDeploymentTargetAnnotation();
if (deploymentTargetAnnotation?.ContainerRegistry is not IContainerRegistry registry)
{
// No registry available, skip push
return [];
}

var steps = new List<PipelineStep>();

if (targetResource.RequiresImageBuildAndPush())
{
// Create push step for this deployment target
var pushStep = new PipelineStep
{
Name = $"push-{targetResource.Name}",
Action = async ctx =>
{
var containerImageBuilder = ctx.Services.GetRequiredService<IResourceContainerImageBuilder>();

await AzureEnvironmentResourceHelpers.PushImageToRegistryAsync(
registry,
targetResource,
ctx,
containerImageBuilder).ConfigureAwait(false);
},
Tags = [WellKnownPipelineTags.PushContainerImage]
};

steps.Add(pushStep);
}

if (!targetResource.TryGetEndpoints(out var endpoints))
{
endpoints = [];
}

var anyPublicEndpoints = endpoints.Any(e => e.IsExternal);

var printResourceSummary = new PipelineStep
{
Name = $"print-{targetResource.Name}-summary",
Action = async ctx =>
{
var containerAppEnv = (AzureContainerAppEnvironmentResource)deploymentTargetAnnotation.ComputeEnvironment!;

var domainValue = await containerAppEnv.ContainerAppDomain.GetValueAsync(ctx.CancellationToken).ConfigureAwait(false);

if (anyPublicEndpoints)
{
var endpoint = $"https://{targetResource.Name.ToLowerInvariant()}.{domainValue}";

ctx.ReportingStep.Log(LogLevel.Information, $"Successfully deployed **{targetResource.Name}** to [{endpoint}]({endpoint})", enableMarkdown: true);
}
else
{
ctx.ReportingStep.Log(LogLevel.Information, $"Successfully deployed **{targetResource.Name}** to Azure Container Apps environment **{containerAppEnv.Name}**. No public endpoints were configured.", enableMarkdown: true);
}
},
Tags = ["print-summary"],
RequiredBySteps = [WellKnownPipelineSteps.Deploy]
};

var deployStep = new PipelineStep
{
Name = $"deploy-{targetResource.Name}",
Action = _ => Task.CompletedTask,
Tags = [WellKnownPipelineTags.DeployCompute]
};

deployStep.DependsOn(printResourceSummary);

steps.Add(printResourceSummary);
steps.Add(deployStep);

return steps;
}));

// Add pipeline configuration annotation to wire up dependencies
Annotations.Add(new PipelineConfigurationAnnotation((context) =>
{
// Find the push step for this resource
var pushSteps = context.GetSteps(this, WellKnownPipelineTags.PushContainerImage);

// Make push step depend on build steps of the target resource
var buildSteps = context.GetSteps(targetResource, WellKnownPipelineTags.BuildCompute);

pushSteps.DependsOn(buildSteps);

// Make push step depend on the registry being provisioned
var deploymentTargetAnnotation = targetResource.GetDeploymentTargetAnnotation();
if (deploymentTargetAnnotation?.ContainerRegistry is IResource registryResource)
{
var registryProvisionSteps = context.GetSteps(registryResource, WellKnownPipelineTags.ProvisionInfrastructure);

pushSteps.DependsOn(registryProvisionSteps);
}

var provisionSteps = context.GetSteps(this, WellKnownPipelineTags.ProvisionInfrastructure);

// Make provision steps depend on push steps
provisionSteps.DependsOn(pushSteps);

// Ensure summary step runs after provision
context.GetSteps(this, "print-summary").DependsOn(provisionSteps);
}));
}

/// <summary>
/// Gets the target resource that this Azure Container App is being created for.
/// </summary>
public IResource TargetResource { get; } = targetResource;
public IResource TargetResource { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public async Task<AzureBicepResource> CreateContainerAppAsync(IResource resource
await context.ProcessResourceAsync(cancellationToken).ConfigureAwait(false);
}

var provisioningResource = new AzureContainerAppResource(resource.Name, context.BuildContainerApp, resource)
var provisioningResource = new AzureContainerAppResource(resource.Name + "-containerapp", context.BuildContainerApp, resource)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to make this change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want resources with the same name because we look them up by name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooooh yeah, we'll want to make sure to document this. I can see it being a bit of failure for people who don't know how the internal map is built.

{
ProvisioningBuildOptions = provisioningOptions.ProvisioningBuildOptions
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public async Task<AzureBicepResource> CreateAppServiceAsync(IResource resource,
await context.ProcessAsync(cancellationToken).ConfigureAwait(false);
}

var provisioningResource = new AzureAppServiceWebSiteResource(resource.Name, context.BuildWebSite, resource)
var provisioningResource = new AzureAppServiceWebSiteResource(resource.Name + "-website", context.BuildWebSite, resource)
{
ProvisioningBuildOptions = provisioningOptions.ProvisioningBuildOptions
};
Expand Down
Loading