diff --git a/src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj b/src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj index 9a3b8a93018..2aeb93f852a 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj +++ b/src/Aspire.Hosting.Azure.AppContainers/Aspire.Hosting.Azure.AppContainers.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs index 03d7a9821f0..a170bf91bb9 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppEnvironmentResource.cs @@ -32,14 +32,6 @@ public AzureContainerAppEnvironmentResource(string name, Action(); - var loginToAcrStep = new PipelineStep - { - Name = $"login-to-acr-{name}", - Description = $"Logs in to Azure Container Registry for {name}.", - Action = context => AzureEnvironmentResourceHelpers.LoginToRegistryAsync(this, context), - Tags = ["acr-login"] - }; - // Add print-dashboard-url step var printDashboardUrlStep = new PipelineStep { @@ -51,7 +43,6 @@ public AzureContainerAppEnvironmentResource(string name, Action { - 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()) @@ -113,9 +102,6 @@ public AzureContainerAppEnvironmentResource(string name, Action internal BicepOutputReference ContainerAppDomain => new("AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN", this); + /// + /// Gets the name of the associated Azure Container Registry. + /// + internal BicepOutputReference ContainerRegistryName => new("AZURE_CONTAINER_REGISTRY_NAME", this); + /// /// Gets the URL endpoint of the associated Azure Container Registry. /// @@ -179,17 +168,28 @@ await context.ReportingStep.CompleteAsync( /// public BicepOutputReference NameOutputReference => new("AZURE_CONTAINER_APPS_ENVIRONMENT_NAME", this); + internal Dictionary VolumeNames { get; } = []; + /// - /// Gets the container registry name. + /// Gets the default container registry for this environment. /// - private BicepOutputReference ContainerRegistryName => new("AZURE_CONTAINER_REGISTRY_NAME", this); + internal AzureContainerRegistryResource? DefaultContainerRegistry { get; set; } - internal Dictionary VolumeNames { get; } = []; + ReferenceExpression IContainerRegistry.Name => GetContainerRegistry()?.Name ?? ReferenceExpression.Create($"{ContainerRegistryName}"); + + ReferenceExpression IContainerRegistry.Endpoint => GetContainerRegistry()?.Endpoint ?? ReferenceExpression.Create($"{ContainerRegistryUrl}"); - // Implement IAzureContainerRegistry interface - ReferenceExpression IContainerRegistry.Name => ReferenceExpression.Create($"{ContainerRegistryName}"); + private IContainerRegistry? GetContainerRegistry() + { + // Check for explicit container registry reference annotation + if (this.TryGetLastAnnotation(out var annotation)) + { + return annotation.Registry; + } - ReferenceExpression IContainerRegistry.Endpoint => ReferenceExpression.Create($"{ContainerRegistryUrl}"); + // Fall back to default container registry + return DefaultContainerRegistry; + } ReferenceExpression IAzureContainerRegistry.ManagedIdentityId => ReferenceExpression.Create($"{ContainerRegistryManagedIdentityId}"); diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs index 5494975bac7..e4287e4716a 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs @@ -89,19 +89,23 @@ public static IResourceBuilder AddAzureCon infra.Add(identity); - ContainerRegistryService? containerRegistry = null; - if (appEnvResource.TryGetLastAnnotation(out var registryReferenceAnnotation) && registryReferenceAnnotation.Registry is AzureProvisioningResource registry) + AzureProvisioningResource? registry = null; + if (appEnvResource.TryGetLastAnnotation(out var registryReferenceAnnotation) && + registryReferenceAnnotation.Registry is AzureProvisioningResource explicitRegistry) { - containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra); + registry = explicitRegistry; } - else + else if (appEnvResource.DefaultContainerRegistry is not null) { - containerRegistry = new ContainerRegistryService(Infrastructure.NormalizeBicepIdentifier($"{appEnvResource.Name}_acr")) - { - Sku = new() { Name = ContainerRegistrySkuName.Basic }, - Tags = tags - }; + registry = appEnvResource.DefaultContainerRegistry; } + + if (registry is null) + { + throw new InvalidOperationException($"No container registry associated with environment '{appEnvResource.Name}'. This should have been added automatically."); + } + + var containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra); infra.Add(containerRegistry); var pullRa = containerRegistry.CreateRoleAssignment(ContainerRegistryBuiltInRole.AcrPull, identity); @@ -323,14 +327,19 @@ public static IResourceBuilder AddAzureCon }); }); - if (builder.ExecutionContext.IsRunMode) - { + // Create the default container registry resource before creating the environment + var registryName = $"{name}-acr"; + var defaultRegistry = CreateDefaultAzureContainerRegistry(builder, registryName, containerAppEnvResource); + containerAppEnvResource.DefaultContainerRegistry = defaultRegistry; + + // Create the resource builder first, then attach the registry to avoid recreating builders + var appEnvBuilder = builder.ExecutionContext.IsRunMode // HACK: We need to return a valid resource builder for the container app environment // but in run mode, we don't want to add the resource to the builder. - return builder.CreateResourceBuilder(containerAppEnvResource); - } + ? builder.CreateResourceBuilder(containerAppEnvResource) + : builder.AddResource(containerAppEnvResource); - return builder.AddResource(containerAppEnvResource); + return appEnvBuilder; } /// @@ -379,4 +388,57 @@ public static IResourceBuilder WithAzureLo return builder; } + + private static AzureContainerRegistryResource CreateDefaultAzureContainerRegistry(IDistributedApplicationBuilder builder, string name, AzureContainerAppEnvironmentResource containerAppEnvironment) + { + var configureInfrastructure = (AzureResourceInfrastructure infrastructure) => + { + var registry = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(infrastructure, + (identifier, resourceName) => + { + var resource = ContainerRegistryService.FromExisting(identifier); + resource.Name = resourceName; + return resource; + }, + (infra) => + { + var newRegistry = new ContainerRegistryService(infra.AspireResource.GetBicepIdentifier()) + { + Sku = new ContainerRegistrySku { Name = ContainerRegistrySkuName.Basic }, + Tags = { { "aspire-resource-name", infra.AspireResource.Name } } + }; + + if (containerAppEnvironment.UseAzdNamingConvention) + { + var resourceToken = new ProvisioningVariable("resourceToken", typeof(string)) + { + Value = BicepFunction.GetUniqueString(BicepFunction.GetResourceGroup().Id) + }; + infrastructure.Add(resourceToken); + + newRegistry.Name = new FunctionCallExpression( + new IdentifierExpression("replace"), + new InterpolatedStringExpression([ + new StringLiteralExpression("acr-"), + new IdentifierExpression(resourceToken.BicepIdentifier) + ]), + new StringLiteralExpression("-"), + new StringLiteralExpression("")); + } + + return newRegistry; + }); + + infrastructure.Add(registry); + infrastructure.Add(new ProvisioningOutput("name", typeof(string)) { Value = registry.Name }); + infrastructure.Add(new ProvisioningOutput("loginServer", typeof(string)) { Value = registry.LoginServer }); + }; + + var resource = new AzureContainerRegistryResource(name, configureInfrastructure); + if (builder.ExecutionContext.IsPublishMode) + { + builder.AddResource(resource); + } + return resource; + } } diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppResource.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppResource.cs index 45646fa3a0f..40e5dc5f26d 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppResource.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppResource.cs @@ -7,8 +7,6 @@ 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; @@ -29,42 +27,18 @@ public AzureContainerAppResource(string name, Action { - // Get the registry from the target resource's deployment target annotation + // Get the deployment target annotation var deploymentTargetAnnotation = targetResource.GetDeploymentTargetAnnotation(); - if (deploymentTargetAnnotation?.ContainerRegistry is not IContainerRegistry registry) + if (deploymentTargetAnnotation is null) { - // 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}", - Description = $"Pushes the container image for {targetResource.Name} to Azure Container Registry.", - 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 = []; @@ -116,26 +90,10 @@ await AzureEnvironmentResourceHelpers.PushImageToRegistryAsync( // 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 + // The app deployment should depend on push steps from the target resource + var pushSteps = context.GetSteps(targetResource, WellKnownPipelineTags.PushContainerImage); provisionSteps.DependsOn(pushSteps); // Ensure summary step runs after provision diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs index fb59d97b28c..fe15eaedc35 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs @@ -30,6 +30,13 @@ private async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken foreach (var environment in caes) { + // Remove the default container registry from the model if an explicit registry is configured + if (environment.HasAnnotationOfType() && + environment.DefaultContainerRegistry is not null) + { + @event.Model.Resources.Remove(environment.DefaultContainerRegistry); + } + var containerAppEnvironmentContext = new ContainerAppEnvironmentContext( logger, executionContext, diff --git a/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj b/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj index 7c51e304132..29c6d288b13 100644 --- a/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj +++ b/src/Aspire.Hosting.Azure.AppService/Aspire.Hosting.Azure.AppService.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs index 9ffa8abed9f..7f50acc0b4c 100644 --- a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs +++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs @@ -44,6 +44,10 @@ public static IResourceBuilder AddAzureAppSe { builder.AddAzureAppServiceInfrastructureCore(); + // Create the default container registry resource before creating the environment + var registryName = $"{name}-acr"; + var defaultRegistry = CreateDefaultAzureContainerRegistry(builder, registryName); + var resource = new AzureAppServiceEnvironmentResource(name, static infra => { var prefix = infra.AspireResource.Name; @@ -67,20 +71,23 @@ public static IResourceBuilder AddAzureAppSe infra.Add(identity); - ContainerRegistryService? containerRegistry = null; - if (resource.TryGetLastAnnotation(out var registryReferenceAnnotation) && registryReferenceAnnotation.Registry is AzureProvisioningResource registry) + AzureProvisioningResource? registry = null; + if (resource.TryGetLastAnnotation(out var registryReferenceAnnotation) && + registryReferenceAnnotation.Registry is AzureProvisioningResource explicitRegistry) { - containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra); + registry = explicitRegistry; } - else + else if (resource.DefaultContainerRegistry is not null) { - containerRegistry = new ContainerRegistryService(Infrastructure.NormalizeBicepIdentifier($"{prefix}_acr")) - { - Sku = new() { Name = ContainerRegistrySkuName.Basic }, - Tags = tags - }; + registry = resource.DefaultContainerRegistry; + } + + if (registry is null) + { + throw new InvalidOperationException($"No container registry associated with environment '{resource.Name}'. This should have been added automatically."); } + var containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra); infra.Add(containerRegistry); var pullRa = containerRegistry.CreateRoleAssignment(ContainerRegistryBuiltInRole.AcrPull, identity); @@ -205,14 +212,17 @@ public static IResourceBuilder AddAzureAppSe Value = applicationInsights.ConnectionString }); } - }); - - if (!builder.ExecutionContext.IsPublishMode) + }) { - return builder.CreateResourceBuilder(resource); - } + DefaultContainerRegistry = defaultRegistry + }; + + // Create the resource builder first, then attach the registry to avoid recreating builders + var appServiceEnvBuilder = builder.ExecutionContext.IsPublishMode + ? builder.AddResource(resource) + : builder.CreateResourceBuilder(resource); - return builder.AddResource(resource); + return appServiceEnvBuilder; } /// @@ -277,4 +287,34 @@ public static IResourceBuilder WithAzureAppl builder.Resource.ApplicationInsightsResource = applicationInsightsBuilder.Resource; return builder; } + + private static AzureContainerRegistryResource CreateDefaultAzureContainerRegistry(IDistributedApplicationBuilder builder, string name) + { + var configureInfrastructure = (AzureResourceInfrastructure infrastructure) => + { + var registry = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(infrastructure, + (identifier, resourceName) => + { + var resource = ContainerRegistryService.FromExisting(identifier); + resource.Name = resourceName; + return resource; + }, + (infra) => new ContainerRegistryService(infra.AspireResource.GetBicepIdentifier()) + { + Sku = new ContainerRegistrySku { Name = ContainerRegistrySkuName.Basic }, + Tags = { { "aspire-resource-name", infra.AspireResource.Name } } + }); + + infrastructure.Add(registry); + infrastructure.Add(new ProvisioningOutput("name", typeof(string)) { Value = registry.Name }); + infrastructure.Add(new ProvisioningOutput("loginServer", typeof(string)) { Value = registry.LoginServer }); + }; + + var resource = new AzureContainerRegistryResource(name, configureInfrastructure); + if (builder.ExecutionContext.IsPublishMode) + { + builder.AddResource(resource); + } + return resource; + } } diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs index a5f4762c7d0..490be41fc3e 100644 --- a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentResource.cs @@ -35,14 +35,6 @@ public AzureAppServiceEnvironmentResource(string name, Action(); - var loginToAcrStep = new PipelineStep - { - Name = $"login-to-acr-{name}", - Description = $"Logs in to Azure Container Registry for {name}.", - Action = context => AzureEnvironmentResourceHelpers.LoginToRegistryAsync(this, context), - Tags = ["acr-login"] - }; - // Add print-dashboard-url step var printDashboardUrlStep = new PipelineStep { @@ -54,7 +46,6 @@ public AzureAppServiceEnvironmentResource(string name, Action { - 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()) @@ -116,9 +105,6 @@ public AzureAppServiceEnvironmentResource(string name, Action GetWebSiteSuffixBicep() => BicepFunction.GetUniqueString(BicepFunction.GetResourceGroup().Id); - ReferenceExpression IAzureContainerRegistry.ManagedIdentityId => - ReferenceExpression.Create($"{ContainerRegistryManagedIdentityId}"); + /// + /// Gets the default container registry for this environment. + /// + internal AzureContainerRegistryResource? DefaultContainerRegistry { get; set; } - ReferenceExpression IContainerRegistry.Name => - ReferenceExpression.Create($"{ContainerRegistryName}"); + ReferenceExpression IContainerRegistry.Name => GetContainerRegistry()?.Name ?? ReferenceExpression.Create($"{ContainerRegistryName}"); + + ReferenceExpression IContainerRegistry.Endpoint => GetContainerRegistry()?.Endpoint ?? ReferenceExpression.Create($"{ContainerRegistryUrl}"); + + private IContainerRegistry? GetContainerRegistry() + { + // Check for explicit container registry reference annotation + if (this.TryGetLastAnnotation(out var annotation)) + { + return annotation.Registry; + } + + // Fall back to default container registry + return DefaultContainerRegistry; + } - ReferenceExpression IContainerRegistry.Endpoint => - ReferenceExpression.Create($"{ContainerRegistryUrl}"); + ReferenceExpression IAzureContainerRegistry.ManagedIdentityId => ReferenceExpression.Create($"{ContainerRegistryManagedIdentityId}"); ReferenceExpression IComputeEnvironmentResource.GetHostAddressExpression(EndpointReference endpointReference) { diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceInfrastructure.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceInfrastructure.cs index 8681ac3a885..8f86f009518 100644 --- a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceInfrastructure.cs +++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceInfrastructure.cs @@ -32,6 +32,13 @@ private async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken foreach (var appServiceEnvironment in appServiceEnvironments) { + // Remove the default container registry from the model if an explicit registry is configured + if (appServiceEnvironment.HasAnnotationOfType() && + appServiceEnvironment.DefaultContainerRegistry is not null) + { + @event.Model.Resources.Remove(appServiceEnvironment.DefaultContainerRegistry); + } + var appServiceEnvironmentContext = new AzureAppServiceEnvironmentContext( logger, executionContext, diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebSiteResource.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebSiteResource.cs index d5fbf5bf2c2..56a2923a961 100644 --- a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebSiteResource.cs +++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceWebSiteResource.cs @@ -7,8 +7,6 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Pipelines; -using Aspire.Hosting.Publishing; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Aspire.Hosting.Azure; @@ -29,42 +27,18 @@ public AzureAppServiceWebSiteResource(string name, Action { - // Get the registry from the target resource's deployment target annotation + // Get the deployment target annotation var deploymentTargetAnnotation = targetResource.GetDeploymentTargetAnnotation(); - if (deploymentTargetAnnotation?.ContainerRegistry is not IContainerRegistry registry) + if (deploymentTargetAnnotation is null) { - // 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}", - Description = $"Pushes the container image for {targetResource.Name} to Azure Container Registry.", - 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 = []; @@ -111,26 +85,10 @@ await AzureEnvironmentResourceHelpers.PushImageToRegistryAsync( // 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 + // The app deployment should depend on push steps from the target resource + var pushSteps = context.GetSteps(targetResource, WellKnownPipelineTags.PushContainerImage); provisionSteps.DependsOn(pushSteps); // Ensure summary step runs after provision diff --git a/src/Aspire.Hosting.Azure.ContainerRegistry/Aspire.Hosting.Azure.ContainerRegistry.csproj b/src/Aspire.Hosting.Azure.ContainerRegistry/Aspire.Hosting.Azure.ContainerRegistry.csproj index 0098a25dab1..85835e0091a 100644 --- a/src/Aspire.Hosting.Azure.ContainerRegistry/Aspire.Hosting.Azure.ContainerRegistry.csproj +++ b/src/Aspire.Hosting.Azure.ContainerRegistry/Aspire.Hosting.Azure.ContainerRegistry.csproj @@ -11,6 +11,10 @@ + + + + diff --git a/src/Aspire.Hosting.Azure/AzureEnvironmentResourceHelpers.cs b/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryHelpers.cs similarity index 56% rename from src/Aspire.Hosting.Azure/AzureEnvironmentResourceHelpers.cs rename to src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryHelpers.cs index 346659723b6..77c1731da61 100644 --- a/src/Aspire.Hosting.Azure/AzureEnvironmentResourceHelpers.cs +++ b/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryHelpers.cs @@ -9,15 +9,14 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure.Provisioning.Internal; using Aspire.Hosting.Pipelines; -using Aspire.Hosting.Publishing; using Microsoft.Extensions.DependencyInjection; namespace Aspire.Hosting.Azure; /// -/// Helper methods for Azure environment resources to handle container image operations. +/// Helper methods for Azure Container Registry operations. /// -internal static class AzureEnvironmentResourceHelpers +internal static class AzureContainerRegistryHelpers { public static async Task LoginToRegistryAsync(IContainerRegistry registry, PipelineStepContext context) { @@ -25,17 +24,13 @@ public static async Task LoginToRegistryAsync(IContainerRegistry registry, Pipel var tokenCredentialProvider = context.Services.GetRequiredService(); // Find the AzureEnvironmentResource from the application model - var azureEnvironment = context.Model.Resources.OfType().FirstOrDefault(); - if (azureEnvironment == null) - { + var azureEnvironment = context.Model.Resources.OfType().FirstOrDefault() ?? throw new InvalidOperationException("AzureEnvironmentResource must be present in the application model."); - } - var registryName = await registry.Name.GetValueAsync(context.CancellationToken).ConfigureAwait(false) ?? - throw new InvalidOperationException("Failed to retrieve container registry information."); - + throw new InvalidOperationException("Failed to retrieve container registry information."); + var registryEndpoint = await registry.Endpoint.GetValueAsync(context.CancellationToken).ConfigureAwait(false) ?? - throw new InvalidOperationException("Failed to retrieve container registry endpoint."); + throw new InvalidOperationException("Failed to retrieve container registry endpoint."); var loginTask = await context.ReportingStep.CreateTaskAsync($"Logging in to **{registryName}**", context.CancellationToken).ConfigureAwait(false); await using (loginTask.ConfigureAwait(false)) @@ -63,32 +58,4 @@ await acrLoginService.LoginAsync( } } } - - public static async Task PushImageToRegistryAsync(IContainerRegistry registry, IResource resource, PipelineStepContext context, IResourceContainerImageManager containerImageBuilder) - { - var registryName = await registry.Name.GetValueAsync(context.CancellationToken).ConfigureAwait(false) ?? - throw new InvalidOperationException("Failed to retrieve container registry information."); - - 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 containerImageBuilder.PushImageAsync(resource, context.CancellationToken).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; - } - } - } } diff --git a/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryResource.cs b/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryResource.cs index c417cfaf0da..f0c91f471c5 100644 --- a/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryResource.cs +++ b/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryResource.cs @@ -1,7 +1,11 @@ // 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 ASPIREPIPELINES003 + using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Pipelines; using Azure.Provisioning.ContainerRegistry; using Azure.Provisioning.Primitives; @@ -10,9 +14,38 @@ namespace Aspire.Hosting.Azure; /// /// Represents an Azure Container Registry resource. /// -public class AzureContainerRegistryResource(string name, Action configureInfrastructure) - : AzureProvisioningResource(name, configureInfrastructure), IContainerRegistry +public class AzureContainerRegistryResource : AzureProvisioningResource, IContainerRegistry { + /// + /// Initializes a new instance of the class. + /// + /// The name of the resource. + /// The callback to configure the Azure infrastructure for this resource. + public AzureContainerRegistryResource(string name, Action configureInfrastructure) + : base(name, configureInfrastructure) + { + Annotations.Add(new PipelineStepAnnotation((factoryContext) => + { + var loginStep = new PipelineStep + { + Name = $"login-to-acr-{name}", + Action = context => AzureContainerRegistryHelpers.LoginToRegistryAsync(this, context), + Tags = ["acr-login"], + RequiredBySteps = [WellKnownPipelineSteps.PushPrereq], + Resource = this + }; + return [loginStep]; + })); + + Annotations.Add(new PipelineConfigurationAnnotation((context) => + { + var loginSteps = context.GetSteps(this, "acr-login"); + var provisionSteps = context.GetSteps(this, WellKnownPipelineTags.ProvisionInfrastructure); + + loginSteps.DependsOn(provisionSteps); + })); + } + /// /// The name of the Azure Container Registry. /// @@ -34,15 +67,15 @@ public override ProvisionableResource AddAsExistingResource(AzureResourceInfrast { var bicepIdentifier = this.GetBicepIdentifier(); var resources = infra.GetProvisionableResources(); - + // Check if a ContainerRegistryService with the same identifier already exists var existingStore = resources.OfType().SingleOrDefault(store => store.BicepIdentifier == bicepIdentifier); - + if (existingStore is not null) { return existingStore; } - + // Create and add new resource if it doesn't exist var store = ContainerRegistryService.FromExisting(bicepIdentifier); diff --git a/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj b/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj index c86c7db11f1..c4902bd73ff 100644 --- a/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj +++ b/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj @@ -41,8 +41,7 @@ - - + diff --git a/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs b/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs index 22cd794ce3c..c821a483669 100644 --- a/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs +++ b/src/Aspire.Hosting/ApplicationModel/ProjectResource.cs @@ -29,12 +29,14 @@ public class ProjectResource : Resource, IResourceWithEnvironment, IResourceWith /// The name of the resource. public ProjectResource(string name) : base(name) { - // Add pipeline step annotation to create a build step for this project + // Add pipeline step annotation to create build and push steps for this project Annotations.Add(new PipelineStepAnnotation((factoryContext) => { + var steps = new List(); + if (factoryContext.Resource.IsExcludedFromPublish()) { - return []; + return steps; } var buildStep = new PipelineStep @@ -44,10 +46,25 @@ public ProjectResource(string name) : base(name) Action = BuildProjectImage, Tags = [WellKnownPipelineTags.BuildCompute], RequiredBySteps = [WellKnownPipelineSteps.Build], - DependsOnSteps = [WellKnownPipelineSteps.BuildPrereq] + DependsOnSteps = [WellKnownPipelineSteps.BuildPrereq], + Resource = this }; + steps.Add(buildStep); - return [buildStep]; + if (this.RequiresImageBuildAndPush()) + { + var pushStep = new PipelineStep + { + Name = $"push-{name}", + Action = ctx => PipelineStepHelpers.PushImageToRegistryAsync(this, ctx), + Tags = [WellKnownPipelineTags.PushContainerImage], + RequiredBySteps = [WellKnownPipelineSteps.Push], + Resource = this + }; + steps.Add(pushStep); + } + + return steps; })); // Add default container build options annotation @@ -70,6 +87,13 @@ public ProjectResource(string name) : base(name) buildSteps.DependsOn(context.GetSteps(containerFile.Source, WellKnownPipelineTags.BuildCompute)); } } + + // Wire up dependencies for push steps + var projectBuildSteps = context.GetSteps(this, WellKnownPipelineTags.BuildCompute); + var pushSteps = context.GetSteps(this, WellKnownPipelineTags.PushContainerImage); + + pushSteps.DependsOn(projectBuildSteps); + pushSteps.DependsOn(WellKnownPipelineSteps.PushPrereq); })); } // Keep track of the config host for each Kestrel endpoint annotation diff --git a/src/Aspire.Hosting/ApplicationModel/RegistryTargetAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/RegistryTargetAnnotation.cs new file mode 100644 index 00000000000..9f675cf6d0e --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/RegistryTargetAnnotation.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Annotation that indicates a resource should use a specific container registry as its default target. +/// +/// +/// This annotation is automatically added to resources when a container registry is added to the application model. +/// It provides a default registry for resources that don't have an explicit . +/// +/// The container registry resource. +internal sealed class RegistryTargetAnnotation(IContainerRegistry registry) : IResourceAnnotation +{ + /// + /// Gets the container registry resource. + /// + public IContainerRegistry Registry { get; } = registry; +} diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index 242acb3345d..e41c30b4f5a 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -1132,8 +1132,12 @@ internal static async Task ProcessImagePushOptionsCal /// The container registry associated with the resource. /// Thrown when the resource does not have a container registry reference. /// - /// This method first checks for a container registry in the . - /// If not found, it falls back to the . + /// This method checks for a container registry in the following order: + /// + /// The on the resource. + /// The on the resource (set via WithContainerRegistry). + /// The on the resource (automatically added when a registry is added to the app model). + /// /// internal static IContainerRegistry GetContainerRegistry(this IResource resource) { @@ -1144,10 +1148,29 @@ internal static IContainerRegistry GetContainerRegistry(this IResource resource) return deploymentTarget.ContainerRegistry; } - // Fall back to ContainerRegistryReferenceAnnotation - var registryAnnotation = resource.Annotations.OfType().LastOrDefault() - ?? throw new InvalidOperationException($"Resource '{resource.Name}' does not have a container registry reference."); - return registryAnnotation.Registry; + // Try ContainerRegistryReferenceAnnotation (explicit WithContainerRegistry call) + var registryAnnotation = resource.Annotations.OfType().LastOrDefault(); + if (registryAnnotation is not null) + { + return registryAnnotation.Registry; + } + + // Fall back to RegistryTargetAnnotation (added automatically via BeforeStartEvent) + var registryTargetAnnotations = resource.Annotations.OfType().ToArray(); + if (registryTargetAnnotations.Length == 1) + { + return registryTargetAnnotations[0].Registry; + } + + if (registryTargetAnnotations.Length > 1) + { + var registryNames = string.Join(", ", registryTargetAnnotations.Select(a => a.Registry is IResource res ? res.Name : a.Registry.ToString())); + throw new InvalidOperationException( + $"Resource '{resource.Name}' has multiple container registries available - '{registryNames}'. " + + $"Please specify which registry to use with '.WithContainerRegistry(registryBuilder)'."); + } + + throw new InvalidOperationException($"Resource '{resource.Name}' does not have a container registry reference."); } /// diff --git a/src/Aspire.Hosting/ContainerRegistryResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerRegistryResourceBuilderExtensions.cs index 63d76e1310d..70f95b21bff 100644 --- a/src/Aspire.Hosting/ContainerRegistryResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerRegistryResourceBuilderExtensions.cs @@ -4,6 +4,8 @@ using System.Diagnostics.CodeAnalysis; using Aspire.Hosting.ApplicationModel; +#pragma warning disable ASPIRECOMPUTE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + namespace Aspire.Hosting; /// @@ -50,9 +52,13 @@ public static IResourceBuilder AddContainerRegistry( var resource = new ContainerRegistryResource(name, endpointExpression, repositoryExpression); - return builder.ExecutionContext.IsRunMode + var resourceBuilder = builder.ExecutionContext.IsRunMode ? builder.CreateResourceBuilder(resource) : builder.AddResource(resource); + + SubscribeToAddRegistryTargetAnnotations(builder, resource); + + return resourceBuilder; } /// @@ -98,9 +104,30 @@ public static IResourceBuilder AddContainerRegistry( var resource = new ContainerRegistryResource(name, endpointExpression, repositoryExpression); - return builder.ExecutionContext.IsRunMode + var resourceBuilder = builder.ExecutionContext.IsRunMode ? builder.CreateResourceBuilder(resource) : builder.AddResource(resource); + + SubscribeToAddRegistryTargetAnnotations(builder, resource); + + return resourceBuilder; + } + + /// + /// Subscribes to BeforeStartEvent to add RegistryTargetAnnotation to all resources in the model. + /// + private static void SubscribeToAddRegistryTargetAnnotations(IDistributedApplicationBuilder builder, ContainerRegistryResource registry) + { + builder.Eventing.Subscribe((beforeStartEvent, cancellationToken) => + { + foreach (var resource in beforeStartEvent.Model.Resources) + { + // Add a RegistryTargetAnnotation to indicate this registry is available as a default target + resource.Annotations.Add(new RegistryTargetAnnotation(registry)); + } + + return Task.CompletedTask; + }); } /// diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index 28e7b75c867..a42d815a369 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -23,39 +23,65 @@ namespace Aspire.Hosting; public static class ContainerResourceBuilderExtensions { /// - /// Ensures that a container resource has a PipelineStepAnnotation for building if it has a DockerfileBuildAnnotation. + /// Ensures that a container resource has PipelineStepAnnotations for building and pushing. /// /// The type of container resource. /// The resource builder. - internal static IResourceBuilder EnsureBuildPipelineStepAnnotation(this IResourceBuilder builder) where T : ContainerResource + internal static IResourceBuilder EnsureBuildAndPushPipelineAnnotations(this IResourceBuilder builder) where T : ContainerResource { - // Use replace semantics to ensure we only have one PipelineStepAnnotation for building - return builder.WithAnnotation(new PipelineStepAnnotation((factoryContext) => + builder.WithAnnotation(new PipelineStepAnnotation((factoryContext) => { - if (!builder.Resource.RequiresImageBuild() || builder.Resource.IsExcludedFromPublish()) + var steps = new List(); + + if (builder.Resource.IsExcludedFromPublish()) { - return []; + return steps; } - var buildStep = new PipelineStep + if (builder.Resource.RequiresImageBuild()) { - Name = $"build-{builder.Resource.Name}", - Description = $"Builds the container image for the {builder.Resource.Name} container.", - Action = async ctx => + var buildStep = new PipelineStep { - var containerImageBuilder = ctx.Services.GetRequiredService(); - - await containerImageBuilder.BuildImageAsync( - builder.Resource, - ctx.CancellationToken).ConfigureAwait(false); - }, - Tags = [WellKnownPipelineTags.BuildCompute], - RequiredBySteps = [WellKnownPipelineSteps.Build], - DependsOnSteps = [WellKnownPipelineSteps.BuildPrereq] - }; + Name = $"build-{builder.Resource.Name}", + Action = async ctx => + { + var containerImageBuilder = ctx.Services.GetRequiredService(); + await containerImageBuilder.BuildImageAsync( + builder.Resource, + ctx.CancellationToken).ConfigureAwait(false); + }, + Tags = [WellKnownPipelineTags.BuildCompute], + RequiredBySteps = [WellKnownPipelineSteps.Build], + DependsOnSteps = [WellKnownPipelineSteps.BuildPrereq], + Resource = builder.Resource + }; + steps.Add(buildStep); + } + + if (builder.Resource.RequiresImageBuildAndPush()) + { + var pushStep = new PipelineStep + { + Name = $"push-{builder.Resource.Name}", + Action = ctx => PipelineStepHelpers.PushImageToRegistryAsync(builder.Resource, ctx), + Tags = [WellKnownPipelineTags.PushContainerImage], + RequiredBySteps = [WellKnownPipelineSteps.Push], + Resource = builder.Resource + }; + steps.Add(pushStep); + } - return [buildStep]; + return steps; }), ResourceAnnotationMutationBehavior.Replace); + + return builder.WithAnnotation(new PipelineConfigurationAnnotation(context => + { + var buildSteps = context.GetSteps(builder.Resource, WellKnownPipelineTags.BuildCompute); + var pushSteps = context.GetSteps(builder.Resource, WellKnownPipelineTags.PushContainerImage); + + pushSteps.DependsOn(buildSteps); + pushSteps.DependsOn(WellKnownPipelineSteps.PushPrereq); + }), ResourceAnnotationMutationBehavior.Append); } /// @@ -560,11 +586,13 @@ public static IResourceBuilder WithDockerfile(this IResourceBuilder bui { context.LocalImageName = dockerfileAnnotation.ImageName ?? context.Resource.Name; context.LocalImageTag = dockerfileAnnotation.ImageTag ?? "latest"; + context.TargetPlatform = ContainerTargetPlatform.LinuxAmd64; } else { context.LocalImageName = context.Resource.Name; context.LocalImageTag = "latest"; + context.TargetPlatform = ContainerTargetPlatform.LinuxAmd64; } context.TargetPlatform = ContainerTargetPlatform.LinuxAmd64; }); @@ -577,7 +605,7 @@ public static IResourceBuilder WithDockerfile(this IResourceBuilder bui annotation.ImageTag = imageTag; return builder.WithAnnotation(annotation, ResourceAnnotationMutationBehavior.Replace) .WithAnnotation(defaultContainerBuildOptions) - .EnsureBuildPipelineStepAnnotation(); + .EnsureBuildAndPushPipelineAnnotations(); } return builder.WithAnnotation(annotation, ResourceAnnotationMutationBehavior.Replace) @@ -585,7 +613,7 @@ public static IResourceBuilder WithDockerfile(this IResourceBuilder bui .WithImageRegistry(registry: null) .WithImage(imageName) .WithImageTag(imageTag) - .EnsureBuildPipelineStepAnnotation(); + .EnsureBuildAndPushPipelineAnnotations(); } /// @@ -700,11 +728,13 @@ public static IResourceBuilder WithDockerfileFactory(this IResourceBuilder { context.LocalImageName = dockerfileAnnotation.ImageName ?? context.Resource.Name; context.LocalImageTag = dockerfileAnnotation.ImageTag ?? "latest"; + context.TargetPlatform = ContainerTargetPlatform.LinuxAmd64; } else { context.LocalImageName = context.Resource.Name; context.LocalImageTag = "latest"; + context.TargetPlatform = ContainerTargetPlatform.LinuxAmd64; } context.TargetPlatform = ContainerTargetPlatform.LinuxAmd64; }); @@ -717,7 +747,7 @@ public static IResourceBuilder WithDockerfileFactory(this IResourceBuilder annotation.ImageTag = imageTag; return builder.WithAnnotation(annotation, ResourceAnnotationMutationBehavior.Replace) .WithAnnotation(defaultContainerBuildOptions, ResourceAnnotationMutationBehavior.Append) - .EnsureBuildPipelineStepAnnotation(); + .EnsureBuildAndPushPipelineAnnotations(); } return builder.WithAnnotation(annotation, ResourceAnnotationMutationBehavior.Replace) @@ -725,7 +755,7 @@ public static IResourceBuilder WithDockerfileFactory(this IResourceBuilder .WithImageRegistry(registry: null) .WithImage(imageName) .WithImageTag(imageTag) - .EnsureBuildPipelineStepAnnotation(); + .EnsureBuildAndPushPipelineAnnotations(); } /// diff --git a/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs b/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs index e01b3951986..feb10891129 100644 --- a/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs +++ b/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs @@ -152,6 +152,69 @@ public DistributedApplicationPipeline() Action = context => Task.CompletedTask }); + // Add a default "Push" meta-step that all push steps should be required by + // Push unconditionally depends on PushPrereq to ensure annotations are set up + var pushStep = new PipelineStep + { + Name = WellKnownPipelineSteps.Push, + Description = "Aggregation step for all push operations. All push steps should be required by this step.", + Action = _ => Task.CompletedTask + }; + pushStep.DependsOn(WellKnownPipelineSteps.PushPrereq); + _steps.Add(pushStep); + + _steps.Add(new PipelineStep + { + Name = WellKnownPipelineSteps.PushPrereq, + Description = "Prerequisite step that runs before any push operations.", + Action = context => + { + foreach (var resource in context.Model.Resources) + { + if (!resource.RequiresImageBuildAndPush()) + { + continue; + } + + // Skip if resource already has a ContainerRegistryReferenceAnnotation (explicit WithContainerRegistry call) + if (resource.TryGetAnnotationsIncludingAncestorsOfType(out var annotations) && + annotations.Any()) + { + continue; + } + + // Skip if resource has a deployment target with a ContainerRegistry set + var deploymentTargetAnnotation = resource.GetDeploymentTargetAnnotation(); + if (deploymentTargetAnnotation?.ContainerRegistry is not null) + { + continue; + } + + // Check for RegistryTargetAnnotations (automatically added via BeforeStartEvent) + var registryTargetAnnotations = resource.Annotations.OfType().ToArray(); + + // When multiple registries exist, require explicit WithContainerRegistry call + if (registryTargetAnnotations.Length > 1) + { + var registryNames = string.Join(", ", registryTargetAnnotations.Select(a => a.Registry is IResource res ? res.Name : a.Registry.ToString())); + throw new InvalidOperationException( + $"Resource '{resource.Name}' requires image push but has multiple container registries available - '{registryNames}'. " + + $"Please specify which registry to use with '.WithContainerRegistry(registryBuilder)'."); + } + + // When no registry is available, throw an error + if (registryTargetAnnotations.Length == 0) + { + throw new InvalidOperationException( + $"Resource '{resource.Name}' requires image push but no container registry is available. " + + $"Please add a container registry using 'builder.AddContainerRegistry(...)' or specify one with '.WithContainerRegistry(registryBuilder)'."); + } + } + + return Task.CompletedTask; + }, + }); + // Add a default "Publish" aggregation step that all publish steps should be required by _steps.Add(new PipelineStep { diff --git a/src/Aspire.Hosting/Pipelines/PipelineStepHelpers.cs b/src/Aspire.Hosting/Pipelines/PipelineStepHelpers.cs new file mode 100644 index 00000000000..96422165c37 --- /dev/null +++ b/src/Aspire.Hosting/Pipelines/PipelineStepHelpers.cs @@ -0,0 +1,66 @@ +// 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. +#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 ASPIRECONTAINERRUNTIME001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#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. + +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Publishing; +using Microsoft.Extensions.DependencyInjection; + +namespace Aspire.Hosting.Pipelines; + +/// +/// Helper methods for common pipeline step operations. +/// +internal static class PipelineStepHelpers +{ + /// + /// Pushes a container image to a registry with proper logging and task reporting. + /// + /// The resource whose image should be pushed. + /// The pipeline step context for logging and task reporting. + /// A task representing the async operation. + public static async Task PushImageToRegistryAsync(IResource resource, PipelineStepContext context) + { + var registry = resource.GetContainerRegistry(); + var registryName = await registry.Name.GetValueAsync(context.CancellationToken).ConfigureAwait(false) + ?? throw new InvalidOperationException("Failed to retrieve container registry information."); + + 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 is null) + { + throw new InvalidOperationException($"Failed to get target tag for {resource.Name}"); + } + + var containerImageManager = context.Services.GetRequiredService(); + await containerImageManager.PushImageAsync(resource, context.CancellationToken).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; + } + } + } +} diff --git a/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs b/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs index 7d15fb4d325..855271cefa1 100644 --- a/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs +++ b/src/Aspire.Hosting/Pipelines/WellKnownPipelineSteps.cs @@ -48,6 +48,17 @@ public static class WellKnownPipelineSteps /// public const string BuildPrereq = "build-prereq"; + /// + /// The meta-step that coordinates all push operations. + /// All push steps should be required by this step. + /// + public const string Push = "push"; + + /// + /// The prerequisite step that runs before any push operations. + /// + public const string PushPrereq = "push-prereq"; + /// /// The diagnostic step that dumps dependency graph information for troubleshooting. /// diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs index 53a05e3a9c5..a7c3f8cfa3b 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureContainerAppsTests.cs @@ -5,10 +5,12 @@ #pragma warning disable ASPIRECOMPUTE002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable ASPIREAZURE002 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable ASPIREDOCKERFILEBUILDER001 // 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.Text.Json.Nodes; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure.AppContainers; +using Aspire.Hosting.Pipelines; using Aspire.Hosting.Utils; using Azure.Provisioning; using Azure.Provisioning.AppContainers; @@ -2051,4 +2053,115 @@ public async Task GetHostAddressExpression() Assert.Equal(env.Resource, output.Resource); Assert.Equal("AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN", output.Name); } + + [Fact] + public async Task ContainerAppProvisionDependsOnTargetPushStep() + { + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + builder.AddAzureContainerAppEnvironment("env"); + builder.AddProject("api", launchProfileName: null) + .WithHttpEndpoint(); + + using var app = builder.Build(); + await ExecuteBeforeStartHooksAsync(app, default); + + var model = app.Services.GetRequiredService(); + var projectResource = Assert.Single(model.GetProjectResources()); + + projectResource.TryGetLastAnnotation(out var target); + var containerAppResource = target?.DeploymentTarget as AzureContainerAppResource; + Assert.NotNull(containerAppResource); + + var configAnnotations = containerAppResource.Annotations.OfType().ToList(); + Assert.NotEmpty(configAnnotations); + } + + [Fact] + public async Task EnvironmentCreatesDefaultAcrWhenNoExplicitRegistry() + { + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + builder.AddAzureContainerAppEnvironment("env"); + + builder.AddProject("api", launchProfileName: null) + .WithHttpEndpoint(); + + using var app = builder.Build(); + await ExecuteBeforeStartHooksAsync(app, default); + + var model = app.Services.GetRequiredService(); + + var acrResources = model.Resources.OfType().ToList(); + Assert.Single(acrResources); + + var defaultAcr = acrResources[0]; + Assert.Contains("acr", defaultAcr.Name); + } + + [Fact] + public async Task DefaultAcrNotAddedToModelWhenExplicitRegistryExists() + { + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var customRegistry = builder.AddAzureContainerRegistry("customregistry"); + builder.AddAzureContainerAppEnvironment("env") + .WithAzureContainerRegistry(customRegistry); + + builder.AddProject("api", launchProfileName: null) + .WithHttpEndpoint(); + + using var app = builder.Build(); + await ExecuteBeforeStartHooksAsync(app, default); + + var model = app.Services.GetRequiredService(); + + var acrResources = model.Resources.OfType().ToList(); + Assert.Single(acrResources); + Assert.Equal("customregistry", acrResources[0].Name); + } + + [Fact] + public async Task EnvironmentDelegatesToAssociatedRegistry() + { + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var customRegistry = builder.AddAzureContainerRegistry("customregistry"); + var env = builder.AddAzureContainerAppEnvironment("env") + .WithAzureContainerRegistry(customRegistry); + + using var app = builder.Build(); + await ExecuteBeforeStartHooksAsync(app, default); + + var containerRegistryInterface = env.Resource as IContainerRegistry; + Assert.NotNull(containerRegistryInterface); + Assert.NotNull(containerRegistryInterface.Endpoint); + Assert.NotNull(containerRegistryInterface.Name); + } + + [Fact] + public async Task DefaultContainerRegistryUsesAzdNamingWhenEnvironmentDoes() + { + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + builder.AddAzureContainerAppEnvironment("env") + .WithAzdResourceNaming(); + + builder.AddProject("api", launchProfileName: null) + .WithHttpEndpoint(); + + using var app = builder.Build(); + await ExecuteBeforeStartHooksAsync(app, default); + + var model = app.Services.GetRequiredService(); + + var acrResources = model.Resources.OfType().ToList(); + Assert.Single(acrResources); + + var defaultAcr = acrResources[0]; + var (manifest, bicep) = await GetManifestWithBicep(defaultAcr); + + await Verify(manifest.ToString(), "json") + .AppendContentAsFile(bicep, "bicep"); + } } diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureContainerRegistryTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureContainerRegistryTests.cs index a6c5e0be56a..b505388c6b8 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureContainerRegistryTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureContainerRegistryTests.cs @@ -1,8 +1,11 @@ // 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.Hosting.ApplicationModel; using Aspire.Hosting.Azure.AppContainers; +using Aspire.Hosting.Pipelines; using Aspire.Hosting.Utils; using Azure.Provisioning.ContainerRegistry; using Microsoft.Extensions.DependencyInjection; @@ -127,6 +130,105 @@ await Verify(manifest.ToString(), "json") .AppendContentAsFile(bicep, "bicep"); } + [Fact] + public async Task AzureContainerRegistryHasLoginStep() + { + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var acr = builder.AddAzureContainerRegistry("acr"); + + using var app = builder.Build(); + await ExecuteBeforeStartHooksAsync(app, default); + + var pipelineStepAnnotations = acr.Resource.Annotations.OfType().ToList(); + Assert.True(pipelineStepAnnotations.Count >= 2); + + var factoryContext = new PipelineStepFactoryContext + { + PipelineContext = null!, + Resource = acr.Resource + }; + + var allSteps = new List(); + foreach (var annotation in pipelineStepAnnotations) + { + allSteps.AddRange(await annotation.CreateStepsAsync(factoryContext)); + } + + var loginStep = allSteps.FirstOrDefault(s => s.Name == "login-to-acr-acr"); + Assert.NotNull(loginStep); + Assert.Contains("acr-login", loginStep.Tags); + } + + [Fact] + public async Task LoginStepRequiredByPushPrereq() + { + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var acr = builder.AddAzureContainerRegistry("acr"); + + using var app = builder.Build(); + await ExecuteBeforeStartHooksAsync(app, default); + + var pipelineStepAnnotations = acr.Resource.Annotations.OfType().ToList(); + + var factoryContext = new PipelineStepFactoryContext + { + PipelineContext = null!, + Resource = acr.Resource + }; + + var allSteps = new List(); + foreach (var annotation in pipelineStepAnnotations) + { + allSteps.AddRange(await annotation.CreateStepsAsync(factoryContext)); + } + + var loginStep = allSteps.FirstOrDefault(s => s.Name == "login-to-acr-acr"); + Assert.NotNull(loginStep); + Assert.Contains(WellKnownPipelineSteps.PushPrereq, loginStep.RequiredBySteps); + } + + [Fact] + public async Task AzureContainerRegistryHasProvisionStep() + { + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var acr = builder.AddAzureContainerRegistry("acr"); + + using var app = builder.Build(); + await ExecuteBeforeStartHooksAsync(app, default); + + var pipelineStepAnnotations = acr.Resource.Annotations.OfType().ToList(); + + var factoryContext = new PipelineStepFactoryContext + { + PipelineContext = null!, + Resource = acr.Resource + }; + + var allSteps = new List(); + foreach (var annotation in pipelineStepAnnotations) + { + allSteps.AddRange(await annotation.CreateStepsAsync(factoryContext)); + } + + var provisionStep = allSteps.FirstOrDefault(s => s.Name == "provision-acr"); + Assert.NotNull(provisionStep); + Assert.Contains(WellKnownPipelineTags.ProvisionInfrastructure, provisionStep.Tags); + } + + [Fact] + public void LoginStepDependsOnProvisionStep() + { + var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var acr = builder.AddAzureContainerRegistry("acr"); + + var configAnnotations = acr.Resource.Annotations.OfType().ToList(); + Assert.True(configAnnotations.Count >= 2); + } + private sealed class Project : IProjectMetadata { public string ProjectPath => "project"; diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs index 330df7206e3..cb81da9e83d 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs @@ -128,13 +128,25 @@ public async Task DeployAsync_WithBuildOnlyContainers() // Arrange using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.Deploy); var mockActivityReporter = new TestPublishingActivityReporter(testOutputHelper); - var armClientProvider = new TestArmClientProvider(new Dictionary + var armClientProvider = new TestArmClientProvider(deploymentName => { - ["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" } + return deploymentName switch + { + string name when name.StartsWith("env-acr") => new Dictionary + { + ["name"] = new { type = "String", value = "testregistry" }, + ["loginServer"] = new { type = "String", value = "testregistry.azurecr.io" } + }, + string name when name.StartsWith("env") => 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: mockActivityReporter); @@ -177,13 +189,25 @@ public async Task DeployAsync_WithAzureStorageResourcesWorks() var mockActivityReporter = new TestPublishingActivityReporter(testOutputHelper); var fakeContainerRuntime = new FakeContainerRuntime(); using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.Deploy); - var armClientProvider = new TestArmClientProvider(new Dictionary + var armClientProvider = new TestArmClientProvider(deploymentName => { - ["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" } + return deploymentName switch + { + string name when name.StartsWith("env-acr") => new Dictionary + { + ["name"] = new { type = "String", value = "testregistry" }, + ["loginServer"] = new { type = "String", value = "testregistry.azurecr.io" } + }, + string name when name.StartsWith("env") => 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: mockActivityReporter, containerRuntime: fakeContainerRuntime); @@ -224,13 +248,25 @@ public async Task DeployAsync_WithContainer_Works() var mockProcessRunner = new MockProcessRunner(); var fakeContainerRuntime = new FakeContainerRuntime(); using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.Deploy); - var armClientProvider = new TestArmClientProvider(new Dictionary + var armClientProvider = new TestArmClientProvider(deploymentName => { - ["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" } + return deploymentName switch + { + string name when name.StartsWith("env-acr") => new Dictionary + { + ["name"] = new { type = "String", value = "testregistry" }, + ["loginServer"] = new { type = "String", value = "testregistry.azurecr.io" } + }, + string name when name.StartsWith("env") => 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: mockActivityReporter, processRunner: mockProcessRunner, containerRuntime: fakeContainerRuntime); @@ -272,13 +308,25 @@ public async Task DeployAsync_WithDockerfile_Works() var mockProcessRunner = new MockProcessRunner(); var fakeContainerRuntime = new FakeContainerRuntime(); using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.Deploy); - var armClientProvider = new TestArmClientProvider(new Dictionary + var armClientProvider = new TestArmClientProvider(deploymentName => { - ["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" } + return deploymentName switch + { + string name when name.StartsWith("env-acr") => new Dictionary + { + ["name"] = new { type = "String", value = "testregistry" }, + ["loginServer"] = new { type = "String", value = "testregistry.azurecr.io" } + }, + string name when name.StartsWith("env") => 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: mockActivityReporter, processRunner: mockProcessRunner, containerRuntime: fakeContainerRuntime); @@ -326,13 +374,25 @@ public async Task DeployAsync_WithProjectResource_Works() var fakeContainerRuntime = new FakeContainerRuntime(); var mockActivityReporter = new TestPublishingActivityReporter(testOutputHelper); using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.Deploy); - var armClientProvider = new TestArmClientProvider(new Dictionary + var armClientProvider = new TestArmClientProvider(deploymentName => { - ["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" } + return deploymentName switch + { + string name when name.StartsWith("env-acr") => new Dictionary + { + ["name"] = new { type = "String", value = "testregistry" }, + ["loginServer"] = new { type = "String", value = "testregistry.azurecr.io" } + }, + string name when name.StartsWith("env") => 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, processRunner: mockProcessRunner, activityReporter: mockActivityReporter, containerRuntime: fakeContainerRuntime); @@ -386,6 +446,11 @@ public async Task DeployAsync_WithMultipleComputeEnvironments_Works(string step) { return deploymentName switch { + string name when name.StartsWith("aca-env-acr") => new Dictionary + { + ["name"] = new { type = "String", value = "acaregistry" }, + ["loginServer"] = new { type = "String", value = "acaregistry.azurecr.io" } + }, string name when name.StartsWith("aca-env") => new Dictionary { ["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "acaregistry" }, @@ -395,6 +460,11 @@ public async Task DeployAsync_WithMultipleComputeEnvironments_Works(string step) ["AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN"] = new { type = "String", value = "aca.westus.azurecontainerapps.io" }, ["AZURE_CONTAINER_APPS_ENVIRONMENT_ID"] = new { type = "String", value = "/subscriptions/test/resourceGroups/test-rg/providers/Microsoft.App/managedEnvironments/acaenv" } }, + string name when name.StartsWith("aas-env-acr") => new Dictionary + { + ["name"] = new { type = "String", value = "aasregistry" }, + ["loginServer"] = new { type = "String", value = "aasregistry.azurecr.io" } + }, string name when name.StartsWith("aas-env") => new Dictionary { ["AZURE_CONTAINER_REGISTRY_NAME"] = new { type = "String", value = "aasregistry" }, @@ -605,13 +675,25 @@ public async Task DeployAsync_WithSingleRedisCache_CallsDeployingComputeResource var fakeContainerRuntime = new FakeContainerRuntime(); var mockActivityReporter = new TestPublishingActivityReporter(testOutputHelper); using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.Deploy); - var armClientProvider = new TestArmClientProvider(new Dictionary + var armClientProvider = new TestArmClientProvider(deploymentName => { - ["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" } + return deploymentName switch + { + string name when name.StartsWith("env-acr") => new Dictionary + { + ["name"] = new { type = "String", value = "testregistry" }, + ["loginServer"] = new { type = "String", value = "testregistry.azurecr.io" } + }, + string name when name.StartsWith("env") => 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, processRunner: mockProcessRunner, activityReporter: mockActivityReporter); @@ -657,10 +739,22 @@ public async Task DeployAsync_WithOnlyAzureResources_PrintsDashboardUrl() var fakeContainerRuntime = new FakeContainerRuntime(); var mockActivityReporter = new TestPublishingActivityReporter(testOutputHelper); using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.Deploy); - var armClientProvider = new TestArmClientProvider(new Dictionary + var armClientProvider = new TestArmClientProvider(deploymentName => { - ["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" } + return deploymentName switch + { + string name when name.StartsWith("env-acr") => new Dictionary + { + ["name"] = new { type = "String", value = "testregistry" }, + ["loginServer"] = new { type = "String", value = "testregistry.azurecr.io" } + }, + string name when name.StartsWith("env") => new Dictionary + { + ["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, processRunner: mockProcessRunner, activityReporter: mockActivityReporter); @@ -815,6 +909,11 @@ public async Task DeployAsync_WithAzureFunctionsProject_Works() using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, step: WellKnownPipelineSteps.Deploy); var deploymentOutputsProvider = (string deploymentName) => deploymentName switch { + string name when name.StartsWith("env-acr") => new Dictionary + { + ["name"] = new { type = "String", value = "testregistry" }, + ["loginServer"] = new { type = "String", value = "testregistry.azurecr.io" } + }, string name when name.StartsWith("env") => new Dictionary { ["name"] = new { type = "String", value = "env" }, diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureUserAssignedIdentityTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureUserAssignedIdentityTests.cs index 97c1806fa9a..802259687ff 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureUserAssignedIdentityTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureUserAssignedIdentityTests.cs @@ -69,21 +69,22 @@ public async Task AddAzureUserAssignedIdentity_WithRoleAssignments_Works() var model = app.Services.GetRequiredService(); await ExecuteBeforeStartHooksAsync(app, default); - Assert.Collection(model.Resources.OrderBy(r => r.Name), + Assert.Collection(model.Resources, r => Assert.IsType(r), + r => Assert.IsType(r), r => Assert.IsType(r), + r => Assert.IsType(r), r => Assert.IsType(r), r => { Assert.IsType(r); Assert.Equal("myidentity-roles-myregistry", r.Name); - }, - r => Assert.IsType(r)); + }); var identityResource = Assert.Single(model.Resources.OfType()); var (_, identityBicep) = await GetManifestWithBicep(identityResource, skipPreparer: true); - var registryResource = Assert.Single(model.Resources.OfType()); + var registryResource = Assert.Single(model.Resources.OfType(), r => r.Name == "myregistry"); var (_, registryBicep) = await GetManifestWithBicep(registryResource, skipPreparer: true); var identityRoleAssignments = Assert.Single(model.Resources.OfType(), r => r.Name == "myidentity-roles-myregistry"); @@ -153,6 +154,7 @@ public async Task WithAzureUserAssignedIdentity_WithRoleAssignments_Works() // Validate that only the resources we expect to see are in the model Assert.Collection(model.Resources, r => Assert.IsType(r), + r => Assert.IsType(r), r => Assert.IsType(r), r => Assert.IsType(r), r => Assert.IsType(r), @@ -208,6 +210,7 @@ public async Task WithAzureUserAssignedIdentity_WithRoleAssignments_AzureAppServ // Validate that only the resources we expect to see are in the model Assert.Collection(model.Resources, r => Assert.IsType(r), + r => Assert.IsType(r), r => Assert.IsType(r), r => Assert.IsType(r), r => Assert.IsType(r), @@ -284,6 +287,7 @@ public async Task WithAzureUserAssignedIdentity_WithRoleAssignments_MultipleProj // Validate that only the resources we expect to see are in the model Assert.Collection(model.Resources, r => Assert.IsType(r), + r => Assert.IsType(r), r => Assert.IsType(r), r => Assert.IsType(r), r => Assert.IsType(r), diff --git a/tests/Aspire.Hosting.Azure.Tests/ContainerRegistryTests.cs b/tests/Aspire.Hosting.Azure.Tests/ContainerRegistryTests.cs index ab75fb4d602..9235020dd26 100644 --- a/tests/Aspire.Hosting.Azure.Tests/ContainerRegistryTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/ContainerRegistryTests.cs @@ -106,8 +106,6 @@ public async Task ContainerRegistryInfoIsAccessibleFromPublisher() // Verify the container registry properties on the environment Assert.NotNull(publisher.EnvironmentRegistry.Name); Assert.NotNull(publisher.EnvironmentRegistry.Endpoint); - var azureRegistry = Assert.IsType(publisher.EnvironmentRegistry, exactMatch: false); - Assert.NotNull(azureRegistry.ManagedIdentityId); // Verify container registry info was found in child compute resources Assert.True(publisher.ComputeResourceRegistryFound); @@ -116,17 +114,18 @@ public async Task ContainerRegistryInfoIsAccessibleFromPublisher() // Verify the container registry properties on the compute resource Assert.NotNull(publisher.ComputeResourceRegistry.Name); Assert.NotNull(publisher.ComputeResourceRegistry.Endpoint); - azureRegistry = Assert.IsType(publisher.ComputeResourceRegistry, exactMatch: false); - Assert.NotNull(azureRegistry.ManagedIdentityId); - // Verify both registries are the same instance (or at least have the same values) + // Verify both registries have the same values Assert.Equal(publisher.EnvironmentRegistry.Name.ToString(), publisher.ComputeResourceRegistry.Name.ToString()); Assert.Equal(publisher.EnvironmentRegistry.Endpoint.ToString(), publisher.ComputeResourceRegistry.Endpoint.ToString()); - azureRegistry = Assert.IsType(publisher.EnvironmentRegistry, exactMatch: false); - Assert.Equal(publisher.AzureContainerRegistry?.ManagedIdentityId.ToString(), - azureRegistry.ManagedIdentityId.ToString()); + + // If the registry is an Azure container registry with managed identity, verify it + if (publisher.EnvironmentRegistry is IAzureContainerRegistry azureRegistry) + { + Assert.NotNull(azureRegistry.ManagedIdentityId); + } } /// diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceEnvironmentWithoutDashboardAddsEnvironmentResource.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceEnvironmentWithoutDashboardAddsEnvironmentResource.verified.bicep index 4fc6860b1c3..a9a464c42ef 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceEnvironmentWithoutDashboardAddsEnvironmentResource.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceEnvironmentWithoutDashboardAddsEnvironmentResource.verified.bicep @@ -1,23 +1,20 @@ -@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 userPrincipalId string = '' param tags object = { } +param env_acr_outputs_name string + resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { name: take('env_mi-${uniqueString(resourceGroup().id)}', 128) location: location tags: tags } -resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('envacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: env_acr_outputs_name } resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -50,7 +47,7 @@ output planId string = env_asplan.id output webSiteSuffix string = uniqueString(resourceGroup().id) -output AZURE_CONTAINER_REGISTRY_NAME string = env_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = env_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceEnvironmentWithoutDashboardAddsEnvironmentResource.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceEnvironmentWithoutDashboardAddsEnvironmentResource.verified.json index 0bc8a391d67..aff9b2f4d4f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceEnvironmentWithoutDashboardAddsEnvironmentResource.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceEnvironmentWithoutDashboardAddsEnvironmentResource.verified.json @@ -1,7 +1,8 @@ -{ +{ "type": "azure.bicep.v0", "path": "env.module.bicep", "params": { + "env_acr_outputs_name": "{env-acr.outputs.name}", "userPrincipalId": "" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceToEnvironmentWithAutomaticScaling.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceToEnvironmentWithAutomaticScaling.verified.bicep index 04a7112eac6..cae57cbe6d9 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceToEnvironmentWithAutomaticScaling.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceToEnvironmentWithAutomaticScaling.verified.bicep @@ -5,19 +5,16 @@ param userPrincipalId string = '' param tags object = { } +param env_acr_outputs_name string + resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { name: take('env_mi-${uniqueString(resourceGroup().id)}', 128) location: location tags: tags } -resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('envacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: env_acr_outputs_name } resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -134,7 +131,7 @@ output planId string = env_asplan.id output webSiteSuffix string = uniqueString(resourceGroup().id) -output AZURE_CONTAINER_REGISTRY_NAME string = env_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = env_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceToEnvironmentWithAutomaticScaling.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceToEnvironmentWithAutomaticScaling.verified.json index d3afcb328bb..aff9b2f4d4f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceToEnvironmentWithAutomaticScaling.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceToEnvironmentWithAutomaticScaling.verified.json @@ -2,6 +2,7 @@ "type": "azure.bicep.v0", "path": "env.module.bicep", "params": { + "env_acr_outputs_name": "{env-acr.outputs.name}", "userPrincipalId": "" } } \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsDefaultLocation.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsDefaultLocation.verified.bicep index 6ef1ab716fc..fefad8e0fb2 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsDefaultLocation.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsDefaultLocation.verified.bicep @@ -1,23 +1,20 @@ -@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 userPrincipalId string = '' param tags object = { } +param env_acr_outputs_name string + resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { name: take('env_mi-${uniqueString(resourceGroup().id)}', 128) location: location tags: tags } -resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('envacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: env_acr_outputs_name } resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -151,7 +148,7 @@ output planId string = env_asplan.id output webSiteSuffix string = uniqueString(resourceGroup().id) -output AZURE_CONTAINER_REGISTRY_NAME string = env_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = env_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer @@ -167,4 +164,4 @@ output AZURE_APP_SERVICE_DASHBOARD_URI string = 'https://${take('${toLower('env' output AZURE_APPLICATION_INSIGHTS_INSTRUMENTATIONKEY string = env_ai.properties.InstrumentationKey -output AZURE_APPLICATION_INSIGHTS_CONNECTION_STRING string = env_ai.properties.ConnectionString +output AZURE_APPLICATION_INSIGHTS_CONNECTION_STRING string = env_ai.properties.ConnectionString \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsDefaultLocation.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsDefaultLocation.verified.json index 0bc8a391d67..aff9b2f4d4f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsDefaultLocation.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsDefaultLocation.verified.json @@ -1,7 +1,8 @@ -{ +{ "type": "azure.bicep.v0", "path": "env.module.bicep", "params": { + "env_acr_outputs_name": "{env-acr.outputs.name}", "userPrincipalId": "" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocation.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocation.verified.bicep index f44fa16e1d0..db0e5c88d55 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocation.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocation.verified.bicep @@ -1,23 +1,20 @@ -@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 userPrincipalId string = '' param tags object = { } +param env_acr_outputs_name string + resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { name: take('env_mi-${uniqueString(resourceGroup().id)}', 128) location: location tags: tags } -resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('envacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: env_acr_outputs_name } resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -151,7 +148,7 @@ output planId string = env_asplan.id output webSiteSuffix string = uniqueString(resourceGroup().id) -output AZURE_CONTAINER_REGISTRY_NAME string = env_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = env_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocation.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocation.verified.json index 0bc8a391d67..aff9b2f4d4f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocation.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocation.verified.json @@ -1,7 +1,8 @@ -{ +{ "type": "azure.bicep.v0", "path": "env.module.bicep", "params": { + "env_acr_outputs_name": "{env-acr.outputs.name}", "userPrincipalId": "" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocationParam.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocationParam.verified.bicep index 090d3108606..d4e7519f40c 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocationParam.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocationParam.verified.bicep @@ -1,10 +1,12 @@ -@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 userPrincipalId string = '' param tags object = { } +param env_acr_outputs_name string + param appInsightsLocation string resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { @@ -13,13 +15,8 @@ resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = tags: tags } -resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('envacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: env_acr_outputs_name } resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -153,7 +150,7 @@ output planId string = env_asplan.id output webSiteSuffix string = uniqueString(resourceGroup().id) -output AZURE_CONTAINER_REGISTRY_NAME string = env_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = env_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocationParam.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocationParam.verified.json index f1763823cd6..43767c55cff 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocationParam.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithApplicationInsightsLocationParam.verified.json @@ -1,8 +1,9 @@ -{ +{ "type": "azure.bicep.v0", "path": "env.module.bicep", "params": { + "env_acr_outputs_name": "{env-acr.outputs.name}", "appInsightsLocation": "{appInsightsLocation.value}", "userPrincipalId": "" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithExistingApplicationInsights.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithExistingApplicationInsights.verified.bicep index bc7c5590c6d..742706e45e3 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithExistingApplicationInsights.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithExistingApplicationInsights.verified.bicep @@ -1,10 +1,12 @@ -@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 userPrincipalId string = '' param tags object = { } +param env_acr_outputs_name string + param existingappinsights_outputs_name string resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { @@ -13,13 +15,8 @@ resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = tags: tags } -resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('envacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: env_acr_outputs_name } resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -136,7 +133,7 @@ output planId string = env_asplan.id output webSiteSuffix string = uniqueString(resourceGroup().id) -output AZURE_CONTAINER_REGISTRY_NAME string = env_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = env_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithExistingApplicationInsights.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithExistingApplicationInsights.verified.json index f18a373f1f8..75f212c0623 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithExistingApplicationInsights.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddAppServiceWithExistingApplicationInsights.verified.json @@ -1,8 +1,9 @@ -{ +{ "type": "azure.bicep.v0", "path": "env.module.bicep", "params": { + "env_acr_outputs_name": "{env-acr.outputs.name}", "existingappinsights_outputs_name": "{existingAppInsights.outputs.name}", "userPrincipalId": "" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsEnvironmentResource.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsEnvironmentResource.verified.bicep index 4a03ba0008c..d0db9433307 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsEnvironmentResource.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsEnvironmentResource.verified.bicep @@ -1,23 +1,20 @@ -@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 userPrincipalId string = '' param tags object = { } +param env_acr_outputs_name string + resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { name: take('env_mi-${uniqueString(resourceGroup().id)}', 128) location: location tags: tags } -resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('envacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: env_acr_outputs_name } resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -130,7 +127,7 @@ output planId string = env_asplan.id output webSiteSuffix string = uniqueString(resourceGroup().id) -output AZURE_CONTAINER_REGISTRY_NAME string = env_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = env_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsEnvironmentResource.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsEnvironmentResource.verified.json index d3afcb328bb..aff9b2f4d4f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsEnvironmentResource.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AddContainerAppEnvironmentAddsEnvironmentResource.verified.json @@ -2,6 +2,7 @@ "type": "azure.bicep.v0", "path": "env.module.bicep", "params": { + "env_acr_outputs_name": "{env-acr.outputs.name}", "userPrincipalId": "" } } \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceEnvironmentCanReferenceExistingAppServicePlan.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceEnvironmentCanReferenceExistingAppServicePlan.verified.bicep index 4a03ba0008c..d0db9433307 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceEnvironmentCanReferenceExistingAppServicePlan.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceEnvironmentCanReferenceExistingAppServicePlan.verified.bicep @@ -1,23 +1,20 @@ -@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 userPrincipalId string = '' param tags object = { } +param env_acr_outputs_name string + resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { name: take('env_mi-${uniqueString(resourceGroup().id)}', 128) location: location tags: tags } -resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('envacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: env_acr_outputs_name } resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -130,7 +127,7 @@ output planId string = env_asplan.id output webSiteSuffix string = uniqueString(resourceGroup().id) -output AZURE_CONTAINER_REGISTRY_NAME string = env_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = env_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceEnvironmentCanReferenceExistingAppServicePlan.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceEnvironmentCanReferenceExistingAppServicePlan.verified.json index d3afcb328bb..aff9b2f4d4f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceEnvironmentCanReferenceExistingAppServicePlan.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.AzureAppServiceEnvironmentCanReferenceExistingAppServicePlan.verified.json @@ -2,6 +2,7 @@ "type": "azure.bicep.v0", "path": "env.module.bicep", "params": { + "env_acr_outputs_name": "{env-acr.outputs.name}", "userPrincipalId": "" } } \ No newline at end of file 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 325c9cbf4fb..b09b1238c1a 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.MultipleAzureAppServiceEnvironmentsSupported.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureAppServiceTests.MultipleAzureAppServiceEnvironmentsSupported.verified.json @@ -1,17 +1,27 @@ { "$schema": "https://json.schemastore.org/aspire-8.0.json", "resources": { + "env1-acr": { + "type": "azure.bicep.v0", + "path": "env1-acr.module.bicep" + }, "env1": { "type": "azure.bicep.v0", "path": "env1.module.bicep", "params": { + "env1_acr_outputs_name": "{env1-acr.outputs.name}", "userPrincipalId": "" } }, + "env2-acr": { + "type": "azure.bicep.v0", + "path": "env2-acr.module.bicep" + }, "env2": { "type": "azure.bicep.v0", "path": "env2.module.bicep", "params": { + "env2_acr_outputs_name": "{env2-acr.outputs.name}", "userPrincipalId": "" } }, diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppEnvironmentExtensionsTests.WithAzureLogAnalyticsWorkspace_RespectsExistingWorkspaceInDifferentResourceGroup.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppEnvironmentExtensionsTests.WithAzureLogAnalyticsWorkspace_RespectsExistingWorkspaceInDifferentResourceGroup.verified.bicep index f0f23e5c9d1..657b91abbfb 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppEnvironmentExtensionsTests.WithAzureLogAnalyticsWorkspace_RespectsExistingWorkspaceInDifferentResourceGroup.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppEnvironmentExtensionsTests.WithAzureLogAnalyticsWorkspace_RespectsExistingWorkspaceInDifferentResourceGroup.verified.bicep @@ -5,6 +5,8 @@ param userPrincipalId string = '' param tags object = { } +param app_host_acr_outputs_name string + param log_env_shared_name string param log_env_shared_rg string @@ -15,13 +17,8 @@ resource app_host_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-3 tags: tags } -resource app_host_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('apphostacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource app_host_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: app_host_acr_outputs_name } resource app_host_acr_app_host_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -72,7 +69,7 @@ output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = log_env_shared.name output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = log_env_shared.id -output AZURE_CONTAINER_REGISTRY_NAME string = app_host_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = app_host_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = app_host_acr.properties.loginServer diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppEnvironmentExtensionsTests.WithAzureLogAnalyticsWorkspace_RespectsExistingWorkspaceInDifferentResourceGroup.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppEnvironmentExtensionsTests.WithAzureLogAnalyticsWorkspace_RespectsExistingWorkspaceInDifferentResourceGroup.verified.json index 26e00384caa..0d767c250b8 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppEnvironmentExtensionsTests.WithAzureLogAnalyticsWorkspace_RespectsExistingWorkspaceInDifferentResourceGroup.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppEnvironmentExtensionsTests.WithAzureLogAnalyticsWorkspace_RespectsExistingWorkspaceInDifferentResourceGroup.verified.json @@ -2,6 +2,7 @@ "type": "azure.bicep.v0", "path": "app-host.module.bicep", "params": { + "app_host_acr_outputs_name": "{app-host-acr.outputs.name}", "log_env_shared_name": "{log-env-shared-name.value}", "log_env_shared_rg": "{log-env-shared-rg.value}", "userPrincipalId": "" diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=False.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=False.verified.bicep index c7d3019bf86..cf2311305b3 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=False.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=False.verified.bicep @@ -5,19 +5,16 @@ param userPrincipalId string = '' param tags object = { } +param env_acr_outputs_name string + resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { name: take('env_mi-${uniqueString(resourceGroup().id)}', 128) location: location tags: tags } -resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('envacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: env_acr_outputs_name } resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -117,7 +114,7 @@ output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = env_law.name output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = env_law.id -output AZURE_CONTAINER_REGISTRY_NAME string = env_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = env_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=False.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=False.verified.json index d3afcb328bb..aff9b2f4d4f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=False.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=False.verified.json @@ -2,6 +2,7 @@ "type": "azure.bicep.v0", "path": "env.module.bicep", "params": { + "env_acr_outputs_name": "{env-acr.outputs.name}", "userPrincipalId": "" } } \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=True.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=True.verified.bicep index fdd209194a6..02c6f6f5ae5 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=True.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=True.verified.bicep @@ -5,6 +5,8 @@ param userPrincipalId string = '' param tags object = { } +param env_acr_outputs_name string + var resourceToken = uniqueString(resourceGroup().id) resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { @@ -13,13 +15,8 @@ resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = tags: tags } -resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { name: replace('acr-${resourceToken}', '-', '') - location: location - sku: { - name: 'Basic' - } - tags: tags } resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=True.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=True.verified.json index d3afcb328bb..aff9b2f4d4f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=True.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.AddContainerAppEnvironmentAddsEnvironmentResource_useAzdNaming=True.verified.json @@ -2,6 +2,7 @@ "type": "azure.bicep.v0", "path": "env.module.bicep", "params": { + "env_acr_outputs_name": "{env-acr.outputs.name}", "userPrincipalId": "" } } \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomWorkspace#00.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomWorkspace#00.verified.bicep index 59284bc6187..71f4a563423 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomWorkspace#00.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomWorkspace#00.verified.bicep @@ -5,6 +5,8 @@ param userPrincipalId string = '' param tags object = { } +param env_acr_outputs_name string + param customworkspace_outputs_name string resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { @@ -13,13 +15,8 @@ resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = tags: tags } -resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('envacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: env_acr_outputs_name } resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -69,7 +66,7 @@ output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = customworkspace_outputs_name output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = customworkspace.id -output AZURE_CONTAINER_REGISTRY_NAME string = env_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = env_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomWorkspace#00.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomWorkspace#00.verified.json index ebfcb10a37a..0a71a394f85 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomWorkspace#00.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithCustomWorkspace#00.verified.json @@ -2,7 +2,8 @@ "type": "azure.bicep.v0", "path": "env.module.bicep", "params": { + "env_acr_outputs_name": "{env-acr.outputs.name}", "customworkspace_outputs_name": "{customworkspace.outputs.name}", "userPrincipalId": "" } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardDisabled.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardDisabled.verified.bicep index 7f38a22e186..9445bd45184 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardDisabled.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardDisabled.verified.bicep @@ -5,19 +5,16 @@ param userPrincipalId string = '' param tags object = { } +param env_acr_outputs_name string + resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { name: take('env_mi-${uniqueString(resourceGroup().id)}', 128) location: location tags: tags } -resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('envacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: env_acr_outputs_name } resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -66,7 +63,7 @@ output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = env_law.name output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = env_law.id -output AZURE_CONTAINER_REGISTRY_NAME string = env_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = env_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardDisabled.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardDisabled.verified.json index d3afcb328bb..aff9b2f4d4f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardDisabled.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardDisabled.verified.json @@ -2,6 +2,7 @@ "type": "azure.bicep.v0", "path": "env.module.bicep", "params": { + "env_acr_outputs_name": "{env-acr.outputs.name}", "userPrincipalId": "" } } \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardEnabled.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardEnabled.verified.bicep index b105cc690c4..1faa4b8334a 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardEnabled.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardEnabled.verified.bicep @@ -5,19 +5,16 @@ param userPrincipalId string = '' param tags object = { } +param env_acr_outputs_name string + resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { name: take('env_mi-${uniqueString(resourceGroup().id)}', 128) location: location tags: tags } -resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('envacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: env_acr_outputs_name } resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -74,7 +71,7 @@ output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = env_law.name output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = env_law.id -output AZURE_CONTAINER_REGISTRY_NAME string = env_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = env_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardEnabled.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardEnabled.verified.json index d3afcb328bb..aff9b2f4d4f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardEnabled.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.ContainerAppEnvironmentWithDashboardEnabled.verified.json @@ -2,6 +2,7 @@ "type": "azure.bicep.v0", "path": "env.module.bicep", "params": { + "env_acr_outputs_name": "{env-acr.outputs.name}", "userPrincipalId": "" } } \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.DefaultContainerRegistryUsesAzdNamingWhenEnvironmentDoes.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.DefaultContainerRegistryUsesAzdNamingWhenEnvironmentDoes.verified.bicep new file mode 100644 index 00000000000..3f89e9df542 --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.DefaultContainerRegistryUsesAzdNamingWhenEnvironmentDoes.verified.bicep @@ -0,0 +1,19 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +var resourceToken = uniqueString(resourceGroup().id) + +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { + name: replace('acr-${resourceToken}', '-', '') + location: location + sku: { + name: 'Basic' + } + tags: { + 'aspire-resource-name': 'env-acr' + } +} + +output name string = replace('acr-${resourceToken}', '-', '') + +output loginServer string = env_acr.properties.loginServer \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.DefaultContainerRegistryUsesAzdNamingWhenEnvironmentDoes.verified.json b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.DefaultContainerRegistryUsesAzdNamingWhenEnvironmentDoes.verified.json new file mode 100644 index 00000000000..9d5b7aac4e6 --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.DefaultContainerRegistryUsesAzdNamingWhenEnvironmentDoes.verified.json @@ -0,0 +1,4 @@ +{ + "type": "azure.bicep.v0", + "path": "env-acr.module.bicep" +} \ No newline at end of file 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 5a40e7831db..5b1ff4d2335 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleAzureContainerAppEnvironmentsSupported.verified.json +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleAzureContainerAppEnvironmentsSupported.verified.json @@ -1,17 +1,27 @@ { "$schema": "https://json.schemastore.org/aspire-8.0.json", "resources": { + "env1-acr": { + "type": "azure.bicep.v0", + "path": "env1-acr.module.bicep" + }, "env1": { "type": "azure.bicep.v0", "path": "env1.module.bicep", "params": { + "env1_acr_outputs_name": "{env1-acr.outputs.name}", "userPrincipalId": "" } }, + "env2-acr": { + "type": "azure.bicep.v0", + "path": "env2-acr.module.bicep" + }, "env2": { "type": "azure.bicep.v0", "path": "env2.module.bicep", "params": { + "env2_acr_outputs_name": "{env2-acr.outputs.name}", "userPrincipalId": "" } }, diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleVolumesHaveUniqueNamesInBicep#02.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleVolumesHaveUniqueNamesInBicep#02.verified.txt index e7370e9f1f5..5f4e5709703 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleVolumesHaveUniqueNamesInBicep#02.verified.txt +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleVolumesHaveUniqueNamesInBicep#02.verified.txt @@ -2,6 +2,7 @@ "type": "azure.bicep.v0", "path": "my-ace.module.bicep", "params": { + "my_ace_acr_outputs_name": "{my-ace-acr.outputs.name}", "userPrincipalId": "" } } \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleVolumesHaveUniqueNamesInBicep#03.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleVolumesHaveUniqueNamesInBicep#03.verified.txt index fb5c0833a90..5689d8c8692 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleVolumesHaveUniqueNamesInBicep#03.verified.txt +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureContainerAppsTests.MultipleVolumesHaveUniqueNamesInBicep#03.verified.txt @@ -5,19 +5,16 @@ param userPrincipalId string = '' param tags object = { } +param my_ace_acr_outputs_name string + resource my_ace_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { name: take('my_ace_mi-${uniqueString(resourceGroup().id)}', 128) location: location tags: tags } -resource my_ace_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('myaceacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource my_ace_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: my_ace_acr_outputs_name } resource my_ace_acr_my_ace_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -165,7 +162,7 @@ output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = my_ace_law.name output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = my_ace_law.id -output AZURE_CONTAINER_REGISTRY_NAME string = my_ace_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = my_ace_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = my_ace_acr.properties.loginServer diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithAzureResourceDependencies_DoesNotHang_step=diagnostics.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithAzureResourceDependencies_DoesNotHang_step=diagnostics.verified.txt index c3bfd6d3fa2..785150d6cbd 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithAzureResourceDependencies_DoesNotHang_step=diagnostics.verified.txt +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithAzureResourceDependencies_DoesNotHang_step=diagnostics.verified.txt @@ -5,7 +5,7 @@ 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: 24 +Total steps defined: 27 Analysis for full pipeline execution (showing all steps and their relationships) @@ -22,22 +22,25 @@ Steps with no dependencies run first, followed by steps that depend on them. 6. validate-azure-login 7. create-provisioning-context 8. provision-api-identity - 9. provision-env - 10. provision-kv - 11. login-to-acr-env - 12. push-api - 13. provision-api-website - 14. print-api-summary - 15. provision-api-roles-kv - 16. provision-azure-bicep-resources - 17. print-dashboard-url-env - 18. deploy - 19. deploy-api - 20. diagnostics - 21. publish-prereq - 22. publish-azure634f9 - 23. publish - 24. publish-manifest + 9. provision-env-acr + 10. provision-env + 11. provision-kv + 12. login-to-acr-env-acr + 13. push-prereq + 14. push-api + 15. provision-api-website + 16. print-api-summary + 17. provision-api-roles-kv + 18. provision-azure-bicep-resources + 19. print-dashboard-url-env + 20. deploy + 21. deploy-api + 22. diagnostics + 23. publish-prereq + 24. publish-azure634f9 + 25. publish + 26. publish-manifest + 27. push DETAILED STEP ANALYSIS ====================== @@ -81,10 +84,9 @@ Step: diagnostics Description: Dumps dependency graph information for troubleshooting pipeline execution. Dependencies: none -Step: login-to-acr-env - Description: Logs in to Azure Container Registry for env. - Dependencies: ✓ provision-env - Resource: env (AzureAppServiceEnvironmentResource) +Step: login-to-acr-env-acr + Dependencies: ✓ provision-env-acr + Resource: env-acr (AzureContainerRegistryResource) Tags: acr-login Step: print-api-summary @@ -123,16 +125,22 @@ Step: provision-api-website Step: provision-azure-bicep-resources Description: Aggregation step for all Azure infrastructure provisioning operations. - Dependencies: ✓ create-provisioning-context, ✓ deploy-prereq, ✓ provision-api-identity, ✓ provision-api-roles-kv, ✓ provision-api-website, ✓ provision-env, ✓ provision-kv + Dependencies: ✓ create-provisioning-context, ✓ deploy-prereq, ✓ provision-api-identity, ✓ provision-api-roles-kv, ✓ provision-api-website, ✓ provision-env, ✓ provision-env-acr, ✓ provision-kv Resource: azure634f9 (AzureEnvironmentResource) Tags: provision-infra Step: provision-env Description: Provisions the Azure Bicep resource env using Azure infrastructure. - Dependencies: ✓ create-provisioning-context + Dependencies: ✓ create-provisioning-context, ✓ provision-env-acr Resource: env (AzureAppServiceEnvironmentResource) Tags: provision-infra +Step: provision-env-acr + Description: Provisions the Azure Bicep resource env-acr using Azure infrastructure. + Dependencies: ✓ create-provisioning-context + Resource: env-acr (AzureContainerRegistryResource) + Tags: provision-infra + Step: provision-kv Description: Provisions the Azure Bicep resource kv using Azure infrastructure. Dependencies: ✓ create-provisioning-context @@ -156,12 +164,19 @@ Step: publish-prereq Description: Prerequisite step that runs before any publish operations. Dependencies: ✓ process-parameters +Step: push + Description: Aggregation step for all push operations. All push steps should be required by this step. + Dependencies: ✓ push-api, ✓ push-prereq + Step: push-api - Description: Pushes the container image for api to Azure Container Registry. - Dependencies: ✓ build-api, ✓ login-to-acr-env, ✓ provision-env - Resource: api-website (AzureAppServiceWebSiteResource) + Dependencies: ✓ build-api, ✓ push-prereq + Resource: api (ProjectResource) Tags: push-container-image +Step: push-prereq + Description: Prerequisite step that runs before any push operations. + Dependencies: ✓ login-to-acr-env-acr + Step: validate-azure-login Description: Validates Azure CLI authentication before deployment. Dependencies: ✓ deploy-prereq @@ -213,34 +228,36 @@ If targeting 'create-provisioning-context': If targeting 'deploy': Direct dependencies: build-api, create-provisioning-context, print-api-summary, print-dashboard-url-env, provision-azure-bicep-resources, validate-azure-login - Total steps: 17 + Total steps: 19 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-api-identity | provision-env | provision-kv (parallel) - [5] login-to-acr-env | provision-api-roles-kv (parallel) - [6] push-api - [7] provision-api-website - [8] print-api-summary | provision-azure-bicep-resources (parallel) - [9] print-dashboard-url-env - [10] deploy + [4] provision-api-identity | provision-env-acr | provision-kv (parallel) + [5] login-to-acr-env-acr | provision-api-roles-kv | provision-env (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-website + [9] print-api-summary | provision-azure-bicep-resources (parallel) + [10] print-dashboard-url-env + [11] deploy If targeting 'deploy-api': Direct dependencies: print-api-summary - Total steps: 14 + Total steps: 16 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-api-identity | provision-env | provision-kv (parallel) - [5] login-to-acr-env - [6] push-api - [7] provision-api-website - [8] print-api-summary - [9] deploy-api + [4] provision-api-identity | provision-env-acr | provision-kv (parallel) + [5] login-to-acr-env-acr | provision-env (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-website + [9] print-api-summary + [10] deploy-api If targeting 'deploy-prereq': Direct dependencies: process-parameters @@ -255,45 +272,47 @@ If targeting 'diagnostics': Execution order: [0] diagnostics -If targeting 'login-to-acr-env': - Direct dependencies: provision-env +If targeting 'login-to-acr-env-acr': + Direct dependencies: provision-env-acr Total steps: 6 Execution order: [0] process-parameters [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context - [4] provision-env - [5] login-to-acr-env + [4] provision-env-acr + [5] login-to-acr-env-acr If targeting 'print-api-summary': Direct dependencies: provision-api-website - Total steps: 13 + Total steps: 15 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-api-identity | provision-env | provision-kv (parallel) - [5] login-to-acr-env - [6] push-api - [7] provision-api-website - [8] print-api-summary + [4] provision-api-identity | provision-env-acr | provision-kv (parallel) + [5] login-to-acr-env-acr | provision-env (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-website + [9] print-api-summary If targeting 'print-dashboard-url-env': Direct dependencies: provision-azure-bicep-resources, provision-env - Total steps: 15 + Total steps: 17 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-api-identity | provision-env | provision-kv (parallel) - [5] login-to-acr-env | provision-api-roles-kv (parallel) - [6] push-api - [7] provision-api-website - [8] provision-azure-bicep-resources - [9] print-dashboard-url-env + [4] provision-api-identity | provision-env-acr | provision-kv (parallel) + [5] login-to-acr-env-acr | provision-api-roles-kv | provision-env (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-website + [9] provision-azure-bicep-resources + [10] print-dashboard-url-env If targeting 'process-parameters': Direct dependencies: none @@ -324,32 +343,45 @@ If targeting 'provision-api-roles-kv': If targeting 'provision-api-website': Direct dependencies: create-provisioning-context, provision-api-identity, provision-env, provision-kv, push-api - Total steps: 12 + Total steps: 14 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-api-identity | provision-env | provision-kv (parallel) - [5] login-to-acr-env - [6] push-api - [7] provision-api-website + [4] provision-api-identity | provision-env-acr | provision-kv (parallel) + [5] login-to-acr-env-acr | provision-env (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-website If targeting 'provision-azure-bicep-resources': - Direct dependencies: create-provisioning-context, deploy-prereq, provision-api-identity, provision-api-roles-kv, provision-api-website, provision-env, provision-kv - Total steps: 14 + Direct dependencies: create-provisioning-context, deploy-prereq, provision-api-identity, provision-api-roles-kv, provision-api-website, provision-env, provision-env-acr, provision-kv + Total steps: 16 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-api-identity | provision-env | provision-kv (parallel) - [5] login-to-acr-env | provision-api-roles-kv (parallel) - [6] push-api - [7] provision-api-website - [8] provision-azure-bicep-resources + [4] provision-api-identity | provision-env-acr | provision-kv (parallel) + [5] login-to-acr-env-acr | provision-api-roles-kv | provision-env (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-website + [9] provision-azure-bicep-resources If targeting 'provision-env': + Direct dependencies: create-provisioning-context, provision-env-acr + Total steps: 6 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-env-acr + [5] provision-env + +If targeting 'provision-env-acr': Direct dependencies: create-provisioning-context Total steps: 5 Execution order: @@ -357,7 +389,7 @@ If targeting 'provision-env': [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context - [4] provision-env + [4] provision-env-acr If targeting 'provision-kv': Direct dependencies: create-provisioning-context @@ -399,17 +431,44 @@ If targeting 'publish-prereq': [0] process-parameters [1] publish-prereq +If targeting 'push': + Direct dependencies: push-api, push-prereq + Total steps: 11 + Execution order: + [0] process-parameters + [1] build-prereq | deploy-prereq (parallel) + [2] build-api | validate-azure-login (parallel) + [3] create-provisioning-context + [4] provision-env-acr + [5] login-to-acr-env-acr + [6] push-prereq + [7] push-api + [8] push + If targeting 'push-api': - Direct dependencies: build-api, login-to-acr-env, provision-env - Total steps: 9 + Direct dependencies: build-api, push-prereq + Total steps: 10 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-env - [5] login-to-acr-env - [6] push-api + [4] provision-env-acr + [5] login-to-acr-env-acr + [6] push-prereq + [7] push-api + +If targeting 'push-prereq': + Direct dependencies: login-to-acr-env-acr + Total steps: 7 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-env-acr + [5] login-to-acr-env-acr + [6] push-prereq If targeting 'validate-azure-login': Direct dependencies: deploy-prereq 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 index d06ef1dabe6..b8879a28206 100644 --- 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 @@ -5,7 +5,7 @@ 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: 33 +Total steps defined: 37 Analysis for full pipeline execution (showing all steps and their relationships) @@ -22,31 +22,35 @@ Steps with no dependencies run first, followed by steps that depend on them. 6. build 7. validate-azure-login 8. create-provisioning-context - 9. provision-aas-env - 10. login-to-acr-aas-env - 11. push-api-service - 12. provision-api-service-website - 13. print-api-service-summary - 14. provision-aca-env - 15. provision-cache-containerapp - 16. print-cache-summary - 17. login-to-acr-aca-env - 18. push-python-app - 19. provision-python-app-containerapp - 20. provision-storage - 21. provision-azure-bicep-resources - 22. print-dashboard-url-aas-env - 23. print-dashboard-url-aca-env - 24. print-python-app-summary - 25. deploy - 26. deploy-api-service - 27. deploy-cache - 28. deploy-python-app - 29. diagnostics - 30. publish-prereq - 31. publish-azure634f9 - 32. publish - 33. publish-manifest + 9. provision-aas-env-acr + 10. provision-aas-env + 11. login-to-acr-aas-env-acr + 12. provision-aca-env-acr + 13. login-to-acr-aca-env-acr + 14. push-prereq + 15. push-api-service + 16. provision-api-service-website + 17. print-api-service-summary + 18. provision-aca-env + 19. provision-cache-containerapp + 20. print-cache-summary + 21. push-python-app + 22. provision-python-app-containerapp + 23. provision-storage + 24. provision-azure-bicep-resources + 25. print-dashboard-url-aas-env + 26. print-dashboard-url-aca-env + 27. print-python-app-summary + 28. deploy + 29. deploy-api-service + 30. deploy-cache + 31. deploy-python-app + 32. diagnostics + 33. publish-prereq + 34. publish-azure634f9 + 35. publish + 36. publish-manifest + 37. push DETAILED STEP ANALYSIS ====================== @@ -68,7 +72,6 @@ Step: build-prereq Dependencies: ✓ process-parameters Step: build-python-app - Description: Builds the container image for the python-app container. Dependencies: ✓ build-prereq, ✓ deploy-prereq, ✓ deploy-prereq Resource: python-app (ContainerResource) Tags: build-compute @@ -108,16 +111,14 @@ Step: diagnostics Description: Dumps dependency graph information for troubleshooting pipeline execution. Dependencies: none -Step: login-to-acr-aas-env - Description: Logs in to Azure Container Registry for aas-env. - Dependencies: ✓ provision-aas-env - Resource: aas-env (AzureAppServiceEnvironmentResource) +Step: login-to-acr-aas-env-acr + Dependencies: ✓ provision-aas-env-acr + Resource: aas-env-acr (AzureContainerRegistryResource) Tags: acr-login -Step: login-to-acr-aca-env - Description: Logs in to Azure Container Registry for aca-env. - Dependencies: ✓ provision-aca-env - Resource: aca-env (AzureContainerAppEnvironmentResource) +Step: login-to-acr-aca-env-acr + Dependencies: ✓ provision-aca-env-acr + Resource: aca-env-acr (AzureContainerRegistryResource) Tags: acr-login Step: print-api-service-summary @@ -156,16 +157,28 @@ Step: process-parameters Step: provision-aas-env Description: Provisions the Azure Bicep resource aas-env using Azure infrastructure. - Dependencies: ✓ create-provisioning-context + Dependencies: ✓ create-provisioning-context, ✓ provision-aas-env-acr Resource: aas-env (AzureAppServiceEnvironmentResource) Tags: provision-infra +Step: provision-aas-env-acr + Description: Provisions the Azure Bicep resource aas-env-acr using Azure infrastructure. + Dependencies: ✓ create-provisioning-context + Resource: aas-env-acr (AzureContainerRegistryResource) + Tags: provision-infra + Step: provision-aca-env Description: Provisions the Azure Bicep resource aca-env using Azure infrastructure. - Dependencies: ✓ create-provisioning-context + Dependencies: ✓ create-provisioning-context, ✓ provision-aca-env-acr Resource: aca-env (AzureContainerAppEnvironmentResource) Tags: provision-infra +Step: provision-aca-env-acr + Description: Provisions the Azure Bicep resource aca-env-acr using Azure infrastructure. + Dependencies: ✓ create-provisioning-context + Resource: aca-env-acr (AzureContainerRegistryResource) + Tags: provision-infra + Step: provision-api-service-website Description: Provisions the Azure Bicep resource api-service-website using Azure infrastructure. Dependencies: ✓ create-provisioning-context, ✓ provision-aas-env, ✓ push-api-service @@ -174,7 +187,7 @@ Step: provision-api-service-website Step: provision-azure-bicep-resources Description: Aggregation step for all Azure infrastructure provisioning operations. - 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 + Dependencies: ✓ create-provisioning-context, ✓ deploy-prereq, ✓ provision-aas-env, ✓ provision-aas-env-acr, ✓ provision-aca-env, ✓ provision-aca-env-acr, ✓ provision-api-service-website, ✓ provision-cache-containerapp, ✓ provision-python-app-containerapp, ✓ provision-storage Resource: azure634f9 (AzureEnvironmentResource) Tags: provision-infra @@ -213,16 +226,22 @@ Step: publish-prereq Description: Prerequisite step that runs before any publish operations. Dependencies: ✓ process-parameters +Step: push + Description: Aggregation step for all push operations. All push steps should be required by this step. + Dependencies: ✓ push-api-service, ✓ push-prereq, ✓ push-python-app + Step: push-api-service - Description: Pushes the container image for api-service to Azure Container Registry. - Dependencies: ✓ build-api-service, ✓ login-to-acr-aas-env, ✓ provision-aas-env - Resource: api-service-website (AzureAppServiceWebSiteResource) + Dependencies: ✓ build-api-service, ✓ push-prereq + Resource: api-service (ProjectResource) Tags: push-container-image +Step: push-prereq + Description: Prerequisite step that runs before any push operations. + Dependencies: ✓ login-to-acr-aas-env-acr, ✓ login-to-acr-aca-env-acr + Step: push-python-app - Description: Pushes the container image for python-app to Azure Container Registry. - Dependencies: ✓ build-python-app, ✓ login-to-acr-aca-env, ✓ provision-aca-env - Resource: python-app-containerapp (AzureContainerAppResource) + Dependencies: ✓ build-python-app, ✓ push-prereq + Resource: python-app (ContainerResource) Tags: push-container-image Step: validate-azure-login @@ -284,47 +303,50 @@ If targeting 'create-provisioning-context': If targeting 'deploy': Direct dependencies: build-api-service, build-python-app, create-provisioning-context, print-api-service-summary, 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: 24 + Total steps: 27 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | build-python-app | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-aas-env | provision-aca-env | provision-storage (parallel) - [5] login-to-acr-aas-env | login-to-acr-aca-env | provision-cache-containerapp (parallel) - [6] print-cache-summary | push-api-service | push-python-app (parallel) - [7] provision-api-service-website | provision-python-app-containerapp (parallel) - [8] print-api-service-summary | print-python-app-summary | provision-azure-bicep-resources (parallel) - [9] print-dashboard-url-aas-env | print-dashboard-url-aca-env (parallel) - [10] deploy + [4] provision-aas-env-acr | provision-aca-env-acr | provision-storage (parallel) + [5] login-to-acr-aas-env-acr | login-to-acr-aca-env-acr | provision-aas-env | provision-aca-env (parallel) + [6] provision-cache-containerapp | push-prereq (parallel) + [7] print-cache-summary | push-api-service | push-python-app (parallel) + [8] provision-api-service-website | provision-python-app-containerapp (parallel) + [9] print-api-service-summary | print-python-app-summary | provision-azure-bicep-resources (parallel) + [10] print-dashboard-url-aas-env | print-dashboard-url-aca-env (parallel) + [11] deploy If targeting 'deploy-api-service': Direct dependencies: print-api-service-summary - Total steps: 12 + Total steps: 16 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-aas-env - [5] login-to-acr-aas-env - [6] push-api-service - [7] provision-api-service-website - [8] print-api-service-summary - [9] deploy-api-service + [4] provision-aas-env-acr | provision-aca-env-acr (parallel) + [5] login-to-acr-aas-env-acr | login-to-acr-aca-env-acr | provision-aas-env (parallel) + [6] push-prereq + [7] push-api-service + [8] provision-api-service-website + [9] print-api-service-summary + [10] deploy-api-service If targeting 'deploy-cache': Direct dependencies: print-cache-summary - Total steps: 8 + Total steps: 9 Execution order: [0] process-parameters [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context - [4] provision-aca-env - [5] provision-cache-containerapp - [6] print-cache-summary - [7] deploy-cache + [4] provision-aca-env-acr + [5] provision-aca-env + [6] provision-cache-containerapp + [7] print-cache-summary + [8] deploy-cache If targeting 'deploy-prereq': Direct dependencies: process-parameters @@ -335,18 +357,19 @@ If targeting 'deploy-prereq': If targeting 'deploy-python-app': Direct dependencies: print-python-app-summary - Total steps: 12 + Total steps: 16 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-python-app | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-aca-env - [5] login-to-acr-aca-env - [6] push-python-app - [7] provision-python-app-containerapp - [8] print-python-app-summary - [9] deploy-python-app + [4] provision-aas-env-acr | provision-aca-env-acr (parallel) + [5] login-to-acr-aas-env-acr | login-to-acr-aca-env-acr | provision-aca-env (parallel) + [6] push-prereq + [7] push-python-app + [8] provision-python-app-containerapp + [9] print-python-app-summary + [10] deploy-python-app If targeting 'diagnostics': Direct dependencies: none @@ -354,97 +377,102 @@ If targeting 'diagnostics': Execution order: [0] diagnostics -If targeting 'login-to-acr-aas-env': - Direct dependencies: provision-aas-env +If targeting 'login-to-acr-aas-env-acr': + Direct dependencies: provision-aas-env-acr Total steps: 6 Execution order: [0] process-parameters [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context - [4] provision-aas-env - [5] login-to-acr-aas-env + [4] provision-aas-env-acr + [5] login-to-acr-aas-env-acr -If targeting 'login-to-acr-aca-env': - Direct dependencies: provision-aca-env +If targeting 'login-to-acr-aca-env-acr': + Direct dependencies: provision-aca-env-acr Total steps: 6 Execution order: [0] process-parameters [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context - [4] provision-aca-env - [5] login-to-acr-aca-env + [4] provision-aca-env-acr + [5] login-to-acr-aca-env-acr If targeting 'print-api-service-summary': Direct dependencies: provision-api-service-website - Total steps: 11 + Total steps: 15 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-aas-env - [5] login-to-acr-aas-env - [6] push-api-service - [7] provision-api-service-website - [8] print-api-service-summary + [4] provision-aas-env-acr | provision-aca-env-acr (parallel) + [5] login-to-acr-aas-env-acr | login-to-acr-aca-env-acr | provision-aas-env (parallel) + [6] push-prereq + [7] push-api-service + [8] provision-api-service-website + [9] print-api-service-summary If targeting 'print-cache-summary': Direct dependencies: provision-cache-containerapp - Total steps: 7 + Total steps: 8 Execution order: [0] process-parameters [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context - [4] provision-aca-env - [5] provision-cache-containerapp - [6] print-cache-summary + [4] provision-aca-env-acr + [5] provision-aca-env + [6] provision-cache-containerapp + [7] print-cache-summary If targeting 'print-dashboard-url-aas-env': Direct dependencies: provision-aas-env, provision-azure-bicep-resources - Total steps: 19 + Total steps: 22 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | build-python-app | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-aas-env | provision-aca-env | provision-storage (parallel) - [5] login-to-acr-aas-env | login-to-acr-aca-env | provision-cache-containerapp (parallel) - [6] push-api-service | push-python-app (parallel) - [7] provision-api-service-website | provision-python-app-containerapp (parallel) - [8] provision-azure-bicep-resources - [9] print-dashboard-url-aas-env + [4] provision-aas-env-acr | provision-aca-env-acr | provision-storage (parallel) + [5] login-to-acr-aas-env-acr | login-to-acr-aca-env-acr | provision-aas-env | provision-aca-env (parallel) + [6] provision-cache-containerapp | push-prereq (parallel) + [7] push-api-service | push-python-app (parallel) + [8] provision-api-service-website | provision-python-app-containerapp (parallel) + [9] provision-azure-bicep-resources + [10] print-dashboard-url-aas-env If targeting 'print-dashboard-url-aca-env': Direct dependencies: provision-aca-env, provision-azure-bicep-resources - Total steps: 19 + Total steps: 22 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | build-python-app | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-aas-env | provision-aca-env | provision-storage (parallel) - [5] login-to-acr-aas-env | login-to-acr-aca-env | provision-cache-containerapp (parallel) - [6] push-api-service | push-python-app (parallel) - [7] provision-api-service-website | provision-python-app-containerapp (parallel) - [8] provision-azure-bicep-resources - [9] print-dashboard-url-aca-env + [4] provision-aas-env-acr | provision-aca-env-acr | provision-storage (parallel) + [5] login-to-acr-aas-env-acr | login-to-acr-aca-env-acr | provision-aas-env | provision-aca-env (parallel) + [6] provision-cache-containerapp | push-prereq (parallel) + [7] push-api-service | push-python-app (parallel) + [8] provision-api-service-website | provision-python-app-containerapp (parallel) + [9] provision-azure-bicep-resources + [10] print-dashboard-url-aca-env If targeting 'print-python-app-summary': Direct dependencies: provision-python-app-containerapp - Total steps: 11 + Total steps: 15 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-python-app | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-aca-env - [5] login-to-acr-aca-env - [6] push-python-app - [7] provision-python-app-containerapp - [8] print-python-app-summary + [4] provision-aas-env-acr | provision-aca-env-acr (parallel) + [5] login-to-acr-aas-env-acr | login-to-acr-aca-env-acr | provision-aca-env (parallel) + [6] push-prereq + [7] push-python-app + [8] provision-python-app-containerapp + [9] print-python-app-summary If targeting 'process-parameters': Direct dependencies: none @@ -453,6 +481,17 @@ If targeting 'process-parameters': [0] process-parameters If targeting 'provision-aas-env': + Direct dependencies: create-provisioning-context, provision-aas-env-acr + Total steps: 6 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-aas-env-acr + [5] provision-aas-env + +If targeting 'provision-aas-env-acr': Direct dependencies: create-provisioning-context Total steps: 5 Execution order: @@ -460,9 +499,20 @@ If targeting 'provision-aas-env': [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context - [4] provision-aas-env + [4] provision-aas-env-acr If targeting 'provision-aca-env': + Direct dependencies: create-provisioning-context, provision-aca-env-acr + Total steps: 6 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-aca-env-acr + [5] provision-aca-env + +If targeting 'provision-aca-env-acr': Direct dependencies: create-provisioning-context Total steps: 5 Execution order: @@ -470,58 +520,62 @@ If targeting 'provision-aca-env': [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context - [4] provision-aca-env + [4] provision-aca-env-acr If targeting 'provision-api-service-website': Direct dependencies: create-provisioning-context, provision-aas-env, push-api-service - Total steps: 10 + Total steps: 14 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-aas-env - [5] login-to-acr-aas-env - [6] push-api-service - [7] provision-api-service-website + [4] provision-aas-env-acr | provision-aca-env-acr (parallel) + [5] login-to-acr-aas-env-acr | login-to-acr-aca-env-acr | provision-aas-env (parallel) + [6] push-prereq + [7] push-api-service + [8] 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: 18 + Direct dependencies: create-provisioning-context, deploy-prereq, provision-aas-env, provision-aas-env-acr, provision-aca-env, provision-aca-env-acr, provision-api-service-website, provision-cache-containerapp, provision-python-app-containerapp, provision-storage + Total steps: 21 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | build-python-app | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-aas-env | provision-aca-env | provision-storage (parallel) - [5] login-to-acr-aas-env | login-to-acr-aca-env | provision-cache-containerapp (parallel) - [6] push-api-service | push-python-app (parallel) - [7] provision-api-service-website | provision-python-app-containerapp (parallel) - [8] provision-azure-bicep-resources + [4] provision-aas-env-acr | provision-aca-env-acr | provision-storage (parallel) + [5] login-to-acr-aas-env-acr | login-to-acr-aca-env-acr | provision-aas-env | provision-aca-env (parallel) + [6] provision-cache-containerapp | push-prereq (parallel) + [7] push-api-service | push-python-app (parallel) + [8] provision-api-service-website | provision-python-app-containerapp (parallel) + [9] provision-azure-bicep-resources If targeting 'provision-cache-containerapp': Direct dependencies: create-provisioning-context, provision-aca-env - Total steps: 6 + Total steps: 7 Execution order: [0] process-parameters [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context - [4] provision-aca-env - [5] provision-cache-containerapp + [4] provision-aca-env-acr + [5] provision-aca-env + [6] provision-cache-containerapp If targeting 'provision-python-app-containerapp': Direct dependencies: create-provisioning-context, provision-aca-env, push-python-app - Total steps: 10 + Total steps: 14 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-python-app | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-aca-env - [5] login-to-acr-aca-env - [6] push-python-app - [7] provision-python-app-containerapp + [4] provision-aas-env-acr | provision-aca-env-acr (parallel) + [5] login-to-acr-aas-env-acr | login-to-acr-aca-env-acr | provision-aca-env (parallel) + [6] push-prereq + [7] push-python-app + [8] provision-python-app-containerapp If targeting 'provision-storage': Direct dependencies: create-provisioning-context @@ -563,29 +617,57 @@ If targeting 'publish-prereq': [0] process-parameters [1] publish-prereq +If targeting 'push': + Direct dependencies: push-api-service, push-prereq, push-python-app + Total steps: 15 + Execution order: + [0] process-parameters + [1] build-prereq | deploy-prereq (parallel) + [2] build-api-service | build-python-app | validate-azure-login (parallel) + [3] create-provisioning-context + [4] provision-aas-env-acr | provision-aca-env-acr (parallel) + [5] login-to-acr-aas-env-acr | login-to-acr-aca-env-acr (parallel) + [6] push-prereq + [7] push-api-service | push-python-app (parallel) + [8] push + If targeting 'push-api-service': - Direct dependencies: build-api-service, login-to-acr-aas-env, provision-aas-env - Total steps: 9 + Direct dependencies: build-api-service, push-prereq + Total steps: 12 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api-service | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-aas-env - [5] login-to-acr-aas-env - [6] push-api-service + [4] provision-aas-env-acr | provision-aca-env-acr (parallel) + [5] login-to-acr-aas-env-acr | login-to-acr-aca-env-acr (parallel) + [6] push-prereq + [7] push-api-service -If targeting 'push-python-app': - Direct dependencies: build-python-app, login-to-acr-aca-env, provision-aca-env +If targeting 'push-prereq': + Direct dependencies: login-to-acr-aas-env-acr, login-to-acr-aca-env-acr Total steps: 9 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-aas-env-acr | provision-aca-env-acr (parallel) + [5] login-to-acr-aas-env-acr | login-to-acr-aca-env-acr (parallel) + [6] push-prereq + +If targeting 'push-python-app': + Direct dependencies: build-python-app, push-prereq + Total steps: 12 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-python-app | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-aca-env - [5] login-to-acr-aca-env - [6] push-python-app + [4] provision-aas-env-acr | provision-aca-env-acr (parallel) + [5] login-to-acr-aas-env-acr | login-to-acr-aca-env-acr (parallel) + [6] push-prereq + [7] push-python-app If targeting 'validate-azure-login': Direct dependencies: deploy-prereq diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithRedisAccessKeyAuthentication_CreatesCorrectDependencies.verified.txt b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithRedisAccessKeyAuthentication_CreatesCorrectDependencies.verified.txt index 107388b54dc..023df734203 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithRedisAccessKeyAuthentication_CreatesCorrectDependencies.verified.txt +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureDeployerTests.DeployAsync_WithRedisAccessKeyAuthentication_CreatesCorrectDependencies.verified.txt @@ -5,7 +5,7 @@ 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: 31 +Total steps defined: 34 Analysis for full pipeline execution (showing all steps and their relationships) @@ -26,25 +26,28 @@ Steps with no dependencies run first, followed by steps that depend on them. 10. provision-cache 11. provision-cosmos-kv 12. provision-cosmos - 13. provision-env - 14. provision-pg-kv - 15. provision-pg - 16. login-to-acr-env - 17. push-api - 18. provision-api-website - 19. print-api-summary - 20. provision-api-roles-cache-kv - 21. provision-api-roles-cosmos-kv - 22. provision-api-roles-pg-kv - 23. provision-azure-bicep-resources - 24. print-dashboard-url-env - 25. deploy - 26. deploy-api - 27. diagnostics - 28. publish-prereq - 29. publish-azure634f9 - 30. publish - 31. publish-manifest + 13. provision-env-acr + 14. provision-env + 15. provision-pg-kv + 16. provision-pg + 17. login-to-acr-env-acr + 18. push-prereq + 19. push-api + 20. provision-api-website + 21. print-api-summary + 22. provision-api-roles-cache-kv + 23. provision-api-roles-cosmos-kv + 24. provision-api-roles-pg-kv + 25. provision-azure-bicep-resources + 26. print-dashboard-url-env + 27. deploy + 28. deploy-api + 29. diagnostics + 30. publish-prereq + 31. publish-azure634f9 + 32. publish + 33. publish-manifest + 34. push DETAILED STEP ANALYSIS ====================== @@ -88,10 +91,9 @@ Step: diagnostics Description: Dumps dependency graph information for troubleshooting pipeline execution. Dependencies: none -Step: login-to-acr-env - Description: Logs in to Azure Container Registry for env. - Dependencies: ✓ provision-env - Resource: env (AzureAppServiceEnvironmentResource) +Step: login-to-acr-env-acr + Dependencies: ✓ provision-env-acr + Resource: env-acr (AzureContainerRegistryResource) Tags: acr-login Step: print-api-summary @@ -142,7 +144,7 @@ Step: provision-api-website Step: provision-azure-bicep-resources Description: Aggregation step for all Azure infrastructure provisioning operations. - Dependencies: ✓ create-provisioning-context, ✓ deploy-prereq, ✓ provision-api-identity, ✓ provision-api-roles-cache-kv, ✓ provision-api-roles-cosmos-kv, ✓ provision-api-roles-pg-kv, ✓ provision-api-website, ✓ provision-cache, ✓ provision-cache-kv, ✓ provision-cosmos, ✓ provision-cosmos-kv, ✓ provision-env, ✓ provision-pg, ✓ provision-pg-kv + Dependencies: ✓ create-provisioning-context, ✓ deploy-prereq, ✓ provision-api-identity, ✓ provision-api-roles-cache-kv, ✓ provision-api-roles-cosmos-kv, ✓ provision-api-roles-pg-kv, ✓ provision-api-website, ✓ provision-cache, ✓ provision-cache-kv, ✓ provision-cosmos, ✓ provision-cosmos-kv, ✓ provision-env, ✓ provision-env-acr, ✓ provision-pg, ✓ provision-pg-kv Resource: azure634f9 (AzureEnvironmentResource) Tags: provision-infra @@ -172,10 +174,16 @@ Step: provision-cosmos-kv Step: provision-env Description: Provisions the Azure Bicep resource env using Azure infrastructure. - Dependencies: ✓ create-provisioning-context + Dependencies: ✓ create-provisioning-context, ✓ provision-env-acr Resource: env (AzureAppServiceEnvironmentResource) Tags: provision-infra +Step: provision-env-acr + Description: Provisions the Azure Bicep resource env-acr using Azure infrastructure. + Dependencies: ✓ create-provisioning-context + Resource: env-acr (AzureContainerRegistryResource) + Tags: provision-infra + Step: provision-pg Description: Provisions the Azure Bicep resource pg using Azure infrastructure. Dependencies: ✓ create-provisioning-context, ✓ provision-pg-kv @@ -205,12 +213,19 @@ Step: publish-prereq Description: Prerequisite step that runs before any publish operations. Dependencies: ✓ process-parameters +Step: push + Description: Aggregation step for all push operations. All push steps should be required by this step. + Dependencies: ✓ push-api, ✓ push-prereq + Step: push-api - Description: Pushes the container image for api to Azure Container Registry. - Dependencies: ✓ build-api, ✓ login-to-acr-env, ✓ provision-env - Resource: api-website (AzureAppServiceWebSiteResource) + Dependencies: ✓ build-api, ✓ push-prereq + Resource: api (ProjectResource) Tags: push-container-image +Step: push-prereq + Description: Prerequisite step that runs before any push operations. + Dependencies: ✓ login-to-acr-env-acr + Step: validate-azure-login Description: Validates Azure CLI authentication before deployment. Dependencies: ✓ deploy-prereq @@ -262,34 +277,36 @@ If targeting 'create-provisioning-context': If targeting 'deploy': Direct dependencies: build-api, create-provisioning-context, print-api-summary, print-dashboard-url-env, provision-azure-bicep-resources, validate-azure-login - Total steps: 24 + Total steps: 26 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-api-identity | provision-cache-kv | provision-cosmos-kv | provision-env | provision-pg-kv (parallel) - [5] login-to-acr-env | provision-api-roles-cache-kv | provision-api-roles-cosmos-kv | provision-api-roles-pg-kv | provision-cache | provision-cosmos | provision-pg (parallel) - [6] push-api - [7] provision-api-website - [8] print-api-summary | provision-azure-bicep-resources (parallel) - [9] print-dashboard-url-env - [10] deploy + [4] provision-api-identity | provision-cache-kv | provision-cosmos-kv | provision-env-acr | provision-pg-kv (parallel) + [5] login-to-acr-env-acr | provision-api-roles-cache-kv | provision-api-roles-cosmos-kv | provision-api-roles-pg-kv | provision-cache | provision-cosmos | provision-env | provision-pg (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-website + [9] print-api-summary | provision-azure-bicep-resources (parallel) + [10] print-dashboard-url-env + [11] deploy If targeting 'deploy-api': Direct dependencies: print-api-summary - Total steps: 19 + Total steps: 21 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-api-identity | provision-cache-kv | provision-cosmos-kv | provision-env | provision-pg-kv (parallel) - [5] login-to-acr-env | provision-cache | provision-cosmos | provision-pg (parallel) - [6] push-api - [7] provision-api-website - [8] print-api-summary - [9] deploy-api + [4] provision-api-identity | provision-cache-kv | provision-cosmos-kv | provision-env-acr | provision-pg-kv (parallel) + [5] login-to-acr-env-acr | provision-cache | provision-cosmos | provision-env | provision-pg (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-website + [9] print-api-summary + [10] deploy-api If targeting 'deploy-prereq': Direct dependencies: process-parameters @@ -304,45 +321,47 @@ If targeting 'diagnostics': Execution order: [0] diagnostics -If targeting 'login-to-acr-env': - Direct dependencies: provision-env +If targeting 'login-to-acr-env-acr': + Direct dependencies: provision-env-acr Total steps: 6 Execution order: [0] process-parameters [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context - [4] provision-env - [5] login-to-acr-env + [4] provision-env-acr + [5] login-to-acr-env-acr If targeting 'print-api-summary': Direct dependencies: provision-api-website - Total steps: 18 + Total steps: 20 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-api-identity | provision-cache-kv | provision-cosmos-kv | provision-env | provision-pg-kv (parallel) - [5] login-to-acr-env | provision-cache | provision-cosmos | provision-pg (parallel) - [6] push-api - [7] provision-api-website - [8] print-api-summary + [4] provision-api-identity | provision-cache-kv | provision-cosmos-kv | provision-env-acr | provision-pg-kv (parallel) + [5] login-to-acr-env-acr | provision-cache | provision-cosmos | provision-env | provision-pg (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-website + [9] print-api-summary If targeting 'print-dashboard-url-env': Direct dependencies: provision-azure-bicep-resources, provision-env - Total steps: 22 + Total steps: 24 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-api-identity | provision-cache-kv | provision-cosmos-kv | provision-env | provision-pg-kv (parallel) - [5] login-to-acr-env | provision-api-roles-cache-kv | provision-api-roles-cosmos-kv | provision-api-roles-pg-kv | provision-cache | provision-cosmos | provision-pg (parallel) - [6] push-api - [7] provision-api-website - [8] provision-azure-bicep-resources - [9] print-dashboard-url-env + [4] provision-api-identity | provision-cache-kv | provision-cosmos-kv | provision-env-acr | provision-pg-kv (parallel) + [5] login-to-acr-env-acr | provision-api-roles-cache-kv | provision-api-roles-cosmos-kv | provision-api-roles-pg-kv | provision-cache | provision-cosmos | provision-env | provision-pg (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-website + [9] provision-azure-bicep-resources + [10] print-dashboard-url-env If targeting 'process-parameters': Direct dependencies: none @@ -395,30 +414,32 @@ If targeting 'provision-api-roles-pg-kv': If targeting 'provision-api-website': Direct dependencies: create-provisioning-context, provision-api-identity, provision-cache, provision-cache-kv, provision-cosmos, provision-cosmos-kv, provision-env, provision-pg, provision-pg-kv, push-api - Total steps: 17 + Total steps: 19 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-api-identity | provision-cache-kv | provision-cosmos-kv | provision-env | provision-pg-kv (parallel) - [5] login-to-acr-env | provision-cache | provision-cosmos | provision-pg (parallel) - [6] push-api - [7] provision-api-website + [4] provision-api-identity | provision-cache-kv | provision-cosmos-kv | provision-env-acr | provision-pg-kv (parallel) + [5] login-to-acr-env-acr | provision-cache | provision-cosmos | provision-env | provision-pg (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-website If targeting 'provision-azure-bicep-resources': - Direct dependencies: create-provisioning-context, deploy-prereq, provision-api-identity, provision-api-roles-cache-kv, provision-api-roles-cosmos-kv, provision-api-roles-pg-kv, provision-api-website, provision-cache, provision-cache-kv, provision-cosmos, provision-cosmos-kv, provision-env, provision-pg, provision-pg-kv - Total steps: 21 + Direct dependencies: create-provisioning-context, deploy-prereq, provision-api-identity, provision-api-roles-cache-kv, provision-api-roles-cosmos-kv, provision-api-roles-pg-kv, provision-api-website, provision-cache, provision-cache-kv, provision-cosmos, provision-cosmos-kv, provision-env, provision-env-acr, provision-pg, provision-pg-kv + Total steps: 23 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-api-identity | provision-cache-kv | provision-cosmos-kv | provision-env | provision-pg-kv (parallel) - [5] login-to-acr-env | provision-api-roles-cache-kv | provision-api-roles-cosmos-kv | provision-api-roles-pg-kv | provision-cache | provision-cosmos | provision-pg (parallel) - [6] push-api - [7] provision-api-website - [8] provision-azure-bicep-resources + [4] provision-api-identity | provision-cache-kv | provision-cosmos-kv | provision-env-acr | provision-pg-kv (parallel) + [5] login-to-acr-env-acr | provision-api-roles-cache-kv | provision-api-roles-cosmos-kv | provision-api-roles-pg-kv | provision-cache | provision-cosmos | provision-env | provision-pg (parallel) + [6] push-prereq + [7] push-api + [8] provision-api-website + [9] provision-azure-bicep-resources If targeting 'provision-cache': Direct dependencies: create-provisioning-context, provision-cache-kv @@ -463,6 +484,17 @@ If targeting 'provision-cosmos-kv': [4] provision-cosmos-kv If targeting 'provision-env': + Direct dependencies: create-provisioning-context, provision-env-acr + Total steps: 6 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-env-acr + [5] provision-env + +If targeting 'provision-env-acr': Direct dependencies: create-provisioning-context Total steps: 5 Execution order: @@ -470,7 +502,7 @@ If targeting 'provision-env': [1] deploy-prereq [2] validate-azure-login [3] create-provisioning-context - [4] provision-env + [4] provision-env-acr If targeting 'provision-pg': Direct dependencies: create-provisioning-context, provision-pg-kv @@ -523,17 +555,44 @@ If targeting 'publish-prereq': [0] process-parameters [1] publish-prereq +If targeting 'push': + Direct dependencies: push-api, push-prereq + Total steps: 11 + Execution order: + [0] process-parameters + [1] build-prereq | deploy-prereq (parallel) + [2] build-api | validate-azure-login (parallel) + [3] create-provisioning-context + [4] provision-env-acr + [5] login-to-acr-env-acr + [6] push-prereq + [7] push-api + [8] push + If targeting 'push-api': - Direct dependencies: build-api, login-to-acr-env, provision-env - Total steps: 9 + Direct dependencies: build-api, push-prereq + Total steps: 10 Execution order: [0] process-parameters [1] build-prereq | deploy-prereq (parallel) [2] build-api | validate-azure-login (parallel) [3] create-provisioning-context - [4] provision-env - [5] login-to-acr-env - [6] push-api + [4] provision-env-acr + [5] login-to-acr-env-acr + [6] push-prereq + [7] push-api + +If targeting 'push-prereq': + Direct dependencies: login-to-acr-env-acr + Total steps: 7 + Execution order: + [0] process-parameters + [1] deploy-prereq + [2] validate-azure-login + [3] create-provisioning-context + [4] provision-env-acr + [5] login-to-acr-env-acr + [6] push-prereq If targeting 'validate-azure-login': Direct dependencies: deploy-prereq diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.AzurePublishingContext_CapturesParametersAndOutputsCorrectly_WithSnapshot#00.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.AzurePublishingContext_CapturesParametersAndOutputsCorrectly_WithSnapshot#00.verified.bicep index 84d95d31e32..b66d29a6f82 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.AzurePublishingContext_CapturesParametersAndOutputsCorrectly_WithSnapshot#00.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.AzurePublishingContext_CapturesParametersAndOutputsCorrectly_WithSnapshot#00.verified.bicep @@ -15,11 +15,20 @@ resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = { location: location } +module acaEnv_acr 'acaEnv-acr/acaEnv-acr.bicep' = { + name: 'acaEnv-acr' + scope: rg + params: { + location: location + } +} + module acaEnv 'acaEnv/acaEnv.bicep' = { name: 'acaEnv' scope: rg params: { location: location + acaenv_acr_outputs_name: acaEnv_acr.outputs.name userPrincipalId: principalId } } @@ -78,9 +87,9 @@ module fe_roles_account 'fe-roles-account/fe-roles-account.bicep' = { } } -output acaEnv_AZURE_CONTAINER_REGISTRY_NAME string = acaEnv.outputs.AZURE_CONTAINER_REGISTRY_NAME +output acaEnv_acr_name string = acaEnv_acr.outputs.name -output acaEnv_AZURE_CONTAINER_REGISTRY_ENDPOINT string = acaEnv.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT +output acaEnv_acr_loginServer string = acaEnv_acr.outputs.loginServer output acaEnv_AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = acaEnv.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID @@ -88,6 +97,8 @@ output acaEnv_AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = acaEnv.ou output acaEnv_AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = acaEnv.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID +output acaEnv_AZURE_CONTAINER_REGISTRY_ENDPOINT string = acaEnv.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT + output fe_identity_id string = fe_identity.outputs.id output storage_blobEndpoint string = storage.outputs.blobEndpoint diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.PublishAsync_GeneratesMainBicep_WithSnapshots.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.PublishAsync_GeneratesMainBicep_WithSnapshots.verified.bicep index 072a24148d6..4ca4110c778 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.PublishAsync_GeneratesMainBicep_WithSnapshots.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.PublishAsync_GeneratesMainBicep_WithSnapshots.verified.bicep @@ -19,11 +19,20 @@ resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = { location: location } +module acaEnv_acr 'acaEnv-acr/acaEnv-acr.bicep' = { + name: 'acaEnv-acr' + scope: rg + params: { + location: location + } +} + module acaEnv 'acaEnv/acaEnv.bicep' = { name: 'acaEnv' scope: rg params: { location: location + acaenv_acr_outputs_name: acaEnv_acr.outputs.name userPrincipalId: principalId } } @@ -116,9 +125,9 @@ module fe_roles_storage 'fe-roles-storage/fe-roles-storage.bicep' = { } } -output acaEnv_AZURE_CONTAINER_REGISTRY_NAME string = acaEnv.outputs.AZURE_CONTAINER_REGISTRY_NAME +output acaEnv_acr_name string = acaEnv_acr.outputs.name -output acaEnv_AZURE_CONTAINER_REGISTRY_ENDPOINT string = acaEnv.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT +output acaEnv_acr_loginServer string = acaEnv_acr.outputs.loginServer output acaEnv_AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = acaEnv.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID @@ -132,6 +141,8 @@ output account_connectionString string = account.outputs.connectionString output myapp_identity_clientId string = myapp_identity.outputs.clientId +output acaEnv_AZURE_CONTAINER_REGISTRY_ENDPOINT string = acaEnv.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT + output fe_identity_id string = fe_identity.outputs.id output storage_blobEndpoint string = storage.outputs.blobEndpoint diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.WhenUsedWithAzureContainerAppsEnvironment_GeneratesProperBicep#00.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.WhenUsedWithAzureContainerAppsEnvironment_GeneratesProperBicep#00.verified.bicep index a19fed818bb..c2170bb2bef 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.WhenUsedWithAzureContainerAppsEnvironment_GeneratesProperBicep#00.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.WhenUsedWithAzureContainerAppsEnvironment_GeneratesProperBicep#00.verified.bicep @@ -11,18 +11,27 @@ resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = { location: location } +module env_acr 'env-acr/env-acr.bicep' = { + name: 'env-acr' + scope: rg + params: { + location: location + } +} + module env 'env/env.bicep' = { name: 'env' scope: rg params: { location: location + env_acr_outputs_name: env_acr.outputs.name userPrincipalId: principalId } } -output env_AZURE_CONTAINER_REGISTRY_NAME string = env.outputs.AZURE_CONTAINER_REGISTRY_NAME +output env_acr_name string = env_acr.outputs.name -output env_AZURE_CONTAINER_REGISTRY_ENDPOINT string = env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT +output env_acr_loginServer string = env_acr.outputs.loginServer output env_AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.WhenUsedWithAzureContainerAppsEnvironment_GeneratesProperBicep#01.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.WhenUsedWithAzureContainerAppsEnvironment_GeneratesProperBicep#01.verified.bicep index b105cc690c4..1faa4b8334a 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.WhenUsedWithAzureContainerAppsEnvironment_GeneratesProperBicep#01.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.WhenUsedWithAzureContainerAppsEnvironment_GeneratesProperBicep#01.verified.bicep @@ -5,19 +5,16 @@ param userPrincipalId string = '' param tags object = { } +param env_acr_outputs_name string + resource env_mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { name: take('env_mi-${uniqueString(resourceGroup().id)}', 128) location: location tags: tags } -resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: take('envacr${uniqueString(resourceGroup().id)}', 50) - location: location - sku: { - name: 'Basic' - } - tags: tags +resource env_acr 'Microsoft.ContainerRegistry/registries@2025-04-01' existing = { + name: env_acr_outputs_name } resource env_acr_env_mi_AcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { @@ -74,7 +71,7 @@ output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = env_law.name output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = env_law.id -output AZURE_CONTAINER_REGISTRY_NAME string = env_acr.name +output AZURE_CONTAINER_REGISTRY_NAME string = env_acr_outputs_name output AZURE_CONTAINER_REGISTRY_ENDPOINT string = env_acr.properties.loginServer diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.WhenUsedWithAzureContainerAppsEnvironment_RespectsStronglyTypedProperties.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.WhenUsedWithAzureContainerAppsEnvironment_RespectsStronglyTypedProperties.verified.bicep index 11f1b437e64..57ddc9b1adb 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.WhenUsedWithAzureContainerAppsEnvironment_RespectsStronglyTypedProperties.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureEnvironmentResourceTests.WhenUsedWithAzureContainerAppsEnvironment_RespectsStronglyTypedProperties.verified.bicep @@ -11,18 +11,27 @@ resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = { location: location } +module env_acr 'env-acr/env-acr.bicep' = { + name: 'env-acr' + scope: rg + params: { + location: location + } +} + module env 'env/env.bicep' = { name: 'env' scope: rg params: { location: location + env_acr_outputs_name: env_acr.outputs.name userPrincipalId: principalId } } -output env_AZURE_CONTAINER_REGISTRY_NAME string = env.outputs.AZURE_CONTAINER_REGISTRY_NAME +output env_acr_name string = env_acr.outputs.name -output env_AZURE_CONTAINER_REGISTRY_ENDPOINT string = env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT +output env_acr_loginServer string = env_acr.outputs.loginServer output env_AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID diff --git a/tests/Aspire.Hosting.Containers.Tests/WithDockerfileTests.cs b/tests/Aspire.Hosting.Containers.Tests/WithDockerfileTests.cs index 0ad78a44613..5090d746158 100644 --- a/tests/Aspire.Hosting.Containers.Tests/WithDockerfileTests.cs +++ b/tests/Aspire.Hosting.Containers.Tests/WithDockerfileTests.cs @@ -784,12 +784,17 @@ public async Task WithDockerfileFactorySyncFactoryCreatesAnnotationWithFactory() Resource = container.Resource }; var steps = (await stepsAnnotation.CreateStepsAsync(factoryContext)).ToList(); - var buildStep = Assert.Single(steps); + Assert.Equal(2, steps.Count); + + var buildStep = steps.Single(s => s.Tags.Contains(WellKnownPipelineTags.BuildCompute)); Assert.Equal("build-mycontainer", buildStep.Name); - Assert.Contains(WellKnownPipelineTags.BuildCompute, buildStep.Tags); Assert.Contains(WellKnownPipelineSteps.Build, buildStep.RequiredBySteps); Assert.Contains(WellKnownPipelineSteps.BuildPrereq, buildStep.DependsOnSteps); + var pushStep = steps.Single(s => s.Tags.Contains(WellKnownPipelineTags.PushContainerImage)); + Assert.Equal("push-mycontainer", pushStep.Name); + Assert.Contains(WellKnownPipelineSteps.Push, pushStep.RequiredBySteps); + // Verify the factory produces the expected content var context = new DockerfileFactoryContext { @@ -941,12 +946,16 @@ public async Task WithDockerfile_AutomaticallyGeneratesBuildStep_WithCorrectDepe }; var steps = (await pipelineStepAnnotation.CreateStepsAsync(factoryContext)).ToList(); + Assert.Equal(2, steps.Count); - var buildStep = Assert.Single(steps); + var buildStep = steps.Single(s => s.Tags.Contains(WellKnownPipelineTags.BuildCompute)); 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); + + var pushStep = steps.Single(s => s.Tags.Contains(WellKnownPipelineTags.PushContainerImage)); + Assert.Equal("push-test-container", pushStep.Name); + Assert.Contains(WellKnownPipelineSteps.Push, pushStep.RequiredBySteps); } [Fact] @@ -977,10 +986,15 @@ public async Task WithDockerfile_CalledMultipleTimes_OverwritesPreviousBuildStep }; var steps = (await pipelineStepAnnotation1.CreateStepsAsync(factoryContext)).ToList(); - var buildStep = Assert.Single(steps); + Assert.Equal(2, steps.Count); + + var buildStep = steps.Single(s => s.Tags.Contains(WellKnownPipelineTags.BuildCompute)); 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); + + var pushStep = steps.Single(s => s.Tags.Contains(WellKnownPipelineTags.PushContainerImage)); + Assert.Equal("push-test-container", pushStep.Name); + Assert.Contains(WellKnownPipelineSteps.Push, pushStep.RequiredBySteps); } } diff --git a/tests/Aspire.Hosting.Tests/ContainerRegistryResourceTests.cs b/tests/Aspire.Hosting.Tests/ContainerRegistryResourceTests.cs index 68e2690dec0..1c7f1ec7c9d 100644 --- a/tests/Aspire.Hosting.Tests/ContainerRegistryResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/ContainerRegistryResourceTests.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIRECOMPUTE003 // 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 Aspire.Hosting.Pipelines; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; @@ -329,4 +331,355 @@ public void WithContainerRegistryWithNullRegistryThrows() Assert.Throws(() => container.WithContainerRegistry(registry)); } + + [Fact] + public void ContainerWithDockerfileHasPushStepAndConfigurationAnnotations() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + builder.AddContainerRegistry("docker-hub", "docker.io", "myuser"); + var container = builder.AddDockerfile("mycontainer", "../myapp"); + + var pipelineStepAnnotations = container.Resource.Annotations.OfType().ToList(); + var pipelineConfigAnnotations = container.Resource.Annotations.OfType().ToList(); + + Assert.NotEmpty(pipelineStepAnnotations); + Assert.NotEmpty(pipelineConfigAnnotations); + } + + [Fact] + public void ProjectResourceHasPushStepAndConfigurationAnnotations() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + builder.AddContainerRegistry("docker-hub", "docker.io", "myuser"); + var project = builder.AddProject("api"); + + var pipelineStepAnnotations = project.Resource.Annotations.OfType().ToList(); + var pipelineConfigAnnotations = project.Resource.Annotations.OfType().ToList(); + + Assert.NotEmpty(pipelineStepAnnotations); + Assert.NotEmpty(pipelineConfigAnnotations); + } + + [Fact] + public async Task ProjectResourcePushStepHasPushContainerImageTag() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + builder.AddContainerRegistry("docker-hub", "docker.io", "myuser"); + var project = builder.AddProject("api"); + + var pipelineStepAnnotation = Assert.Single(project.Resource.Annotations.OfType()); + + var factoryContext = new PipelineStepFactoryContext + { + PipelineContext = null!, + Resource = project.Resource + }; + + var steps = (await pipelineStepAnnotation.CreateStepsAsync(factoryContext)).ToList(); + + var pushStep = steps.FirstOrDefault(s => s.Name == "push-api"); + Assert.NotNull(pushStep); + Assert.Contains(WellKnownPipelineTags.PushContainerImage, pushStep.Tags); + Assert.Contains(WellKnownPipelineSteps.Push, pushStep.RequiredBySteps); + } + + [Fact] + public async Task ContainerWithDockerfilePushStepHasPushContainerImageTag() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + builder.AddContainerRegistry("docker-hub", "docker.io", "myuser"); + var container = builder.AddDockerfile("mycontainer", "../myapp"); + + var pipelineStepAnnotation = Assert.Single(container.Resource.Annotations.OfType()); + + var factoryContext = new PipelineStepFactoryContext + { + PipelineContext = null!, + Resource = container.Resource + }; + + var steps = (await pipelineStepAnnotation.CreateStepsAsync(factoryContext)).ToList(); + + var pushStep = steps.FirstOrDefault(s => s.Name == "push-mycontainer"); + Assert.NotNull(pushStep); + Assert.Contains(WellKnownPipelineTags.PushContainerImage, pushStep.Tags); + Assert.Contains(WellKnownPipelineSteps.Push, pushStep.RequiredBySteps); + } + + [Fact] + public async Task ContainerWithoutDockerfileHasNoPushStep() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + builder.AddContainerRegistry("docker-hub", "docker.io", "myuser"); + var container = builder.AddContainer("mycontainer", "myimage"); + + var pipelineStepAnnotations = container.Resource.Annotations.OfType().ToList(); + + foreach (var annotation in pipelineStepAnnotations) + { + var factoryContext = new PipelineStepFactoryContext + { + PipelineContext = null!, + Resource = container.Resource + }; + + var steps = (await annotation.CreateStepsAsync(factoryContext)).ToList(); + + Assert.DoesNotContain(steps, s => s.Tags.Contains(WellKnownPipelineTags.PushContainerImage)); + } + } + + [Fact] + public async Task ProjectResourcePushStepDependsOnBuildStep() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + builder.AddContainerRegistry("docker-hub", "docker.io", "myuser"); + var project = builder.AddProject("api"); + + var pipelineStepAnnotation = Assert.Single(project.Resource.Annotations.OfType()); + + var factoryContext = new PipelineStepFactoryContext + { + PipelineContext = null!, + Resource = project.Resource + }; + + var steps = (await pipelineStepAnnotation.CreateStepsAsync(factoryContext)).ToList(); + + var buildStep = steps.FirstOrDefault(s => s.Name == "build-api"); + var pushStep = steps.FirstOrDefault(s => s.Name == "push-api"); + + Assert.NotNull(buildStep); + Assert.NotNull(pushStep); + + Assert.Contains(WellKnownPipelineTags.BuildCompute, buildStep.Tags); + } + + [Fact] + public void WithContainerRegistryAddsAnnotationToProject() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var registry = builder.AddContainerRegistry("docker-hub", "docker.io", "myuser"); + var project = builder.AddProject("api") + .WithContainerRegistry(registry); + + var annotation = project.Resource.Annotations.OfType().FirstOrDefault(); + Assert.NotNull(annotation); + Assert.Same(registry.Resource, annotation.Registry); + } + + [Fact] + public void MultipleRegistriesCanBeAddedWithExplicitSelection() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var registry1 = builder.AddContainerRegistry("docker-hub", "docker.io", "user1"); + var registry2 = builder.AddContainerRegistry("ghcr", "ghcr.io", "user2"); + + var project = builder.AddProject("api") + .WithContainerRegistry(registry1); + + var annotations = project.Resource.Annotations.OfType().ToList(); + Assert.Single(annotations); + Assert.Same(registry1.Resource, annotations[0].Registry); + } + + [Fact] + public void ContainerRegistryReferenceAnnotationHoldsCorrectRegistry() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var registry = builder.AddContainerRegistry("acr", "myregistry.azurecr.io"); + + var annotation = new ContainerRegistryReferenceAnnotation(registry.Resource); + + Assert.Same(registry.Resource, annotation.Registry); + } + + [Fact] + public async Task RegistryTargetAnnotationIsAddedToResourcesOnBeforeStartEvent() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var registry = builder.AddContainerRegistry("docker-hub", "docker.io", "myuser"); + var project = builder.AddProject("api"); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + + // Before BeforeStartEvent, the project should not have RegistryTargetAnnotation + Assert.Empty(project.Resource.Annotations.OfType()); + + // Simulate BeforeStartEvent + var beforeStartEvent = new BeforeStartEvent(app.Services, appModel); + await builder.Eventing.PublishAsync(beforeStartEvent); + + // After BeforeStartEvent, the project should have RegistryTargetAnnotation + var registryTargetAnnotation = Assert.Single(project.Resource.Annotations.OfType()); + Assert.Same(registry.Resource, registryTargetAnnotation.Registry); + } + + [Fact] + public async Task MultipleRegistriesAddMultipleRegistryTargetAnnotations() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var registry1 = builder.AddContainerRegistry("docker-hub", "docker.io", "user1"); + var registry2 = builder.AddContainerRegistry("ghcr", "ghcr.io", "user2"); + var project = builder.AddProject("api"); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + + // Simulate BeforeStartEvent + var beforeStartEvent = new BeforeStartEvent(app.Services, appModel); + await builder.Eventing.PublishAsync(beforeStartEvent); + + // Project should have two RegistryTargetAnnotations + var registryTargetAnnotations = project.Resource.Annotations.OfType().ToList(); + Assert.Equal(2, registryTargetAnnotations.Count); + + var registryResources = registryTargetAnnotations.Select(a => a.Registry).ToList(); + Assert.Contains(registry1.Resource, registryResources); + Assert.Contains(registry2.Resource, registryResources); + } + + [Fact] + public async Task GetContainerRegistryReturnsRegistryFromRegistryTargetAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var registry = builder.AddContainerRegistry("docker-hub", "docker.io", "myuser"); + var project = builder.AddProject("api"); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + + // Simulate BeforeStartEvent to add RegistryTargetAnnotation + var beforeStartEvent = new BeforeStartEvent(app.Services, appModel); + await builder.Eventing.PublishAsync(beforeStartEvent); + + // GetContainerRegistry should return the registry from RegistryTargetAnnotation + var containerRegistry = project.Resource.GetContainerRegistry(); + Assert.Same(registry.Resource, containerRegistry); + } + + [Fact] + public async Task GetContainerRegistryPrefersContainerRegistryReferenceAnnotationOverRegistryTargetAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var registry1 = builder.AddContainerRegistry("docker-hub", "docker.io", "user1"); + var registry2 = builder.AddContainerRegistry("ghcr", "ghcr.io", "user2"); + var project = builder.AddProject("api") + .WithContainerRegistry(registry2); // Explicit preference for registry2 + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + + // Simulate BeforeStartEvent to add RegistryTargetAnnotations + var beforeStartEvent = new BeforeStartEvent(app.Services, appModel); + await builder.Eventing.PublishAsync(beforeStartEvent); + + // GetContainerRegistry should return registry2 (from ContainerRegistryReferenceAnnotation) + // even though both registries added RegistryTargetAnnotations + var containerRegistry = project.Resource.GetContainerRegistry(); + Assert.Same(registry2.Resource, containerRegistry); + } + + [Fact] + public async Task GetContainerRegistryThrowsWhenMultipleRegistryTargetAnnotationsAndNoExplicitSelection() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var registry1 = builder.AddContainerRegistry("docker-hub", "docker.io", "user1"); + var registry2 = builder.AddContainerRegistry("ghcr", "ghcr.io", "user2"); + var project = builder.AddProject("api"); + // No explicit WithContainerRegistry call + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + + // Simulate BeforeStartEvent to add RegistryTargetAnnotations + var beforeStartEvent = new BeforeStartEvent(app.Services, appModel); + await builder.Eventing.PublishAsync(beforeStartEvent); + + // GetContainerRegistry should throw because there are multiple registries and no explicit selection + var exception = Assert.Throws(project.Resource.GetContainerRegistry); + Assert.Contains("multiple container registries", exception.Message); + Assert.Contains("WithContainerRegistry", exception.Message); + } + + [Fact] + public void GetContainerRegistryThrowsWhenNoRegistryAvailable() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var project = builder.AddProject("api"); + // No container registry added + + // GetContainerRegistry should throw because there's no registry available + var exception = Assert.Throws(project.Resource.GetContainerRegistry); + Assert.Contains("does not have a container registry reference", exception.Message); + } + + [Fact] + public async Task RegistryTargetAnnotationIsAddedToAllResourcesInModel() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var registry = builder.AddContainerRegistry("docker-hub", "docker.io", "myuser"); + var project1 = builder.AddProject("api1"); + var project2 = builder.AddProject("api2"); + var container = builder.AddContainer("redis", "redis:latest"); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + + // Simulate BeforeStartEvent + var beforeStartEvent = new BeforeStartEvent(app.Services, appModel); + await builder.Eventing.PublishAsync(beforeStartEvent); + + // All resources should have RegistryTargetAnnotation + Assert.Single(project1.Resource.Annotations.OfType()); + Assert.Single(project2.Resource.Annotations.OfType()); + Assert.Single(container.Resource.Annotations.OfType()); + } + + [Fact] + public async Task WithContainerRegistryOverridesDefaultRegistryTargetAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var defaultRegistry = builder.AddContainerRegistry("docker-hub", "docker.io", "default"); + var specificRegistry = builder.AddContainerRegistry("acr", "myregistry.azurecr.io", "specific"); + + var project = builder.AddProject("api") + .WithContainerRegistry(specificRegistry); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + + // Simulate BeforeStartEvent + var beforeStartEvent = new BeforeStartEvent(app.Services, appModel); + await builder.Eventing.PublishAsync(beforeStartEvent); + + // The project has both RegistryTargetAnnotations (from BeforeStartEvent) and ContainerRegistryReferenceAnnotation + var registryTargetAnnotations = project.Resource.Annotations.OfType().ToList(); + Assert.Equal(2, registryTargetAnnotations.Count); + + var containerRegistryRefAnnotation = Assert.Single(project.Resource.Annotations.OfType()); + Assert.Same(specificRegistry.Resource, containerRegistryRefAnnotation.Registry); + + // GetContainerRegistry should return specificRegistry because ContainerRegistryReferenceAnnotation takes precedence + var containerRegistry = project.Resource.GetContainerRegistry(); + Assert.Same(specificRegistry.Resource, containerRegistry); + } } diff --git a/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs b/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs index edcc4c8e9d3..b5f704c9be1 100644 --- a/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs +++ b/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs @@ -1422,12 +1422,14 @@ public async Task ExecuteAsync_WithConfigurationCallback_ExecutesCallback() await pipeline.ExecuteAsync(context).DefaultTimeout(); Assert.True(callbackExecuted); - Assert.Equal(10, capturedSteps.Count); // Updated to account for all default steps including process-parameters + Assert.Equal(12, capturedSteps.Count); // Updated to account for all default steps including process-parameters, push, push-prereq Assert.Contains(capturedSteps, s => s.Name == "deploy"); Assert.Contains(capturedSteps, s => s.Name == "process-parameters"); 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 == "push"); + Assert.Contains(capturedSteps, s => s.Name == "push-prereq"); Assert.Contains(capturedSteps, s => s.Name == "publish"); Assert.Contains(capturedSteps, s => s.Name == "publish-prereq"); Assert.Contains(capturedSteps, s => s.Name == "diagnostics"); diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs index 56ec7bd1179..01eff1f94d8 100644 --- a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs @@ -753,23 +753,26 @@ public async Task ProjectResource_AutomaticallyGeneratesBuildStep_WithCorrectDep var resource = Assert.Single(projectResources); - // Verify the project has a PipelineStepAnnotation + // Verify the project has a single PipelineStepAnnotation that emits build and push steps 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 + PipelineContext = null!, Resource = resource }; var steps = (await pipelineStepAnnotation.CreateStepsAsync(factoryContext)).ToList(); + Assert.Equal(2, steps.Count); - var buildStep = Assert.Single(steps); - Assert.Equal("build-test-project", buildStep.Name); + var buildStep = steps.First(s => s.Name == "build-test-project"); Assert.Contains(WellKnownPipelineTags.BuildCompute, buildStep.Tags); Assert.Contains(WellKnownPipelineSteps.Build, buildStep.RequiredBySteps); Assert.Contains(WellKnownPipelineSteps.BuildPrereq, buildStep.DependsOnSteps); + + var pushStep = steps.First(s => s.Name == "push-test-project"); + Assert.Contains(WellKnownPipelineTags.PushContainerImage, pushStep.Tags); + Assert.Contains(WellKnownPipelineSteps.Push, pushStep.RequiredBySteps); } [Fact] @@ -799,8 +802,7 @@ public void ProjectResourceWithContainerFilesDestinationAnnotationCreatesPipelin Assert.Equal(sourceContainer.Resource, containerFilesAnnotation.Source); Assert.Equal("./wwwroot", containerFilesAnnotation.DestinationPath); - var pipelineStepAnnotations = resource.Annotations.OfType().ToList(); - Assert.Single(pipelineStepAnnotations); + Assert.Single(resource.Annotations.OfType()); } [Fact]