- 
                Notifications
    You must be signed in to change notification settings 
- Fork 715
Decentralize build/push/deploy pipeline logic to resources and deployment targets #12385
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 all commits
fa4ef2d
              6963222
              1f8227d
              f6ff318
              92b4632
              3c0dbc9
              b93fb71
              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,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 | 
|---|---|---|
|  | @@ -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) | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to make this change? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| }; | ||
|  | ||
There was a problem hiding this comment.
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.