diff --git a/src/Aspire.Cli/Utils/ConsoleActivityLogger.cs b/src/Aspire.Cli/Utils/ConsoleActivityLogger.cs index 3276b1a9ecc..86a61bc3ff1 100644 --- a/src/Aspire.Cli/Utils/ConsoleActivityLogger.cs +++ b/src/Aspire.Cli/Utils/ConsoleActivityLogger.cs @@ -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"; } } diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs index b2c41c06be8..581f0630fcd 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs @@ -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; @@ -11,13 +17,135 @@ namespace Aspire.Hosting.Azure.AppContainers; /// /// Represents an Azure Container App Environment resource. /// -/// The name of the Container App Environment. -/// The callback to configure the Azure infrastructure for this resource. -public class AzureContainerAppEnvironmentResource(string name, Action 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 { + /// + /// Initializes a new instance of the class. + /// + /// The name of the Container App Environment. + /// The callback to configure the Azure infrastructure for this resource. + public AzureContainerAppEnvironmentResource(string name, Action 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(); + + 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(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(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"); + 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; } /// diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppResource.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppResource.cs index bf69a22f348..f8ba630359c 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppResource.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppResource.cs @@ -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; /// /// Represents an Azure Container App resource. /// -/// The name of the resource in the Aspire application model. -/// Callback to configure the Azure resources. -/// The target compute resource that this Azure Container App is being created for. -public class AzureContainerAppResource(string name, Action configureInfrastructure, IResource targetResource) - : AzureProvisioningResource(name, configureInfrastructure) +public class AzureContainerAppResource : AzureProvisioningResource { + /// + /// Initializes a new instance of the class. + /// + /// The name of the resource in the Aspire application model. + /// Callback to configure the Azure resources. + /// The target compute resource that this Azure Container App is being created for. + public AzureContainerAppResource(string name, Action 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(); + + 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(); + + 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); + })); + } + /// /// Gets the target resource that this Azure Container App is being created for. /// - public IResource TargetResource { get; } = targetResource; + public IResource TargetResource { get; } } diff --git a/src/Aspire.Hosting.Azure.AppContainers/ContainerAppEnvironmentContext.cs b/src/Aspire.Hosting.Azure.AppContainers/ContainerAppEnvironmentContext.cs index b40a06b1084..3387616d398 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/ContainerAppEnvironmentContext.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/ContainerAppEnvironmentContext.cs @@ -43,7 +43,7 @@ public async Task 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) { ProvisioningBuildOptions = provisioningOptions.ProvisioningBuildOptions }; diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentContext.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentContext.cs index dae7f2d65d9..b451f2f1da3 100644 --- a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentContext.cs +++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentContext.cs @@ -41,7 +41,7 @@ public async Task 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 }; diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs index 62788a0a46e..0a4309cc7d2 100644 --- a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs @@ -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.AppService; using Azure.Provisioning.Expressions; @@ -12,15 +18,134 @@ namespace Aspire.Hosting.Azure; /// /// Represents an Azure App Service Environment resource. /// -/// The name of the Azure App Service Environment. -/// The callback to configure the Azure infrastructure for this resource. -public class AzureAppServiceEnvironmentResource(string name, Action configureInfrastructure) : - AzureProvisioningResource(name, 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. +public class AzureAppServiceEnvironmentResource : + AzureProvisioningResource, 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. { + /// + /// Initializes a new instance of the class. + /// + /// The name of the Azure App Service Environment. + /// The callback to configure the Azure infrastructure for this resource. + public AzureAppServiceEnvironmentResource(string name, Action 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(); + + 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(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(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); + })); + } + + 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); @@ -34,7 +159,7 @@ public class AzureAppServiceEnvironmentResource(string name, Action /// Gets the suffix added to each web app created in this App Service Environment. /// - private BicepOutputReference WebSiteSuffix => new("webSiteSuffix", this); + internal BicepOutputReference WebSiteSuffix => new("webSiteSuffix", this); /// /// Gets or sets a value indicating whether the Aspire dashboard should be included in the container app environment. @@ -87,13 +212,13 @@ public class AzureAppServiceEnvironmentResource(string name, Action GetWebSiteSuffixBicep() => BicepFunction.GetUniqueString(BicepFunction.GetResourceGroup().Id); - ReferenceExpression IAzureContainerRegistry.ManagedIdentityId => + ReferenceExpression IAzureContainerRegistry.ManagedIdentityId => ReferenceExpression.Create($"{ContainerRegistryManagedIdentityId}"); - ReferenceExpression IContainerRegistry.Name => + ReferenceExpression IContainerRegistry.Name => ReferenceExpression.Create($"{ContainerRegistryName}"); - ReferenceExpression IContainerRegistry.Endpoint => + ReferenceExpression IContainerRegistry.Endpoint => ReferenceExpression.Create($"{ContainerRegistryUrl}"); ReferenceExpression IComputeEnvironmentResource.GetHostAddressExpression(EndpointReference endpointReference) @@ -107,15 +232,15 @@ public override ProvisionableResource AddAsExistingResource(AzureResourceInfrast { var bicepIdentifier = this.GetBicepIdentifier(); var resources = infra.GetProvisionableResources(); - + // Check if an AppServicePlan with the same identifier already exists var existingPlan = resources.OfType().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); diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebSiteResource.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebSiteResource.cs index a5274e5b6c6..2f30ac7bb51 100644 --- a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebSiteResource.cs +++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebSiteResource.cs @@ -1,21 +1,143 @@ // 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; /// /// Represents an Azure App Service Web Site resource. /// -/// The name of the resource in the Aspire application model. -/// Callback to configure the Azure resources. -/// The target resource that this Azure Web Site is being created for. -public class AzureAppServiceWebSiteResource(string name, Action configureInfrastructure, IResource targetResource) - : AzureProvisioningResource(name, configureInfrastructure) +public class AzureAppServiceWebSiteResource : AzureProvisioningResource { + /// + /// Initializes a new instance of the class. + /// + /// The name of the resource in the Aspire application model. + /// Callback to configure the Azure resources. + /// The target resource that this Azure Web Site is being created for. + public AzureAppServiceWebSiteResource(string name, Action 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(); + + 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(); + + await AzureEnvironmentResourceHelpers.PushImageToRegistryAsync( + registry, + targetResource, + ctx, + containerImageBuilder).ConfigureAwait(false); + }, + Tags = [WellKnownPipelineTags.PushContainerImage] + }; + + steps.Add(pushStep); + } + + if (!targetResource.TryGetEndpoints(out var endpoints)) + { + endpoints = []; + } + + var printResourceSummary = new PipelineStep + { + Name = $"print-{targetResource.Name}-summary", + Action = async ctx => + { + var computerEnv = (AzureAppServiceEnvironmentResource)deploymentTargetAnnotation.ComputeEnvironment!; + + var websiteSuffix = await computerEnv.WebSiteSuffix.GetValueAsync(ctx.CancellationToken).ConfigureAwait(false); + + var hostName = $"{targetResource.Name.ToLowerInvariant()}-{websiteSuffix}"; + if (hostName.Length > 60) + { + hostName = hostName.Substring(0, 60); + } + var endpoint = $"https://{hostName}.azurewebsites.net"; + ctx.ReportingStep.Log(LogLevel.Information, $"Successfully deployed **{targetResource.Name}** to [{endpoint}]({endpoint})", enableMarkdown: true); + }, + Tags = ["print-summary"] + }; + + var deployStep = new PipelineStep + { + Name = $"deploy-{targetResource.Name}", + Action = _ => Task.CompletedTask, + Tags = [WellKnownPipelineTags.DeployCompute] + }; + + deployStep.DependsOn(printResourceSummary); + + steps.Add(deployStep); + steps.Add(printResourceSummary); + + 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); + + var provisionSteps = context.GetSteps(this, WellKnownPipelineTags.ProvisionInfrastructure); + + // 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); + } + + // The app deployment should depend on the push step + provisionSteps.DependsOn(pushSteps); + + // Ensure summary step runs after provision + context.GetSteps(this, "print-summary").DependsOn(provisionSteps); + })); + } + /// /// Gets the target resource that this Azure Web Site is being created for. /// - public IResource TargetResource { get; } = targetResource; + public IResource TargetResource { get; } } diff --git a/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj b/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj index ad9b7fd4e33..5011dc7e00f 100644 --- a/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj +++ b/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj @@ -40,6 +40,8 @@ + + diff --git a/src/Aspire.Hosting.Azure/AzureBicepResource.cs b/src/Aspire.Hosting.Azure/AzureBicepResource.cs index 25506dce210..35141a07936 100644 --- a/src/Aspire.Hosting.Azure/AzureBicepResource.cs +++ b/src/Aspire.Hosting.Azure/AzureBicepResource.cs @@ -16,6 +16,7 @@ using Azure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Aspire.Hosting.Azure; @@ -44,11 +45,12 @@ public AzureBicepResource(string name, string? templateFile = null, string? temp { // Initialize the provisioning task completion source during step creation ProvisioningTaskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - + var provisionStep = new PipelineStep { Name = $"provision-{name}", - Action = async ctx => await ProvisionAzureBicepResourceAsync(ctx, this).ConfigureAwait(false) + Action = async ctx => await ProvisionAzureBicepResourceAsync(ctx, this).ConfigureAwait(false), + Tags = [WellKnownPipelineTags.ProvisionInfrastructure] }; provisionStep.RequiredBy(AzureEnvironmentResource.ProvisionInfrastructureStepName); provisionStep.DependsOn(AzureEnvironmentResource.CreateProvisioningContextStepName); @@ -255,6 +257,7 @@ private static async Task ProvisionAzureBicepResourceAsync(PipelineStepContext c // Skip if the resource is excluded from publish if (resource.IsExcludedFromPublish()) { + context.Logger.LogDebug("Resource {ResourceName} is excluded from publish. Skipping provisioning.", resource.Name); return; } @@ -262,19 +265,20 @@ private static async Task ProvisionAzureBicepResourceAsync(PipelineStepContext c if (resource.ProvisioningTaskCompletionSource != null && resource.ProvisioningTaskCompletionSource.Task.IsCompleted) { + context.Logger.LogDebug("Resource {ResourceName} is already provisioned. Skipping provisioning.", resource.Name); return; } var bicepProvisioner = context.Services.GetRequiredService(); var configuration = context.Services.GetRequiredService(); - + // Find the AzureEnvironmentResource from the application model var azureEnvironment = context.Model.Resources.OfType().FirstOrDefault(); if (azureEnvironment == null) { throw new InvalidOperationException("AzureEnvironmentResource must be present in the application model."); } - + var provisioningContext = await azureEnvironment.ProvisioningContextTask.Task.ConfigureAwait(false); var resourceTask = await context.ReportingStep diff --git a/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs b/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs index f54b86d3e70..92e619d860b 100644 --- a/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs @@ -5,21 +5,13 @@ #pragma warning disable ASPIREAZURE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable ASPIREPIPELINES003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. -#pragma warning disable ASPIREPIPELINES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Nodes; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure.Provisioning; using Aspire.Hosting.Azure.Provisioning.Internal; -using Aspire.Hosting.Dcp.Process; using Aspire.Hosting.Pipelines; -using Aspire.Hosting.Publishing; -using Azure; using Azure.Core; -using Azure.Identity; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -32,8 +24,6 @@ namespace Aspire.Hosting.Azure; [Experimental("ASPIREAZURE001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")] public sealed class AzureEnvironmentResource : Resource { - private const string DefaultImageStepTag = "default-image-tags"; - /// /// The name of the step that creates the provisioning context. /// @@ -42,7 +32,7 @@ public sealed class AzureEnvironmentResource : Resource /// /// The name of the step that provisions Azure infrastructure resources. /// - internal const string ProvisionInfrastructureStepName = "provision-azure-bicep-resources"; + public static readonly string ProvisionInfrastructureStepName = "provision-azure-bicep-resources"; /// /// Gets or sets the Azure location that the resources will be deployed to. @@ -65,8 +55,6 @@ public sealed class AzureEnvironmentResource : Resource /// internal TaskCompletionSource ProvisioningContextTask { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly List _computeResourcesToBuild = []; - /// /// Initializes a new instance of the class. /// @@ -83,16 +71,18 @@ public AzureEnvironmentResource(string name, ParameterResource location, Paramet var publishStep = new PipelineStep { Name = $"publish-{Name}", - Action = ctx => PublishAsync(ctx) + Action = ctx => PublishAsync(ctx), + RequiredBySteps = [WellKnownPipelineSteps.Publish], + DependsOnSteps = [WellKnownPipelineSteps.PublishPrereq] }; - publishStep.RequiredBy(WellKnownPipelineSteps.Publish); var validateStep = new PipelineStep { - Name = "validate-azure-cli-login", - Action = ctx => ValidateAzureCliLoginAsync(ctx) + Name = "validate-azure-login", + Action = ctx => ValidateAzureLoginAsync(ctx), + RequiredBySteps = [WellKnownPipelineSteps.Deploy], + DependsOnSteps = [WellKnownPipelineSteps.DeployPrereq] }; - validateStep.DependsOn(WellKnownPipelineSteps.ParameterPrompt); var createContextStep = new PipelineStep { @@ -102,7 +92,9 @@ public AzureEnvironmentResource(string name, ParameterResource location, Paramet var provisioningContextProvider = ctx.Services.GetRequiredService(); var provisioningContext = await provisioningContextProvider.CreateProvisioningContextAsync(ctx.CancellationToken).ConfigureAwait(false); ProvisioningContextTask.TrySetResult(provisioningContext); - } + }, + RequiredBySteps = [WellKnownPipelineSteps.Deploy], + DependsOnSteps = [WellKnownPipelineSteps.DeployPrereq] }; createContextStep.DependsOn(validateStep); @@ -110,87 +102,14 @@ public AzureEnvironmentResource(string name, ParameterResource location, Paramet { Name = ProvisionInfrastructureStepName, Action = _ => Task.CompletedTask, - Tags = [WellKnownPipelineTags.ProvisionInfrastructure] - }; - provisionStep.DependsOn(createContextStep); - - var addImageTagsStep = new PipelineStep - { - Name = DefaultImageStepTag, - Action = ctx => DefaultImageTags(ctx), - Tags = [DefaultImageStepTag], - }; - addImageTagsStep.DependsOn(WellKnownPipelineSteps.ParameterPrompt); - - var buildStep = new PipelineStep - { - Name = "build-container-images", - Action = ctx => BuildContainerImagesAsync(ctx), - Tags = [WellKnownPipelineTags.BuildCompute] - }; - buildStep.DependsOn(addImageTagsStep); - - var pushStep = new PipelineStep - { - Name = "push-container-images", - Action = ctx => PushContainerImagesAsync(ctx) + Tags = [WellKnownPipelineTags.ProvisionInfrastructure], + RequiredBySteps = [WellKnownPipelineSteps.Deploy], + DependsOnSteps = [WellKnownPipelineSteps.DeployPrereq] }; - pushStep.DependsOn(buildStep); - pushStep.DependsOn(provisionStep); - var deployStep = new PipelineStep - { - Name = "deploy-compute-resources", - Action = async ctx => - { - var provisioningContext = await ProvisioningContextTask.Task.ConfigureAwait(false); - await DeployComputeResourcesAsync(ctx, provisioningContext).ConfigureAwait(false); - }, - Tags = [WellKnownPipelineTags.DeployCompute] - }; - deployStep.DependsOn(pushStep); - deployStep.DependsOn(provisionStep); - - var printDashboardUrlStep = new PipelineStep - { - Name = "print-dashboard-url", - Action = ctx => PrintDashboardUrlAsync(ctx) - }; - printDashboardUrlStep.DependsOn(deployStep); - printDashboardUrlStep.RequiredBy(WellKnownPipelineSteps.Deploy); - - return [publishStep, validateStep, createContextStep, provisionStep, addImageTagsStep, buildStep, pushStep, deployStep, printDashboardUrlStep]; - })); - - Annotations.Add(new PipelineConfigurationAnnotation(context => - { - var defaultImageTags = context.GetSteps(this, DefaultImageStepTag).Single(); - var myBuildStep = context.GetSteps(this, WellKnownPipelineTags.BuildCompute).Single(); - - var computeResources = context.Model.Resources - .Where(r => r.RequiresImageBuild()) - .ToList(); - - foreach (var computeResource in computeResources) - { - var computeResourceBuildSteps = context.GetSteps(computeResource, WellKnownPipelineTags.BuildCompute); - if (computeResourceBuildSteps.Any()) - { - // add the appropriate dependencies to the compute resource's build steps - foreach (var computeBuildStep in computeResourceBuildSteps) - { - computeBuildStep.DependsOn(defaultImageTags); - myBuildStep.DependsOn(computeBuildStep); - } - } - else - { - // No build step exists for this compute resource, so we add it to the main build step - _computeResourcesToBuild.Add(computeResource); - } - } + provisionStep.DependsOn(createContextStep); - return Task.CompletedTask; + return [publishStep, validateStep, createContextStep, provisionStep]; })); Annotations.Add(ManifestPublishingCallbackAnnotation.Ignore); @@ -200,26 +119,6 @@ public AzureEnvironmentResource(string name, ParameterResource location, Paramet PrincipalId = principalId; } - private static Task DefaultImageTags(PipelineStepContext context) - { - var computeResources = context.Model.Resources - .Where(r => r.RequiresImageBuild()) - .ToList(); - - var deploymentTag = $"aspire-deploy-{DateTime.UtcNow:yyyyMMddHHmmss}"; - foreach (var resource in computeResources) - { - if (resource.TryGetLastAnnotation(out _)) - { - continue; - } - resource.Annotations.Add( - new DeploymentImageTagCallbackAnnotation(_ => deploymentTag)); - } - - return Task.CompletedTask; - } - private Task PublishAsync(PipelineStepContext context) { var azureProvisioningOptions = context.Services.GetRequiredService>(); @@ -234,19 +133,14 @@ private Task PublishAsync(PipelineStepContext context) return publishingContext.WriteModelAsync(context.Model, this); } - private static async Task ValidateAzureCliLoginAsync(PipelineStepContext context) + private static async Task ValidateAzureLoginAsync(PipelineStepContext context) { var tokenCredentialProvider = context.Services.GetRequiredService(); - if (tokenCredentialProvider.TokenCredential is not AzureCliCredential azureCliCredential) - { - return; - } - try { var tokenRequest = new TokenRequestContext(["https://management.azure.com/.default"]); - await azureCliCredential.GetTokenAsync(tokenRequest, context.CancellationToken) + await tokenCredentialProvider.TokenCredential.GetTokenAsync(tokenRequest, context.CancellationToken) .ConfigureAwait(false); await context.ReportingStep.CompleteAsync( @@ -263,436 +157,4 @@ await context.ReportingStep.CompleteAsync( throw; } } - - private async Task BuildContainerImagesAsync(PipelineStepContext context) - { - if (!_computeResourcesToBuild.Any()) - { - return; - } - - var containerImageBuilder = context.Services.GetRequiredService(); - - await containerImageBuilder.BuildImagesAsync( - _computeResourcesToBuild, - new ContainerBuildOptions - { - TargetPlatform = ContainerTargetPlatform.LinuxAmd64 - }, - context.CancellationToken).ConfigureAwait(false); - } - - private static async Task PushContainerImagesAsync(PipelineStepContext context) - { - var containerImageBuilder = context.Services.GetRequiredService(); - var processRunner = context.Services.GetRequiredService(); - var configuration = context.Services.GetRequiredService(); - - var computeResources = context.Model.GetComputeResources() - .Where(r => r.RequiresImageBuildAndPush()) - .ToList(); - - if (!computeResources.Any()) - { - return; - } - - var resourcesByRegistry = new Dictionary>(); - foreach (var computeResource in computeResources) - { - if (TryGetContainerRegistry(computeResource, out var registry)) - { - if (!resourcesByRegistry.TryGetValue(registry, out var resourceList)) - { - resourceList = []; - resourcesByRegistry[registry] = resourceList; - } - resourceList.Add(computeResource); - } - } - - await LoginToAllRegistriesAsync(resourcesByRegistry.Keys, context, processRunner, configuration) - .ConfigureAwait(false); - - await PushImagesToAllRegistriesAsync(resourcesByRegistry, context, containerImageBuilder) - .ConfigureAwait(false); - } - - private static async Task DeployComputeResourcesAsync(PipelineStepContext context, ProvisioningContext provisioningContext) - { - var bicepProvisioner = context.Services.GetRequiredService(); - var computeResources = context.Model.GetComputeResources().ToList(); - - if (computeResources.Count == 0) - { - return; - } - - var deploymentTasks = computeResources.Select(async computeResource => - { - var resourceTask = await context.ReportingStep - .CreateTaskAsync($"Deploying **{computeResource.Name}**", context.CancellationToken) - .ConfigureAwait(false); - - await using (resourceTask.ConfigureAwait(false)) - { - try - { - if (computeResource.GetDeploymentTargetAnnotation() is { } deploymentTarget) - { - if (deploymentTarget.DeploymentTarget is AzureBicepResource bicepResource) - { - await bicepProvisioner.GetOrCreateResourceAsync( - bicepResource, provisioningContext, context.CancellationToken) - .ConfigureAwait(false); - - var completionMessage = $"Successfully deployed **{computeResource.Name}**"; - - if (deploymentTarget.ComputeEnvironment is IAzureComputeEnvironmentResource azureComputeEnv) - { - completionMessage += TryGetComputeResourceEndpoint( - computeResource, azureComputeEnv); - } - - await resourceTask.CompleteAsync( - completionMessage, - CompletionState.Completed, - context.CancellationToken).ConfigureAwait(false); - } - else - { - await resourceTask.CompleteAsync( - $"Skipped **{computeResource.Name}** - no Bicep deployment target", - CompletionState.CompletedWithWarning, - context.CancellationToken).ConfigureAwait(false); - } - } - else - { - await resourceTask.CompleteAsync( - $"Skipped **{computeResource.Name}** - no deployment target annotation", - CompletionState.Completed, - context.CancellationToken).ConfigureAwait(false); - } - } - catch (Exception ex) - { - var errorMessage = ex switch - { - RequestFailedException requestEx => - $"Deployment failed: {ExtractDetailedErrorMessage(requestEx)}", - _ => $"Deployment failed: {ex.Message}" - }; - await resourceTask.CompleteAsync( - $"Failed to deploy {computeResource.Name}: {errorMessage}", - CompletionState.CompletedWithError, - context.CancellationToken).ConfigureAwait(false); - throw; - } - } - }); - - await Task.WhenAll(deploymentTasks).ConfigureAwait(false); - } - - private static bool TryGetContainerRegistry(IResource computeResource, [NotNullWhen(true)] out IContainerRegistry? containerRegistry) - { - if (computeResource.GetDeploymentTargetAnnotation() is { } deploymentTarget && - deploymentTarget.ContainerRegistry is { } registry) - { - containerRegistry = registry; - return true; - } - - containerRegistry = null; - return false; - } - - private static async Task LoginToAllRegistriesAsync(IEnumerable registries, PipelineStepContext context, IProcessRunner processRunner, IConfiguration configuration) - { - var registryList = registries.ToList(); - if (!registryList.Any()) - { - return; - } - - try - { - var loginTasks = registryList.Select(async registry => - { - var registryName = await registry.Name.GetValueAsync(context.CancellationToken).ConfigureAwait(false) ?? - throw new InvalidOperationException("Failed to retrieve container registry information."); - - var loginTask = await context.ReportingStep.CreateTaskAsync($"Logging in to **{registryName}**", context.CancellationToken).ConfigureAwait(false); - await using (loginTask.ConfigureAwait(false)) - { - await AuthenticateToAcrHelper(loginTask, registryName, context.CancellationToken, processRunner, configuration).ConfigureAwait(false); - } - }); - - await Task.WhenAll(loginTasks).ConfigureAwait(false); - } - catch (Exception) - { - throw; - } - } - - private static async Task AuthenticateToAcrHelper(IReportingTask loginTask, string registryName, CancellationToken cancellationToken, IProcessRunner processRunner, IConfiguration configuration) - { - var command = BicepCliCompiler.FindFullPathFromPath("az") ?? throw new InvalidOperationException("Failed to find 'az' command"); - try - { - var loginSpec = new ProcessSpec(command) - { - Arguments = $"acr login --name {registryName}", - ThrowOnNonZeroReturnCode = false - }; - - // Set DOCKER_COMMAND environment variable if using podman - var containerRuntime = GetContainerRuntime(configuration); - if (string.Equals(containerRuntime, "podman", StringComparison.OrdinalIgnoreCase)) - { - loginSpec.EnvironmentVariables["DOCKER_COMMAND"] = "podman"; - } - - var (pendingResult, processDisposable) = processRunner.Run(loginSpec); - await using (processDisposable.ConfigureAwait(false)) - { - var result = await pendingResult.WaitAsync(cancellationToken).ConfigureAwait(false); - - if (result.ExitCode != 0) - { - await loginTask.FailAsync($"Login to ACR **{registryName}** failed with exit code {result.ExitCode}", cancellationToken: cancellationToken).ConfigureAwait(false); - } - else - { - await loginTask.CompleteAsync($"Successfully logged in to **{registryName}**", CompletionState.Completed, cancellationToken).ConfigureAwait(false); - } - } - } - catch (Exception) - { - throw; - } - } - - private static string? GetContainerRuntime(IConfiguration configuration) - { - // Fall back to known config names (primary and legacy) - return configuration["ASPIRE_CONTAINER_RUNTIME"] ?? configuration["DOTNET_ASPIRE_CONTAINER_RUNTIME"]; - } - - private static async Task PushImagesToAllRegistriesAsync(Dictionary> resourcesByRegistry, PipelineStepContext context, IResourceContainerImageBuilder containerImageBuilder) - { - var allPushTasks = new List(); - - foreach (var (registry, resources) in resourcesByRegistry) - { - var registryName = await registry.Name.GetValueAsync(context.CancellationToken).ConfigureAwait(false) ?? - throw new InvalidOperationException("Failed to retrieve container registry information."); - - var resourcePushTasks = resources - .Where(r => r.RequiresImageBuildAndPush()) - .Select(async resource => - { - if (!resource.TryGetContainerImageName(out var localImageName)) - { - localImageName = resource.Name.ToLowerInvariant(); - } - - IValueProvider cir = new ContainerImageReference(resource); - var targetTag = await cir.GetValueAsync(context.CancellationToken).ConfigureAwait(false); - - var pushTask = await context.ReportingStep.CreateTaskAsync($"Pushing **{resource.Name}** to **{registryName}**", context.CancellationToken).ConfigureAwait(false); - await using (pushTask.ConfigureAwait(false)) - { - try - { - if (targetTag == null) - { - throw new InvalidOperationException($"Failed to get target tag for {resource.Name}"); - } - await TagAndPushImage(localImageName, targetTag, context.CancellationToken, containerImageBuilder).ConfigureAwait(false); - await pushTask.CompleteAsync($"Successfully pushed **{resource.Name}** to `{targetTag}`", CompletionState.Completed, context.CancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - await pushTask.CompleteAsync($"Failed to push **{resource.Name}**: {ex.Message}", CompletionState.CompletedWithError, context.CancellationToken).ConfigureAwait(false); - throw; - } - } - }); - - allPushTasks.AddRange(resourcePushTasks); - } - - await Task.WhenAll(allPushTasks).ConfigureAwait(false); - } - - private static async Task TagAndPushImage(string localTag, string targetTag, CancellationToken cancellationToken, IResourceContainerImageBuilder containerImageBuilder) - { - await containerImageBuilder.TagImageAsync(localTag, targetTag, cancellationToken).ConfigureAwait(false); - await containerImageBuilder.PushImageAsync(targetTag, cancellationToken).ConfigureAwait(false); - } - - private static string TryGetComputeResourceEndpoint(IResource computeResource, IAzureComputeEnvironmentResource azureComputeEnv) - { - // Check if the compute environment has the default domain output (for Azure Container Apps) - // We could add a reference to AzureContainerAppEnvironmentResource here so we can resolve - // the `ContainerAppDomain` property but we use a string-based lookup here to avoid adding - // explicit references to a compute environment type - if (azureComputeEnv is AzureProvisioningResource provisioningResource && - provisioningResource.Outputs.TryGetValue("AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN", out var domainValue)) - { - // Only produce endpoints for resources that have external endpoints - if (computeResource.TryGetEndpoints(out var endpoints) && endpoints.Any(e => e.IsExternal)) - { - var endpoint = $"https://{computeResource.Name.ToLowerInvariant()}.{domainValue}"; - return $" to [{endpoint}]({endpoint})"; - } - } - - // Check if the compute environment is an App Service Environment - // if yes, we return the web app endpoint using the webSiteSuffix output (unique string derived from resource group name) - if (azureComputeEnv is AzureProvisioningResource appsvcProvisioningResource && - appsvcProvisioningResource.Outputs.TryGetValue("webSiteSuffix", out var webSiteSuffix)) - { - var hostName = $"{computeResource.Name.ToLowerInvariant()}-{webSiteSuffix}"; - if (hostName.Length > 60) - { - hostName = hostName.Substring(0, 60); - } - var endpoint = $"https://{hostName}.azurewebsites.net"; - return $" to [{endpoint}]({endpoint})"; - } - - return string.Empty; - } - - /// - /// Extracts detailed error information from Azure RequestFailedException responses. - /// Parses the following JSON error structures: - /// 1. Standard Azure error format: { "error": { "code": "...", "message": "...", "details": [...] } } - /// 2. Deployment-specific error format: { "properties": { "error": { "code": "...", "message": "..." } } } - /// 3. Nested error details with recursive parsing for deeply nested error hierarchies - /// - /// The Azure RequestFailedException containing the error response - /// The most specific error message found, or the original exception message if parsing fails - private static string ExtractDetailedErrorMessage(RequestFailedException requestEx) - { - try - { - var response = requestEx.GetRawResponse(); - if (response?.Content is not null) - { - var responseContent = response.Content.ToString(); - if (!string.IsNullOrEmpty(responseContent)) - { - if (JsonNode.Parse(responseContent) is JsonObject responseObj) - { - if (responseObj["error"] is JsonObject errorObj) - { - var code = errorObj["code"]?.ToString(); - var message = errorObj["message"]?.ToString(); - - if (!string.IsNullOrEmpty(code) && !string.IsNullOrEmpty(message)) - { - if (errorObj["details"] is JsonArray detailsArray && detailsArray.Count > 0) - { - var deepestErrorMessage = ExtractDeepestErrorMessage(detailsArray); - if (!string.IsNullOrEmpty(deepestErrorMessage)) - { - return deepestErrorMessage; - } - } - - return $"{code}: {message}"; - } - } - - if (responseObj["properties"]?["error"] is JsonObject deploymentErrorObj) - { - var code = deploymentErrorObj["code"]?.ToString(); - var message = deploymentErrorObj["message"]?.ToString(); - - if (!string.IsNullOrEmpty(code) && !string.IsNullOrEmpty(message)) - { - return $"{code}: {message}"; - } - } - } - } - } - } - catch (JsonException) { } - - return requestEx.Message; - } - - private static string ExtractDeepestErrorMessage(JsonArray detailsArray) - { - foreach (var detail in detailsArray) - { - if (detail is JsonObject detailObj) - { - var detailCode = detailObj["code"]?.ToString(); - var detailMessage = detailObj["message"]?.ToString(); - - if (detailObj["details"] is JsonArray nestedDetailsArray && nestedDetailsArray.Count > 0) - { - var deeperMessage = ExtractDeepestErrorMessage(nestedDetailsArray); - if (!string.IsNullOrEmpty(deeperMessage)) - { - return deeperMessage; - } - } - - if (!string.IsNullOrEmpty(detailCode) && !string.IsNullOrEmpty(detailMessage)) - { - return $"{detailCode}: {detailMessage}"; - } - } - } - - return string.Empty; - } - - private static async Task PrintDashboardUrlAsync(PipelineStepContext context) - { - var dashboardUrl = TryGetDashboardUrl(context.Model); - - if (dashboardUrl != null) - { - await context.ReportingStep.CompleteAsync( - $"Dashboard available at [dashboard URL]({dashboardUrl})", - CompletionState.Completed, - context.CancellationToken).ConfigureAwait(false); - } - } - - private static string? TryGetDashboardUrl(DistributedApplicationModel model) - { - foreach (var resource in model.Resources) - { - if (resource is IAzureComputeEnvironmentResource && - resource is AzureBicepResource environmentBicepResource) - { - // If the resource is a compute environment, we can use its properties - // to construct the dashboard URL. - if (environmentBicepResource.Outputs.TryGetValue($"AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN", out var domainValue)) - { - return $"https://aspire-dashboard.ext.{domainValue}"; - } - // If the resource is a compute environment (app service), we can use its properties - // to get the dashboard URL. - if (environmentBicepResource.Outputs.TryGetValue($"AZURE_APP_SERVICE_DASHBOARD_URI", out var dashboardUri)) - { - return (string?)dashboardUri; - } - } - } - - return null; - } } diff --git a/src/Aspire.Hosting.Azure/AzureEnvironmentResourceHelpers.cs b/src/Aspire.Hosting.Azure/AzureEnvironmentResourceHelpers.cs new file mode 100644 index 00000000000..201cc2d3824 --- /dev/null +++ b/src/Aspire.Hosting.Azure/AzureEnvironmentResourceHelpers.cs @@ -0,0 +1,122 @@ +// 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 ASPIREPUBLISHERS001 +#pragma warning disable ASPIREPIPELINES001 +#pragma warning disable ASPIREPIPELINES003 + +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Azure.Provisioning.Internal; +using Aspire.Hosting.Dcp.Process; +using Aspire.Hosting.Pipelines; +using Aspire.Hosting.Publishing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Aspire.Hosting.Azure; + +/// +/// Helper methods for Azure environment resources to handle container image operations. +/// +internal static class AzureEnvironmentResourceHelpers +{ + public static async Task LoginToRegistryAsync(IContainerRegistry registry, PipelineStepContext context) + { + var processRunner = context.Services.GetRequiredService(); + var configuration = context.Services.GetRequiredService(); + + var registryName = await registry.Name.GetValueAsync(context.CancellationToken).ConfigureAwait(false) ?? + throw new InvalidOperationException("Failed to retrieve container registry information."); + + var loginTask = await context.ReportingStep.CreateTaskAsync($"Logging in to **{registryName}**", context.CancellationToken).ConfigureAwait(false); + await using (loginTask.ConfigureAwait(false)) + { + await AuthenticateToAcrHelper(loginTask, registryName, context.CancellationToken, processRunner, configuration).ConfigureAwait(false); + } + } + + private static async Task AuthenticateToAcrHelper(IReportingTask loginTask, string registryName, CancellationToken cancellationToken, IProcessRunner processRunner, IConfiguration configuration) + { + var command = BicepCliCompiler.FindFullPathFromPath("az") ?? throw new InvalidOperationException("Failed to find 'az' command"); + try + { + var loginSpec = new ProcessSpec(command) + { + Arguments = $"acr login --name {registryName}", + ThrowOnNonZeroReturnCode = false + }; + + // Set DOCKER_COMMAND environment variable if using podman + var containerRuntime = GetContainerRuntime(configuration); + if (string.Equals(containerRuntime, "podman", StringComparison.OrdinalIgnoreCase)) + { + loginSpec.EnvironmentVariables["DOCKER_COMMAND"] = "podman"; + } + + var (pendingResult, processDisposable) = processRunner.Run(loginSpec); + await using (processDisposable.ConfigureAwait(false)) + { + var result = await pendingResult.WaitAsync(cancellationToken).ConfigureAwait(false); + + if (result.ExitCode != 0) + { + await loginTask.FailAsync($"Login to ACR **{registryName}** failed with exit code {result.ExitCode}", cancellationToken: cancellationToken).ConfigureAwait(false); + } + else + { + await loginTask.CompleteAsync($"Successfully logged in to **{registryName}**", CompletionState.Completed, cancellationToken).ConfigureAwait(false); + } + } + } + catch (Exception) + { + throw; + } + } + + private static string? GetContainerRuntime(IConfiguration configuration) + { + // Fall back to known config names (primary and legacy) + return configuration["ASPIRE_CONTAINER_RUNTIME"] ?? configuration["DOTNET_ASPIRE_CONTAINER_RUNTIME"]; + } + + public static async Task PushImageToRegistryAsync(IContainerRegistry registry, IResource resource, PipelineStepContext context, IResourceContainerImageBuilder containerImageBuilder) + { + var registryName = await registry.Name.GetValueAsync(context.CancellationToken).ConfigureAwait(false) ?? + throw new InvalidOperationException("Failed to retrieve container registry information."); + + if (!resource.TryGetContainerImageName(out var localImageName)) + { + localImageName = resource.Name.ToLowerInvariant(); + } + + IValueProvider cir = new ContainerImageReference(resource); + var targetTag = await cir.GetValueAsync(context.CancellationToken).ConfigureAwait(false); + + var pushTask = await context.ReportingStep.CreateTaskAsync($"Pushing **{resource.Name}** to **{registryName}**", context.CancellationToken).ConfigureAwait(false); + await using (pushTask.ConfigureAwait(false)) + { + try + { + if (targetTag == null) + { + throw new InvalidOperationException($"Failed to get target tag for {resource.Name}"); + } + await TagAndPushImage(localImageName, targetTag, context.CancellationToken, containerImageBuilder).ConfigureAwait(false); + await pushTask.CompleteAsync($"Successfully pushed **{resource.Name}** to `{targetTag}`", CompletionState.Completed, context.CancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + await pushTask.CompleteAsync($"Failed to push **{resource.Name}**: {ex.Message}", CompletionState.CompletedWithError, context.CancellationToken).ConfigureAwait(false); + throw; + } + } + } + + private static async Task TagAndPushImage(string localTag, string targetTag, CancellationToken cancellationToken, IResourceContainerImageBuilder containerImageBuilder) + { + await containerImageBuilder.TagImageAsync(localTag, targetTag, cancellationToken).ConfigureAwait(false); + await containerImageBuilder.PushImageAsync(targetTag, cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/Aspire.Hosting.Azure/Provisioning/BicepUtilities.cs b/src/Aspire.Hosting.Azure/Provisioning/BicepUtilities.cs index 89f72f093ea..0e263b7f1a3 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/BicepUtilities.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/BicepUtilities.cs @@ -14,30 +14,14 @@ namespace Aspire.Hosting.Azure.Provisioning; /// internal static class BicepUtilities { - // Known values since they will be filled in by the provisioner - private static readonly string[] s_knownParameterNames = - [ - AzureBicepResource.KnownParameters.PrincipalName, - AzureBicepResource.KnownParameters.PrincipalId, - AzureBicepResource.KnownParameters.PrincipalType, - AzureBicepResource.KnownParameters.UserPrincipalId, - AzureBicepResource.KnownParameters.Location, - ]; - /// /// Converts the parameters to a JSON object compatible with the ARM template. /// - public static async Task SetParametersAsync(JsonObject parameters, AzureBicepResource resource, bool skipDynamicValues = false, CancellationToken cancellationToken = default) + public static async Task SetParametersAsync(JsonObject parameters, AzureBicepResource resource, CancellationToken cancellationToken = default) { // Convert the parameters to a JSON object foreach (var parameter in resource.Parameters) { - if (skipDynamicValues && - (s_knownParameterNames.Contains(parameter.Key) || IsParameterWithGeneratedValue(parameter.Value))) - { - continue; - } - // Execute parameter values which are deferred. var parameterValue = parameter.Value is Func f ? f() : parameter.Value; @@ -121,10 +105,11 @@ public static string GetChecksum(AzureBicepResource resource, JsonObject paramet return null; } - // Now overwrite with live object values skipping known and generated values. - // This is important because the provisioner will fill in the known values and - // generated values would change every time, so they can't be part of the checksum. - await SetParametersAsync(parameters, resource, skipDynamicValues: true, cancellationToken: cancellationToken).ConfigureAwait(false); + // Force evaluation of the Bicep template to ensure parameters are expanded + _ = resource.GetBicepTemplateString(); + + // Now overwrite with live object values skipping known values. + await SetParametersAsync(parameters, resource, cancellationToken: cancellationToken).ConfigureAwait(false); if (scope is not null) { await SetScopeAsync(scope, resource, cancellationToken).ConfigureAwait(false); @@ -145,9 +130,4 @@ public static string GetChecksum(AzureBicepResource resource, JsonObject paramet (resource.TryGetLastAnnotation(out var existingResource) ? existingResource.ResourceGroup : null); - - private static bool IsParameterWithGeneratedValue(object? value) - { - return value is ParameterResource { Default: not null }; - } } \ No newline at end of file diff --git a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs index 17e66caba3b..6085376ebfb 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs @@ -22,7 +22,8 @@ internal sealed class BicepProvisioner( IBicepCompiler bicepCompiler, ISecretClientProvider secretClientProvider, IDeploymentStateManager deploymentStateManager, - DistributedApplicationExecutionContext executionContext) : IBicepProvisioner + DistributedApplicationExecutionContext executionContext, + ILogger logger) : IBicepProvisioner { /// public async Task ConfigureResourceAsync(IConfiguration configuration, AzureBicepResource resource, CancellationToken cancellationToken) @@ -39,9 +40,12 @@ public async Task ConfigureResourceAsync(IConfiguration configuration, Azu if (currentCheckSum != configCheckSum) { + logger.LogDebug("Checksum mismatch for resource {ResourceName}. Expected: {ExpectedChecksum}, Actual: {ActualChecksum}", resource.Name, currentCheckSum, configCheckSum); return false; } + logger.LogDebug("Configuring resource {ResourceName} from existing deployment state.", resource.Name); + if (section["Outputs"] is string outputJson) { JsonNode? outputObj = null; @@ -149,6 +153,7 @@ await notificationService.PublishUpdateAsync(resource, state => var armTemplateContents = await bicepCompiler.CompileBicepToArmAsync(path, cancellationToken).ConfigureAwait(false); + logger.LogDebug("Setting parameters and scope for resource {ResourceName}", resource.Name); // Convert the parameters to a JSON object var parameters = new JsonObject(); await BicepUtilities.SetParametersAsync(parameters, resource, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -168,6 +173,7 @@ await notificationService.PublishUpdateAsync(resource, state => .ConfigureAwait(false); resourceLogger.LogInformation("Deploying {Name} to {ResourceGroup}", resource.Name, resourceGroup.Name); + logger.LogDebug("Starting deployment of resource {ResourceName} to resource group {ResourceGroupName}", resource.Name, resourceGroup.Name); // Resources with a Subscription scope should use a subscription-level deployment. var deployments = resource.Scope?.Subscription != null @@ -202,6 +208,7 @@ await notificationService.PublishUpdateAsync(resource, state => sw.Stop(); resourceLogger.LogInformation("Deployment of {Name} to {ResourceGroup} took {Elapsed}", resource.Name, resourceGroup.Name, sw.Elapsed); + logger.LogDebug("Deployment of resource {ResourceName} to resource group {ResourceGroupName} completed in {Elapsed}", resource.Name, resourceGroup.Name, sw.Elapsed); var deployment = operation.Value; diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index 1e77e891bad..c961d657bc7 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -6,7 +6,6 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.ApplicationModel.Docker; using Aspire.Hosting.Pipelines; -using Aspire.Hosting.Publishing; using Aspire.Hosting.Python; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -542,48 +541,22 @@ private static IResourceBuilder AddPythonAppCore( }); }); - resourceBuilder.WithPipelineStepFactory(factoryContext => + resourceBuilder.WithPipelineConfiguration(context => { - List steps = []; - var buildStep = CreateBuildImageBuildStep($"{factoryContext.Resource.Name}-build-compute", factoryContext.Resource); - steps.Add(buildStep); - - // ensure any static file references' images are built first - if (factoryContext.Resource.TryGetAnnotationsOfType(out var containerFilesAnnotations)) + if (resourceBuilder.Resource.TryGetAnnotationsOfType(out var containerFilesAnnotations)) { + var buildSteps = context.GetSteps(resourceBuilder.Resource, WellKnownPipelineTags.BuildCompute); + foreach (var containerFile in containerFilesAnnotations) { - var source = containerFile.Source; - var staticFileBuildStep = CreateBuildImageBuildStep($"{factoryContext.Resource.Name}-{source.Name}-build-compute", source); - buildStep.DependsOn(staticFileBuildStep); - steps.Add(staticFileBuildStep); + buildSteps.DependsOn(context.GetSteps(containerFile.Source, WellKnownPipelineTags.BuildCompute)); } } - - return steps; }); return resourceBuilder; } - private static PipelineStep CreateBuildImageBuildStep(string stepName, IResource resource) => - new() - { - Name = stepName, - Action = async ctx => - { - var containerImageBuilder = ctx.Services.GetRequiredService(); - await containerImageBuilder.BuildImageAsync( - resource, - new ContainerBuildOptions - { - TargetPlatform = ContainerTargetPlatform.LinuxAmd64 - }, - ctx.CancellationToken).ConfigureAwait(false); - }, - Tags = [WellKnownPipelineTags.BuildCompute] - }; - private static DockerfileStage AddContainerFiles(this DockerfileStage stage, IResource resource, string rootDestinationPath) { if (resource.TryGetAnnotationsOfType(out var containerFilesDestinationAnnotations)) diff --git a/src/Aspire.Hosting/ApplicationModel/DistributedApplicationModelExtensions.cs b/src/Aspire.Hosting/ApplicationModel/DistributedApplicationModelExtensions.cs index fa57a0e11cd..f994ede3cf9 100644 --- a/src/Aspire.Hosting/ApplicationModel/DistributedApplicationModelExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/DistributedApplicationModelExtensions.cs @@ -36,4 +36,26 @@ public static IEnumerable GetComputeResources(this DistributedApplica yield return r; } } + + /// + /// Returns the build resources from the . + /// Build resources are those that are either build-only containers or project resources, and are not marked to be ignored by the manifest publishing callback annotation. + /// + /// The distributed application model to extract build resources from. + /// An enumerable of build in the model. + public static IEnumerable GetBuildResources(this DistributedApplicationModel model) + { + foreach (var r in model.Resources) + { + if (r.IsExcludedFromPublish()) + { + continue; + } + + if (r.RequiresImageBuild()) + { + yield return r; + } + } + } } diff --git a/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs b/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs index 6d30912a497..9c776e996b6 100644 --- a/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs +++ b/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs @@ -1,19 +1,60 @@ #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. #pragma warning disable ASPIREPROBES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPIPELINES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPUBLISHERS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPIPELINES003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. // 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.Pipelines; +using Aspire.Hosting.Publishing; +using Microsoft.Extensions.DependencyInjection; + namespace Aspire.Hosting.ApplicationModel; /// /// A resource that represents a specified .NET project. /// -/// The name of the resource. -public class ProjectResource(string name) - : Resource(name), IResourceWithEnvironment, IResourceWithArgs, IResourceWithServiceDiscovery, IResourceWithWaitSupport, IResourceWithProbes, +public class ProjectResource : Resource, IResourceWithEnvironment, IResourceWithArgs, IResourceWithServiceDiscovery, IResourceWithWaitSupport, IResourceWithProbes, IComputeResource { + /// + /// Initializes a new instance of the class. + /// + /// The name of the resource. + public ProjectResource(string name) : base(name) + { + // Add pipeline step annotation to create a build step for this project + Annotations.Add(new PipelineStepAnnotation((factoryContext) => + { + if (factoryContext.Resource.IsExcludedFromPublish()) + { + return []; + } + + var buildStep = new PipelineStep + { + Name = $"build-{name}", + Action = async ctx => + { + var containerImageBuilder = ctx.Services.GetRequiredService(); + await containerImageBuilder.BuildImageAsync( + this, + new ContainerBuildOptions + { + TargetPlatform = ContainerTargetPlatform.LinuxAmd64 + }, + ctx.CancellationToken).ConfigureAwait(false); + }, + Tags = [WellKnownPipelineTags.BuildCompute], + RequiredBySteps = [WellKnownPipelineSteps.Build], + DependsOnSteps = [WellKnownPipelineSteps.BuildPrereq] + }; + + return [buildStep]; + })); + } // Keep track of the config host for each Kestrel endpoint annotation internal Dictionary KestrelEndpointAnnotationHosts { get; } = new(); diff --git a/src/Aspire.Hosting/Aspire.Hosting.csproj b/src/Aspire.Hosting/Aspire.Hosting.csproj index d0828fa7ef1..8a6075c40ec 100644 --- a/src/Aspire.Hosting/Aspire.Hosting.csproj +++ b/src/Aspire.Hosting/Aspire.Hosting.csproj @@ -24,6 +24,7 @@ + diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index 50cd8600d68..5e15ed12cb7 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -1,11 +1,19 @@ // 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 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPIPELINES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPUBLISHERS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPIPELINES003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + using System.Diagnostics.CodeAnalysis; using System.Text; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.ApplicationModel.Docker; +using Aspire.Hosting.Pipelines; +using Aspire.Hosting.Publishing; using Aspire.Hosting.Utils; +using Microsoft.Extensions.DependencyInjection; namespace Aspire.Hosting; @@ -14,6 +22,45 @@ namespace Aspire.Hosting; /// public static class ContainerResourceBuilderExtensions { + /// + /// Ensures that a container resource has a PipelineStepAnnotation for building if it has a DockerfileBuildAnnotation. + /// + /// The type of container resource. + /// The resource builder. + internal static IResourceBuilder EnsureBuildPipelineStepAnnotation(this IResourceBuilder builder) where T : ContainerResource + { + // Use replace semantics to ensure we only have one PipelineStepAnnotation for building + return builder.WithAnnotation(new PipelineStepAnnotation((factoryContext) => + { + if (!builder.Resource.RequiresImageBuild() || builder.Resource.IsExcludedFromPublish()) + { + return []; + } + + var buildStep = new PipelineStep + { + Name = $"build-{builder.Resource.Name}", + Action = async ctx => + { + var containerImageBuilder = ctx.Services.GetRequiredService(); + + await containerImageBuilder.BuildImageAsync( + builder.Resource, + new ContainerBuildOptions + { + TargetPlatform = ContainerTargetPlatform.LinuxAmd64 + }, + ctx.CancellationToken).ConfigureAwait(false); + }, + Tags = [WellKnownPipelineTags.BuildCompute], + RequiredBySteps = [WellKnownPipelineSteps.Build], + DependsOnSteps = [WellKnownPipelineSteps.BuildPrereq] + }; + + return [buildStep]; + }), ResourceAnnotationMutationBehavior.Replace); + } + /// /// Adds a container resource to the application. Uses the "latest" tag. /// @@ -514,13 +561,15 @@ public static IResourceBuilder WithDockerfile(this IResourceBuilder bui { annotation.ImageName = imageName; annotation.ImageTag = imageTag; - return builder.WithAnnotation(annotation, ResourceAnnotationMutationBehavior.Replace); + return builder.WithAnnotation(annotation, ResourceAnnotationMutationBehavior.Replace) + .EnsureBuildPipelineStepAnnotation(); } return builder.WithAnnotation(annotation, ResourceAnnotationMutationBehavior.Replace) .WithImageRegistry(registry: null) .WithImage(imageName) - .WithImageTag(imageTag); + .WithImageTag(imageTag) + .EnsureBuildPipelineStepAnnotation(); } /// @@ -632,13 +681,15 @@ public static IResourceBuilder WithDockerfileFactory(this IResourceBuilder { annotation.ImageName = imageName; annotation.ImageTag = imageTag; - return builder.WithAnnotation(annotation, ResourceAnnotationMutationBehavior.Replace); + return builder.WithAnnotation(annotation, ResourceAnnotationMutationBehavior.Replace) + .EnsureBuildPipelineStepAnnotation(); } return builder.WithAnnotation(annotation, ResourceAnnotationMutationBehavior.Replace) .WithImageRegistry(registry: null) .WithImage(imageName) - .WithImageTag(imageTag); + .WithImageTag(imageTag) + .EnsureBuildPipelineStepAnnotation(); } /// diff --git a/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs b/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs index 32aba906574..e7ff78f2fd4 100644 --- a/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs +++ b/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs @@ -3,14 +3,20 @@ #pragma warning disable ASPIREPIPELINES001 #pragma warning disable ASPIREINTERACTION001 +#pragma warning disable ASPIRECOMPUTE001 +#pragma warning disable ASPIREPIPELINES002 using System.Diagnostics; using System.Globalization; using System.Runtime.ExceptionServices; using System.Text; using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Publishing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; namespace Aspire.Hosting.Pipelines; @@ -20,27 +26,139 @@ internal sealed class DistributedApplicationPipeline : IDistributedApplicationPi private readonly List _steps = []; private readonly List> _configurationCallbacks = []; + // Store resolved pipeline data for diagnostics + private List? _lastResolvedSteps; + public DistributedApplicationPipeline() { + // Dependency order + // {verb} -> {user steps} -> {verb}-prereq + // Initialize with a "deploy" step that has a no-op callback _steps.Add(new PipelineStep { Name = WellKnownPipelineSteps.Deploy, - Action = _ => Task.CompletedTask + Action = _ => Task.CompletedTask, }); + + _steps.Add(new PipelineStep + { + Name = WellKnownPipelineSteps.DeployPrereq, + Action = async context => + { + // REVIEW: Break this up into smaller steps + + var hostEnvironment = context.Services.GetRequiredService(); + var options = context.Services.GetRequiredService>(); + + context.Logger.LogInformation("Initializing deployment for environment '{EnvironmentName}'", hostEnvironment.EnvironmentName); + var deploymentStateManager = context.Services.GetRequiredService(); + + if (deploymentStateManager.StateFilePath is string stateFilePath && File.Exists(stateFilePath)) + { + // Check if --clear-cache flag is set and prompt user before deleting deployment state + if (!options.Value.ClearCache) + { + // Add a task to show the deployment state file path if available + context.Logger.LogInformation("Deployment state will be loaded from: {StateFilePath}", stateFilePath); + } + else + { + var interactionService = context.Services.GetRequiredService(); + if (interactionService.IsAvailable) + { + var result = await interactionService.PromptNotificationAsync( + "Clear Deployment State", + $"The deployment state for the '{hostEnvironment.EnvironmentName}' environment will be deleted. All Azure resources will be re-provisioned. Do you want to continue?", + new NotificationInteractionOptions + { + Intent = MessageIntent.Confirmation, + ShowSecondaryButton = true, + ShowDismiss = false, + PrimaryButtonText = "Yes", + SecondaryButtonText = "No" + }, + context.CancellationToken).ConfigureAwait(false); + + if (result.Canceled || !result.Data) + { + // User declined or canceled - exit the deployment + context.Logger.LogInformation("User declined to clear deployment state. Canceling pipeline execution."); + + throw new OperationCanceledException("Pipeline execution canceled by user."); + } + + // User confirmed - delete the deployment state file + context.Logger.LogInformation("Deleting deployment state file at {Path} due to --clear-cache flag", stateFilePath); + File.Delete(stateFilePath); + } + } + } + + // Parameter processing - ensure all parameters are initialized and resolved + + var parameterProcessor = context.Services.GetRequiredService(); + await parameterProcessor.InitializeParametersAsync(context.Model, waitForResolution: true, context.CancellationToken).ConfigureAwait(false); + + var computeResources = context.Model.Resources + .Where(r => r.RequiresImageBuild()) + .ToList(); + + var uniqueDeployTag = $"aspire-deploy-{DateTime.UtcNow:yyyyMMddHHmmss}"; + + context.Logger.LogInformation("Setting default deploy tag '{Tag}' for compute resource(s).", uniqueDeployTag); + + // Resources that were built, will get this tag unless they have a custom DeploymentImageTagCallbackAnnotation + foreach (var resource in context.Model.GetBuildResources()) + { + if (resource.TryGetLastAnnotation(out _)) + { + continue; + } + + resource.Annotations.Add(new DeploymentImageTagCallbackAnnotation(_ => uniqueDeployTag)); + } + } + }); + + // Add a default "build" step + _steps.Add(new PipelineStep + { + Name = WellKnownPipelineSteps.Build, + Action = _ => Task.CompletedTask, + }); + + _steps.Add(new PipelineStep + { + Name = WellKnownPipelineSteps.BuildPrereq, + Action = context => Task.CompletedTask + }); + // Add a default "Publish" meta-step that all publish steps should be required by _steps.Add(new PipelineStep { Name = WellKnownPipelineSteps.Publish, Action = _ => Task.CompletedTask }); + _steps.Add(new PipelineStep { - Name = WellKnownPipelineSteps.ParameterPrompt, + Name = WellKnownPipelineSteps.PublishPrereq, + Action = _ => Task.CompletedTask, + }); + + // Add diagnostic step for dependency graph analysis + _steps.Add(new PipelineStep + { + Name = WellKnownPipelineSteps.Diagnostics, Action = async context => { - var parameterProcessor = context.Services.GetRequiredService(); - await parameterProcessor.InitializeParametersAsync(context.Model, waitForResolution: true, context.CancellationToken).ConfigureAwait(false); + // Use the resolved pipeline data from the last ExecuteAsync call + var stepsToAnalyze = _lastResolvedSteps ?? throw new InvalidOperationException( + "No resolved pipeline data available for diagnostics. Ensure that the pipeline has been executed before running diagnostics."); + + // Generate the diagnostic output using the resolved data + DumpDependencyGraphDiagnostics(stepsToAnalyze, context); } }); } @@ -138,12 +256,12 @@ public void AddPipelineConfiguration(Func ca public async Task ExecuteAsync(PipelineContext context) { - var (annotationSteps, stepToResourceMap) = await CollectStepsFromAnnotationsAsync(context).ConfigureAwait(false); + var annotationSteps = await CollectStepsFromAnnotationsAsync(context).ConfigureAwait(false); var allSteps = _steps.Concat(annotationSteps).ToList(); // Execute configuration callbacks even if there are no steps // This allows callbacks to run validation or other logic - await ExecuteConfigurationCallbacksAsync(context, allSteps, stepToResourceMap).ConfigureAwait(false); + await ExecuteConfigurationCallbacksAsync(context, allSteps).ConfigureAwait(false); if (allSteps.Count == 0) { @@ -156,6 +274,9 @@ public async Task ExecuteAsync(PipelineContext context) var allStepsByName = allSteps.ToDictionary(s => s.Name, StringComparer.Ordinal); NormalizeRequiredByToDependsOn(allSteps, allStepsByName); + // Capture resolved pipeline data for diagnostics (before filtering) + _lastResolvedSteps = allSteps; + var (stepsToExecute, stepsByName) = FilterStepsForExecution(allSteps, context); // Build dependency graph and execute with readiness-based scheduler @@ -252,10 +373,9 @@ void Visit(string stepName) return result; } - private static async Task<(List Steps, Dictionary StepToResourceMap)> CollectStepsFromAnnotationsAsync(PipelineContext context) + private static async Task> CollectStepsFromAnnotationsAsync(PipelineContext context) { var steps = new List(); - var stepToResourceMap = new Dictionary(); foreach (var resource in context.Model.Resources) { @@ -274,18 +394,17 @@ void Visit(string stepName) foreach (var step in annotationSteps) { steps.Add(step); - stepToResourceMap[step] = resource; + step.Resource ??= resource; } } } - return (steps, stepToResourceMap); + return steps; } private async Task ExecuteConfigurationCallbacksAsync( PipelineContext pipelineContext, - List allSteps, - Dictionary stepToResourceMap) + List allSteps) { // Collect callbacks from the pipeline itself var callbacks = new List>(); @@ -309,8 +428,7 @@ private async Task ExecuteConfigurationCallbacksAsync( { Services = pipelineContext.Services, Steps = allSteps.AsReadOnly(), - Model = pipelineContext.Model, - StepToResourceMap = stepToResourceMap + Model = pipelineContext.Model }; foreach (var callback in callbacks) @@ -424,20 +542,22 @@ async Task ExecuteStepWithDependencies(PipelineStep step) await using (publishingStep.ConfigureAwait(false)) { - try + var stepContext = new PipelineStepContext { - var stepContext = new PipelineStepContext - { - PipelineContext = context, - ReportingStep = publishingStep - }; + PipelineContext = context, + ReportingStep = publishingStep + }; + try + { PipelineLoggerProvider.CurrentLogger = stepContext.Logger; await ExecuteStepAsync(step, stepContext).ConfigureAwait(false); } catch (Exception ex) { + stepContext.Logger.LogError(ex, "Step '{StepName}' failed.", step.Name); + // Report the failure to the activity reporter before disposing await publishingStep.FailAsync(ex.Message, CancellationToken.None).ConfigureAwait(false); throw; @@ -626,6 +746,333 @@ private static async Task ExecuteStepAsync(PipelineStep step, PipelineStepContex } } + /// + /// Dumps comprehensive diagnostic information about the dependency graph, including + /// reasons why certain steps may not be executed. + /// + private static void DumpDependencyGraphDiagnostics( + List allSteps, + PipelineStepContext context) + { + var sb = new StringBuilder(); + + sb.AppendLine(); + sb.AppendLine("PIPELINE DEPENDENCY GRAPH DIAGNOSTICS"); + sb.AppendLine("====================================="); + sb.AppendLine(); + sb.AppendLine("This diagnostic output shows the complete pipeline dependency graph structure."); + sb.AppendLine("Use this to understand step relationships and troubleshoot execution issues."); + sb.AppendLine(); + + // Summary statistics + sb.AppendLine(CultureInfo.InvariantCulture, $"Total steps defined: {allSteps.Count}"); + sb.AppendLine(); + + // Always show full pipeline analysis for diagnostics + sb.AppendLine("Analysis for full pipeline execution (showing all steps and their relationships)"); + sb.AppendLine(); + + var allStepsByName = allSteps.ToDictionary(s => s.Name, StringComparer.Ordinal); + + // Build execution order (topological sort) + var executionOrder = GetTopologicalOrder(allSteps); + + sb.AppendLine("EXECUTION ORDER"); + sb.AppendLine("==============="); + sb.AppendLine("This shows the order in which steps would execute, respecting all dependencies."); + sb.AppendLine("Steps with no dependencies run first, followed by steps that depend on them."); + sb.AppendLine(); + for (var i = 0; i < executionOrder.Count; i++) + { + var step = executionOrder[i]; + sb.AppendLine(CultureInfo.InvariantCulture, $"{i + 1,3}. {step.Name}"); + } + sb.AppendLine(); + + // Detailed step analysis + sb.AppendLine("DETAILED STEP ANALYSIS"); + sb.AppendLine("======================"); + sb.AppendLine("Shows each step's dependencies, associated resources, and tags."); + sb.AppendLine("✓ = dependency exists, ? = dependency missing"); + sb.AppendLine(); + + foreach (var step in allSteps.OrderBy(s => s.Name, StringComparer.Ordinal)) + { + sb.AppendLine(CultureInfo.InvariantCulture, $"Step: {step.Name}"); + + // Show dependencies + if (step.DependsOnSteps.Count > 0) + { + sb.Append(" Dependencies: "); + var depStatuses = step.DependsOnSteps + .OrderBy(dep => dep, StringComparer.Ordinal) + .Select(dep => + { + var depExists = allStepsByName.ContainsKey(dep); + var icon = depExists ? "✓" : "?"; + var status = depExists ? "" : " [missing]"; + return $"{icon} {dep}{status}"; + }); + sb.AppendLine(string.Join(", ", depStatuses)); + } + else + { + sb.AppendLine(" Dependencies: none"); + } + + // Show resource association if available + if (step.Resource != null) + { + sb.AppendLine(CultureInfo.InvariantCulture, $" Resource: {step.Resource.Name} ({step.Resource.GetType().Name})"); + } + + // Show tags if any + if (step.Tags.Count > 0) + { + var sortedTags = step.Tags.OrderBy(tag => tag, StringComparer.Ordinal); + sb.AppendLine(CultureInfo.InvariantCulture, $" Tags: {string.Join(", ", sortedTags)}"); + } + + // Since we're showing full pipeline analysis, no steps are filtered out + // All steps will be marked as "WILL EXECUTE" in this diagnostic view + + sb.AppendLine(); + } + + // Show potential issues + sb.AppendLine("POTENTIAL ISSUES:"); + sb.AppendLine("Identifies problems in the pipeline configuration that could prevent execution."); + sb.AppendLine("─────────────────"); + var hasIssues = false; + + // Check for missing dependencies + foreach (var step in allSteps) + { + foreach (var dep in step.DependsOnSteps) + { + if (!allStepsByName.ContainsKey(dep)) + { + sb.AppendLine(CultureInfo.InvariantCulture, $"WARNING: Step '{step.Name}' depends on missing step '{dep}'"); + hasIssues = true; + } + } + } + + // Check for orphaned steps (no dependencies and not required by anything) + var orphanedSteps = allSteps.Where(step => + step.DependsOnSteps.Count == 0 && + !allSteps.Any(other => other.DependsOnSteps.Contains(step.Name))) + .OrderBy(step => step.Name, StringComparer.Ordinal) + .ToList(); + + if (orphanedSteps.Count > 0) + { + sb.AppendLine("INFO: Orphaned steps (no dependencies, not required by others):"); + foreach (var step in orphanedSteps) + { + sb.AppendLine(CultureInfo.InvariantCulture, $" - {step.Name}"); + } + hasIssues = true; + } + + if (!hasIssues) + { + sb.AppendLine("No issues detected"); + } + + // What-if execution simulation + sb.AppendLine(); + sb.AppendLine("EXECUTION SIMULATION (\"What If\" Analysis):"); + sb.AppendLine("Shows what steps would run for each possible target step and in what order."); + sb.AppendLine("Steps at the same level can run concurrently."); + sb.AppendLine("─────────────────────────────────────────────────────────────────────────────"); + + // Show execution simulation for each step as a potential target + foreach (var targetStep in allSteps.OrderBy(s => s.Name, StringComparer.Ordinal)) + { + sb.AppendLine(CultureInfo.InvariantCulture, $"If targeting '{targetStep.Name}':"); + + // Debug: Show what dependencies this step has after normalization + if (targetStep.DependsOnSteps.Count > 0) + { + var sortedDeps = targetStep.DependsOnSteps.OrderBy(dep => dep, StringComparer.Ordinal); + sb.AppendLine(CultureInfo.InvariantCulture, $" Direct dependencies: {string.Join(", ", sortedDeps)}"); + } + else + { + sb.AppendLine(" Direct dependencies: none"); + } + + // Compute what would execute for this target + var stepsForTarget = ComputeTransitiveDependencies(targetStep, allStepsByName); + var executionLevels = GetExecutionLevelsByStep(stepsForTarget, allStepsByName); + + if (stepsForTarget.Count == 0) + { + sb.AppendLine(" No steps would execute (isolated step with missing dependencies)"); + sb.AppendLine(); + continue; + } + + sb.AppendLine(CultureInfo.InvariantCulture, $" Total steps: {stepsForTarget.Count}"); + + // Group steps by execution level for concurrency visualization + var stepsByLevel = executionLevels.GroupBy(kvp => kvp.Value) + .OrderBy(g => g.Key) + .ToDictionary(g => g.Key, g => g.Select(kvp => kvp.Key).OrderBy(s => s, StringComparer.Ordinal).ToList()); + + sb.AppendLine(" Execution order:"); + + foreach (var level in stepsByLevel.Keys.OrderBy(l => l)) + { + var stepsAtLevel = stepsByLevel[level]; + + if (stepsAtLevel.Count == 1) + { + sb.AppendLine(CultureInfo.InvariantCulture, $" [{level}] {stepsAtLevel[0]}"); + } + else + { + var parallelSteps = string.Join(" | ", stepsAtLevel); + sb.AppendLine(CultureInfo.InvariantCulture, $" [{level}] {parallelSteps} (parallel)"); + } + } + sb.AppendLine(); + } + + context.ReportingStep.Log(LogLevel.Information, sb.ToString(), enableMarkdown: false); + } + + /// + /// Gets all transitive dependencies for a step (recursive). + /// + private static HashSet GetAllTransitiveDependencies( + PipelineStep step, + Dictionary stepsByName, + HashSet visited) + { + var result = new HashSet(StringComparer.Ordinal); + + foreach (var depName in step.DependsOnSteps) + { + if (visited.Contains(depName)) + { + continue; // Avoid infinite recursion + } + + result.Add(depName); + + if (stepsByName.TryGetValue(depName, out var depStep)) + { + visited.Add(depName); + var transitiveDeps = GetAllTransitiveDependencies(depStep, stepsByName, visited); + result.UnionWith(transitiveDeps); + visited.Remove(depName); + } + } + + return result; + } + + /// + /// Gets the execution level (distance from root steps) for a step. + /// + private static int GetExecutionLevel(PipelineStep step, Dictionary stepsByName) + { + var visited = new HashSet(StringComparer.Ordinal); + return GetExecutionLevelRecursive(step, stepsByName, visited); + } + + /// + /// Gets the execution levels for all steps in a collection. + /// + private static Dictionary GetExecutionLevelsByStep( + List steps, + Dictionary stepsByName) + { + var result = new Dictionary(StringComparer.Ordinal); + + foreach (var step in steps) + { + result[step.Name] = GetExecutionLevel(step, stepsByName); + } + + return result; + } + + private static int GetExecutionLevelRecursive( + PipelineStep step, + Dictionary stepsByName, + HashSet visited) + { + if (visited.Contains(step.Name)) + { + return 0; // Circular reference, treat as level 0 + } + + if (step.DependsOnSteps.Count == 0) + { + return 0; // Root step + } + + visited.Add(step.Name); + + var maxLevel = 0; + foreach (var depName in step.DependsOnSteps) + { + if (stepsByName.TryGetValue(depName, out var depStep)) + { + var depLevel = GetExecutionLevelRecursive(depStep, stepsByName, visited); + maxLevel = Math.Max(maxLevel, depLevel + 1); + } + } + + visited.Remove(step.Name); + return maxLevel; + } + + /// + /// Gets the topological order of steps for execution. + /// + private static List GetTopologicalOrder(List steps) + { + var stepsByName = steps.ToDictionary(s => s.Name, StringComparer.Ordinal); + var visited = new HashSet(StringComparer.Ordinal); + var result = new List(); + + void Visit(PipelineStep step) + { + if (!visited.Add(step.Name)) + { + return; + } + + // Visit dependencies in sorted order for deterministic output + var sortedDeps = step.DependsOnSteps.OrderBy(dep => dep, StringComparer.Ordinal); + foreach (var depName in sortedDeps) + { + if (stepsByName.TryGetValue(depName, out var depStep)) + { + Visit(depStep); + } + } + + result.Add(step); + } + + // Process steps in sorted order for deterministic output + var sortedSteps = steps.OrderBy(s => s.Name, StringComparer.Ordinal); + foreach (var step in sortedSteps) + { + if (!visited.Contains(step.Name)) + { + Visit(step); + } + } + + return result; + } + public override string ToString() { if (_steps.Count == 0) diff --git a/src/Aspire.Hosting/Pipelines/PipelineActivityReporter.cs b/src/Aspire.Hosting/Pipelines/PipelineActivityReporter.cs index ef2ce9af713..65eb7b6a9fb 100644 --- a/src/Aspire.Hosting/Pipelines/PipelineActivityReporter.cs +++ b/src/Aspire.Hosting/Pipelines/PipelineActivityReporter.cs @@ -233,7 +233,7 @@ public async Task CompletePublishAsync(string? completionMessage = null, Complet // Use provided state or aggregate from all steps var finalState = completionState ?? CalculateOverallAggregatedState(); - var operationName = isDeploy ? "Deployment" : "Publishing"; + var operationName = "Pipeline"; var state = new PublishingActivity { Type = PublishingActivityTypes.PublishComplete, diff --git a/src/Aspire.Hosting/Pipelines/PipelineConfigurationContext.cs b/src/Aspire.Hosting/Pipelines/PipelineConfigurationContext.cs index ef5b201c00a..781e6d8d2de 100644 --- a/src/Aspire.Hosting/Pipelines/PipelineConfigurationContext.cs +++ b/src/Aspire.Hosting/Pipelines/PipelineConfigurationContext.cs @@ -20,14 +20,25 @@ public class PipelineConfigurationContext /// /// Gets the list of pipeline steps collected during the first pass. /// - public required IReadOnlyList Steps { get; init; } + public required IReadOnlyList Steps + { + get; + init + { + field = value; + // IMPORTANT: The ResourceNameComparer must be used here to ensure correct lookup behavior + // based on resource names, NOT the default reference equality. This is because resources + // may be swapped out (referred to as bait-and-switch) during model transformations. + StepToResourceMap = field.ToLookup(s => s.Resource, s => s, new ResourceNameComparer()); + } + } /// /// Gets the distributed application model containing all resources. /// public required DistributedApplicationModel Model { get; init; } - internal IReadOnlyDictionary? StepToResourceMap { get; init; } + internal ILookup? StepToResourceMap { get; init; } /// /// Gets all pipeline steps with the specified tag. @@ -48,7 +59,8 @@ public IEnumerable GetSteps(string tag) public IEnumerable GetSteps(IResource resource) { ArgumentNullException.ThrowIfNull(resource); - return StepToResourceMap?.Where(kvp => kvp.Value == resource).Select(kvp => kvp.Key) ?? []; + + return StepToResourceMap?[resource] ?? []; } /// diff --git a/src/Aspire.Hosting/Pipelines/PipelineStep.cs b/src/Aspire.Hosting/Pipelines/PipelineStep.cs index 935ccfd5f0c..d2011b7aed5 100644 --- a/src/Aspire.Hosting/Pipelines/PipelineStep.cs +++ b/src/Aspire.Hosting/Pipelines/PipelineStep.cs @@ -4,6 +4,7 @@ #pragma warning disable ASPIREPIPELINES001 using System.Diagnostics.CodeAnalysis; +using Aspire.Hosting.ApplicationModel; namespace Aspire.Hosting.Pipelines; @@ -32,13 +33,18 @@ public class PipelineStep /// Gets or initializes the list of step names that require this step to complete before they can finish. /// This is used internally during pipeline construction and is converted to DependsOn relationships. /// - internal List RequiredBySteps { get; init; } = []; + public List RequiredBySteps { get; init; } = []; /// /// Gets or initializes the list of tags that categorize this step. /// public List Tags { get; init; } = []; + /// + /// Gets or initializes the resource that this step is associated with, if any. + /// + public IResource? Resource { get; set; } + /// /// Adds a dependency on another step. /// diff --git a/src/Aspire.Hosting/Pipelines/PipelineStepsExtensions.cs b/src/Aspire.Hosting/Pipelines/PipelineStepsExtensions.cs new file mode 100644 index 00000000000..6b376da8dc2 --- /dev/null +++ b/src/Aspire.Hosting/Pipelines/PipelineStepsExtensions.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Aspire.Hosting.Pipelines; + +#pragma warning disable ASPIREPIPELINES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +/// +/// Extension methods for pipeline steps. +/// +[Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] +public static class PipelineStepsExtensions +{ + /// + /// Makes each step in the collection depend on the specified step. + /// + /// The collection of steps. + /// The step to depend on. + /// The original collection of steps. + public static IEnumerable DependsOn(this IEnumerable steps, PipelineStep? step) + { + if (step is null) + { + return steps; + } + + foreach (var s in steps) + { + s.DependsOn(step); + } + + return steps; + } + + /// + /// Makes each step in the collection depend on the specified step name. + /// + /// The collection of steps. + /// The name of the step to depend on. + /// The original collection of steps. + public static IEnumerable DependsOn(this IEnumerable steps, string stepName) + { + if (string.IsNullOrEmpty(stepName)) + { + return steps; + } + + foreach (var s in steps) + { + s.DependsOn(stepName); + } + + return steps; + } + + /// + /// Makes each step in the collection depend on the specified target steps. + /// + /// The collection of steps. + /// The target steps to depend on. + /// The original collection of steps. + public static IEnumerable DependsOn(this IEnumerable steps, IEnumerable targetSteps) + { + foreach (var step in targetSteps) + { + foreach (var s in steps) + { + s.DependsOn(step); + } + } + + return steps; + } + + /// + /// Specifies that each step in the collection is required by the specified step. + /// + /// The collection of steps. + /// The step that requires these steps. + /// The original collection of steps. + public static IEnumerable RequiredBy(this IEnumerable steps, PipelineStep? step) + { + if (step is null) + { + return steps; + } + + foreach (var s in steps) + { + s.RequiredBy(step); + } + + return steps; + } + + /// + /// Specifies that each step in the collection is required by the specified step name. + /// + /// The collection of steps. + /// The name of the step that requires these steps. + /// The original collection of steps. + public static IEnumerable RequiredBy(this IEnumerable steps, string stepName) + { + if (string.IsNullOrEmpty(stepName)) + { + return steps; + } + + foreach (var s in steps) + { + s.RequiredBy(stepName); + } + + return steps; + } + + /// + /// Specifies that each step in the collection is required by the specified target steps. + /// + /// The collection of steps. + /// The target steps that require these steps. + /// The original collection of steps. + public static IEnumerable RequiredBy(this IEnumerable steps, IEnumerable targetSteps) + { + foreach (var step in targetSteps) + { + foreach (var s in steps) + { + s.RequiredBy(step); + } + } + + return steps; + } +} diff --git a/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs b/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs index dc95c407aca..72313b9ec21 100644 --- a/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs +++ b/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs @@ -15,17 +15,36 @@ public static class WellKnownPipelineSteps /// The meta-step that coordinates all publish operations. /// All publish steps should be required by this step. /// - public const string Publish = "publish"; + public static readonly string Publish = "publish"; + + /// + /// The prerequisite step that runs before any publish operations. + /// + public static readonly string PublishPrereq = "publish-prereq"; /// /// The meta-step that coordinates all deploy operations. /// All deploy steps should be required by this step. /// - public const string Deploy = "deploy"; + public static readonly string Deploy = "deploy"; + + /// + /// The prerequisite step that runs before any deploy operations. + /// + public static readonly string DeployPrereq = "deploy-prereq"; /// - /// The well-known step for prompting for parameters. + /// The well-known step for building resources. /// + public static readonly string Build = "build"; - public const string ParameterPrompt = "parameter-prompt"; + /// + /// The prerequisite step that runs before any build operations. + /// + public static readonly string BuildPrereq = "build-prereq"; + + /// + /// The diagnostic step that dumps dependency graph information for troubleshooting. + /// + public static readonly string Diagnostics = "diagnostics"; } diff --git a/src/Aspire.Hosting/Pipelines/WellKnownPipelineTags.cs b/src/Aspire.Hosting/Pipelines/WellKnownPipelineTags.cs index 7a163adb1a8..0346d438521 100644 --- a/src/Aspire.Hosting/Pipelines/WellKnownPipelineTags.cs +++ b/src/Aspire.Hosting/Pipelines/WellKnownPipelineTags.cs @@ -21,6 +21,11 @@ public static class WellKnownPipelineTags /// public const string BuildCompute = "build-compute"; + /// + /// Tag for steps that push container images to a registry. + /// + public const string PushContainerImage = "push-container-image"; + /// /// Tag for steps that deploy to compute infrastructure. /// diff --git a/src/Aspire.Hosting/Publishing/ContainerRuntimeBase.cs b/src/Aspire.Hosting/Publishing/ContainerRuntimeBase.cs index f2d2a1afa11..3c2df71da7a 100644 --- a/src/Aspire.Hosting/Publishing/ContainerRuntimeBase.cs +++ b/src/Aspire.Hosting/Publishing/ContainerRuntimeBase.cs @@ -147,7 +147,7 @@ protected async Task ExecuteContainerCommandWithExitCodeAsync( return processResult.ExitCode; } - _logger.LogInformation(successLogTemplate, logArguments); + _logger.LogDebug(successLogTemplate, logArguments); return processResult.ExitCode; } } diff --git a/src/Aspire.Hosting/Publishing/PipelineExecutor.cs b/src/Aspire.Hosting/Publishing/PipelineExecutor.cs index 46fd9288cf8..08ff3319fbf 100644 --- a/src/Aspire.Hosting/Publishing/PipelineExecutor.cs +++ b/src/Aspire.Hosting/Publishing/PipelineExecutor.cs @@ -83,129 +83,6 @@ await eventing.PublishAsync( public async Task ExecutePipelineAsync(DistributedApplicationModel model, CancellationToken cancellationToken) { - // Add a step to display the target environment - var environmentStep = await activityReporter.CreateStepAsync( - "display-environment", - cancellationToken).ConfigureAwait(false); - - await using (environmentStep.ConfigureAwait(false)) - { - var hostEnvironment = serviceProvider.GetService(); - var environmentName = hostEnvironment?.EnvironmentName ?? "Production"; - - var environmentTask = await environmentStep.CreateTaskAsync( - $"Discovering target environment", - cancellationToken) - .ConfigureAwait(false); - - await environmentTask.CompleteAsync( - $"Target environment: {environmentName.ToLowerInvariant()}", - CompletionState.Completed, - cancellationToken) - .ConfigureAwait(false); - } - - // Check if --clear-cache flag is set and prompt user before deleting deployment state - if (options.Value.ClearCache) - { - var deploymentStateManager = serviceProvider.GetService(); - if (deploymentStateManager?.StateFilePath is not null && File.Exists(deploymentStateManager.StateFilePath)) - { - var interactionService = serviceProvider.GetService(); - if (interactionService?.IsAvailable == true) - { - var hostEnvironment = serviceProvider.GetService(); - var environmentName = hostEnvironment?.EnvironmentName ?? "Production"; - var result = await interactionService.PromptNotificationAsync( - "Clear Deployment State", - $"The deployment state for the '{environmentName}' environment will be deleted. All Azure resources will be re-provisioned. Do you want to continue?", - new NotificationInteractionOptions - { - Intent = MessageIntent.Confirmation, - ShowSecondaryButton = true, - ShowDismiss = false, - PrimaryButtonText = "Yes", - SecondaryButtonText = "No" - }, - cancellationToken).ConfigureAwait(false); - - if (result.Canceled || !result.Data) - { - // User declined or canceled - exit the deployment - logger.LogInformation("User declined to clear deployment state. Canceling pipeline execution."); - return; - } - - // User confirmed - delete the deployment state file - logger.LogInformation("Deleting deployment state file at {Path} due to --clear-cache flag", deploymentStateManager.StateFilePath); - File.Delete(deploymentStateManager.StateFilePath); - } - } - } - - // Add a step to do model analysis before publishing/deploying - var step = await activityReporter.CreateStepAsync( - "analyze-model", - cancellationToken).ConfigureAwait(false); - - await using (step.ConfigureAwait(false)) - { - - var task = await step.CreateTaskAsync( - "Analyzing the distributed application model for publishing and deployment capabilities.", - cancellationToken) - .ConfigureAwait(false); - - string message; - CompletionState state; - - var hasResourcesWithSteps = model.Resources.Any(r => r.HasAnnotationOfType()); - var pipeline = serviceProvider.GetRequiredService(); - var hasDirectlyRegisteredSteps = pipeline is DistributedApplicationPipeline concretePipeline && concretePipeline.HasSteps; - - if (!hasResourcesWithSteps && !hasDirectlyRegisteredSteps) - { - message = "No pipeline steps found in the application."; - state = CompletionState.CompletedWithError; - } - else - { - message = "Found pipeline steps in the application."; - state = CompletionState.Completed; - } - - await task.CompleteAsync( - message, - state, - cancellationToken) - .ConfigureAwait(false); - - // Add a task to show the deployment state file path if available - if (!options.Value.ClearCache) - { - var deploymentStateManager = serviceProvider.GetService(); - if (deploymentStateManager?.StateFilePath is not null && File.Exists(deploymentStateManager.StateFilePath)) - { - var statePathTask = await step.CreateTaskAsync( - "Checking deployment state configuration.", - cancellationToken) - .ConfigureAwait(false); - - await statePathTask.CompleteAsync( - $"Deployment state will be loaded from: {deploymentStateManager.StateFilePath}", - CompletionState.Completed, - cancellationToken) - .ConfigureAwait(false); - } - } - - if (state == CompletionState.CompletedWithError) - { - // If there are no pipeline steps, we can exit early - return; - } - } - var pipelineContext = new PipelineContext(model, executionContext, serviceProvider, logger, cancellationToken, options.Value.OutputPath is not null ? Path.GetFullPath(options.Value.OutputPath) : null); diff --git a/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs b/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs index e2865515192..902fa070ac1 100644 --- a/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs +++ b/src/Aspire.Hosting/Publishing/ResourceContainerImageBuilder.cs @@ -9,7 +9,6 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Dcp; using Aspire.Hosting.Dcp.Process; -using Aspire.Hosting.Pipelines; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -134,9 +133,11 @@ public interface IResourceContainerImageBuilder internal sealed class ResourceContainerImageBuilder( ILogger logger, IOptions dcpOptions, - IServiceProvider serviceProvider, - IPipelineActivityReporter activityReporter) : IResourceContainerImageBuilder + IServiceProvider serviceProvider) : IResourceContainerImageBuilder { + // Disable concurrent builds for project resources to avoid issues with overlapping msbuild projects + private readonly SemaphoreSlim _throttle = new(1); + private IContainerRuntime? _containerRuntime; private IContainerRuntime ContainerRuntime => _containerRuntime ??= dcpOptions.Value.ContainerRuntime switch { @@ -146,57 +147,34 @@ internal sealed class ResourceContainerImageBuilder( public async Task BuildImagesAsync(IEnumerable resources, ContainerBuildOptions? options = null, CancellationToken cancellationToken = default) { - var step = await activityReporter.CreateStepAsync( - "build-images", - cancellationToken).ConfigureAwait(false); + logger.LogInformation("Starting to build container images"); - await using (step.ConfigureAwait(false)) + // Only check container runtime health if there are resources that need it + if (ResourcesRequireContainerRuntime(resources, options)) { - // Only check container runtime health if there are resources that need it - if (ResourcesRequireContainerRuntime(resources, options)) - { - var task = await step.CreateTaskAsync( - $"Checking {ContainerRuntime.Name} health", - cancellationToken).ConfigureAwait(false); - - await using (task.ConfigureAwait(false)) - { - var containerRuntimeHealthy = await ContainerRuntime.CheckIfRunningAsync(cancellationToken).ConfigureAwait(false); - - if (!containerRuntimeHealthy) - { - logger.LogError("Container runtime is not running or is unhealthy. Cannot build container images."); - - await task.FailAsync( - $"{ContainerRuntime.Name} is not running or is unhealthy.", - cancellationToken).ConfigureAwait(false); + logger.LogDebug("Checking {ContainerRuntimeName} health", ContainerRuntime.Name); - await step.CompleteAsync("Building container images failed", CompletionState.CompletedWithError, cancellationToken).ConfigureAwait(false); - throw new InvalidOperationException("Container runtime is not running or is unhealthy."); - } + var containerRuntimeHealthy = await ContainerRuntime.CheckIfRunningAsync(cancellationToken).ConfigureAwait(false); - await task.SucceedAsync( - $"{ContainerRuntime.Name} is healthy.", - cancellationToken).ConfigureAwait(false); - } - } - - foreach (var resource in resources) + if (!containerRuntimeHealthy) { - // TODO: Consider parallelizing this. - await BuildImageAsync(step, resource, options, cancellationToken).ConfigureAwait(false); + logger.LogError("Container runtime is not running or is unhealthy. Cannot build container images."); + throw new InvalidOperationException("Container runtime is not running or is unhealthy."); } - await step.CompleteAsync("Building container images completed", CompletionState.Completed, cancellationToken).ConfigureAwait(false); + logger.LogDebug("{ContainerRuntimeName} is healthy", ContainerRuntime.Name); } - } - public Task BuildImageAsync(IResource resource, ContainerBuildOptions? options = null, CancellationToken cancellationToken = default) - { - return BuildImageAsync(step: null, resource, options, cancellationToken); + foreach (var resource in resources) + { + // TODO: Consider parallelizing this. + await BuildImageAsync(resource, options, cancellationToken).ConfigureAwait(false); + } + + logger.LogDebug("Building container images completed"); } - private async Task BuildImageAsync(IReportingStep? step, IResource resource, ContainerBuildOptions? options, CancellationToken cancellationToken) + public async Task BuildImageAsync(IResource resource, ContainerBuildOptions? options = null, CancellationToken cancellationToken = default) { logger.LogInformation("Building container image for resource {ResourceName}", resource.Name); @@ -206,7 +184,6 @@ private async Task BuildImageAsync(IReportingStep? step, IResource resource, Con // using the .NET SDK. await BuildProjectContainerImageAsync( resource, - step, options, cancellationToken).ConfigureAwait(false); return; @@ -223,7 +200,6 @@ await BuildContainerImageFromDockerfileAsync( resource, dockerfileBuildAnnotation, imageName, - step, options, cancellationToken).ConfigureAwait(false); return; @@ -231,7 +207,7 @@ await BuildContainerImageFromDockerfileAsync( else if (resource.TryGetLastAnnotation(out var _)) { // This resource already has a container image associated with it so no build is needed. - logger.LogInformation("Resource {ResourceName} already has a container image associated and no build annotation. Skipping build.", resource.Name); + logger.LogDebug("Resource {ResourceName} already has a container image associated and no build annotation. Skipping build.", resource.Name); return; } else @@ -240,34 +216,30 @@ await BuildContainerImageFromDockerfileAsync( } } - private async Task BuildProjectContainerImageAsync(IResource resource, IReportingStep? step, ContainerBuildOptions? options, CancellationToken cancellationToken) + private async Task BuildProjectContainerImageAsync(IResource resource, ContainerBuildOptions? options, CancellationToken cancellationToken) { - var publishingTask = await CreateTaskAsync( - step, - $"Building image: {resource.Name}", - cancellationToken - ).ConfigureAwait(false); - - var success = await ExecuteDotnetPublishAsync(resource, options, cancellationToken).ConfigureAwait(false); + await _throttle.WaitAsync(cancellationToken).ConfigureAwait(false); - if (publishingTask is not null) + try { - await using (publishingTask.ConfigureAwait(false)) + + logger.LogInformation("Building image: {ResourceName}", resource.Name); + + var success = await ExecuteDotnetPublishAsync(resource, options, cancellationToken).ConfigureAwait(false); + + if (!success) { - if (!success) - { - await publishingTask.FailAsync($"Building image for {resource.Name} failed", cancellationToken).ConfigureAwait(false); - } - else - { - await publishingTask.SucceedAsync($"Building image for {resource.Name} completed", cancellationToken).ConfigureAwait(false); - } + logger.LogError("Building image for {ResourceName} failed", resource.Name); + throw new DistributedApplicationException($"Failed to build container image."); + } + else + { + logger.LogInformation("Building image for {ResourceName} completed", resource.Name); } } - - if (!success) + finally { - throw new DistributedApplicationException($"Failed to build container image."); + _throttle.Release(); } } @@ -319,7 +291,7 @@ private async Task ExecuteDotnetPublishAsync(IResource resource, Container } }; - logger.LogInformation( + logger.LogDebug( "Starting .NET CLI with arguments: {Arguments}", string.Join(" ", spec.Arguments) ); @@ -347,13 +319,9 @@ private async Task ExecuteDotnetPublishAsync(IResource resource, Container } } - private async Task BuildContainerImageFromDockerfileAsync(IResource resource, DockerfileBuildAnnotation dockerfileBuildAnnotation, string imageName, IReportingStep? step, ContainerBuildOptions? options, CancellationToken cancellationToken) + private async Task BuildContainerImageFromDockerfileAsync(IResource resource, DockerfileBuildAnnotation dockerfileBuildAnnotation, string imageName, ContainerBuildOptions? options, CancellationToken cancellationToken) { - var publishingTask = await CreateTaskAsync( - step, - $"Building image: {resource.Name}", - cancellationToken - ).ConfigureAwait(false); + logger.LogInformation("Building image: {ResourceName}", resource.Name); // If there's a factory, generate the Dockerfile content and write it to the specified path if (dockerfileBuildAnnotation.DockerfileFactory is not null) @@ -388,52 +356,24 @@ private async Task BuildContainerImageFromDockerfileAsync(IResource resource, Do Directory.CreateDirectory(outputPath); } - if (publishingTask is not null) + try { - await using (publishingTask.ConfigureAwait(false)) - { - try - { - await ContainerRuntime.BuildImageAsync( - dockerfileBuildAnnotation.ContextPath, - dockerfileBuildAnnotation.DockerfilePath, - imageName, - options, - resolvedBuildArguments, - resolvedBuildSecrets, - dockerfileBuildAnnotation.Stage, - cancellationToken).ConfigureAwait(false); - - await publishingTask.SucceedAsync($"Building image for {resource.Name} completed", cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to build container image from Dockerfile."); - await publishingTask.FailAsync($"Building image for {resource.Name} failed", cancellationToken).ConfigureAwait(false); - throw; - } - } + await ContainerRuntime.BuildImageAsync( + dockerfileBuildAnnotation.ContextPath, + dockerfileBuildAnnotation.DockerfilePath, + imageName, + options, + resolvedBuildArguments, + resolvedBuildSecrets, + dockerfileBuildAnnotation.Stage, + cancellationToken).ConfigureAwait(false); + + logger.LogInformation("Building image for {ResourceName} completed", resource.Name); } - else + catch (Exception ex) { - // Handle case when publishingTask is null (no step provided) - try - { - await ContainerRuntime.BuildImageAsync( - dockerfileBuildAnnotation.ContextPath, - dockerfileBuildAnnotation.DockerfilePath, - imageName, - options, - resolvedBuildArguments, - resolvedBuildSecrets, - dockerfileBuildAnnotation.Stage, - cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to build container image from Dockerfile."); - throw; - } + logger.LogError(ex, "Failed to build container image from Dockerfile for {ResourceName}", resource.Name); + throw; } } @@ -460,20 +400,6 @@ await ContainerRuntime.BuildImageAsync( } } - private static async Task CreateTaskAsync( - IReportingStep? step, - string description, - CancellationToken cancellationToken) - { - - if (step is null) - { - return null; - } - - return await step.CreateTaskAsync(description, cancellationToken).ConfigureAwait(false); - } - public async Task TagImageAsync(string localImageName, string targetImageName, CancellationToken cancellationToken = default) { await ContainerRuntime.TagImageAsync(localImageName, targetImageName, cancellationToken).ConfigureAwait(false); diff --git a/src/Shared/ResourceNameComparer.cs b/src/Shared/ResourceNameComparer.cs index 8f6592d3e6b..f6817affc0f 100644 --- a/src/Shared/ResourceNameComparer.cs +++ b/src/Shared/ResourceNameComparer.cs @@ -5,7 +5,7 @@ namespace Aspire.Hosting; -internal sealed class ResourceNameComparer : IEqualityComparer +internal sealed class ResourceNameComparer : IEqualityComparer { public bool Equals(IResource? x, IResource? y) { @@ -18,5 +18,5 @@ public bool Equals(IResource? x, IResource? y) } public int GetHashCode(IResource obj) => - obj.Name.GetHashCode(StringComparison.Ordinal); + obj?.Name.GetHashCode(StringComparison.Ordinal) ?? 0; } diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureBicepProvisionerTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureBicepProvisionerTests.cs index ba1c70d9a63..b69af8182ba 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureBicepProvisionerTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureBicepProvisionerTests.cs @@ -12,6 +12,7 @@ using Azure.Core; using Azure.Security.KeyVault.Secrets; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; namespace Aspire.Hosting.Azure.Tests; @@ -98,7 +99,8 @@ public void BicepProvisioner_CanBeInstantiated() bicepExecutor, secretClientProvider, services.GetRequiredService(), - new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run)); + new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run), + NullLogger.Instance); // Assert Assert.NotNull(provisioner); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs index 9108bdb649f..d17166331dd 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs @@ -19,7 +19,6 @@ using Aspire.Hosting.Testing; using Aspire.Hosting.Tests; using Aspire.Hosting.Utils; -using Aspire.TestUtilities; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -108,75 +107,6 @@ public async Task DeployAsync_PromptsViaInteractionService() await runTask.WaitAsync(TimeSpan.FromSeconds(10)); } - /// - /// Verifies that deploying an application with resources that define their own build steps does not trigger default - /// image build and they have the correct pipeline configuration. - /// - [Fact] - public async Task DeployAsync_WithResourcesWithBuildSteps() - { - // Arrange - var mockProcessRunner = new MockProcessRunner(); - using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.Deploy); - var armClientProvider = new TestArmClientProvider(new Dictionary - { - ["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "testregistry" }, - ["AZURE_CONTAINER_REGISTRY_ENDPOINT"] = new { type = "String", value = "testregistry.azurecr.io" }, - ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" }, - ["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = new { type = "String", value = "test.westus.azurecontainerapps.io" }, - ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv" } - }); - ConfigureTestServices(builder, armClientProvider: armClientProvider); - - var containerAppEnv = builder.AddAzureContainerAppEnvironment("env"); - - var configCalled = false; - - // Add a compute resource with its own build step - builder.AddProject("api", launchProfileName: null) - .WithPipelineStepFactory(factoryContext => - { - return - [ - new PipelineStep - { - Name = "api-build", - Action = _ => Task.CompletedTask, - Tags = [WellKnownPipelineTags.BuildCompute] - } - ]; - }) - .WithPipelineConfiguration(configContext => - { - var mainBuildStep = configContext.GetSteps(WellKnownPipelineTags.BuildCompute) - .Where(s => s.Name == "build-container-images") - .Single(); - - Assert.Contains("api-build", mainBuildStep.DependsOnSteps); - - var apiBuildStep = configContext.GetSteps(WellKnownPipelineTags.BuildCompute) - .Where(s => s.Name == "api-build") - .Single(); - - Assert.Contains("default-image-tags", apiBuildStep.DependsOnSteps); - - configCalled = true; - }); - - using var app = builder.Build(); - await app.StartAsync(); - await app.WaitForShutdownAsync(); - - Assert.True(configCalled); - - // Assert - Verify MockImageBuilder was NOT called because the project resource has its own build step - var mockImageBuilder = app.Services.GetRequiredService() as MockImageBuilder; - Assert.NotNull(mockImageBuilder); - Assert.False(mockImageBuilder.BuildImageCalled); - Assert.False(mockImageBuilder.BuildImagesCalled); - Assert.Empty(mockImageBuilder.BuildImageResources); - } - /// /// Verifies that deploying an application with resources that are build-only containers only builds /// the containers and does not attempt to push them. @@ -185,7 +115,6 @@ public async Task DeployAsync_WithResourcesWithBuildSteps() public async Task DeployAsync_WithBuildOnlyContainers() { // Arrange - var mockProcessRunner = new MockProcessRunner(); using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.Deploy); var armClientProvider = new TestArmClientProvider(new Dictionary { @@ -220,8 +149,7 @@ public async Task DeployAsync_WithBuildOnlyContainers() // Assert - Verify MockImageBuilder was only called to build an image and not push it var mockImageBuilder = app.Services.GetRequiredService() as MockImageBuilder; Assert.NotNull(mockImageBuilder); - Assert.False(mockImageBuilder.BuildImageCalled); - Assert.True(mockImageBuilder.BuildImagesCalled); + Assert.True(mockImageBuilder.BuildImageCalled); var builtImage = Assert.Single(mockImageBuilder.BuildImageResources); Assert.Equal("exe", builtImage.Name); Assert.False(mockImageBuilder.PushImageCalled); @@ -432,12 +360,15 @@ public async Task DeployAsync_WithProjectResource_Works() imageName.Contains("aspire-deploy-")); } - [Fact] - public async Task DeployAsync_WithMultipleComputeEnvironments_Works() + [Theory] + [InlineData("deploy")] + [InlineData("diagnostics")] + public async Task DeployAsync_WithMultipleComputeEnvironments_Works(string step) { // Arrange var mockProcessRunner = new MockProcessRunner(); - using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.Deploy); + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: step); + var mockActivityReporter = new TestPublishingActivityReporter(); var armClientProvider = new TestArmClientProvider(deploymentName => { return deploymentName switch @@ -461,7 +392,7 @@ public async Task DeployAsync_WithMultipleComputeEnvironments_Works() _ => [] }; }); - ConfigureTestServices(builder, armClientProvider: armClientProvider, processRunner: mockProcessRunner); + ConfigureTestServices(builder, armClientProvider: armClientProvider, processRunner: mockProcessRunner, activityReporter: mockActivityReporter); var acaEnv = builder.AddAzureContainerAppEnvironment("aca-env"); var aasEnv = builder.AddAzureAppServiceEnvironment("aas-env"); @@ -481,6 +412,18 @@ public async Task DeployAsync_WithMultipleComputeEnvironments_Works() await app.StartAsync(); await app.WaitForShutdownAsync(); + if (step == "diagnostics") + { + // In diagnostics mode, just verify logs match snapshot + var logs = mockActivityReporter.LoggedMessages + .Where(s => s.StepTitle == "diagnostics") + .Select(s => s.Message) + .ToList(); + + await Verify(logs); + return; + } + // Assert ACA environment outputs are properly set Assert.Equal("acaregistry", acaEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_NAME"]); Assert.Equal("acaregistry.azurecr.io", acaEnv.Resource.Outputs["AZURE_CONTAINER_REGISTRY_ENDPOINT"]); @@ -693,8 +636,7 @@ public async Task DeployAsync_WithSingleRedisCache_CallsDeployingComputeResource cmd.Arguments == "acr login --name testregistry"); // Assert that deploying steps executed - Assert.Contains("deploy-compute-resources", mockActivityReporter.CreatedSteps); - Assert.Contains(("deploy-compute-resources", "Deploying **cache**"), mockActivityReporter.CreatedTasks); + Assert.Contains("provision-cache-containerapp", mockActivityReporter.CreatedSteps); } [Fact] @@ -1013,60 +955,6 @@ public Task GetOrCreateResourceAsync(AzureBicepResource resource, ProvisioningCo } } - [Fact(Skip = "az cli not available on azdo", SkipType = typeof(PlatformDetection), SkipWhen = nameof(PlatformDetection.IsRunningFromAzdo))] - public async Task DeployAsync_ShowsEndpointOnlyForExternalEndpoints() - { - // Arrange - var activityReporter = new TestPublishingActivityReporter(); - using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.Deploy); - var armClientProvider = new TestArmClientProvider(new Dictionary - { - ["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "testregistry" }, - ["AZURE_CONTAINER_REGISTRY_ENDPOINT"] = new { type = "String", value = "testregistry.azurecr.io" }, - ["AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" }, - ["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = new { type = "String", value = "test.westus.azurecontainerapps.io" }, - ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/testenv" } - }); - ConfigureTestServices(builder, armClientProvider: armClientProvider, activityReporter: activityReporter); - - var containerAppEnv = builder.AddAzureContainerAppEnvironment("env"); - var azureEnv = builder.AddAzureEnvironment(); - - // Add container with external endpoint - var externalContainer = builder.AddContainer("external-api", "external-image:latest") - .WithHttpEndpoint(port: 80, name: "http") - .WithExternalHttpEndpoints(); - - // Add container with internal endpoint only - var internalContainer = builder.AddContainer("internal-api", "internal-image:latest") - .WithHttpEndpoint(port: 80, name: "http"); - - // Add container with no endpoints - var noEndpointContainer = builder.AddContainer("worker", "worker-image:latest"); - - // Act - using var app = builder.Build(); - await app.StartAsync(); - await app.WaitForShutdownAsync(); - - // Assert - Verify that external container shows URL in completion message - var externalTask = activityReporter.CompletedTasks.FirstOrDefault(t => t.TaskStatusText.Contains("external-api")); - Assert.NotNull(externalTask.CompletionMessage); - Assert.Contains("https://external-api.test.westus.azurecontainerapps.io", externalTask.CompletionMessage); - - // Assert - Verify that internal container does NOT show URL in completion message - var internalTask = activityReporter.CompletedTasks.FirstOrDefault(t => t.TaskStatusText.Contains("internal-api")); - Assert.NotNull(internalTask.CompletionMessage); - Assert.DoesNotContain("https://", internalTask.CompletionMessage); - Assert.Equal("Successfully deployed **internal-api**", internalTask.CompletionMessage); - - // Assert - Verify that container with no endpoints does NOT show URL in completion message - var noEndpointTask = activityReporter.CompletedTasks.FirstOrDefault(t => t.TaskStatusText.Contains("worker")); - Assert.NotNull(noEndpointTask.CompletionMessage); - Assert.DoesNotContain("https://", noEndpointTask.CompletionMessage); - Assert.Equal("Successfully deployed **worker**", noEndpointTask.CompletionMessage); - } - private sealed class Project : IProjectMetadata { public string ProjectPath => "project"; @@ -1294,6 +1182,7 @@ private sealed class TestPublishingActivityReporter : IPipelineActivityReporter public List<(string StepTitle, string CompletionText, CompletionState CompletionState)> CompletedSteps { get; } = []; public List<(string TaskStatusText, string? CompletionMessage, CompletionState CompletionState)> CompletedTasks { get; } = []; public List<(string TaskStatusText, string StatusText)> UpdatedTasks { get; } = []; + public List<(string StepTitle, LogLevel LogLevel, string Message)> LoggedMessages { get; } = []; public Task CompletePublishAsync(string? completionMessage = null, CompletionState? completionState = null, bool isDeploy = false, CancellationToken cancellationToken = default) { @@ -1335,9 +1224,7 @@ public Task CreateTaskAsync(string statusText, CancellationToken public void Log(LogLevel logLevel, string message, bool enableMarkdown) { - // For testing purposes, we just track that Log was called - _ = logLevel; - _ = message; + _reporter.LoggedMessages.Add((_title, logLevel, message)); } } diff --git a/tests/Aspire.Hosting.Azure.Tests/BicepUtilitiesTests.cs b/tests/Aspire.Hosting.Azure.Tests/BicepUtilitiesTests.cs index e406f4caa8c..86f1a60fbb1 100644 --- a/tests/Aspire.Hosting.Azure.Tests/BicepUtilitiesTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/BicepUtilitiesTests.cs @@ -144,34 +144,6 @@ public async Task ResourceWithSameTemplateButDifferentParametersHaveDifferentChe Assert.NotEqual(checkSum0, checkSum1); } - [Fact] - public async Task GetCurrentChecksumSkipsKnownValuesForCheckSumCreation() - { - using var builder = TestDistributedApplicationBuilder.Create(); - - var bicep0 = builder.AddBicepTemplateString("bicep0", "param name string") - .WithParameter("name", "david"); - - // Simulate the case where a known parameter has a value - var bicep1 = builder.AddBicepTemplateString("bicep1", "param name string") - .WithParameter("name", "david") - .WithParameter(AzureBicepResource.KnownParameters.PrincipalId, "id") - .WithParameter(AzureBicepResource.KnownParameters.Location, "tomorrow") - .WithParameter(AzureBicepResource.KnownParameters.PrincipalType, "type"); - - var parameters0 = new JsonObject(); - await BicepUtilities.SetParametersAsync(parameters0, bicep0.Resource); - var checkSum0 = BicepUtilities.GetChecksum(bicep0.Resource, parameters0, null); - - // Save the old version of this resource's parameters to config - var config = new ConfigurationManager(); - config["Parameters"] = parameters0.ToJsonString(); - - var checkSum1 = await BicepUtilities.GetCurrentChecksumAsync(bicep1.Resource, config); - - Assert.Equal(checkSum0, checkSum1); - } - [Fact] public async Task ResourceWithDifferentScopeHaveDifferentChecksums() { @@ -334,28 +306,6 @@ public void GetChecksum_ConsistentBehavior_ForParameterComparisons(string? value } } - [Fact] - public async Task SetParametersAsync_SkipsKnownParametersWhenSkipDynamicValuesIsTrue() - { - // Arrange - using var builder = TestDistributedApplicationBuilder.Create(); - var bicep = builder.AddBicepTemplateString("test", "param name string").Resource; - bicep.Parameters["normalParam"] = "normalValue"; - bicep.Parameters[AzureBicepResource.KnownParameters.PrincipalId] = "someId"; - bicep.Parameters[AzureBicepResource.KnownParameters.Location] = "someLocation"; - - var parameters = new JsonObject(); - - // Act - await BicepUtilities.SetParametersAsync(parameters, bicep, skipDynamicValues: true); - - // Assert - Assert.Single(parameters); - Assert.True(parameters.ContainsKey("normalParam")); - Assert.False(parameters.ContainsKey(AzureBicepResource.KnownParameters.PrincipalId)); - Assert.False(parameters.ContainsKey(AzureBicepResource.KnownParameters.Location)); - } - [Fact] public async Task SetParametersAsync_IncludesAllParametersWhenSkipDynamicValuesIsFalse() { @@ -369,7 +319,7 @@ public async Task SetParametersAsync_IncludesAllParametersWhenSkipDynamicValuesI var parameters = new JsonObject(); // Act - await BicepUtilities.SetParametersAsync(parameters, bicep, skipDynamicValues: false); + await BicepUtilities.SetParametersAsync(parameters, bicep); // Assert Assert.Equal(3, parameters.Count); diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceContainerWithoutTargetPortUsesDefaultPort.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceContainerWithoutTargetPortUsesDefaultPort.verified.bicep index a8b10fbaeb7..1184bf8c04a 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceContainerWithoutTargetPortUsesDefaultPort.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceContainerWithoutTargetPortUsesDefaultPort.verified.bicep @@ -83,7 +83,7 @@ resource webapp 'Microsoft.Web/sites@2024-11-01' = { } } -resource container1_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource container1_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) properties: { principalId: env_outputs_azure_website_contributor_managed_identity_principal_id @@ -91,4 +91,4 @@ resource container1_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { principalType: 'ServicePrincipal' } scope: webapp -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceContainerWithoutTargetPortUsesDefaultPort.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceContainerWithoutTargetPortUsesDefaultPort.verified.json index bab212ee17e..97ce358e2c2 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceContainerWithoutTargetPortUsesDefaultPort.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceContainerWithoutTargetPortUsesDefaultPort.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "container1.module.bicep", + "path": "container1-website.module.bicep", "params": { "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env_outputs_planid": "{env.outputs.planId}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithApplicationInsightsSetsAppSettings.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithApplicationInsightsSetsAppSettings.verified.bicep index 58e713f8754..ad242e283c4 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithApplicationInsightsSetsAppSettings.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithApplicationInsightsSetsAppSettings.verified.bicep @@ -1,4 +1,4 @@ -@description('The location for the resource(s) to be deployed.') +@description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location param env_outputs_azure_container_registry_endpoint string @@ -121,7 +121,7 @@ resource webapp 'Microsoft.Web/sites@2024-11-01' = { } } -resource project1_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource project1_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) properties: { principalId: env_outputs_azure_website_contributor_managed_identity_principal_id @@ -129,4 +129,4 @@ resource project1_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { principalType: 'ServicePrincipal' } scope: webapp -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithApplicationInsightsSetsAppSettings.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithApplicationInsightsSetsAppSettings.verified.json index b1c2382e9d0..f7f0a58fa3c 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithApplicationInsightsSetsAppSettings.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithApplicationInsightsSetsAppSettings.verified.json @@ -1,6 +1,6 @@ -{ +{ "type": "azure.bicep.v0", - "path": "project1.module.bicep", + "path": "project1-website.module.bicep", "params": { "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env_outputs_planid": "{env.outputs.planId}", @@ -14,4 +14,4 @@ "env_outputs_azure_application_insights_instrumentationkey": "{env.outputs.AZURE_APPLICATION_INSIGHTS_INSTRUMENTATIONKEY}", "env_outputs_azure_application_insights_connection_string": "{env.outputs.AZURE_APPLICATION_INSIGHTS_CONNECTION_STRING}" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithoutTargetPortUsesContainerPort.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithoutTargetPortUsesContainerPort.verified.bicep index 14c9739d01e..0088ed9e60a 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithoutTargetPortUsesContainerPort.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithoutTargetPortUsesContainerPort.verified.bicep @@ -105,7 +105,7 @@ resource webapp 'Microsoft.Web/sites@2024-11-01' = { } } -resource project1_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource project1_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) properties: { principalId: env_outputs_azure_website_contributor_managed_identity_principal_id @@ -113,4 +113,4 @@ resource project1_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { principalType: 'ServicePrincipal' } scope: webapp -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithoutTargetPortUsesContainerPort.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithoutTargetPortUsesContainerPort.verified.json index 6c8a3f2ecb1..0ceaf54a288 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithoutTargetPortUsesContainerPort.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceProjectWithoutTargetPortUsesContainerPort.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "project1.module.bicep", + "path": "project1-website.module.bicep", "params": { "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env_outputs_planid": "{env.outputs.planId}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceToEnvironmentWithoutDashboard.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceToEnvironmentWithoutDashboard.verified.json index 4e0562d0eff..3c7a9582071 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceToEnvironmentWithoutDashboard.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceToEnvironmentWithoutDashboard.verified.json @@ -1,6 +1,6 @@ -{ +{ "type": "azure.bicep.v0", - "path": "project2.module.bicep", + "path": "project2-website.module.bicep", "params": { "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env_outputs_planid": "{env.outputs.planId}", @@ -9,4 +9,4 @@ "project2_containerimage": "{project2.containerImage}", "project2_containerport": "{project2.containerPort}" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithArgs.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithArgs.verified.bicep index 44a4ca055c1..449011b0714 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithArgs.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithArgs.verified.bicep @@ -117,7 +117,7 @@ resource webapp 'Microsoft.Web/sites@2024-11-01' = { } } -resource project2_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource project2_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) properties: { principalId: env_outputs_azure_website_contributor_managed_identity_principal_id diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithArgs.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithArgs.verified.json index cfe5a2334ae..8767b810415 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithArgs.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithArgs.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "project2.module.bicep", + "path": "project2-website.module.bicep", "params": { "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env_outputs_planid": "{env.outputs.planId}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPort.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPort.verified.bicep index a9498d73f8c..b3ba898439c 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPort.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPort.verified.bicep @@ -1,4 +1,4 @@ -@description('The location for the resource(s) to be deployed.') +@description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location param env_outputs_azure_container_registry_endpoint string @@ -119,7 +119,7 @@ resource webapp 'Microsoft.Web/sites@2024-11-01' = { } } -resource project2_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource project2_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) properties: { principalId: env_outputs_azure_website_contributor_managed_identity_principal_id @@ -127,4 +127,4 @@ resource project2_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { principalType: 'ServicePrincipal' } scope: webapp -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPort.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPort.verified.json index 0396ed3e236..041aab00c02 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPort.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPort.verified.json @@ -1,6 +1,6 @@ -{ +{ "type": "azure.bicep.v0", - "path": "project2.module.bicep", + "path": "project2-website.module.bicep", "params": { "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env_outputs_planid": "{env.outputs.planId}", @@ -11,4 +11,4 @@ "env_outputs_azure_website_contributor_managed_identity_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_ID}", "env_outputs_azure_website_contributor_managed_identity_principal_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_PRINCIPAL_ID}" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPortMultipleEndpoints.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPortMultipleEndpoints.verified.bicep index a045bad5f87..aa70e49d644 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPortMultipleEndpoints.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPortMultipleEndpoints.verified.bicep @@ -1,4 +1,4 @@ -@description('The location for the resource(s) to be deployed.') +@description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location param env_outputs_azure_container_registry_endpoint string @@ -107,7 +107,7 @@ resource webapp 'Microsoft.Web/sites@2024-11-01' = { } } -resource project2_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource project2_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) properties: { principalId: env_outputs_azure_website_contributor_managed_identity_principal_id @@ -115,4 +115,4 @@ resource project2_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { principalType: 'ServicePrincipal' } scope: webapp -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPortMultipleEndpoints.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPortMultipleEndpoints.verified.json index 0396ed3e236..041aab00c02 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPortMultipleEndpoints.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithTargetPortMultipleEndpoints.verified.json @@ -1,6 +1,6 @@ -{ +{ "type": "azure.bicep.v0", - "path": "project2.module.bicep", + "path": "project2-website.module.bicep", "params": { "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env_outputs_planid": "{env.outputs.planId}", @@ -11,4 +11,4 @@ "env_outputs_azure_website_contributor_managed_identity_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_ID}", "env_outputs_azure_website_contributor_managed_identity_principal_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_PRINCIPAL_ID}" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsDeploymentTargetWithContainerAppToProjectResources.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsDeploymentTargetWithContainerAppToProjectResources.verified.bicep index 728e1aa2cf5..7edbd21c53b 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsDeploymentTargetWithContainerAppToProjectResources.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsDeploymentTargetWithContainerAppToProjectResources.verified.bicep @@ -106,7 +106,7 @@ resource webapp 'Microsoft.Web/sites@2024-11-01' = { } } -resource api_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource api_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) properties: { principalId: env_outputs_azure_website_contributor_managed_identity_principal_id diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsDeploymentTargetWithContainerAppToProjectResources.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsDeploymentTargetWithContainerAppToProjectResources.verified.json index dbf9ec3989a..96f4f2b2558 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsDeploymentTargetWithContainerAppToProjectResources.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsDeploymentTargetWithContainerAppToProjectResources.verified.json @@ -1,6 +1,6 @@ -{ +{ "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-website.module.bicep", "params": { "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env_outputs_planid": "{env.outputs.planId}", @@ -12,4 +12,4 @@ "env_outputs_azure_website_contributor_managed_identity_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_ID}", "env_outputs_azure_website_contributor_managed_identity_principal_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_PRINCIPAL_ID}" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddDockerfileWithAppServiceInfrastructureAddsDeploymentTargetWithAppServiceToContainerResources.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddDockerfileWithAppServiceInfrastructureAddsDeploymentTargetWithAppServiceToContainerResources.verified.bicep index 6c09bee4d1c..f7eaa081a05 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddDockerfileWithAppServiceInfrastructureAddsDeploymentTargetWithAppServiceToContainerResources.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddDockerfileWithAppServiceInfrastructureAddsDeploymentTargetWithAppServiceToContainerResources.verified.bicep @@ -1,4 +1,4 @@ -@description('The location for the resource(s) to be deployed.') +@description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location param env_outputs_azure_container_registry_endpoint string @@ -87,7 +87,7 @@ resource webapp 'Microsoft.Web/sites@2024-11-01' = { } } -resource api_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource api_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) properties: { principalId: env_outputs_azure_website_contributor_managed_identity_principal_id @@ -95,4 +95,4 @@ resource api_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { principalType: 'ServicePrincipal' } scope: webapp -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddDockerfileWithAppServiceInfrastructureAddsDeploymentTargetWithAppServiceToContainerResources.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddDockerfileWithAppServiceInfrastructureAddsDeploymentTargetWithAppServiceToContainerResources.verified.json index 069979ae62c..fda03f7e543 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddDockerfileWithAppServiceInfrastructureAddsDeploymentTargetWithAppServiceToContainerResources.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddDockerfileWithAppServiceInfrastructureAddsDeploymentTargetWithAppServiceToContainerResources.verified.json @@ -1,6 +1,6 @@ -{ +{ "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-website.module.bicep", "params": { "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env_outputs_planid": "{env.outputs.planId}", @@ -11,4 +11,4 @@ "env_outputs_azure_website_contributor_managed_identity_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_ID}", "env_outputs_azure_website_contributor_managed_identity_principal_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_PRINCIPAL_ID}" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceSupportBaitAndSwitchResources.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceSupportBaitAndSwitchResources.verified.bicep index 8c18dddc46e..8cb538e8520 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceSupportBaitAndSwitchResources.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceSupportBaitAndSwitchResources.verified.bicep @@ -1,4 +1,4 @@ -@description('The location for the resource(s) to be deployed.') +@description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location param env_outputs_azure_container_registry_endpoint string @@ -103,7 +103,7 @@ resource webapp 'Microsoft.Web/sites@2024-11-01' = { } } -resource api_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource api_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) properties: { principalId: env_outputs_azure_website_contributor_managed_identity_principal_id @@ -111,4 +111,4 @@ resource api_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { principalType: 'ServicePrincipal' } scope: webapp -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceSupportBaitAndSwitchResources.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceSupportBaitAndSwitchResources.verified.json index 069979ae62c..fda03f7e543 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceSupportBaitAndSwitchResources.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceSupportBaitAndSwitchResources.verified.json @@ -1,6 +1,6 @@ -{ +{ "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-website.module.bicep", "params": { "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env_outputs_planid": "{env.outputs.planId}", @@ -11,4 +11,4 @@ "env_outputs_azure_website_contributor_managed_identity_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_ID}", "env_outputs_azure_website_contributor_managed_identity_principal_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_PRINCIPAL_ID}" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.EndpointReferencesAreResolvedAcrossProjects.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.EndpointReferencesAreResolvedAcrossProjects.verified.bicep index 2ea639553fa..053ddceac2a 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.EndpointReferencesAreResolvedAcrossProjects.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.EndpointReferencesAreResolvedAcrossProjects.verified.bicep @@ -113,7 +113,7 @@ resource webapp 'Microsoft.Web/sites@2024-11-01' = { } } -resource project2_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource project2_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) properties: { principalId: env_outputs_azure_website_contributor_managed_identity_principal_id diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.EndpointReferencesAreResolvedAcrossProjects.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.EndpointReferencesAreResolvedAcrossProjects.verified.json index f850deb03d4..8767b810415 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.EndpointReferencesAreResolvedAcrossProjects.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.EndpointReferencesAreResolvedAcrossProjects.verified.json @@ -1,6 +1,6 @@ -{ +{ "type": "azure.bicep.v0", - "path": "project2.module.bicep", + "path": "project2-website.module.bicep", "params": { "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env_outputs_planid": "{env.outputs.planId}", @@ -12,4 +12,4 @@ "env_outputs_azure_website_contributor_managed_identity_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_ID}", "env_outputs_azure_website_contributor_managed_identity_principal_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_PRINCIPAL_ID}" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.KeyvaultReferenceHandling.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.KeyvaultReferenceHandling.verified.bicep index 0b1781cc434..4338bba5136 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.KeyvaultReferenceHandling.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.KeyvaultReferenceHandling.verified.bicep @@ -144,7 +144,7 @@ resource webapp 'Microsoft.Web/sites@2024-11-01' = { } } -resource api_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource api_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) properties: { principalId: env_outputs_azure_website_contributor_managed_identity_principal_id diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.KeyvaultReferenceHandling.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.KeyvaultReferenceHandling.verified.json index a06b13cfce1..2f44c95b6b6 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.KeyvaultReferenceHandling.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.KeyvaultReferenceHandling.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-website.module.bicep", "params": { "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env_outputs_planid": "{env.outputs.planId}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.MultipleAzureAppServiceEnvironmentsSupported.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.MultipleAzureAppServiceEnvironmentsSupported.verified.json index 576fa8a046d..325c9cbf4fb 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.MultipleAzureAppServiceEnvironmentsSupported.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.MultipleAzureAppServiceEnvironmentsSupported.verified.json @@ -1,4 +1,4 @@ -{ +{ "$schema": "https://json.schemastore.org/aspire-8.0.json", "resources": { "env1": { @@ -19,7 +19,7 @@ "type": "project.v1", "deployment": { "type": "azure.bicep.v0", - "path": "ServiceA.module.bicep", + "path": "ServiceA-website.module.bicep", "params": { "env1_outputs_azure_container_registry_endpoint": "{env1.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env1_outputs_planid": "{env1.outputs.planId}", @@ -58,7 +58,7 @@ "type": "project.v1", "deployment": { "type": "azure.bicep.v0", - "path": "ServiceB.module.bicep", + "path": "ServiceB-website.module.bicep", "params": { "env2_outputs_azure_container_registry_endpoint": "{env2.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env2_outputs_planid": "{env2.outputs.planId}", @@ -94,4 +94,4 @@ } } } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.ResourceWithProbes.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.ResourceWithProbes.verified.bicep index a8506781256..d55cd08d401 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.ResourceWithProbes.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.ResourceWithProbes.verified.bicep @@ -106,7 +106,7 @@ resource webapp 'Microsoft.Web/sites@2024-11-01' = { } } -resource project1_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource project1_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) properties: { principalId: env_outputs_azure_website_contributor_managed_identity_principal_id diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.UnknownManifestExpressionProviderIsHandledWithAllocateParameter.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.UnknownManifestExpressionProviderIsHandledWithAllocateParameter.verified.bicep index 9e640f0052e..ee2b765565e 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.UnknownManifestExpressionProviderIsHandledWithAllocateParameter.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.UnknownManifestExpressionProviderIsHandledWithAllocateParameter.verified.bicep @@ -111,7 +111,7 @@ resource webapp 'Microsoft.Web/sites@2024-11-01' = { } } -resource api_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource api_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(webapp.id, env_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) properties: { principalId: env_outputs_azure_website_contributor_managed_identity_principal_id diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.UnknownManifestExpressionProviderIsHandledWithAllocateParameter.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.UnknownManifestExpressionProviderIsHandledWithAllocateParameter.verified.json index c22be722ee9..b7b48a3336c 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.UnknownManifestExpressionProviderIsHandledWithAllocateParameter.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.UnknownManifestExpressionProviderIsHandledWithAllocateParameter.verified.json @@ -1,6 +1,6 @@ -{ +{ "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-website.module.bicep", "params": { "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", "env_outputs_planid": "{env.outputs.planId}", @@ -13,4 +13,4 @@ "env_outputs_azure_website_contributor_managed_identity_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_ID}", "env_outputs_azure_website_contributor_managed_identity_principal_id": "{env.outputs.AZURE_WEBSITE_CONTRIBUTOR_MANAGED_IDENTITY_PRINCIPAL_ID}" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsDeploymentTargetWithContainerAppToProjectResources.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsDeploymentTargetWithContainerAppToProjectResources.verified.json index c98f5c51623..4ef6c2b9922 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsDeploymentTargetWithContainerAppToProjectResources.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsDeploymentTargetWithContainerAppToProjectResources.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppsEntrypointAndArgs.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppsEntrypointAndArgs.verified.json index 0fcb1f910b5..e4d4267563d 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppsEntrypointAndArgs.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppsEntrypointAndArgs.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources.verified.json index 0fcb1f910b5..e4d4267563d 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppsInfrastructureWithParameterReference.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppsInfrastructureWithParameterReference.verified.json index 14fc6a102fa..afc10fe9ec5 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppsInfrastructureWithParameterReference.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppsInfrastructureWithParameterReference.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddDockerfileWithAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddDockerfileWithAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources.verified.json index 238bdde83df..8e21fc7d3aa 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddDockerfileWithAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddDockerfileWithAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddExecutableResourceWithPublishAsDockerFileWithAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddExecutableResourceWithPublishAsDockerFileWithAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources.verified.json index cc5ae885dba..7d8eaba5e84 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddExecutableResourceWithPublishAsDockerFileWithAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddExecutableResourceWithPublishAsDockerFileWithAppsInfrastructureAddsDeploymentTargetWithContainerAppToContainerResources.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "infra_outputs_azure_container_apps_environment_default_domain": "{infra.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "infra_outputs_azure_container_apps_environment_id": "{infra.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AzureContainerAppsBicepGenerationIsIdempotent.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AzureContainerAppsBicepGenerationIsIdempotent.verified.json index 2b4cbfdc341..b21b6d065c8 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AzureContainerAppsBicepGenerationIsIdempotent.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AzureContainerAppsBicepGenerationIsIdempotent.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AzureContainerAppsMapsPortsForBaitAndSwitchResources.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AzureContainerAppsMapsPortsForBaitAndSwitchResources.verified.json index 238bdde83df..8e21fc7d3aa 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AzureContainerAppsMapsPortsForBaitAndSwitchResources.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AzureContainerAppsMapsPortsForBaitAndSwitchResources.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.CanTweakContainerAppEnvironmentUsingPublishAsContainerAppOnExecutable.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.CanTweakContainerAppEnvironmentUsingPublishAsContainerAppOnExecutable.verified.json index 238bdde83df..8e21fc7d3aa 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.CanTweakContainerAppEnvironmentUsingPublishAsContainerAppOnExecutable.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.CanTweakContainerAppEnvironmentUsingPublishAsContainerAppOnExecutable.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ConfigureCustomDomainMutatesIngress.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ConfigureCustomDomainMutatesIngress.verified.json index a445485532a..dac29beaf2c 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ConfigureCustomDomainMutatesIngress.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ConfigureCustomDomainMutatesIngress.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ConfigureDuplicateCustomDomainMutatesIngress.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ConfigureDuplicateCustomDomainMutatesIngress.verified.json index a98458fdccc..1882810f3f3 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ConfigureDuplicateCustomDomainMutatesIngress.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ConfigureDuplicateCustomDomainMutatesIngress.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ConfigureMultipleCustomDomainsMutatesIngress.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ConfigureMultipleCustomDomainsMutatesIngress.verified.json index a455c50e426..b0e22a6a9d6 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ConfigureMultipleCustomDomainsMutatesIngress.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ConfigureMultipleCustomDomainsMutatesIngress.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomRegistry#01.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomRegistry#01.verified.json index c98f5c51623..4ef6c2b9922 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomRegistry#01.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomRegistry#01.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomWorkspace#01.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomWorkspace#01.verified.json index c98f5c51623..4ef6c2b9922 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomWorkspace#01.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomWorkspace#01.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppWithUppercaseName_ShouldUseLowercaseInManifest.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppWithUppercaseName_ShouldUseLowercaseInManifest.verified.json index ace000953d7..5cbaa29130f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppWithUppercaseName_ShouldUseLowercaseInManifest.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppWithUppercaseName_ShouldUseLowercaseInManifest.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "WebFrontEnd.module.bicep", + "path": "WebFrontEnd-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.EndpointWithHttp2SetsTransportToH2.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.EndpointWithHttp2SetsTransportToH2.verified.json index 0fcb1f910b5..e4d4267563d 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.EndpointWithHttp2SetsTransportToH2.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.EndpointWithHttp2SetsTransportToH2.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ExternalEndpointBecomesIngress.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ExternalEndpointBecomesIngress.verified.json index 0fcb1f910b5..e4d4267563d 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ExternalEndpointBecomesIngress.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ExternalEndpointBecomesIngress.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.FirstHttpEndpointBecomesIngress.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.FirstHttpEndpointBecomesIngress.verified.json index 0fcb1f910b5..e4d4267563d 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.FirstHttpEndpointBecomesIngress.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.FirstHttpEndpointBecomesIngress.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.KeyVaultReferenceHandling.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.KeyVaultReferenceHandling.verified.json index 525f3931bdd..fac638bbdc7 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.KeyVaultReferenceHandling.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.KeyVaultReferenceHandling.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleAzureContainerAppEnvironmentsSupported.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleAzureContainerAppEnvironmentsSupported.verified.json index c3093e8c202..5a40e7831db 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleAzureContainerAppEnvironmentsSupported.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleAzureContainerAppEnvironmentsSupported.verified.json @@ -20,7 +20,7 @@ "image": "myimage:latest", "deployment": { "type": "azure.bicep.v0", - "path": "api1.module.bicep", + "path": "api1-containerapp.module.bicep", "params": { "env1_outputs_azure_container_apps_environment_default_domain": "{env1.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env1_outputs_azure_container_apps_environment_id": "{env1.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" @@ -32,7 +32,7 @@ "image": "myimage:latest", "deployment": { "type": "azure.bicep.v0", - "path": "api2.module.bicep", + "path": "api2-containerapp.module.bicep", "params": { "env2_outputs_azure_container_apps_environment_default_domain": "{env2.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env2_outputs_azure_container_apps_environment_id": "{env2.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleVolumesHaveUniqueNamesInBicep#00.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleVolumesHaveUniqueNamesInBicep#00.verified.txt index 5effc1b9315..391026329a1 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleVolumesHaveUniqueNamesInBicep#00.verified.txt +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleVolumesHaveUniqueNamesInBicep#00.verified.txt @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "druid.module.bicep", + "path": "druid-containerapp.module.bicep", "params": { "my_ace_outputs_azure_container_apps_environment_default_domain": "{my-ace.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "my_ace_outputs_azure_container_apps_environment_id": "{my-ace.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ProjectUsesTheTargetPortAsADefaultPortForFirstHttpEndpoint.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ProjectUsesTheTargetPortAsADefaultPortForFirstHttpEndpoint.verified.json index c98f5c51623..4ef6c2b9922 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ProjectUsesTheTargetPortAsADefaultPortForFirstHttpEndpoint.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ProjectUsesTheTargetPortAsADefaultPortForFirstHttpEndpoint.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ProjectWithManyReferenceTypes#00.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ProjectWithManyReferenceTypes#00.verified.json index b319434f1f8..2f413e53a05 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ProjectWithManyReferenceTypes#00.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ProjectWithManyReferenceTypes#00.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ProjectWithManyReferenceTypesAndContainerAppEnvironment#00.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ProjectWithManyReferenceTypesAndContainerAppEnvironment#00.verified.json index 6a639b087fc..579437490d6 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ProjectWithManyReferenceTypesAndContainerAppEnvironment#00.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ProjectWithManyReferenceTypesAndContainerAppEnvironment#00.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "cae_outputs_azure_container_apps_environment_default_domain": "{cae.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "cae_outputs_azure_container_apps_environment_id": "{cae.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.PublishAsAzureContainerAppJobParameterlessConfiguresManualTrigger.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.PublishAsAzureContainerAppJobParameterlessConfiguresManualTrigger.verified.json index 97884e5d4d1..1dd75ceaf7b 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.PublishAsAzureContainerAppJobParameterlessConfiguresManualTrigger.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.PublishAsAzureContainerAppJobParameterlessConfiguresManualTrigger.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "manual-job.module.bicep", + "path": "manual-job-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.PublishAsContainerAppInfluencesContainerAppDefinition.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.PublishAsContainerAppInfluencesContainerAppDefinition.verified.json index 0fcb1f910b5..e4d4267563d 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.PublishAsContainerAppInfluencesContainerAppDefinition.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.PublishAsContainerAppInfluencesContainerAppDefinition.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.PublishAsScheduledAzureContainerAppJobConfiguresScheduleTrigger.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.PublishAsScheduledAzureContainerAppJobConfiguresScheduleTrigger.verified.json index 8931f696e7d..e5b26823a36 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.PublishAsScheduledAzureContainerAppJobConfiguresScheduleTrigger.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.PublishAsScheduledAzureContainerAppJobConfiguresScheduleTrigger.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "scheduled-job.module.bicep", + "path": "scheduled-job-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}" diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.RoleAssignmentsWithAsExisting#00.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.RoleAssignmentsWithAsExisting#00.verified.json index 50c859e1f61..3b0b80ee815 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.RoleAssignmentsWithAsExisting#00.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.RoleAssignmentsWithAsExisting#00.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.RoleAssignmentsWithAsExistingCosmosDB#00.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.RoleAssignmentsWithAsExistingCosmosDB#00.verified.json index 2c2c8b653e5..f9f6329e2dd 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.RoleAssignmentsWithAsExistingCosmosDB#00.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.RoleAssignmentsWithAsExistingCosmosDB#00.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.RoleAssignmentsWithAsExistingRedis#00.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.RoleAssignmentsWithAsExistingRedis#00.verified.json index 3aff84e132a..b8d817c91bc 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.RoleAssignmentsWithAsExistingRedis#00.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.RoleAssignmentsWithAsExistingRedis#00.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.UnknownManifestExpressionProviderIsHandledWithAllocateParameter.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.UnknownManifestExpressionProviderIsHandledWithAllocateParameter.verified.json index 66b18da51f9..4e93c051d73 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.UnknownManifestExpressionProviderIsHandledWithAllocateParameter.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.UnknownManifestExpressionProviderIsHandledWithAllocateParameter.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.VolumesAndBindMountsAreTranslation.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.VolumesAndBindMountsAreTranslation.verified.json index 54a98ef51a4..163a8f29549 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.VolumesAndBindMountsAreTranslation.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.VolumesAndBindMountsAreTranslation.verified.json @@ -1,6 +1,6 @@ { "type": "azure.bicep.v0", - "path": "api.module.bicep", + "path": "api-containerapp.module.bicep", "params": { "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithMultipleComputeEnvironments_Works_step=diagnostics.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithMultipleComputeEnvironments_Works_step=diagnostics.verified.txt new file mode 100644 index 00000000000..00fae39fd7d --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithMultipleComputeEnvironments_Works_step=diagnostics.verified.txt @@ -0,0 +1,515 @@ +[ +PIPELINE DEPENDENCY GRAPH DIAGNOSTICS +===================================== + +This diagnostic output shows the complete pipeline dependency graph structure. +Use this to understand step relationships and troubleshoot execution issues. + +Total steps defined: 32 + +Analysis for full pipeline execution (showing all steps and their relationships) + +EXECUTION ORDER +=============== +This shows the order in which steps would execute, respecting all dependencies. +Steps with no dependencies run first, followed by steps that depend on them. + + 1. build-prereq + 2. deploy-prereq + 3. build-api-service + 4. build-python-app + 5. build + 6. validate-azure-login + 7. create-provisioning-context + 8. provision-cache-containerapp + 9. print-cache-summary + 10. provision-aas-env + 11. provision-aca-env + 12. login-to-acr-aas-env + 13. push-api-service + 14. provision-api-service-website + 15. login-to-acr-aca-env + 16. push-python-app + 17. provision-python-app-containerapp + 18. provision-storage + 19. provision-azure-bicep-resources + 20. print-dashboard-url-aas-env + 21. print-dashboard-url-aca-env + 22. print-python-app-summary + 23. deploy + 24. print-api-service-summary + 25. deploy-api-service + 26. deploy-cache + 27. deploy-python-app + 28. diagnostics + 29. publish-prereq + 30. publish-azure634f9 + 31. publish + 32. publish-manifest + +DETAILED STEP ANALYSIS +====================== +Shows each step's dependencies, associated resources, and tags. +✓ = dependency exists, ? = dependency missing + +Step: build + Dependencies: ✓ build-api-service, ✓ build-python-app + +Step: build-api-service + Dependencies: ✓ build-prereq, ✓ deploy-prereq, ✓ deploy-prereq + Resource: api-service (ProjectResource) + Tags: build-compute + +Step: build-prereq + Dependencies: none + +Step: build-python-app + Dependencies: ✓ build-prereq, ✓ deploy-prereq, ✓ deploy-prereq + Resource: python-app (ContainerResource) + Tags: build-compute + +Step: create-provisioning-context + Dependencies: ✓ deploy-prereq, ✓ validate-azure-login + Resource: azure634f9 (AzureEnvironmentResource) + +Step: deploy + Dependencies: ✓ build-api-service, ✓ build-python-app, ✓ create-provisioning-context, ✓ print-cache-summary, ✓ print-dashboard-url-aas-env, ✓ print-dashboard-url-aca-env, ✓ print-python-app-summary, ✓ provision-azure-bicep-resources, ✓ validate-azure-login + +Step: deploy-api-service + Dependencies: ✓ print-api-service-summary + Resource: api-service-website (AzureAppServiceWebSiteResource) + Tags: deploy-compute + +Step: deploy-cache + Dependencies: ✓ print-cache-summary + Resource: cache-containerapp (AzureContainerAppResource) + Tags: deploy-compute + +Step: deploy-prereq + Dependencies: none + +Step: deploy-python-app + Dependencies: ✓ print-python-app-summary + Resource: python-app-containerapp (AzureContainerAppResource) + Tags: deploy-compute + +Step: diagnostics + Dependencies: none + +Step: login-to-acr-aas-env + Dependencies: none + Resource: aas-env (AzureAppServiceEnvironmentResource) + Tags: acr-login + +Step: login-to-acr-aca-env + Dependencies: ✓ provision-aca-env + Resource: aca-env (AzureContainerAppEnvironmentResource) + Tags: acr-login + +Step: print-api-service-summary + Dependencies: ✓ provision-api-service-website + Resource: api-service-website (AzureAppServiceWebSiteResource) + Tags: print-summary + +Step: print-cache-summary + Dependencies: ✓ provision-cache-containerapp + Resource: cache-containerapp (AzureContainerAppResource) + Tags: print-summary + +Step: print-dashboard-url-aas-env + Dependencies: ✓ provision-aas-env, ✓ provision-azure-bicep-resources + Resource: aas-env (AzureAppServiceEnvironmentResource) + Tags: print-summary + +Step: print-dashboard-url-aca-env + Dependencies: ✓ provision-aca-env, ✓ provision-azure-bicep-resources + Resource: aca-env (AzureContainerAppEnvironmentResource) + Tags: print-summary + +Step: print-python-app-summary + Dependencies: ✓ provision-python-app-containerapp + Resource: python-app-containerapp (AzureContainerAppResource) + Tags: print-summary + +Step: provision-aas-env + Dependencies: ✓ create-provisioning-context + Resource: aas-env (AzureAppServiceEnvironmentResource) + Tags: provision-infra + +Step: provision-aca-env + Dependencies: ✓ create-provisioning-context + Resource: aca-env (AzureContainerAppEnvironmentResource) + Tags: provision-infra + +Step: provision-api-service-website + Dependencies: ✓ create-provisioning-context, ✓ push-api-service + Resource: api-service-website (AzureAppServiceWebSiteResource) + Tags: provision-infra + +Step: provision-azure-bicep-resources + Dependencies: ✓ create-provisioning-context, ✓ deploy-prereq, ✓ provision-aas-env, ✓ provision-aca-env, ✓ provision-api-service-website, ✓ provision-cache-containerapp, ✓ provision-python-app-containerapp, ✓ provision-storage + Resource: azure634f9 (AzureEnvironmentResource) + Tags: provision-infra + +Step: provision-cache-containerapp + Dependencies: ✓ create-provisioning-context + Resource: cache-containerapp (AzureContainerAppResource) + Tags: provision-infra + +Step: provision-python-app-containerapp + Dependencies: ✓ create-provisioning-context, ✓ push-python-app + Resource: python-app-containerapp (AzureContainerAppResource) + Tags: provision-infra + +Step: provision-storage + Dependencies: ✓ create-provisioning-context + Resource: storage (AzureStorageResource) + Tags: provision-infra + +Step: publish + Dependencies: ✓ publish-azure634f9 + +Step: publish-azure634f9 + Dependencies: ✓ publish-prereq + Resource: azure634f9 (AzureEnvironmentResource) + +Step: publish-manifest + Dependencies: none + +Step: publish-prereq + Dependencies: none + +Step: push-api-service + Dependencies: ✓ build-api-service, ✓ login-to-acr-aas-env, ✓ provision-aas-env + Resource: api-service-website (AzureAppServiceWebSiteResource) + Tags: push-container-image + +Step: push-python-app + Dependencies: ✓ build-python-app, ✓ login-to-acr-aca-env, ✓ provision-aca-env + Resource: python-app-containerapp (AzureContainerAppResource) + Tags: push-container-image + +Step: validate-azure-login + Dependencies: ✓ deploy-prereq + Resource: azure634f9 (AzureEnvironmentResource) + +POTENTIAL ISSUES: +Identifies problems in the pipeline configuration that could prevent execution. +───────────────── +INFO: Orphaned steps (no dependencies, not required by others): + - diagnostics + - publish-manifest + +EXECUTION SIMULATION ("What If" Analysis): +Shows what steps would run for each possible target step and in what order. +Steps at the same level can run concurrently. +───────────────────────────────────────────────────────────────────────────── +If targeting 'build': + Direct dependencies: build-api-service, build-python-app + Total steps: 5 + Execution order: + [0] build-prereq | deploy-prereq (parallel) + [1] build-api-service | build-python-app (parallel) + [2] build + +If targeting 'build-api-service': + Direct dependencies: build-prereq, deploy-prereq, deploy-prereq + Total steps: 3 + Execution order: + [0] build-prereq | deploy-prereq (parallel) + [1] build-api-service + +If targeting 'build-prereq': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] build-prereq + +If targeting 'build-python-app': + Direct dependencies: build-prereq, deploy-prereq, deploy-prereq + Total steps: 3 + Execution order: + [0] build-prereq | deploy-prereq (parallel) + [1] build-python-app + +If targeting 'create-provisioning-context': + Direct dependencies: deploy-prereq, validate-azure-login + Total steps: 3 + Execution order: + [0] deploy-prereq + [1] validate-azure-login + [2] create-provisioning-context + +If targeting 'deploy': + Direct dependencies: build-api-service, build-python-app, create-provisioning-context, print-cache-summary, print-dashboard-url-aas-env, print-dashboard-url-aca-env, print-python-app-summary, provision-azure-bicep-resources, validate-azure-login + Total steps: 22 + Execution order: + [0] build-prereq | deploy-prereq | login-to-acr-aas-env (parallel) + [1] build-api-service | build-python-app | validate-azure-login (parallel) + [2] create-provisioning-context + [3] provision-aas-env | provision-aca-env | provision-cache-containerapp | provision-storage (parallel) + [4] login-to-acr-aca-env | print-cache-summary | push-api-service (parallel) + [5] provision-api-service-website | push-python-app (parallel) + [6] provision-python-app-containerapp + [7] print-python-app-summary | provision-azure-bicep-resources (parallel) + [8] print-dashboard-url-aas-env | print-dashboard-url-aca-env (parallel) + [9] deploy + +If targeting 'deploy-api-service': + Direct dependencies: print-api-service-summary + Total steps: 11 + Execution order: + [0] build-prereq | deploy-prereq | login-to-acr-aas-env (parallel) + [1] build-api-service | validate-azure-login (parallel) + [2] create-provisioning-context + [3] provision-aas-env + [4] push-api-service + [5] provision-api-service-website + [6] print-api-service-summary + [7] deploy-api-service + +If targeting 'deploy-cache': + Direct dependencies: print-cache-summary + Total steps: 6 + Execution order: + [0] deploy-prereq + [1] validate-azure-login + [2] create-provisioning-context + [3] provision-cache-containerapp + [4] print-cache-summary + [5] deploy-cache + +If targeting 'deploy-prereq': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] deploy-prereq + +If targeting 'deploy-python-app': + Direct dependencies: print-python-app-summary + Total steps: 11 + Execution order: + [0] build-prereq | deploy-prereq (parallel) + [1] build-python-app | validate-azure-login (parallel) + [2] create-provisioning-context + [3] provision-aca-env + [4] login-to-acr-aca-env + [5] push-python-app + [6] provision-python-app-containerapp + [7] print-python-app-summary + [8] deploy-python-app + +If targeting 'diagnostics': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] diagnostics + +If targeting 'login-to-acr-aas-env': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] login-to-acr-aas-env + +If targeting 'login-to-acr-aca-env': + Direct dependencies: provision-aca-env + Total steps: 5 + Execution order: + [0] deploy-prereq + [1] validate-azure-login + [2] create-provisioning-context + [3] provision-aca-env + [4] login-to-acr-aca-env + +If targeting 'print-api-service-summary': + Direct dependencies: provision-api-service-website + Total steps: 10 + Execution order: + [0] build-prereq | deploy-prereq | login-to-acr-aas-env (parallel) + [1] build-api-service | validate-azure-login (parallel) + [2] create-provisioning-context + [3] provision-aas-env + [4] push-api-service + [5] provision-api-service-website + [6] print-api-service-summary + +If targeting 'print-cache-summary': + Direct dependencies: provision-cache-containerapp + Total steps: 5 + Execution order: + [0] deploy-prereq + [1] validate-azure-login + [2] create-provisioning-context + [3] provision-cache-containerapp + [4] print-cache-summary + +If targeting 'print-dashboard-url-aas-env': + Direct dependencies: provision-aas-env, provision-azure-bicep-resources + Total steps: 18 + Execution order: + [0] build-prereq | deploy-prereq | login-to-acr-aas-env (parallel) + [1] build-api-service | build-python-app | validate-azure-login (parallel) + [2] create-provisioning-context + [3] provision-aas-env | provision-aca-env | provision-cache-containerapp | provision-storage (parallel) + [4] login-to-acr-aca-env | push-api-service (parallel) + [5] provision-api-service-website | push-python-app (parallel) + [6] provision-python-app-containerapp + [7] provision-azure-bicep-resources + [8] print-dashboard-url-aas-env + +If targeting 'print-dashboard-url-aca-env': + Direct dependencies: provision-aca-env, provision-azure-bicep-resources + Total steps: 18 + Execution order: + [0] build-prereq | deploy-prereq | login-to-acr-aas-env (parallel) + [1] build-api-service | build-python-app | validate-azure-login (parallel) + [2] create-provisioning-context + [3] provision-aas-env | provision-aca-env | provision-cache-containerapp | provision-storage (parallel) + [4] login-to-acr-aca-env | push-api-service (parallel) + [5] provision-api-service-website | push-python-app (parallel) + [6] provision-python-app-containerapp + [7] provision-azure-bicep-resources + [8] print-dashboard-url-aca-env + +If targeting 'print-python-app-summary': + Direct dependencies: provision-python-app-containerapp + Total steps: 10 + Execution order: + [0] build-prereq | deploy-prereq (parallel) + [1] build-python-app | validate-azure-login (parallel) + [2] create-provisioning-context + [3] provision-aca-env + [4] login-to-acr-aca-env + [5] push-python-app + [6] provision-python-app-containerapp + [7] print-python-app-summary + +If targeting 'provision-aas-env': + Direct dependencies: create-provisioning-context + Total steps: 4 + Execution order: + [0] deploy-prereq + [1] validate-azure-login + [2] create-provisioning-context + [3] provision-aas-env + +If targeting 'provision-aca-env': + Direct dependencies: create-provisioning-context + Total steps: 4 + Execution order: + [0] deploy-prereq + [1] validate-azure-login + [2] create-provisioning-context + [3] provision-aca-env + +If targeting 'provision-api-service-website': + Direct dependencies: create-provisioning-context, push-api-service + Total steps: 9 + Execution order: + [0] build-prereq | deploy-prereq | login-to-acr-aas-env (parallel) + [1] build-api-service | validate-azure-login (parallel) + [2] create-provisioning-context + [3] provision-aas-env + [4] push-api-service + [5] provision-api-service-website + +If targeting 'provision-azure-bicep-resources': + Direct dependencies: create-provisioning-context, deploy-prereq, provision-aas-env, provision-aca-env, provision-api-service-website, provision-cache-containerapp, provision-python-app-containerapp, provision-storage + Total steps: 17 + Execution order: + [0] build-prereq | deploy-prereq | login-to-acr-aas-env (parallel) + [1] build-api-service | build-python-app | validate-azure-login (parallel) + [2] create-provisioning-context + [3] provision-aas-env | provision-aca-env | provision-cache-containerapp | provision-storage (parallel) + [4] login-to-acr-aca-env | push-api-service (parallel) + [5] provision-api-service-website | push-python-app (parallel) + [6] provision-python-app-containerapp + [7] provision-azure-bicep-resources + +If targeting 'provision-cache-containerapp': + Direct dependencies: create-provisioning-context + Total steps: 4 + Execution order: + [0] deploy-prereq + [1] validate-azure-login + [2] create-provisioning-context + [3] provision-cache-containerapp + +If targeting 'provision-python-app-containerapp': + Direct dependencies: create-provisioning-context, push-python-app + Total steps: 9 + Execution order: + [0] build-prereq | deploy-prereq (parallel) + [1] build-python-app | validate-azure-login (parallel) + [2] create-provisioning-context + [3] provision-aca-env + [4] login-to-acr-aca-env + [5] push-python-app + [6] provision-python-app-containerapp + +If targeting 'provision-storage': + Direct dependencies: create-provisioning-context + Total steps: 4 + Execution order: + [0] deploy-prereq + [1] validate-azure-login + [2] create-provisioning-context + [3] provision-storage + +If targeting 'publish': + Direct dependencies: publish-azure634f9 + Total steps: 3 + Execution order: + [0] publish-prereq + [1] publish-azure634f9 + [2] publish + +If targeting 'publish-azure634f9': + Direct dependencies: publish-prereq + Total steps: 2 + Execution order: + [0] publish-prereq + [1] publish-azure634f9 + +If targeting 'publish-manifest': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] publish-manifest + +If targeting 'publish-prereq': + Direct dependencies: none + Total steps: 1 + Execution order: + [0] publish-prereq + +If targeting 'push-api-service': + Direct dependencies: build-api-service, login-to-acr-aas-env, provision-aas-env + Total steps: 8 + Execution order: + [0] build-prereq | deploy-prereq | login-to-acr-aas-env (parallel) + [1] build-api-service | validate-azure-login (parallel) + [2] create-provisioning-context + [3] provision-aas-env + [4] push-api-service + +If targeting 'push-python-app': + Direct dependencies: build-python-app, login-to-acr-aca-env, provision-aca-env + Total steps: 8 + Execution order: + [0] build-prereq | deploy-prereq (parallel) + [1] build-python-app | validate-azure-login (parallel) + [2] create-provisioning-context + [3] provision-aca-env + [4] login-to-acr-aca-env + [5] push-python-app + +If targeting 'validate-azure-login': + Direct dependencies: deploy-prereq + Total steps: 2 + Execution order: + [0] deploy-prereq + [1] validate-azure-login + + +] \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureUserAssignedIdentityTests.WithAzureUserAssignedIdentity_WithRoleAssignments_AzureAppService_Works#00.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureUserAssignedIdentityTests.WithAzureUserAssignedIdentity_WithRoleAssignments_AzureAppService_Works#00.verified.bicep index 8acada9249b..c9845890936 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureUserAssignedIdentityTests.WithAzureUserAssignedIdentity_WithRoleAssignments_AzureAppService_Works#00.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureUserAssignedIdentityTests.WithAzureUserAssignedIdentity_WithRoleAssignments_AzureAppService_Works#00.verified.bicep @@ -111,7 +111,7 @@ resource webapp 'Microsoft.Web/sites@2024-11-01' = { } } -resource myapp_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource myapp_website_ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: guid(webapp.id, appservice_outputs_azure_website_contributor_managed_identity_id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'de139f84-1756-47ae-9be6-808fbbe84772')) properties: { principalId: appservice_outputs_azure_website_contributor_managed_identity_principal_id diff --git a/tests/Aspire.Hosting.Containers.Tests/WithDockerfileTests.cs b/tests/Aspire.Hosting.Containers.Tests/WithDockerfileTests.cs index 7cc1e5d7b04..0ad78a44613 100644 --- a/tests/Aspire.Hosting.Containers.Tests/WithDockerfileTests.cs +++ b/tests/Aspire.Hosting.Containers.Tests/WithDockerfileTests.cs @@ -1,10 +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 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + using Aspire.TestUtilities; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Dcp; using Aspire.Hosting.Dcp.Model; +using Aspire.Hosting.Pipelines; using Aspire.Hosting.Testing; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; @@ -101,7 +104,7 @@ public async Task AddDockerfileUsesLowercaseResourceNameAsImageName(string resou // The effective image name (from TryGetContainerImageName) should be the lowercase resource name Assert.True(dockerFile.Resource.TryGetContainerImageName(out var imageName)); Assert.StartsWith(resourceName.ToLowerInvariant() + ":", imageName); - + // The DockerfileBuildAnnotation should have the generated image name Assert.True(dockerFile.Resource.TryGetLastAnnotation(out var buildAnnotation)); Assert.Equal(resourceName.ToLowerInvariant(), buildAnnotation.ImageName); @@ -125,11 +128,11 @@ public async Task WithDockerfileUsesLowercaseResourceNameAsImageName(string reso // After the changes, ContainerImageAnnotation should be preserved Assert.True(dockerFile.Resource.TryGetLastAnnotation(out var containerImageAnnotation)); Assert.Equal("someimagename", containerImageAnnotation.Image); - + // The generated image name should be stored in DockerfileBuildAnnotation Assert.True(dockerFile.Resource.TryGetLastAnnotation(out var buildAnnotation)); Assert.Equal(resourceName.ToLowerInvariant(), buildAnnotation.ImageName); - + // TryGetContainerImageName should return the DockerfileBuildAnnotation image name Assert.True(dockerFile.Resource.TryGetContainerImageName(out var imageName)); Assert.StartsWith(resourceName.ToLowerInvariant() + ":", imageName); @@ -175,7 +178,7 @@ public async Task WithDockerfileGeneratedImageTagCanBeOverridden() Assert.NotEqual(generatedTag, overriddenTag); Assert.Equal("latest", overriddenTag); - + // Verify that TryGetContainerImageName returns the overridden tag Assert.True(dockerFile.Resource.TryGetContainerImageName(out var imageName)); Assert.EndsWith(":latest", imageName); @@ -773,12 +776,26 @@ public async Task WithDockerfileFactorySyncFactoryCreatesAnnotationWithFactory() Assert.Equal(tempContextPath, annotation.ContextPath); Assert.NotNull(annotation.DockerfileFactory); + var stepsAnnotation = Assert.Single(container.Resource.Annotations.OfType()); + + var factoryContext = new PipelineStepFactoryContext + { + PipelineContext = null!, + Resource = container.Resource + }; + var steps = (await stepsAnnotation.CreateStepsAsync(factoryContext)).ToList(); + var buildStep = Assert.Single(steps); + Assert.Equal("build-mycontainer", buildStep.Name); + Assert.Contains(WellKnownPipelineTags.BuildCompute, buildStep.Tags); + Assert.Contains(WellKnownPipelineSteps.Build, buildStep.RequiredBySteps); + Assert.Contains(WellKnownPipelineSteps.BuildPrereq, buildStep.DependsOnSteps); + // Verify the factory produces the expected content - var context = new DockerfileFactoryContext - { - Services = builder.Services.BuildServiceProvider(), + var context = new DockerfileFactoryContext + { + Services = builder.Services.BuildServiceProvider(), Resource = container.Resource, - CancellationToken = CancellationToken.None + CancellationToken = CancellationToken.None }; var generatedContent = await annotation.DockerfileFactory(context); @@ -806,11 +823,11 @@ public async Task WithDockerfileFactoryAsyncFactoryCreatesAnnotationWithFactory( Assert.NotNull(annotation.DockerfileFactory); // Verify the factory produces the expected content - var context = new DockerfileFactoryContext - { - Services = builder.Services.BuildServiceProvider(), + var context = new DockerfileFactoryContext + { + Services = builder.Services.BuildServiceProvider(), Resource = container.Resource, - CancellationToken = CancellationToken.None + CancellationToken = CancellationToken.None }; var generatedContent = await annotation.DockerfileFactory(context); @@ -896,4 +913,74 @@ await Verify(actualContent) } } + [Fact] + public async Task WithDockerfile_AutomaticallyGeneratesBuildStep_WithCorrectDependencies() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var (tempContextPath, tempDockerfilePath) = await DockerfileUtils.CreateTemporaryDockerfileAsync(); + + builder.AddContainer("test-container", "test-image") + .WithDockerfile(tempContextPath, tempDockerfilePath); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var containerResources = appModel.GetContainerResources(); + + var resource = Assert.Single(containerResources); + + // Verify the container has a PipelineStepAnnotation + var pipelineStepAnnotation = Assert.Single(resource.Annotations.OfType()); + + // Create a factory context for testing the annotation + var factoryContext = new PipelineStepFactoryContext + { + PipelineContext = null!, // Not needed for this test + Resource = resource + }; + + var steps = (await pipelineStepAnnotation.CreateStepsAsync(factoryContext)).ToList(); + + var buildStep = Assert.Single(steps); + Assert.Equal("build-test-container", buildStep.Name); + Assert.Contains(WellKnownPipelineTags.BuildCompute, buildStep.Tags); + Assert.Contains(WellKnownPipelineSteps.Build, buildStep.RequiredBySteps); + Assert.Contains(WellKnownPipelineSteps.BuildPrereq, buildStep.DependsOnSteps); + } + + [Fact] + public async Task WithDockerfile_CalledMultipleTimes_OverwritesPreviousBuildStep() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var (tempContextPath1, tempDockerfilePath1) = await DockerfileUtils.CreateTemporaryDockerfileAsync(); + var (tempContextPath2, tempDockerfilePath2) = await DockerfileUtils.CreateTemporaryDockerfileAsync(); + + var containerBuilder = builder.AddContainer("test-container", "test-image") + .WithDockerfile(tempContextPath1, tempDockerfilePath1) + .WithDockerfile(tempContextPath1, tempDockerfilePath1); // Call twice to start + + using var app1 = builder.Build(); + var appModel1 = app1.Services.GetRequiredService(); + var containerResources1 = appModel1.GetContainerResources(); + var resource1 = Assert.Single(containerResources1); + + // Get the first pipeline step annotation + var pipelineStepAnnotation1 = Assert.Single(resource1.Annotations.OfType()); + + // Both should create the same build step name + var factoryContext = new PipelineStepFactoryContext + { + PipelineContext = null!, // Not needed for this test + Resource = resource1 + }; + + var steps = (await pipelineStepAnnotation1.CreateStepsAsync(factoryContext)).ToList(); + var buildStep = Assert.Single(steps); + Assert.Equal("build-test-container", buildStep.Name); + Assert.Contains(WellKnownPipelineTags.BuildCompute, buildStep.Tags); + Assert.Contains(WellKnownPipelineSteps.Build, buildStep.RequiredBySteps); + Assert.Contains(WellKnownPipelineSteps.BuildPrereq, buildStep.DependsOnSteps); + } } diff --git a/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs b/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs index 2cb308ca9e2..4418a5fe494 100644 --- a/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs +++ b/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs @@ -656,174 +656,6 @@ public async Task ExecuteAsync_WithFailingStep_PreservesOriginalStackTrace() Assert.Contains("ThrowHelperMethod", exception.InnerException.StackTrace); } - [Fact] - public async Task PublishAsync_Deploy_WithNoResourcesAndNoPipelineSteps_Succeeds() - { - // Arrange - using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: null); - - var interactionService = PublishingActivityReporterTests.CreateInteractionService(); - var reporter = new PipelineActivityReporter(interactionService, NullLogger.Instance); - - builder.Services.AddSingleton(reporter); - - var app = builder.Build(); - var executor = app.Services.GetRequiredService(); - var model = app.Services.GetRequiredService(); - - // Act - await executor.ExecutePipelineAsync(model, CancellationToken.None); - - // Assert - Since the "deploy" step is now always present, this should succeed - var activityReader = reporter.ActivityItemUpdated.Reader; - var foundSuccessActivity = false; - - while (activityReader.TryRead(out var activity)) - { - if (activity.Type == PublishingActivityTypes.Task && - !activity.Data.IsError && - activity.Data.CompletionMessage == "Found pipeline steps in the application.") - { - foundSuccessActivity = true; - break; - } - } - - Assert.True(foundSuccessActivity, "Expected to find a task activity indicating deployment steps were found"); - } - - [Fact] - public async Task PublishAsync_Deploy_WithNoResourcesButHasPipelineSteps_Succeeds() - { - // Arrange - using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: null); - - var interactionService = PublishingActivityReporterTests.CreateInteractionService(); - var reporter = new PipelineActivityReporter(interactionService, NullLogger.Instance); - - builder.Services.AddSingleton(reporter); - - var pipeline = new DistributedApplicationPipeline(); - pipeline.AddStep("test-step", async (context) => await Task.CompletedTask); - - builder.Services.AddSingleton(pipeline); - - var app = builder.Build(); - var executor = app.Services.GetRequiredService(); - var model = app.Services.GetRequiredService(); - - // Act - await executor.ExecutePipelineAsync(model, CancellationToken.None); - - // Assert - var activityReader = reporter.ActivityItemUpdated.Reader; - var foundSuccessActivity = false; - - while (activityReader.TryRead(out var activity)) - { - if (activity.Type == PublishingActivityTypes.Task && - !activity.Data.IsError && - activity.Data.CompletionMessage == "Found pipeline steps in the application.") - { - foundSuccessActivity = true; - break; - } - } - - Assert.True(foundSuccessActivity, "Expected to find a task activity with message about deployment steps in the application pipeline"); - } - - [Fact] - public async Task PublishAsync_Deploy_WithResourcesAndPipelineSteps_ShowsStepsMessage() - { - // Arrange - using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: null); - - var interactionService = PublishingActivityReporterTests.CreateInteractionService(); - var reporter = new PipelineActivityReporter(interactionService, NullLogger.Instance); - - builder.Services.AddSingleton(reporter); - - var resource = builder.AddResource(new CustomResource("test-resource")) - .WithPipelineStepFactory((factoryContext) => new PipelineStep - { - Name = "annotated-step", - Action = async (ctx) => await Task.CompletedTask - }); - - var pipeline = new DistributedApplicationPipeline(); - pipeline.AddStep("direct-step", async (context) => await Task.CompletedTask); - - builder.Services.AddSingleton(pipeline); - - var app = builder.Build(); - var executor = app.Services.GetRequiredService(); - var model = app.Services.GetRequiredService(); - - // Act - await executor.ExecutePipelineAsync(model, CancellationToken.None); - - // Assert - var activityReader = reporter.ActivityItemUpdated.Reader; - var foundSuccessActivity = false; - - while (activityReader.TryRead(out var activity)) - { - if (activity.Type == PublishingActivityTypes.Task && - !activity.Data.IsError && - activity.Data.CompletionMessage == "Found pipeline steps in the application.") - { - foundSuccessActivity = true; - break; - } - } - - Assert.True(foundSuccessActivity, "Expected to find a task activity with message about deployment steps in the application pipeline"); - } - - [Fact] - public async Task PublishAsync_Deploy_WithOnlyResources_ShowsStepsMessage() - { - // Arrange - using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: null); - - var interactionService = PublishingActivityReporterTests.CreateInteractionService(); - var reporter = new PipelineActivityReporter(interactionService, NullLogger.Instance); - - builder.Services.AddSingleton(reporter); - - var resource = builder.AddResource(new CustomResource("test-resource")) - .WithPipelineStepFactory((factoryContext) => new PipelineStep - { - Name = "annotated-step", - Action = async (ctx) => await Task.CompletedTask - }); - - var app = builder.Build(); - var executor = app.Services.GetRequiredService(); - var model = app.Services.GetRequiredService(); - - // Act - await executor.ExecutePipelineAsync(model, CancellationToken.None); - - // Assert - var activityReader = reporter.ActivityItemUpdated.Reader; - var foundSuccessActivity = false; - - while (activityReader.TryRead(out var activity)) - { - if (activity.Type == PublishingActivityTypes.Task && - !activity.Data.IsError && - activity.Data.CompletionMessage == "Found pipeline steps in the application.") - { - foundSuccessActivity = true; - break; - } - } - - Assert.True(foundSuccessActivity, "Expected to find a task activity with message about deployment steps in the application pipeline"); - } - private static void ThrowHelperMethod() { throw new NotSupportedException("Test exception for stack trace"); @@ -1256,7 +1088,7 @@ public async Task ExecuteAsync_WithPipelineLoggerProvider_LogsToStepLogger() var stepActivities = activities.Where(a => a.Type == PublishingActivityTypes.Step).GroupBy(a => a.Data.Id).ToList(); var logActivities = activities.Where(a => a.Type == PublishingActivityTypes.Log).ToList(); - Assert.Equal(4, stepActivities.Count); // deploy, publish, parameter prompt, logging-step + Assert.Equal(8, stepActivities.Count); // deploy, deploy-prereq, build, build-prereq, publish, publish-prereq, diagnostics, logging-step // Find the logging-step activity var loggingStepActivity = stepActivities.FirstOrDefault(g => g.Any(a => a.Data.StatusText == "logging-step")); @@ -1271,11 +1103,12 @@ public async Task ExecuteAsync_WithPipelineLoggerProvider_LogsToStepLogger() { Assert.True(step.Data.IsComplete); }); - var logActivity = Assert.Single(logActivities); - Assert.Equal("Test log message from pipeline step", logActivity.Data.StatusText); - Assert.Equal("Information", logActivity.Data.LogLevel); - Assert.Equal(loggingStepActivity.First().Data.Id, logActivity.Data.StepId); - Assert.False(logActivity.Data.EnableMarkdown); + var testLogActivity = logActivities.SingleOrDefault(l => l.Data.StatusText == "Test log message from pipeline step"); + Assert.NotNull(testLogActivity); + Assert.Equal("Test log message from pipeline step", testLogActivity.Data.StatusText); + Assert.Equal("Information", testLogActivity.Data.LogLevel); + Assert.Equal(loggingStepActivity.First().Data.Id, testLogActivity.Data.StepId); + Assert.False(testLogActivity.Data.EnableMarkdown); } [Fact] @@ -1333,82 +1166,44 @@ public async Task ExecuteAsync_PipelineLoggerProvider_IsolatesLoggingBetweenStep activities.Add(activity); } - var stepOrder = new[] { "parameter-prompt", "publish", "deploy", "step1", "step2" }; // Added "deploy" step - var logOrder = new[] { "Message from step 1", "Message from step 2" }; - var stepActivities = activities.Where(a => a.Type == PublishingActivityTypes.Step) .GroupBy(a => a.Data.Id) - .OrderBy(g => Array.IndexOf(stepOrder, g.First().Data.StatusText)) .ToList(); var logActivities = activities.Where(a => a.Type == PublishingActivityTypes.Log) - .OrderBy(a => Array.IndexOf(logOrder, a.Data.StatusText)) + .Where(a => a.Data.StatusText is "Message from step 1" or "Message from step 2") + .OrderBy(a => a.Data.StatusText) .ToList(); - Assert.Collection(stepActivities, - parameterPromptActivity => - { - Assert.Collection(parameterPromptActivity, - step => - { - Assert.Equal("parameter-prompt", step.Data.StatusText); - Assert.False(step.Data.IsComplete); - }, - step => - { - Assert.True(step.Data.IsComplete); - }); - }, - publishActivity => + // Verify that we have the expected number of step activities (all default steps plus step1 and step2) + Assert.True(stepActivities.Count >= 5, $"Expected at least 5 step activities, but got {stepActivities.Count}"); + + // Find and verify step1 and step2 activities specifically + var step1Activity = stepActivities.FirstOrDefault(g => g.Any(a => a.Data.StatusText == "step1")); + var step2Activity = stepActivities.FirstOrDefault(g => g.Any(a => a.Data.StatusText == "step2")); + + Assert.NotNull(step1Activity); + Assert.NotNull(step2Activity); + + Assert.Collection(step1Activity, + step => { - Assert.Collection(publishActivity, - step => - { - Assert.Equal("publish", step.Data.StatusText); - Assert.False(step.Data.IsComplete); - }, - step => - { - Assert.True(step.Data.IsComplete); - }); + Assert.Equal("step1", step.Data.StatusText); + Assert.False(step.Data.IsComplete); }, - deployActivity => + step => { - Assert.Collection(deployActivity, - step => - { - Assert.Equal("deploy", step.Data.StatusText); - Assert.False(step.Data.IsComplete); - }, - step => - { - Assert.True(step.Data.IsComplete); - }); - }, - step1Activity => + Assert.True(step.Data.IsComplete); + }); + + Assert.Collection(step2Activity, + step => { - Assert.Collection(step1Activity, - step => - { - Assert.Equal("step1", step.Data.StatusText); - Assert.False(step.Data.IsComplete); - }, - step => - { - Assert.True(step.Data.IsComplete); - }); + Assert.Equal("step2", step.Data.StatusText); + Assert.False(step.Data.IsComplete); }, - step2Activity => + step => { - Assert.Collection(step2Activity, - step => - { - Assert.Equal("step2", step.Data.StatusText); - Assert.False(step.Data.IsComplete); - }, - step => - { - Assert.True(step.Data.IsComplete); - }); + Assert.True(step.Data.IsComplete); }); Assert.Collection(logActivities, @@ -1416,15 +1211,13 @@ public async Task ExecuteAsync_PipelineLoggerProvider_IsolatesLoggingBetweenStep { Assert.Equal("Message from step 1", logActivity.Data.StatusText); Assert.Equal("Information", logActivity.Data.LogLevel); - var step1ActivityGroup = stepActivities.First(g => g.First().Data.StatusText == "step1"); - Assert.Equal(step1ActivityGroup.First().Data.Id, logActivity.Data.StepId); + Assert.Equal(step1Activity.First().Data.Id, logActivity.Data.StepId); }, logActivity => { Assert.Equal("Message from step 2", logActivity.Data.StatusText); Assert.Equal("Information", logActivity.Data.LogLevel); - var step2ActivityGroup = stepActivities.First(g => g.First().Data.StatusText == "step2"); - Assert.Equal(step2ActivityGroup.First().Data.Id, logActivity.Data.StepId); + Assert.Equal(step2Activity.First().Data.Id, logActivity.Data.StepId); }); // After execution, current logger should be NullLogger @@ -1482,10 +1275,11 @@ public async Task ExecuteAsync_WhenStepFails_PipelineLoggerIsCleanedUp() Assert.True(step.Data.IsError); }); - var logActivity = Assert.Single(logActivities); - Assert.Equal("About to fail", logActivity.Data.StatusText); - Assert.Equal("Information", logActivity.Data.LogLevel); - Assert.Equal(failingStepActivity.First().Data.Id, logActivity.Data.StepId); + var aboutToFailLogActivity = logActivities.SingleOrDefault(l => l.Data.StatusText == "About to fail"); + Assert.NotNull(aboutToFailLogActivity); + Assert.Equal("About to fail", aboutToFailLogActivity.Data.StatusText); + Assert.Equal("Information", aboutToFailLogActivity.Data.LogLevel); + Assert.Equal(failingStepActivity.First().Data.Id, aboutToFailLogActivity.Data.StepId); // Verify logger is cleaned up even after failure Assert.Same(NullLogger.Instance, PipelineLoggerProvider.CurrentLogger); @@ -1547,7 +1341,7 @@ public async Task ExecuteAsync_PipelineLoggerProvider_PreservesLoggerAfterStepCo activities.Add(activity); } - var stepOrder = new[] { "parameter-prompt", "publish", "deploy", "step1", "step2", "step3" }; // Added "deploy" step + var stepOrder = new[] { "deploy-prereq", "build-prereq", "publish-prereq", "step1", "step2", "step3" }; // Updated for new pipeline structure var logOrder = new[] { "Executing step 1", "Executing step 2", "Executing step 3" }; var stepActivities = activities.Where(a => a.Type == PublishingActivityTypes.Step) @@ -1555,10 +1349,11 @@ public async Task ExecuteAsync_PipelineLoggerProvider_PreservesLoggerAfterStepCo .OrderBy(g => Array.IndexOf(stepOrder, g.First().Data.StatusText)) .ToList(); var logActivities = activities.Where(a => a.Type == PublishingActivityTypes.Log) + .Where(a => logOrder.Contains(a.Data.StatusText)) .OrderBy(a => Array.IndexOf(logOrder, a.Data.StatusText)) .ToList(); - Assert.Equal(6, stepActivities.Count); // deploy, parameter prompt, publish, step 1, step 2, step 3 + Assert.Equal(10, stepActivities.Count); // deploy, deploy-prereq, build, build-prereq, publish, publish-prereq, diagnostics, step1, step2, step3 Assert.Collection(logActivities, logActivity => { @@ -1695,10 +1490,14 @@ public async Task ExecuteAsync_WithConfigurationCallback_ExecutesCallback() await pipeline.ExecuteAsync(context); Assert.True(callbackExecuted); - Assert.Equal(5, capturedSteps.Count); // Updated to account for "deploy" step - Assert.Contains(capturedSteps, s => s.Name == "parameter-prompt"); - Assert.Contains(capturedSteps, s => s.Name == "publish"); + Assert.Equal(9, capturedSteps.Count); // Updated to account for all default steps Assert.Contains(capturedSteps, s => s.Name == "deploy"); + Assert.Contains(capturedSteps, s => s.Name == "deploy-prereq"); + Assert.Contains(capturedSteps, s => s.Name == "build"); + Assert.Contains(capturedSteps, s => s.Name == "build-prereq"); + Assert.Contains(capturedSteps, s => s.Name == "publish"); + Assert.Contains(capturedSteps, s => s.Name == "publish-prereq"); + Assert.Contains(capturedSteps, s => s.Name == "diagnostics"); Assert.Contains(capturedSteps, s => s.Name == "step1"); Assert.Contains(capturedSteps, s => s.Name == "step2"); } diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs index 386a7a14e67..efec63643c1 100644 --- a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs @@ -2,9 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable ASPIREPIPELINES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. using System.Text; +using Aspire.Hosting.Pipelines; using Aspire.Hosting.Publishing; +using Aspire.Hosting.Testing; using Aspire.Hosting.Tests.Helpers; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; @@ -731,6 +734,39 @@ public void SelectLaunchProfileName_AnnotationOverridesFiltering() Assert.Equal("IIS Express", selectedProfile); } + [Fact] + public async Task ProjectResource_AutomaticallyGeneratesBuildStep_WithCorrectDependencies() + { + var appBuilder = CreateBuilder(); + + appBuilder.AddProject("test-project", launchProfileName: null); + using var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var projectResources = appModel.GetProjectResources(); + + var resource = Assert.Single(projectResources); + + // Verify the project has a PipelineStepAnnotation + var pipelineStepAnnotation = Assert.Single(resource.Annotations.OfType()); + + // Create a factory context for testing the annotation + var factoryContext = new PipelineStepFactoryContext + { + PipelineContext = null!, // Not needed for this test + Resource = resource + }; + + var steps = (await pipelineStepAnnotation.CreateStepsAsync(factoryContext)).ToList(); + + var buildStep = Assert.Single(steps); + Assert.Equal("build-test-project", buildStep.Name); + Assert.Contains(WellKnownPipelineTags.BuildCompute, buildStep.Tags); + Assert.Contains(WellKnownPipelineSteps.Build, buildStep.RequiredBySteps); + Assert.Contains(WellKnownPipelineSteps.BuildPrereq, buildStep.DependsOnSteps); + } + internal static IDistributedApplicationBuilder CreateBuilder(string[]? args = null, DistributedApplicationOperation operation = DistributedApplicationOperation.Publish) { var resolvedArgs = new List(); diff --git a/tests/Aspire.Hosting.Tests/Publishing/PipelineActivityReporterTests.cs b/tests/Aspire.Hosting.Tests/Publishing/PipelineActivityReporterTests.cs index ec6c2f1c2e6..db199d08513 100644 --- a/tests/Aspire.Hosting.Tests/Publishing/PipelineActivityReporterTests.cs +++ b/tests/Aspire.Hosting.Tests/Publishing/PipelineActivityReporterTests.cs @@ -278,9 +278,9 @@ public async Task CompleteTaskAsync_ThrowsWhenParentStepIsComplete() } [Theory] - [InlineData(CompletionState.Completed, "Publishing completed successfully", false)] - [InlineData(CompletionState.CompletedWithError, "Publishing completed with errors", true)] - [InlineData(CompletionState.CompletedWithWarning, "Publishing completed with warnings", false)] + [InlineData(CompletionState.Completed, "Pipeline completed successfully", false)] + [InlineData(CompletionState.CompletedWithError, "Pipeline completed with errors", true)] + [InlineData(CompletionState.CompletedWithWarning, "Pipeline completed with warnings", false)] public async Task CompletePublishAsync_EmitsCorrectActivity(CompletionState completionState, string expectedStatusText, bool expectedIsError) { // Arrange @@ -353,7 +353,7 @@ public async Task CompletePublishAsync_AggregatesStateFromSteps() // Assert Assert.True(activityReader.TryRead(out var activity)); Assert.Equal(PublishingActivityTypes.PublishComplete, activity.Type); - Assert.Equal("Publishing completed with errors", activity.Data.StatusText); + Assert.Equal("Pipeline completed with errors", activity.Data.StatusText); Assert.True(activity.Data.IsError); // Should be error because step3 had an error (highest severity) Assert.True(activity.Data.IsComplete); } @@ -744,9 +744,9 @@ public async Task FailAsync_CompletesTaskWithErrorAndEmitsActivity() } [Theory] - [InlineData(CompletionState.Completed, "Deployment completed successfully", false)] - [InlineData(CompletionState.CompletedWithError, "Deployment completed with errors", true)] - [InlineData(CompletionState.CompletedWithWarning, "Deployment completed with warnings", false)] + [InlineData(CompletionState.Completed, "Pipeline completed successfully", false)] + [InlineData(CompletionState.CompletedWithError, "Pipeline completed with errors", true)] + [InlineData(CompletionState.CompletedWithWarning, "Pipeline completed with warnings", false)] public async Task CompletePublishAsync_WithDeployFlag_EmitsCorrectActivity(CompletionState completionState, string expectedStatusText, bool expectedIsError) { // Arrange diff --git a/tests/Aspire.Hosting.Tests/Utils/TestDistributedApplicationBuilder.cs b/tests/Aspire.Hosting.Tests/Utils/TestDistributedApplicationBuilder.cs index a4f3171db3b..9ecb9366ab3 100644 --- a/tests/Aspire.Hosting.Tests/Utils/TestDistributedApplicationBuilder.cs +++ b/tests/Aspire.Hosting.Tests/Utils/TestDistributedApplicationBuilder.cs @@ -5,7 +5,6 @@ using Aspire.Components.Common.TestUtilities; using Aspire.Hosting.Orchestrator; -using Aspire.Hosting.Pipelines; using Aspire.Hosting.Testing; using Aspire.Hosting.Tests.Dcp; using Microsoft.Extensions.DependencyInjection; @@ -20,7 +19,7 @@ namespace Aspire.Hosting.Utils; /// public static class TestDistributedApplicationBuilder { - public static IDistributedApplicationTestingBuilder Create(DistributedApplicationOperation operation, string outputPath = "./", string? logLevel = "information", string? step = WellKnownPipelineSteps.Publish) + public static IDistributedApplicationTestingBuilder Create(DistributedApplicationOperation operation, string outputPath = "./", string? logLevel = "information", string? step = "publish") { var args = operation switch {