Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b582a31
Initial plan
Copilot Dec 3, 2025
bd000f1
Add Push and PushPrereq pipeline steps for ContainerRegistryResource
Copilot Dec 3, 2025
5bbbb76
Address code review feedback: extract helper method and simplify para…
Copilot Dec 3, 2025
64ac7d8
Add ContainerRegistryReferenceAnnotation in PushPrereq step for singl…
Copilot Dec 3, 2025
3569427
Fix GetResourcesToPush to work unconditionally for single registry
Copilot Dec 3, 2025
605b2a1
Simplify: Push unconditionally depends on PushPrereq
Copilot Dec 3, 2025
7a01b1c
Fix up push and build associations
captainsafia Dec 3, 2025
cc998ce
Remove ContainerRegistryReferenceAnnotation requirement for push step…
captainsafia Dec 3, 2025
1ee1489
Update snapshots for tests
captainsafia Dec 3, 2025
8b09904
Move push steps to resources and standalone ACRs
captainsafia Dec 4, 2025
0bbfac0
Update snapshots and tests
captainsafia Dec 4, 2025
4cf2a3d
Bring back logging for image push and source share
captainsafia Dec 4, 2025
457e83a
Clean up IAzureContainerRegistry explicit implementation
captainsafia Dec 4, 2025
72d58ba
Model DefaultRegistry in resource and keep using environment as registry
captainsafia Dec 5, 2025
dcbcbd5
Update snapshots for Hosting.Azure tests
captainsafia Dec 5, 2025
d7cd263
Fix default build and push steps on ProjectResource
captainsafia Dec 5, 2025
6e8e8ad
Fix more tests
captainsafia Dec 5, 2025
406366d
Make DefaultContainerRegistry required init
captainsafia Dec 5, 2025
03f069c
Add tests and update snapshots
captainsafia Dec 5, 2025
5c1e629
Update snapshots
captainsafia Dec 5, 2025
27881a2
Fix naming convention on default registry for azd
captainsafia Dec 6, 2025
19f38c8
Feedback
captainsafia Dec 8, 2025
b941a35
Use RegistryTargetAnnotation and explicitly add resource
captainsafia Dec 9, 2025
5934509
Update snapshots and avoid duplicate registries in model
captainsafia Dec 9, 2025
0b71829
Update tests and default registry cleanup
captainsafia Dec 9, 2025
9c45f96
Update snapshots and fix registry resolution
captainsafia Dec 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<PackageReference Include="Azure.Provisioning.KeyVault" />
<PackageReference Include="Azure.Provisioning.Storage" />
<ProjectReference Include="..\Aspire.Hosting.Azure\Aspire.Hosting.Azure.csproj" />
<ProjectReference Include="..\Aspire.Hosting.Azure.ContainerRegistry\Aspire.Hosting.Azure.ContainerRegistry.csproj" />
<ProjectReference Include="..\Aspire.Hosting.Azure.OperationalInsights\Aspire.Hosting.Azure.OperationalInsights.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,6 @@ public AzureContainerAppEnvironmentResource(string name, Action<AzureResourceInf
var model = factoryContext.PipelineContext.Model;
var steps = new List<PipelineStep>();

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
{
Expand All @@ -51,7 +43,6 @@ public AzureContainerAppEnvironmentResource(string name, Action<AzureResourceInf
RequiredBySteps = [WellKnownPipelineSteps.Deploy]
};

steps.Add(loginToAcrStep);
steps.Add(printDashboardUrlStep);

// Expand deployment target steps for all compute resources
Expand Down Expand Up @@ -92,8 +83,6 @@ public AzureContainerAppEnvironmentResource(string name, Action<AzureResourceInf
// This is where we wire up the build steps created by the resources
Annotations.Add(new PipelineConfigurationAnnotation(context =>
{
var acrLoginSteps = context.GetSteps(this, "acr-login");

// Wire up build step dependencies
// Build steps are created by ProjectResource and ContainerResource
foreach (var computeResource in context.Model.GetComputeResources())
Expand All @@ -113,9 +102,6 @@ public AzureContainerAppEnvironmentResource(string name, Action<AzureResourceInf
annotation.Callback(context);
}
}

context.GetSteps(deploymentTarget, WellKnownPipelineTags.PushContainerImage)
.DependsOn(acrLoginSteps);
}

// This ensures that resources that have to be built before deployments are handled
Expand All @@ -130,8 +116,6 @@ public AzureContainerAppEnvironmentResource(string name, Action<AzureResourceInf
var provisionSteps = context.GetSteps(this, WellKnownPipelineTags.ProvisionInfrastructure);
var printDashboardUrlSteps = context.GetSteps(this, "print-summary");
printDashboardUrlSteps.DependsOn(provisionSteps);

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

Expand Down Expand Up @@ -164,6 +148,11 @@ await context.ReportingStep.CompleteAsync(
/// </summary>
internal BicepOutputReference ContainerAppDomain => new("AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN", this);

/// <summary>
/// Gets the name of the associated Azure Container Registry.
/// </summary>
internal BicepOutputReference ContainerRegistryName => new("AZURE_CONTAINER_REGISTRY_NAME", this);

/// <summary>
/// Gets the URL endpoint of the associated Azure Container Registry.
/// </summary>
Expand All @@ -179,17 +168,28 @@ await context.ReportingStep.CompleteAsync(
/// </summary>
public BicepOutputReference NameOutputReference => new("AZURE_CONTAINER_APPS_ENVIRONMENT_NAME", this);

internal Dictionary<string, (IResource resource, ContainerMountAnnotation volume, int index, BicepOutputReference outputReference)> VolumeNames { get; } = [];

/// <summary>
/// Gets the container registry name.
/// Gets the default container registry for this environment.
/// </summary>
private BicepOutputReference ContainerRegistryName => new("AZURE_CONTAINER_REGISTRY_NAME", this);
internal AzureContainerRegistryResource? DefaultContainerRegistry { get; set; }

internal Dictionary<string, (IResource resource, ContainerMountAnnotation volume, int index, BicepOutputReference outputReference)> 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<ContainerRegistryReferenceAnnotation>(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}");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,23 @@ public static IResourceBuilder<AzureContainerAppEnvironmentResource> AddAzureCon

infra.Add(identity);

ContainerRegistryService? containerRegistry = null;
if (appEnvResource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var registryReferenceAnnotation) && registryReferenceAnnotation.Registry is AzureProvisioningResource registry)
AzureProvisioningResource? registry = null;
if (appEnvResource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(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);
Copy link
Member

Choose a reason for hiding this comment

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

Unrelated but related, role assignments are busted with existing container registries, we can fix that as a follow up but this is all the infrastructure required to fix it.

infra.Add(containerRegistry);

var pullRa = containerRegistry.CreateRoleAssignment(ContainerRegistryBuiltInRole.AcrPull, identity);
Expand Down Expand Up @@ -323,14 +327,19 @@ public static IResourceBuilder<AzureContainerAppEnvironmentResource> 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;
}

/// <summary>
Expand Down Expand Up @@ -379,4 +388,57 @@ public static IResourceBuilder<AzureContainerAppEnvironmentResource> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,42 +27,18 @@ public AzureContainerAppResource(string name, Action<AzureResourceInfrastructure
{
TargetResource = targetResource;

// Add pipeline step annotation for push
// Add pipeline step annotation for deploy
Annotations.Add(new PipelineStepAnnotation((factoryContext) =>
{
// 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<PipelineStep>();

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<IResourceContainerImageManager>();

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

steps.Add(pushStep);
}

if (!targetResource.TryGetEndpoints(out var endpoints))
{
endpoints = [];
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContainerRegistryReferenceAnnotation>() &&
environment.DefaultContainerRegistry is not null)
{
@event.Model.Resources.Remove(environment.DefaultContainerRegistry);
}

var containerAppEnvironmentContext = new ContainerAppEnvironmentContext(
logger,
executionContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<PackageReference Include="Azure.Provisioning.ContainerRegistry" />
<ProjectReference Include="..\Aspire.Hosting.Azure.ApplicationInsights\Aspire.Hosting.Azure.ApplicationInsights.csproj" />
<ProjectReference Include="..\Aspire.Hosting.Azure\Aspire.Hosting.Azure.csproj" />
<ProjectReference Include="..\Aspire.Hosting.Azure.ContainerRegistry\Aspire.Hosting.Azure.ContainerRegistry.csproj" />
</ItemGroup>

</Project>
Loading